QFontMetrics leave extra space between lines - python

I'm trying to draw multiple paragraphs of text with PySide6's QPainter and QFontMetrics. I want to draw them with the same spacing as they would have if I drew them all in a single block of text, but the line spacing isn't quite right.
In the following example, the font metrics say that the font's line spacing is 17. When I measure a single line of text, the bounding rectangle is indeed 17 pixels high. However, when I measure two lines of text, the bounding rectangle is 35 pixels high, not 34. Where does the extra pixel come from, and can I see it on some property of the font or the font metrics?
from PySide6.QtGui import QFont, QFontMetrics
from PySide6.QtWidgets import QApplication
app = QApplication()
font = QFont()
metrics = QFontMetrics(font)
print(metrics.lineSpacing()) # 17
print(metrics.boundingRect(0, 0, 100, 100, 0, 'A').height()) # 17
print(metrics.boundingRect(0, 0, 100, 100, 0, 'A\nB').height()) # 35 != 17 * 2
print(metrics.leading()) # 0
print(metrics.ascent()) # 14
print(metrics.descent()) # 3
By the way, it isn't always one extra pixel. If I make the font bigger, the extra space increases.
Update
I thought I had figured this out with musicamante's suggestion of switching from QFontMetrics to QFontMetricsF, but there's still a difference.
from PySide6.QtCore import QRectF
from PySide6.QtGui import QFont, QFontMetricsF
from PySide6.QtWidgets import QApplication
app = QApplication()
font = QFont()
metrics = QFontMetricsF(font)
print(metrics.height()) # 16.8125
print(metrics.boundingRect(QRectF(0, 0, 100, 100),
0,
'A').getCoords()) # (0.0, 0.0, 9.9375, 16.8125)
print(metrics.boundingRect(QRectF(0, 0, 100, 100),
0,
'A\nB').getCoords()) # (0.0, 0.0, 9.9375, 34.8125)
# Note the height of that rect doesn't match the next calculation.
print(metrics.height() + metrics.lineSpacing()) # 34.046875
# I can't see any combination of these numbers that makes 34.8125
print(metrics.lineSpacing()) # 17.234375
print(metrics.leading()) # 0.421875
print(metrics.ascent()) # 13.984375
print(metrics.descent()) # 2.828125

First of all, the height of a bounding rect of a font metrics doesn't depend on the characters used, but on the font specifications.
Two lines of text don't have the double of the height() of the bounding rect of a single line: instead, you have to consider the lineSpacing().
In practice, the height of a bounding rect is normally the sum of:
the height() multiplied the number of lines;
the leading() multiplied by the number of spaces between the lines (aka: number of lines - 1);
Or, similarly, the sum of:
the ascent();
the lineSpacing() multiplied by the number of spaces between lines;
the descent();
Note that, obviously, the number of lines depends on the input text and the given options, for instance, if word wrapping was enabled and any of the source lines didn't fit the given source rectangle.
Also consider that most fonts are vectorial, meaning that their coordinates and metrics are proportional and in floating point values. QFontMetrics, instead, works with integer values for simplicity and optimization reasons, so you might get inconsistent results caused by rounding in cases for which the point size doesn't give rounded values: non integer numbers are generally "floored" (like int() in python).
In your case, the leading is probably more than 0 (but still less than 1), so you don't get a proper sum of the aforementioned heights.
Specifically, QFontMetrics.boundingRect() returns a QRect resulting by the QRectF.toAlignedRect() of the computed formatted text, which is always "the smallest possible integer rectangle that completely contains this rectangle".
If you need to get precise coordinates, you need to use QFontMetricsF, which is the floating point counterpart of the default basic QFontMetrics.
That said, if you plan on drawing formatted text with QPainter, then consider using QTextDocument or, at least, QTextLayout, which is consistent with the standard Qt text drawing and is generally faster, more reliable and "simpler" (well, once you get to know it). While it might seem a bit too complex than required, it's actually what Qt does when calling boundingRect(), so if you need custom painting, the QTextLayout option is actually better, especially if you can combine it with some smart caching (see QPicture) to avoid the common python bottleneck.

Related

Scaling QPolygon on its origin

