123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539 |
- '''Pygame Drawing algorithms written in Python. (Work in Progress)
- Implement Pygame's Drawing Algorithms in a Python version for testing
- and debugging.
- '''
- from __future__ import division
- import sys
- if sys.version_info >= (3, 0, 0):
- from math import floor, ceil
- else:
- # Python2.7
- # FIXME : the import of the builtin math module is broken ...
- def floor(x):
- int_x = int(x)
- return int_x if (x == int_x or x > 0) else int_x - 1
- def ceil(x):
- int_x = int(x)
- return int_x if (int_x == x or x < 0) else int_x + 1
- # H E L P E R F U N C T I O N S #
- # fractional part of x
- def frac(x):
- '''return fractional part of x'''
- return x - floor(x)
- def inv_frac(x):
- '''return inverse fractional part of x'''
- return 1 - (x - floor(x)) # eg, 1 - frac(x)
- # L O W L E V E L D R A W F U N C T I O N S #
- # (They are too low-level to be translated into python, right?)
- def set_at(surf, x, y, color):
- surf.set_at((x, y), color)
- def draw_pixel(surf, x, y, color, bright, blend=True):
- '''draw one blended pixel with given brightness.'''
- try:
- other_col = surf.get_at((x, y)) if blend else (0, 0, 0, 0)
- except IndexError: # pixel outside the surface
- return
- new_color = tuple((bright * col + (1 - bright) * pix)
- for col, pix in zip(color, other_col))
- # FIXME what should happen if only one, color or surf_col, has alpha?
- surf.set_at((x, y), new_color)
- def _drawhorzline(surf, color, x_from, y, x_to):
- if x_from == x_to:
- surf.set_at((x_from, y), color)
- return
- start, end = (x_from, x_to) if x_from <= x_to else (x_to, x_from)
- for x in range(start, end + 1):
- surf.set_at((x, y), color)
- def _drawvertline(surf, color, x, y_from, y_to):
- if y_from == y_to:
- surf.set_at((x, y_from), color)
- return
- start, end = (y_from, y_to) if y_from <= y_to else (y_to, y_from)
- for y in range(start, end + 1):
- surf.set_at((x, y), color)
- # I N T E R N A L D R A W L I N E F U N C T I O N S #
- def _clip_and_draw_horzline(surf, color, x_from, y, x_to):
- '''draw clipped horizontal line.'''
- # check Y inside surf
- clip = surf.get_clip()
- if y < clip.y or y >= clip.y + clip.h:
- return
- x_from = max(x_from, clip.x)
- x_to = min(x_to, clip.x + clip.w - 1)
- # check any x inside surf
- if x_to < clip.x or x_from >= clip.x + clip.w:
- return
- _drawhorzline(surf, color, x_from, y, x_to)
- def _clip_and_draw_vertline(surf, color, x, y_from, y_to):
- '''draw clipped vertical line.'''
- # check X inside surf
- clip = surf.get_clip()
- if x < clip.x or x >= clip.x + clip.w:
- return
- y_from = max(y_from, clip.y)
- y_to = min(y_to, clip.y + clip.h - 1)
- # check any y inside surf
- if y_to < clip.y or y_from >= clip.y + clip.h:
- return
- _drawvertline(surf, color, x, y_from, y_to)
- # These constans xxx_EDGE are "outside-the-bounding-box"-flags
- LEFT_EDGE = 0x1
- RIGHT_EDGE = 0x2
- BOTTOM_EDGE = 0x4
- TOP_EDGE = 0x8
- def encode(x, y, left, top, right, bottom):
- '''returns a code that defines position with respect to a bounding box'''
- # we use the fact that python interprets booleans (the inqualities)
- # as 0/1, and then multiply them with the xxx_EDGE flags
- return ((x < left) * LEFT_EDGE +
- (x > right) * RIGHT_EDGE +
- (y < top) * TOP_EDGE +
- (y > bottom) * BOTTOM_EDGE)
- INSIDE = lambda a: not a
- ACCEPT = lambda a, b: not (a or b)
- REJECT = lambda a, b: a and b
- def clip_line(line, left, top, right, bottom, use_float=False):
- '''Algorithm to calculate the clipped line.
- We calculate the coordinates of the part of the line segment within the
- bounding box (defined by left, top, right, bottom). The we write
- the coordinates of the line segment into "line", much like the C-algorithm.
- With `use_float` True, clip_line is usable for float-clipping.
- Returns: true if the line segment cuts the bounding box (false otherwise)
- '''
- assert isinstance(line, list)
- x1, y1, x2, y2 = line
- dtype = float if use_float else int
- while True:
- # the coordinates are progressively modified with the codes,
- # until they are either rejected or correspond to the final result.
- code1 = encode(x1, y1, left, top, right, bottom)
- code2 = encode(x2, y2, left, top, right, bottom)
- if ACCEPT(code1, code2):
- # write coordinates into "line" !
- line[:] = x1, y1, x2, y2
- return True
- if REJECT(code1, code2):
- return False
- # We operate on the (x1, y1) point, and swap if it is inside the bbox:
- if INSIDE(code1):
- x1, x2 = x2, x1
- y1, y2 = y2, y1
- code1, code2 = code2, code1
- if (x2 != x1):
- m = (y2 - y1) / float(x2 - x1)
- else:
- m = 1.0
- # Each case, if true, means that we are outside the border:
- # calculate x1 and y1 to be the "first point" inside the bbox...
- if code1 & LEFT_EDGE:
- y1 += dtype((left - x1) * m)
- x1 = left
- elif code1 & RIGHT_EDGE:
- y1 += dtype((right - x1) * m)
- x1 = right
- elif code1 & BOTTOM_EDGE:
- if x2 != x1:
- x1 += dtype((bottom - y1) / m)
- y1 = bottom
- elif code1 & TOP_EDGE:
- if x2 != x1:
- x1 += dtype((top - y1) / m)
- y1 = top
- def _draw_line(surf, color, x1, y1, x2, y2):
- '''draw a non-horizontal line (without anti-aliasing).'''
- # Variant of https://en.wikipedia.org/wiki/Bresenham's_line_algorithm
- #
- # This strongly differs from craw.c implementation, because we use a
- # "slope" variable (instead of delta_x and delta_y) and a "error" variable.
- # And we can not do pointer-arithmetic with "BytesPerPixel", like in
- # the C-algorithm.
- if x1 == x2:
- # This case should not happen...
- raise ValueError
- slope = abs((y2 - y1) / (x2 - x1))
- error = 0.0
- if slope < 1:
- # Here, it's a rather horizontal line
- # 1. check in which octants we are & set init values
- if x2 < x1:
- x1, x2 = x2, x1
- y1, y2 = y2, y1
- y = y1
- dy_sign = 1 if (y1 < y2) else -1
- # 2. step along x coordinate
- for x in range(x1, x2 + 1):
- set_at(surf, x, y, color)
- error += slope
- if error >= 0.5:
- y += dy_sign
- error -= 1
- else:
- # Case of a rather vertical line
- # 1. check in which octants we are & set init values
- if y1 > y2:
- x1, x2 = x2, x1
- y1, y2 = y2, y1
- x = x1
- slope = 1 / slope
- dx_sign = 1 if (x1 < x2) else -1
- # 2. step along y coordinate
- for y in range(y1, y2 + 1):
- set_at(surf, x, y, color)
- error += slope
- if error >= 0.5:
- x += dx_sign
- error -= 1
- def _draw_aaline(surf, color, from_x, from_y, to_x, to_y, blend):
- '''draw an anti-aliased line.
- The algorithm yields identical results with _draw_line for horizontal,
- vertical or diagonal lines, and results changes smoothly when changing
- any of the endpoint coordinates.
- Note that this yields strange results for very short lines, eg
- a line from (0, 0) to (0, 1) will draw 2 pixels, and a line from
- (0, 0) to (0, 1.1) will blend 10 % on the pixel (0, 2).
- '''
- # The different requirements that we have on an antialiasing algorithm
- # implies to make some compromises:
- # 1. We want smooth evolution wrt to the 4 endpoint coordinates
- # (this means also that we want a smooth evolution when the angle
- # passes +/- 45°
- # 2. We want the same behavior when swapping the endpoints
- # 3. We want understandable results for the endpoint values
- # (eg we want to avoid half-integer values to draw a simple plain
- # horizontal or vertical line between two integer l endpoints)
- #
- # This implies to somehow make the line artificially 1 pixel longer
- # and to draw a full pixel when we have the endpoints are identical.
- dx = to_x - from_x
- dy = to_y - from_y
- if dx == 0 and dy == 0:
- # For smoothness reasons, we could also do some blending here,
- # but it seems overshoot...
- set_at(surf, int(from_x), int(from_y), color)
- return
- if abs(dx) >= abs(dy):
- if from_x > to_x:
- from_x, to_x = to_x, from_x
- from_y, to_y = to_y, from_y
- dx = -dx
- dy = -dy
- slope = dy / dx
- def draw_two_pixel(x, float_y, factor):
- y = floor(float_y)
- draw_pixel(surf, x, y, color, factor * inv_frac(float_y), blend)
- draw_pixel(surf, x, y + 1, color, factor * frac(float_y), blend)
- # A and G are respectively left and right to the "from" point, but
- # with integer-x-coordinate, (and only if from_x is not integer).
- # Hence they appear in following order on the line in general case:
- # A from-pt G . . . to-pt S
- # |------*-------|--- . . . ---|-----*------|-
- G_x = ceil(from_x)
- G_y = from_y + (G_x - from_x) * slope
- # 1. Draw start of the segment if we have a non-integer-part
- if from_x < G_x:
- # this corresponds to the point "A"
- draw_two_pixel(floor(from_x), G_y - slope, inv_frac(from_x))
- # 2. Draw end of the segment: we add one pixel for homogenity reasons
- rest = frac(to_x)
- S_x = ceil(to_x)
- if rest > 0:
- # Again we draw only if we have a non-integer-part
- S_y = from_y + slope * (dx + 1 - rest)
- draw_two_pixel(S_x, S_y, rest)
- else:
- S_x += 1
- # 3. loop for other points
- for x in range(G_x, S_x):
- y = G_y + slope * (x - G_x)
- draw_two_pixel(x, y, 1)
- else:
- if from_y > to_y:
- from_x, to_x = to_x, from_x
- from_y, to_y = to_y, from_y
- dx = -dx
- dy = -dy
- slope = dx / dy
- def draw_two_pixel(float_x, y, factor):
- x = floor(float_x)
- draw_pixel(surf, x, y, color, factor * inv_frac(float_x), blend)
- draw_pixel(surf, x + 1, y, color, factor * frac(float_x), blend)
- G_y = ceil(from_y)
- G_x = from_x + (G_y - from_y) * slope
- # 1. Draw start of the segment
- if from_y < G_y:
- draw_two_pixel(G_x - slope, floor(from_y), inv_frac(from_y))
- # 2. Draw end of the segment
- rest = frac(to_y)
- S_y = ceil(to_y)
- if rest > 0:
- S_x = from_x + slope * (dy + 1 - rest)
- draw_two_pixel(S_x, S_y, rest)
- else:
- S_y += 1
- # 3. loop for other points
- for y in range(G_y, S_y):
- x = G_x + slope * (y - G_y)
- draw_two_pixel(x, y, 1)
- # C L I P A N D D R A W L I N E F U N C T I O N S #
- def _clip_and_draw_line(surf, rect, color, pts):
- '''clip the line into the rectangle and draw if needed.
- Returns true if anything has been drawn, else false.'''
- # "pts" is a list with the four coordinates of the two endpoints
- # of the line to be drawn : pts = x1, y1, x2, y2.
- # The data format is like that to stay closer to the C-algorithm.
- if not clip_line(pts, rect.x, rect.y, rect.x + rect.w - 1,
- rect.y + rect.h - 1):
- # The line segment defined by "pts" is not crossing the rectangle
- return 0
- if pts[1] == pts[3]: # eg y1 == y2
- _drawhorzline(surf, color, pts[0], pts[1], pts[2])
- elif pts[0] == pts[2]: # eg x1 == x2
- _drawvertline(surf, color, pts[0], pts[1], pts[3])
- else:
- _draw_line(surf, color, pts[0], pts[1], pts[2], pts[3])
- return 1
- def _clip_and_draw_line_width(surf, rect, color, line, width):
- yinc = xinc = 0
- if abs(line[0] - line[2]) > abs(line[1] - line[3]):
- yinc = 1
- else:
- xinc = 1
- newpts = line[:]
- if _clip_and_draw_line(surf, rect, color, newpts):
- anydrawn = 1
- frame = newpts[:]
- else:
- anydrawn = 0
- frame = [10000, 10000, -10000, -10000]
- for loop in range(1, width // 2 + 1):
- newpts[0] = line[0] + xinc * loop
- newpts[1] = line[1] + yinc * loop
- newpts[2] = line[2] + xinc * loop
- newpts[3] = line[3] + yinc * loop
- if _clip_and_draw_line(surf, rect, color, newpts):
- anydrawn = 1
- frame[0] = min(newpts[0], frame[0])
- frame[1] = min(newpts[1], frame[1])
- frame[2] = max(newpts[2], frame[2])
- frame[3] = max(newpts[3], frame[3])
- if loop * 2 < width:
- newpts[0] = line[0] - xinc * loop
- newpts[1] = line[1] - yinc * loop
- newpts[2] = line[2] - xinc * loop
- newpts[3] = line[3] - yinc * loop
- if _clip_and_draw_line(surf, rect, color, newpts):
- anydrawn = 1
- frame[0] = min(newpts[0], frame[0])
- frame[1] = min(newpts[1], frame[1])
- frame[2] = max(newpts[2], frame[2])
- frame[3] = max(newpts[3], frame[3])
- return anydrawn
- def _clip_and_draw_aaline(surf, rect, color, line, blend):
- '''draw anti-aliased line between two endpoints.'''
- if not clip_line(line, rect.x - 1, rect.y -1, rect.x + rect.w,
- rect.y + rect.h, use_float=True):
- return # TODO Rect(rect.x, rect.y, 0, 0)
- _draw_aaline(surf, color, line[0], line[1], line[2], line[3], blend)
- return # TODO Rect(-- affected area --)
- # D R A W L I N E F U N C T I O N S #
- def draw_aaline(surf, color, from_point, to_point, blend=True):
- '''draw anti-aliased line between two endpoints.'''
- line = [from_point[0], from_point[1], to_point[0], to_point[1]]
- return _clip_and_draw_aaline(surf, surf.get_clip(), color, line, blend)
- def draw_line(surf, color, from_point, to_point, width=1):
- '''draw anti-aliased line between two endpoints.'''
- line = [from_point[0], from_point[1], to_point[0], to_point[1]]
- return _clip_and_draw_line_width(surf, surf.get_clip(), color, line, width)
- # M U L T I L I N E F U N C T I O N S #
- def _multi_lines(surf, color, closed, points, width=1, blend=False, aaline=False):
- '''draw several lines, either anti-aliased or not.'''
- # The code for anti-aliased or not is almost identical, so it's factorized
- length = len(points)
- if length <= 2:
- raise TypeError
- line = [0] * 4 # store x1, y1 & x2, y2 of the lines to be drawn
- xlist = [pt[0] for pt in points]
- ylist = [pt[1] for pt in points]
- left = right = line[0] = xlist[0]
- top = bottom = line[1] = ylist[0]
- for x, y in points[1:]:
- left = min(left, x)
- right = max(right, x)
- top = min(top, y)
- bottom = max(right, x)
- rect = surf.get_clip()
- for loop in range(1, length):
- line[0] = xlist[loop - 1]
- line[1] = ylist[loop - 1]
- line[2] = xlist[loop]
- line[3] = ylist[loop]
- if aaline:
- _clip_and_draw_aaline(surf, rect, color, line, blend)
- else:
- _clip_and_draw_line_width(surf, rect, color, line, width)
- if closed:
- line[0] = xlist[length - 1]
- line[1] = ylist[length - 1]
- line[2] = xlist[0]
- line[3] = ylist[0]
- if aaline:
- _clip_and_draw_aaline(surf, rect, color, line, blend)
- else:
- _clip_and_draw_line_width(surf, rect, color, line, width)
- return # TODO Rect(...)
- def draw_lines(surf, color, closed, points, width=1):
- '''draw several lines connected through the points.'''
- return _multi_lines(surf, color, closed, points, width, aaline=False)
- def draw_aalines(surf, color, closed, points, blend=True):
- '''draw several anti-aliased lines connected through the points.'''
- return _multi_lines(surf, color, closed, points, blend=blend, aaline=True)
- def draw_polygon(surface, color, points, width):
- if width:
- draw_lines(surface, color, 1, points, width)
- return # TODO Rect(...)
- num_points = len(points)
- point_x = [x for x, y in points]
- point_y = [y for x, y in points]
- miny = min(point_y)
- maxy = max(point_y)
- if miny == maxy:
- minx = min(point_x)
- maxx = max(point_x)
- _clip_and_draw_horzline(surface, color, minx, miny, maxx)
- return # TODO Rect(...)
- for y in range(miny, maxy + 1):
- x_intersect = []
- for i in range(num_points):
- i_prev = i - 1 if i else num_points - 1
- y1 = point_y[i_prev]
- y2 = point_y[i]
- if y1 < y2:
- x1 = point_x[i_prev]
- x2 = point_x[i]
- elif y1 > y2:
- y2 = point_y[i_prev]
- y1 = point_y[i]
- x2 = point_x[i_prev]
- x1 = point_x[i]
- else: # special case handled below
- continue
- if ( ((y >= y1) and (y < y2)) or ((y == maxy) and (y <= y2))) :
- x_sect = (y - y1) * (x2 - x1) // (y2 - y1) + x1
- x_intersect.append(x_sect)
- x_intersect.sort()
- for i in range(0, len(x_intersect), 2):
- _clip_and_draw_horzline(surface, color, x_intersect[i], y,
- x_intersect[i + 1])
- # special case : horizontal border lines
- for i in range(num_points):
- i_prev = i - 1 if i else num_points - 1
- y = point_y[i]
- if miny < y == point_y[i_prev] < maxy:
- _clip_and_draw_horzline(surface, color, point_x[i], y, point_x[i_prev])
- return # TODO Rect(...)
|