to center the text on the page you should set the align parameter to center and set the x coordinate of the text to half of the page width:
text(txt, (width()/2, 70), align ='center')
to center the text on the page you should set the align parameter to center and set the x coordinate of the text to half of the page width:
text(txt, (width()/2, 70), align ='center')
@Anna hi
do you get error messages?
if so you should post them.
and maybe post some code.
what is going wrong and what would you like it to do?
i hope i am not missing this in the documentation …
I was wondering if there is a way to list all instances of a variable font? and then possibly get the values of the axes at these instances.
thanks j
You cannot draw a circle with Beziers.
When I first read this some time ago I was a bit confused.
Why is this not a circle. It sure looks like one.
After learning a bit more about curve descriptions and calculations I kind of agree but the quote should better say:
You cannot draw a perfect circle with Beziers.
We have a formula to calculate points on a Bezier Curve.
x = (1-t)**3*p1[0] + 3*(1-t)**2*t*t1[0] + 3*(1-t)*t**2*t2[0] + t**3*p2[0]
y = (1-t)**3*p1[1] + 3*(1-t)**2*t*t1[1] + 3*(1-t)*t**2*t2[1] + t**3*p2[1]
p1 and p2 are the two endpoints and t1 and t2 the two tangent points. t is the factor (0-1) at which we want the point on the curve.
On a bezier curve with the following coördinates:
p1 = (1, 0)
p2 = (0, 1)
t1 = (1, tangent_val)
t2 = (tangent_val, 1)
we can calculate the tangent_val if we assume that the point at t=0.5 (halfway the curve) is exactly on the circle. Halfway of a quartercircle is 45 degrees so x and y are both at 1/sqrt(2). If we plug these numbers into the formula we can calculate the tangent value:
1/sqrt(2) = (1-.5)**3*1 + 3*(1-.5)**2*.5*1 + 3*(1-.5)*.5**2*tangent_val + .5**3*0
...
tangent_val = 4*(sqrt(2)-1)/3 ≈ 0.5522847493
With this tangent_val we can calculate other points on the bezier curve. Of which we can calculate the angle in respect to the center point at (0, 0). And with this angle we can use sine and cosine to get the point that would be on a circle.
We can also calculate the distance of the bezier point and see where the offset is the biggest. As we can see the difference is really tiny. With a radius of 1 the biggest off value is 1.0002725295591013 (not even one permille). The difference is hardly visible so I added only the top part of a really long bar chart to make it show.
See the image below:
# -------------------
# settings
pw, ph = 595, 842 #A4
margin = 70
tang = 4*(sqrt(2)-1)/3
r = pw - 2 * margin
density = 90 # amount of points
dia_ = 1 # diameter to draw the points
r_plot = 100000 # this is a magnifying factor to make the tiny difference visible.
r_plot_y = margin - r_plot #radius plotting
c_plot_y = ph - 3 * margin - r # circle plotting
# -------------------
# functions
def get_bezier_p(p1, t1, t2, p2, t):
''' get the point on a bezier curve at t (should be between 0 and 1) '''
x = (1 - t)**3 * p1[0] + 3*(1 - t)**2 * t * t1[0] + 3*(1 - t)* t**2 * t2[0] + t**3 * p2[0]
y = (1 - t)**3 * p1[1] + 3*(1 - t)**2 * t * t1[1] + 3*(1 - t)* t**2 * t2[1] + t**3 * p2[1]
return x, y
def get_dist(p1, p2):
''' returns the distance between two points (x, y)'''
return sqrt((p2[0]-p1[0])**2 + (p2[1]-p1[1])**2)
# -------------------
# drawings
newPage(pw, ph)
fill(1)
rect(0, 0, pw, ph)
translate(margin, margin)
# draw the tangent points
rect(r - dia_, c_plot_y + tang*r - dia_, dia_*2, dia_*2)
rect(tang*r -dia_, c_plot_y + r - dia_, dia_*2, dia_*2)
max_off = 1
for i in range(density+1):
# -------
# bezier
# calculate the factor f
f = i/density
fill(0)
if i == 0 or i == (density): dia = dia_ * 4
else: dia = dia_
# get the coordinates for the point at factor f
x_b, y_b = get_bezier_p( (1, 0), (1, tang), (tang, 1), (0, 1), f )
# get the distance of the point from the center at (0, 0)
r_b = get_dist((0, 0), (x_b, y_b))
max_off = max(max_off, r_b)
# get the angle of the point
angle = atan2(y_b, x_b)
# draw the point and a rect for the bar chart
oval(x_b * r - dia/2, c_plot_y + y_b * r - dia/2, dia, dia)
rect((angle/(pi/2)) * r - dia_/2, r_plot_y, dia_, r_plot * r_b)
# -------
# circle
fill(1, 0, 0)
# get the point on a circle
x_c, y_c = cos(angle), sin(angle)
# draw the point and a rect for the bar chart
oval(x_c * r - dia/2, c_plot_y + y_c * r - dia/2, dia, dia)
rect((angle/(pi/2)) * r - dia_/2, r_plot_y, dia_, r_plot)
fill(.3)
if i % 10 == 0:
x_p, y_p = r * f, margin #* 1.5
rect(x_p - .25, y_p, .5, 30)
text('%d°' % (f * 90), (x_p-2, y_p * 1.5))
print (max_off)
text('%.6f' % max_off, (-50, max_off * r_plot - r_plot + margin - 2))
text('%.6f' % 1, (-50, margin - 2))
The maybe more interesting peculiarity of bezier curves is the difference between time and distance. We can compare the bezier distance at factor f (0-1) to the circle if we use the same factor to get the appropriate fraction of 90 degrees.
# -------------------
# settings
pw, ph = 595, 842 #A4
margin = 70
tang = 4*(sqrt(2)-1)/3
r = pw - 2 * margin
density = 90 # amount of points
dia_ = 1 # diameter to draw the points
r_plot = 100000 # this is a magnifying factor to make the tiny difference visible.
r_plot_y = margin - r_plot #radius plotting
c_plot_y = ph - 3 * margin - r # circle plotting
# -------------------
# functions
def get_bezier_p(p1, t1, t2, p2, t):
''' get the point on a bezier curve at t (should be between 0 and 1) '''
x = (1 - t)**3 * p1[0] + 3*(1 - t)**2 * t * t1[0] + 3*(1 - t)* t**2 * t2[0] + t**3 * p2[0]
y = (1 - t)**3 * p1[1] + 3*(1 - t)**2 * t * t1[1] + 3*(1 - t)* t**2 * t2[1] + t**3 * p2[1]
return x, y
def get_dist(p1, p2):
''' returns the distance between two points (x, y)'''
return sqrt((p2[0]-p1[0])**2 + (p2[1]-p1[1])**2)
# -------------------
# drawings
newPage(pw, ph)
fill(1)
rect(0, 0, pw, ph)
translate(margin, margin)
# draw the tangent points
rect(r - dia_, c_plot_y + tang*r - dia_, dia_*2, dia_*2)
rect(tang*r -dia_, c_plot_y + r - dia_, dia_*2, dia_*2)
for i in range(density+1):
# -------
# bezier
# calculate the factor f
f = i/density
fill(0)
if i == 0 or i == (density): dia = dia_ * 4
else: dia = dia_
# get the coordinates for the point at factor f
x_b, y_b = get_bezier_p( (1, 0), (1, tang), (tang, 1), (0, 1), f )
# get the distance of the point from the center at (0, 0)
r_b = get_dist((0, 0), (x_b, y_b))
# get the angle of the point
angle = atan2(y_b, x_b)
# draw the point and a rect for the bar chart
oval(x_b * r - dia/2, c_plot_y + y_b * r - dia/2, dia, dia)
rect((angle/(pi/2)) * r - dia_/2, r_plot_y, dia_, r_plot)
# -------
# circle
fill(1, 0, 0)
# get the point on a circle
x_c, y_c = cos( pi/2*f ), sin( pi/2*f )
# draw the point and a rect for the bar chart
oval(x_c * r - dia/2, c_plot_y + y_c * r - dia/2, dia, dia)
rect(f * r - dia_/2, r_plot_y, dia_, r_plot)
fill(.3)
if i % 10 == 0:
x_p, y_p = r * f, margin #* 1.5
rect(x_p - .25, y_p, .5, 30)
text('%d°' % (f * 90), (x_p-2, y_p * 1.5))
oh, yes!
that is of course a very nice solution.
will update some of the scripts in the next days.
thanks!
@frederik thank you!
There is one 'issue' that makes it difficult to abstract the code. And I was wondering if this could be implemented differently in drawbot.
when setting the fontVariations the axis tag cannot be a variable:
fontVariations( axisTag = someValue )
It would be quite convenient to do:
myAxis = 'wght' # or whatever
# ... do some stuff with myAxis
# ... and then just place the variable name
fontVariations( myAxis = someValue )
or to just iterate through axes without ever having to type the axistag.
After a request to add more axes to the previous simple variable fonts tutorial the follow up escalated a bit. Since there are several files I thought it might be better to put them on github.
@hanafolkwang hi, this is half a year too late and probably not even what you were looking for but I made a few more examples to generate .gifs of variable fonts.
Since there are several files I put them on github.
thanks @gferreira and @frederik!
great help. i managed to botch my rough attempt at type on a curve.
# --------------------------------
# imports
from fontTools.pens.basePen import BasePen
# --------------------------------
# settings
pw = ph = 400
r = 89
angle = pi * 3.1
baseline = ph/2 + r
txt = 'HELLO'
tang_f = .333 # tangent factor
center = pw/2, baseline
focus = pw/2, baseline - r
# --------------------------------
# functions
def ip(a, b, f): return a + (b-a) * f
def conv_point(p):
x, y = p
f = (x - pw/2) / (pw/2)
a = -angle/2 * f
x_conv = center[0] + cos(a + pi/2) * (r + y - baseline)
y_conv = center[1] + sin(a + pi/2) * (r + y - baseline) - r
return x_conv, y_conv
class convert_path_pen(BasePen):
def __init__(self, targetPen):
self.targetPen = targetPen
def _moveTo(self, pt):
self.targetPen.moveTo( conv_point(pt) )
self.prev_pt = pt
def _lineTo(self, pt):
if self.prev_pt[0] == pt[0]:
self.targetPen.lineTo( conv_point(pt) )
else:
tang_1 = ip(self.prev_pt[0], pt[0], tang_f), ip(self.prev_pt[1], pt[1], tang_f)
tang_2 = ip(self.prev_pt[0], pt[0], 1-tang_f), ip(self.prev_pt[1], pt[1], 1-tang_f)
self.targetPen.curveTo(conv_point(tang_1), conv_point(tang_2), conv_point(pt))
self.prev_pt = pt
def _curveToOne(self, tang_1, tang_2, pt):
self.targetPen.curveTo(conv_point(tang_1), conv_point(tang_2), conv_point(pt))
self.prev_pt = pt
def _closePath(self):
self.targetPen.closePath()
# --------------------------------
# drawings
path = BezierPath()
path.text(txt, font="Helvetica", fontSize = 80, align = 'center', offset=(0, baseline))
newPage(pw, ph)
trans_path = BezierPath()
trans_pen = convert_path_pen( trans_path )
path.drawToPen( trans_pen )
drawPath(trans_path)
obligatory gifs
Is there some example code on how to feed a BezierPath
into a custom pen (where I manipulate the coordinates with a function and change the _lineTo
) and then draw that transformed path in my canvas?
thanks, j
ok, one more.
very similar to the previous one.
page_s = 500
cell_amount = 12
rhythm = [1, 0, 0, 1, 0, 1, 1, 0]
cell_s = page_s / cell_amount
corner_s = cell_s/4
newPage(page_s, page_s)
translate(-cell_s/2, -cell_s/2)
def lozenge(pos, s):
x, y = pos
polygon((x, y), (x + s/2, y + s/2), (x + s, y), (x + s/2, y - s/2))
for x in range(cell_amount+1):
for y in range(cell_amount+1):
pos_x, pos_y = x * cell_s, y * cell_s
fill(0.5) if (x+y) % 2 == 0 else fill(1)
rect(pos_x, pos_y, cell_s, cell_s)
fill(1)
lozenge((pos_x - corner_s, pos_y), corner_s*2)
fill(0)
shift = (x + y) % len(rhythm)
if rhythm[ shift ]:
lozenge((pos_x - corner_s, pos_y), corner_s)
lozenge((pos_x, pos_y), corner_s)
else:
lozenge((pos_x - corner_s/2, pos_y + corner_s/2), corner_s)
lozenge((pos_x - corner_s/2, pos_y - corner_s/2), corner_s)
nice, ones!
here is one more:
grid = 12
page_s = 600
frames = 48
tile_s = page_s/grid
rhythm = [1, 0, 0, 1, 0, 1, 1, 0]
def cross(pos, s):
x, y = pos
polygon((x - s/2, y),
(x - s/2 + s/8, y - s/8),
(x - s/8, y - s/8),
(x - s/8, y - s/2 + s/8),
(x, y - s/2),
(x + s/8, y - s/2 + s/8),
(x + s/8, y - s/8),
(x + s/2 - s/8, y - s/8),
(x + s/2, y),
(x + s/2 - s/8, y + s/8),
(x + s/8, y + s/8),
(x + s/8, y + s/2 - s/8),
(x, y + s/2),
(x - s/8, y + s/2 - s/8),
(x - s/8, y + s/8),
(x - s/2 + s/8, y + s/8)
)
for f in range(frames):
newPage(page_s, page_s)
frameDuration(.1)
# linear
alpha = f / frames if f < (frames) else 2 - (f / frames)
# sinus wave
alpha = .5 + .5 * sin(f/frames * 2 * pi)
for y in range(grid):
for x in range(grid):
fill(.8) if (x+y) % 2 == 0 else fill(.7)
rect(x * tile_s, y * tile_s, tile_s, tile_s)
shift = (x + y) % len(rhythm)
fill(1, alpha) if rhythm[ shift ] else fill(.5, alpha)
cross((x * tile_s, y * tile_s), tile_s/3 )
# saveImage('wavy_checkerboard.gif')
nautil.us has a nice blogpost of optical illusions. I could not resist the attempt to build one of them in drawbot.
pw = ph = 400 # define variables to set width and height of the canvas
cols = 14 # define how many columns there should be
rows = 4 # define how many rows there should be
pages = 30 # define how many frames the gif should have. more pages mean slower movement
angle_diff = 2 * pi/pages # we want the loop to make one full circle. 2 times pi is 360 degrees. Divided by the amount of pages this gives us the angle difference between pages.
gap = pw/(cols+1) # calculate the gap between the lines.
l = (ph - 2 * gap)/rows # calculate the length of the line.
for p in range(pages):
newPage(pw, ph)
translate(gap, gap)
fill(None)
strokeWidth(2)
for x in range(cols):
y_off = cos( 3 * pi * x/cols + angle_diff * p ) * gap/2 # calculate how high or low the arrowhead should be
for y in range(rows+1):
stroke(1,0 ,0) if y % 2 == 0 else stroke(0, 0, 1)
if y < rows:
line((x*gap, y*l), (x*gap, y*l+l))
stroke(0)
y_off *= -1 # switch to change direction
line( (x*gap, y*l), (x*gap - gap/2, y*l - y_off) )
line( (x*gap, y*l), (x*gap + gap/2, y*l - y_off) )
# saveImage('~/Desktop/opti_illu.gif')
this should generate something like this:
@rafalbuchner did you try to append the directory to the sys.path?
sys.path.append('/usr/local/lib/python3.6/site-packages/')
@mikedug hello mike,
if you want higher resolution you just increase the values for width and height within newPage()
.
In case you are looking for better quality and not higher resolution you might want to generate separate pngs first and then use ffmpeg
from the command line to generate a gif. ffmpeg has a lot of settings,
this link might be a starting point.
@long-season
at what part are you stuck?
did you have a look at the documentation?
http://www.drawbot.com/content/text/drawingText.html
@mauricemeilleur sorry. I made the changes and updated the post. Only after implementing the changes in my local version did I realize that i forgot to adapt the len(points) == 2
option. but great you figured it out anyway,
good luck!
and thanks to @gferreira who helped me getting the initial bits!
@mauricemeilleur I don’t know how Crouwel defined his rules. but to me the corner-rule seems like an integral part of the design.
I had a look at my code and tried to tidy the worst bits. some of the angle calculations seem very inefficient but ¯\(ツ)/¯ here it is:
EDIT 2018-05-28 changed the code to simplify some of the horrible trigonometry and there now is the sq_cap
bool to have squared linecaps.
# Draw a 'folded outline' between a set of points.
# TO DO: There should be an error message when two dots are closer to each other and the weight.
# There should be a warning if the folding is further than the next points.
# There should be a warning for collinear points.
import math
# Just a bunch of points
p01 = (894, 334)
p02 = (808, 86)
p03 = (274, 792)
p04 = (481, 920)
p05 = (583, 730)
p06 = (85, 430)
p07 = (318, 100)
p08 = (870, 600)
p09 = (720, 690)
points = [p01, p02, p03, p04, p05, p06, p07, p08, p09]
def get_dist(p1, p2): return sqrt( (p2[0] - p1[0] )**2 + (p2[1] - p1[1])**2 )
def calc_angle(p1, p2): return atan2((p2[1] - p1[1]), (p2[0] - p1[0]))
def calc_delta(p1, p2, p3):
'''
Returns the angle between three points.
'''
return calc_angle(p2, p3) - calc_angle(p2, p1)
def calc_offset(weight, p1, p2, p3):
'''
Returns the x and y "offset" to be added to the points of the polygon.
'''
halfWeight = weight/2
if p1 == p2:
alpha = calc_angle(p1, p3)
b = -sin(alpha) * halfWeight
a = cos(alpha) * halfWeight
elif p2 == p3:
alpha = calc_angle(p1, p3)
b = sin(alpha) * halfWeight
a = -cos(alpha) * halfWeight
else:
alpha = calc_angle(p2, p3)
delta = calc_delta(p1, p2, p3)
xx = halfWeight / sin((delta - pi) / 2)
delta_f = (2 * alpha - pi - delta ) / 2
a, b = xx * sin(delta_f), xx * cos(delta_f)
return a, b
def wrap_line(p1, p2, p3, p4, weight):
'''
This draws a polygon around a line. Four points (x,y) and a weight parameter are required.
The polygon will be drawn around the two middle points.
The first and last points define the angles.
'''
a, b = calc_offset(weight, p1, p2, p3)
# extend first point
x1b, y1b = p2[0] - b, p2[1] - a
x1c, y1c = p2[0] + b, p2[1] + a
# extend second point
c, d = calc_offset(weight, p2, p3, p4)
x2b, y2b = p3[0] - d, p3[1] - c
x2c, y2c = p3[0] + d, p3[1] + c
polygon((x1b, y1b), (x1c, y1c), (x2b, y2b), (x2c, y2c))
# --------------------------------------------------------------------
# The actual drawing
fill(0, .4)
weight = 180
sq_cap = True
if len(points) == 1:
rect(points[0][0] - weight/2, points[0][1] - weight/2, weight, weight)
for i, point in enumerate(points[:-1]):
# print (get_dist(point, points[i+1]))
if len(points) == 2:
wrap_line(points[i], points[i], points[i+1], points[i+1], weight)
else:
if i == 0:
aaa = calc_angle(points[i], points[i+1])
if sq_cap:
point = point[0] - weight/2 * cos(aaa), point[1] - weight/2 * sin(aaa)
wrap_line(point, point, points[i+1], points[i+2], weight)
elif i == len(points)-2:
next_p = points[i+1]
aaa = calc_angle(points[i], next_p)
if sq_cap:
next_p = next_p[0] + weight/2 * cos(aaa), next_p[1] + weight/2 * sin(aaa)
wrap_line(points[i-1], points[i], next_p, next_p, weight)
else:
# print ( calc_delta(points[i-1], points[i], points[i+1]) )
wrap_line(points[i-1], points[i], points[i+1], points[i+2], weight)
I was working on a fold-font system a few years ago. it took some time to get the trigonometry right. but once that was solved the possibilities were endless ...
so the new alphabet could be rendered with different stroke weights
scaling in the x
or y direction
and there could be multi-liners
seems like i did not compensate for the stroke weight at the end of a stroke but the system could be used for any collection of xy-points
it is all a bit messy but let me know if you need some bits.