I'm trying to scale a QPolygonF that is on a QGraphicsScene's QGraphicsView on its origin.
However, even after translating the polygon (poly_2) to its origin (using QPolygon.translate() and the center coordinates of the polygon received via boundingRect (x+width)/2 and (y+height)/2), the new polygon is still placed on the wrong location.
The blue polygon should be scaled according to the origin of poly_2 (please see the image below, black is the original polygon, blue polygon is the result of the code below, and the orange polygon is representing the intended outcome)
I thought that the issue might be that coordinates are from global and should be local, yet this does solve the issue unfortunately.
Here's the code:
import PyQt5
from PyQt5 import QtCore
import sys
import PyQt5
from PyQt5.QtCore import *#QPointF, QRectF
from PyQt5.QtGui import *#QPainterPath, QPolygonF, QBrush,QPen,QFont,QColor, QTransform
from PyQt5.QtWidgets import *#QApplication, QGraphicsScene, QGraphicsView, QGraphicsSimpleTextItem
poly_2_coords= [PyQt5.QtCore.QPointF(532.35, 274.98), PyQt5.QtCore.QPointF(525.67, 281.66), PyQt5.QtCore.QPointF(518.4, 292.58), PyQt5.QtCore.QPointF(507.72, 315.49), PyQt5.QtCore.QPointF(501.22, 326.04), PyQt5.QtCore.QPointF(497.16, 328.47), PyQt5.QtCore.QPointF(495.53, 331.71), PyQt5.QtCore.QPointF(488.24, 339.02), PyQt5.QtCore.QPointF(480.94, 349.56), PyQt5.QtCore.QPointF(476.09, 360.1), PyQt5.QtCore.QPointF(476.89, 378.76), PyQt5.QtCore.QPointF(492.3, 393.35), PyQt5.QtCore.QPointF(501.22, 398.21), PyQt5.QtCore.QPointF(527.17, 398.21), PyQt5.QtCore.QPointF(535.28, 390.1), PyQt5.QtCore.QPointF(540.96, 373.89), PyQt5.QtCore.QPointF(539.64, 356.93), PyQt5.QtCore.QPointF(541.46, 329.0), PyQt5.QtCore.QPointF(543.39, 313.87), PyQt5.QtCore.QPointF(545.83, 300.89), PyQt5.QtCore.QPointF(545.83, 276.56), PyQt5.QtCore.QPointF(543.39, 267.64), PyQt5.QtCore.QPointF(537.81, 268.91)]
def main():
app = QApplication(sys.argv)
scene = QGraphicsScene()
view = QGraphicsView(scene)
pen = QPen(QColor(0, 20, 255))
scene.addPolygon(QPolygonF(poly_2_coords))
poly_2 = QPolygonF(poly_2_coords)
trans = QTransform().scale(1.5,1.5)
#poly_22 = trans.mapToPolygon(QRect(int(poly_2.boundingRect().x()),int(poly_2.boundingRect().y()),int(poly_2.boundingRect().width()),int(poly_2.boundingRect().height())))
#trans.mapToPolygon()
#scene.addPolygon(QPolygonF(poly_22),QPen(QColor(0, 20, 255)))
poly_2.translate((poly_2.boundingRect().x()+poly_2.boundingRect().width())/2,(poly_2.boundingRect().y()+poly_2.boundingRect().height())/2)
print(f'poly_2.boundingRect().x() {poly_2.boundingRect().x()}+poly_2.boundingRect().width(){poly_2.boundingRect().width()}')
trans = QTransform().scale(1.4,1.4)
#poly_2.setTransformOriginPoint()
poly_22 = trans.map(poly_2)
scene.addPolygon(poly_22,QPen(QColor(0, 20, 255)))
view.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Edit: I've tried saving the polygon as a QGraphicsItem, and set its transformation origin point according the bbox's middle X,Y and then mapped from Global to Scene, yet no luck: the new polygon is still drawn to the wrong place.
poly_2 = QPolygonF(poly_2_coords)
poly = scene.addPolygon(poly_2)
point = QPoint((poly_2.boundingRect().x()+poly_2.boundingRect().width())/2,(poly_2.boundingRect().y()+poly_2.boundingRect().height())/2)
poly.setTransformOriginPoint(point)
poly.setScale(3)
If replacing point to equal only X,Y of the bounding rectangle, the result seems to be closer to what I need. However, in this case the origin point is obviously wrong. Is this just random luck that this answer seems to be closer to what I need?
Before considering the problem of the translation, there is a more important aspect that has to be considered: if you want to create a transformation based on the center of a polygon, you must find that center. That point is called centroid, the geometric center of any polygon.
While there are simple formulas for all basic geometric shapes, finding the centroid of a (possibly irregular) polygon with an arbitrary number of vertices is a bit more complex.
Using the arithmetic mean of vertices is not a viable option, as even in a simple square you might have multiple points on a single side, which would move the computed "center" towards those points.
The formula can be found in the Wikipedia article linked above, while a valid python implementation is available in this answer.
I modified the formula of that answer in order to accept a sequence of QPoints, while improving readability and performance, but the concept remains the same:
def centroid(points):
if len(points) < 3:
raise ValueError('At least 3 points are required')
# https://en.wikipedia.org/wiki/Centroid#Of_a_polygon
# https://en.wikipedia.org/wiki/Shoelace_formula
# computation uses concatenated pairs from the sequence, with the
# last point paired to the first one:
# (p[0], p[1]), (p[1], p[2]) [...] (p[n], p[0])
area = cx = cy = 0
p1 = points[0]
for p2 in points[1:] + [p1]:
shoelace = p1.x() * p2.y() - p2.x() * p1.y()
area += shoelace
cx += (p1.x() + p2.x()) * shoelace
cy += (p1.y() + p2.y()) * shoelace
p1 = p2
A = 0.5 * area
factor = 1 / (6 * A)
return cx * factor, cy * factor
Then, you have two options, depending on what you want to do with the resulting item.
Scale the item
In this case, you create a QGraphicsPolygonItem like the original one, then set its transform origin point using the formula above, and scale it:
poly_2 = QtGui.QPolygonF(poly_2_coords)
item2 = scene.addPolygon(poly_2, QtGui.QPen(QtGui.QColor(0, 20, 255)))
item2.setTransformOriginPoint(*centroid(poly_2_coords))
item2.setScale(1.5)
Use a QTransform
With Qt transformations some special care must be taken, as scaling always uses 0, 0 as origin point.
To scale around a specified point, you must first translate the matrix to that point, then apply the scale, and finally restore the matrix translation to its origin:
poly_2 = QtGui.QPolygonF(poly_2_coords)
cx, cy = centroid(poly_2_coords)
trans = QtGui.QTransform()
trans.translate(cx, cy)
trans.scale(1.5, 1.5)
trans.translate(-cx, -cy)
poly_2_scaled = trans.map(poly_2)
scene.addPolygon(poly_2_scaled, QtGui.QPen(QtGui.QColor(0, 20, 255)))
This is exactly what QGraphicsItems do when using the basic setScale() and setRotation() transformations.
Shape origin point and item position
Remember that QGraphicsItems are always created with their position at 0, 0.
This might not seem obvious especially for basic shapes: when you create a QGraphicsRectItem giving its x, y, width, height, the position will still be 0, 0. When dealing with complex geometry management, it's usually better to create basic shapes with the origin/reference at 0, 0 and then move the item at x, y.
For complex polygons like yours, a possibility could be to translate the centroid of the polygon at 0, 0, and then move it at the actual centroid coordinates:
item = scene.addPolygon(polygon.translated(-cx, -cy))
item.setPos(cx, cy)
item.setScale(1.5)
This might make things easier for development (the mapped points will always be consistent with the item position), and the fact that you don't need to change the transform origin point anymore makes reverse mapping even simpler.

Colorize parts of a complex equation manim

I want to paint variables of MathTex element in different colors, but Manim seems to have problems with comlicated Latex expressions.
Here is my scene.
from manim import *
config.frame_width = 260
class Find_Path(Scene):
def construct(self):
obj = MathTex(r"minimize \quad \sum_{start}^{end}\frac{d_{i,i+1}}{v_{i,i+1}}",
font_size=1000, substrings_to_isolate="d" and "v")
obj.set_color_by_tex("d", YELLOW)
obj.set_color_by_tex("start", GREEN)
obj.set_color_by_tex("end", GREEN)
obj.set_color_by_tex("v", RED)
self.play(Write(obj))
self.wait(3)
Here is the result.
Specifically, I want to color d_{i,i+1} in YELLOW, v_{i,i+1} in RED, start and end in GREEN.
Any advice? Frankly, I do not want to create several MathTex object in different colors and then arrange them.
Manim does a bunch of tex rewriting under the covers, and it seems that over is preferred to frac because of that rewriting.
I was able to apply the colors that you wanted (although I suspect you didn't want the sum symbol colored) with:
from manim import *
class Find_Path(Scene):
def construct(self):
obj1 = MathTex(r"\text{minimize}", r"\quad \sum_{\text{start}}^{\text{end}}")
obj2 = MathTex(r"d_{i,i+1}", r"\over", r"v_{i,i+1}")
obj1.set_color_by_tex("start", GREEN)
obj1.set_color_by_tex("end", GREEN)
obj2.move_to(obj1, RIGHT)
obj2.shift(1.5 * RIGHT)
obj2[0].set_color(YELLOW)
obj2[2].set_color(RED)
self.play(AnimationGroup(Write(obj1), Write(obj2)))
self.wait(3)
but I had to resort to separate objects. Worse still, I aligned them by hand with a fudge factor.
Late answer, but I encountered a similar issue and ended up here before finding the relevant section in the documentation.
Relevant section in documentation: Using index_labels to work with complicated strings
An example with your special case:
from manim import *
config.frame_width = 8
config.frame_size = (1300, 1000)
class FindPath(Scene):
def construct(self):
# You can split the string in parts
minimize = r"minimize \quad "
summ = r"\sum_{start}^{end}"
frac = r"\frac{d_{i,i+1}}{v_{i,i+1}}"
tex = MathTex(minimize, summ, frac).shift(2 * UP)
# Observe first level labels
tex_ = tex.copy().next_to(tex, DOWN)
self.add(index_labels(tex_, color=YELLOW))
# Observe second level labels
tex__ = tex_.copy().next_to(tex_, DOWN)
for part in tex__:
self.add(index_labels(part, color=YELLOW))
# Finally you can color accordingly
tex[1][0:3].set_fill(color=GREEN)
tex[1][4:9].set_fill(color=GREEN)
tex[2][0:6].set_fill(color=YELLOW)
tex[2][7:13].set_fill(color=RED)
self.add(tex, tex_, tex__)

Issues with position manipulation using QGridLayout addWidget(QWidget,x,y)

This is a really simple question I'm just new to PyQt5 and am a bit confused on how QGridLayout works...
def init_main_page(self):
layout = QGridLayout()
b1 = buttons.QPushButton("0",self.main_page)
b2 = buttons.QPushButton("1",self.main_page)
b3 = buttons.QPushButton("2",self.main_page)
layout.addWidget(b1,0,0)
layout.addWidget(b2,5,0)
layout.addWidget(b3,1,0)
self.main_page.setLayout(layout)
The problem I am having is that no matter how high I make the x and y arguments in addwidget(QWidget,x,y), it b1 b2 and b3 always remain equidistant from each other. I'm trying figure out how to manipulate the position of the buttons whilst maintaining a proportional setup (so avoiding QPushButton.move()) and from what I've seen, QGridLayout is the best way to do this.
Setting the coordinates of widgets in a grid layout by "skipping" rows or columns is almost useless, as those are grid coordinates that only tell the layout manager in which "slots" the widgets will be: since there is nothing in the rows 2 to 4, that space will not be used.
To achieve what you want you need to set stretch factors and, possibly, use minimum heights for the specified rows.
layout.addWidget(b1, 0, 0)
layout.addWidget(b3, 1, 0)
layout.addWidget(b2, 2, 0)
layout.setRowStretch(2, 1)
layout.setRowMinimumHeight(2, 50)
But this might not be what you want, since it will place the third button in the vertical center of the row grid (leaving empty space at the bottom).
To avoid that there are two possible solutions:
add the widget by setting the alignment to the bottom:
layout.addWidget(b2, 2, 0, alignment=QtCore.Qt.AlignBottom)
add the last widget to the fourth row, and set stretch and minimum height for the third empty row.
layout.addWidget(b2, 3, 0)
layout.setRowStretch(2, 1)
layout.setRowMinimumHeight(2, 50)

maximum allowed dimention exceeded

I am attempting to make a painting based on the mass of the universe with pi and the gravitational constant of earth at sea level converted to binary. i've done the math and i have the right dimentions and it should only be less than a megabyte of ram but im running into maximum allowed dimention exceeded value error.
Here is the code:
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import numpy as np
boshi = 123456789098765432135790864234579086542098765432135321 # universal mass
genesis = boshi ** 31467 # padding
artifice = np.binary_repr(genesis) # formatting
A = int(artifice)
D = np.array(A).reshape(A, (1348, 4117))
plt.imsave('hello_world.png', D, cmap=cm.gray) # save image
I keep running into the error at D = np.array..., and maybe my reshape is too big but its only a little bigger than 4k. seems like this should be no problem for gpu enhanced colab. Doesn't run on my home machine either with the same error. Would this be fixed with more ram?
Making it Work
The problem is that artifice = np.binary_repr(genesis) creates a string. The string consists of 1348 * 4117 = 5549716 digits, all of them zeros and ones. If you convert the string to a python integer, A = int(artifice), you will (A) wait a very long time, and (B) get a non-iterable object. The array you create with np.array(A) will have a single element.
The good news is that you can bypass the time-consuming step entirely using the fact that the string artifice is already an iterable:
D = np.array(list(artifice), dtype=np.uint8).reshape(1348, 4117)
The step list(artifice) will take a couple of seconds since it has to split up the string, but everything else should be quite fast.
Plotting is easy from there with plt.imsave('hello_world.png', D, cmap=cm.gray):
Colormaps
You can easily change the color map to coolwarm or whatever you want when you save the image. Keep in mind that your image is binary, so only two of the values will actually matter:
plt.imsave('hello_world2.png', D, cmap=cm.coolwarm)
Exploration
You have an opportunity here to add plenty of color to your image. Normally, a PNG is 8-bit. For example, instead of converting genesis to bits, you can take the bytes from it to construct an image. You can also take nibbles (half-bytes) to construct an indexed image with 16 colors. With a little padding, you can even make sure that you have a multiple of three data points, and create a full color RGB image in any number of ways. I will not go into the more complex options, but I would like to explore making a simple image from the bytes.
5549716 bits is 693715 = 5 * 11 * 12613 bytes (with four leading zero bits). This is a very nasty factorization leading to an image size of 55x12613, so let's remove that upper nibble: while 693716's factorization is just as bad as 693715's, 693714 factors very nicely into 597 * 1162.
You can convert your integer to an array of bytes using its own to_bytes method:
from math import ceil
byte_genesis = genesis.to_bytes(ceil(genesis.bit_length() / 8), 'big')
The reason that I use the built-in ceil rather than np.ceil is that it return an integer rather than a float.
Converting the huge integer is very fast because the bytes object has direct access to the data of the integer: even if it makes a copy, it does virtually no processing. It may even share the buffer since both bytes and int are nominally immutable. Similarly, you can create a numpy array from the bytes as just a view to the same memory location using np.frombuffer:
img = np.frombuffer(byte_genesis, dtype=np.uint8)[1:].reshape(597, 1162)
The [1:] is necessary to chop off the leading nibble, since bytes_genesis must be large enough to hold the entirety of genesis. You could also chop off on the bytes side:
img = np.frombuffer(byte_genesis[1:], dtype=np.uint8).reshape(597, 1162)
The results are identical. Here is what the picture looks like:
plt.imsave('hello_world3.png', img, cmap=cm.viridis)
The result is too large to upload (because it's not a binary image), but here is a randomly selected sample:
I am not sure if this is aesthetically what you are looking for, but hopefully this provides you with a place to start looking at how to convert very large numbers into data buffers.
More Options, Because this is Interesting
I wanted to look at using nibbles rather than bytes here, since that would allow you to have 16 colors per pixel, and twice as many pixels. You can get an 1162x1194 image starting from
temp = np.frombuffer(byte_genesis, dtype=np.uint8)[1:]
Here is one way to unpack the nibbles:
img = np.empty((1162, 1194), dtype=np.uint8)
img.ravel()[::2] = np.bitwise_and(temp >> 4, 0x0F)
img.ravel()[1::2] = np.bitwise_and(temp, 0x0F)
With a colormap like jet, you get:
plt.imsave('hello_world4.png', img, cmap=cm.jet)
Another option, going in the opposite direction in a manner of speaking) is not to use colormaps at all. Instead, you can divide your space by a factor of three and generate your own colors in RGB space. Luckily, one of the prime factors of 693714 is 3. You can therefore have a 398x581 image (693714 == 3 * 398 * 581). How you interpret the data is even more than usual up to you.
Side Note Before I Continue
With the black-and-white binary image, you could control the color, size and orientation of the image. With 8-bit data, you could control how the bits were sampled (8 or fewer, as in the 4-bit example), the endianness of your interpretation, the color map, and the image size. With full color, you can treat each triple as a separate color, treat the entire dataset as three consecutive color planes, or even do something like apply a Bayer filter to the array. All in addition to the other options like size, ordering, number of bits per sample, etc.
The following will show the color triples and three color planes options for now.
Full Color Images
To treat each set of 3 consecutive bytes as an RGB triple, you can do something like this:
img = temp.reshape(398, 581, 3)
plt.imsave('hello_world5.png', img)
Notice that there is no colormap in this case.
Interpreting the data as three color planes requires an extra step because plt.imsave expects the last dimension to have size 3. np.rollaxis is a good tool for this:
img = np.rollaxis(temp.reshape(3, 398, 581), 0, 3)
plt.imsave('hello_world6.png', img)
I could not reproduce your problem, because the line A = int(artifice) took like forever. I replaced it with a ,for loop to cast each digit on its own. The code worked then and produced the desired image.
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import numpy as np
boshi = 123456789098765432135790864234579086542098765432135321
genesis = boshi ** 31467
artifice = np.binary_repr(genesis)
D = np.zeros((1348, 4117), dtype=int)
for i, val in enumerate(D):
D[i] = int(artifice[i])
plt.imsave('hello_world.png', D, cmap=cm.gray)

How to simplify my codes?

I wrote a program for a class using recursion to mimic certain kinds of simple branching structures like trees. I thought my code was great until I showed my professor. He told my code was too complicated and said I would need to simplify it. Besides spacing them out, I'm not sure what else I could do. Any tips? (I'm a beginner so go easy on me.) This program creates multiple trees with varying thickness, number of branch and at different coordinates.
import random
import turtle
##I'm using a python module called turtle to visualize results
p1 = turtle.Pen()
##Creates a pen
p1.tracer(True)
## Shows pen drawing
p1.up()
p1.left(90)
d=random.randint(0,2)
## Varying thickness of branch
length=150
##Length of branches
contract=random.uniform(.5,1)
## Varying degree of contraction
branch=random.randint(5,8)
## Varying amount of branches
first=random.randint(30,70)
## Varying first degree of branch
next=random.randint(1,30)
## Varying degree between each branches
number1=random.randint(10,20)
number2=random.randint(-100,100)
number3=random.randint(-100,100)
# Range of numbers used for coordinates
def drawFern1(pen, depth, length, contractBy, branches, firstBranchAngle, nextBranchAngle):
if depth > 0:
#Pen's Position and heading
heading = pen.heading()
position = pen.position()
pen.width(depth)
pen.forward(length)
pen.left(firstBranchAngle)
for i in range(branches):
drawFern1(pen, depth-1, contractBy*length, contractBy,branches,firstBranchAngle,nextBranchAngle)
pen.right(nextBranchAngle)
pen.setheading(heading)
pen.setposition(position)
# Ensures that multiple trees are created each at different coordinates.
for i in range(number1):
p1.sety(number2)
p1.setx(number3)
p1.down()
drawFern1(p1,d,length,contract,branch,first,next)
number2 = random.randint(-100,100)
number3 = random.randint(-100,100)
p1.up()
This code looks pretty solid to me, especially for a Python beginner. I've seen much worse.
If I were writing it, I think I'd calculate number2 and number3 only inside the main for loop - a priming definition as you have here is often convenient for a while loop, but not necessary in this case. I would also try to use more explanatory variable names, and depending on the problem statement I might require the randomly generated depth value to be at least 1 - if depth is generated as 0, nothing will be drawn.
My version of this would look like this:
import random
import turtle
def drawFern(pen, depth, length, contraction, branches, firstBranchAngle, nextBranchAngle):
if depth > 0:
# Pen's Position and heading
heading = pen.heading()
position = pen.position()
pen.width(depth)
pen.forward(length)
pen.left(firstBranchAngle)
for i in xrange(branches):
drawFern(pen, depth-1, contraction*length, contraction, branches, firstBranchAngle, nextBranchAngle)
pen.right(nextBranchAngle)
pen.setheading(heading)
pen.setposition(position)
# I'm using a python module called turtle to visualize results
# Creates a pen
pen = turtle.Pen()
# Shows pen drawing
pen.tracer(True)
pen.up()
pen.left(90)
# Configure initial state
# Varying depth of recursive fern
depth = random.randint(1,2)
# Length of branches
length = 150
# Varying degree of contraction
contraction = random.uniform(.5,1)
# Varying number of branches
branches = random.randint(5,8)
# Varying first degree of branch
first_angle = random.randint(30,70)
# Varying degree between each branches
next_angle = random.randint(1,30)
number_of_trees =random.randint(10,20)
for i in xrange(number_of_trees):
new_x = random.randint(-100, 100)
new_y = random.randint(-100, 100)
pen.setx(new_x)
pen.sety(new_y)
pen.down()
drawFern(pen, depth, length, contraction, branches, first_angle, next_angle)
pen.up()
In addition to moving the x and y coordinate randomization into the main loop, moving the recursive function definition earlier in the file, and using some more explicit variable names, I've used xrange calls instead of range calls - a trivial optimization if you're on Python 2.x. If you're on Python 3, range is correct. But these are minor changes.
You could also throw in an if clause before the range(branches) loop to not even try if depth equals 1 - that's another minor optimization, although not one that will make a big difference.
I thought my code was great until I showed my professor. He told my
code was too complicated and said I would need to simplify it.
This is rather complicated code given the quality of trees that it draws:
Drawing just vertical lines and blank screens are within the random parameters of the program as written! Let's rework the program to move some of the randomness from the static configuration code into the recursive routine itself. We'll also fine tune the random ranges a bit and clean up the code, primarily by eliminating variables that are only set and used once:
from random import randint, uniform
from turtle import Screen, Pen # Using python turtle module to visualize results
# Configure initial state
DEPTH = randint(3, 4) # Varying thickness and splitting of branches
LENGTH = randint(125, 150) # Length of branches
CONTRACT_BY = uniform(0.4, 0.8) # Varying degree of contraction
def drawFern(pen, depth, length, contractBy):
if depth < 1:
return
# Save pen's position and heading
heading = pen.heading()
position = pen.position()
pen.width(depth * 1.5) # pen thickness depends on branching
pen.forward(length)
pen.left(randint(30, 70)) # Varying first degree of branch)
for _ in range(randint(5, 8)): # Varying amount of branches
drawFern(pen, depth - 1, contractBy * length, contractBy)
pen.right(randint(5, 30)) # Varying degree between each branches
# Restore pen's Position and heading
pen.setheading(heading)
pen.setposition(position)
screen = Screen()
pen = Pen(visible=False)
pen.left(90)
screen.tracer(False)
# Ensure that multiple trees are created each at different coordinates.
for i in range(randint(10, 20)):
pen.penup()
pen.setposition(randint(-200, 200), randint(-300, 0))
pen.pendown()
drawFern(pen, DEPTH, LENGTH, CONTRACT_BY)
screen.tracer(True)
screen.mainloop()

Categories