draw_py.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. '''Pygame Drawing algorithms written in Python. (Work in Progress)
  2. Implement Pygame's Drawing Algorithms in a Python version for testing
  3. and debugging.
  4. '''
  5. from __future__ import division
  6. import sys
  7. if sys.version_info >= (3, 0, 0):
  8. from math import floor, ceil
  9. else:
  10. # Python2.7
  11. # FIXME : the import of the builtin math module is broken ...
  12. def floor(x):
  13. int_x = int(x)
  14. return int_x if (x == int_x or x > 0) else int_x - 1
  15. def ceil(x):
  16. int_x = int(x)
  17. return int_x if (int_x == x or x < 0) else int_x + 1
  18. # H E L P E R F U N C T I O N S #
  19. # fractional part of x
  20. def frac(x):
  21. '''return fractional part of x'''
  22. return x - floor(x)
  23. def inv_frac(x):
  24. '''return inverse fractional part of x'''
  25. return 1 - (x - floor(x)) # eg, 1 - frac(x)
  26. # L O W L E V E L D R A W F U N C T I O N S #
  27. # (They are too low-level to be translated into python, right?)
  28. def set_at(surf, x, y, color):
  29. surf.set_at((x, y), color)
  30. def draw_pixel(surf, x, y, color, bright, blend=True):
  31. '''draw one blended pixel with given brightness.'''
  32. try:
  33. other_col = surf.get_at((x, y)) if blend else (0, 0, 0, 0)
  34. except IndexError: # pixel outside the surface
  35. return
  36. new_color = tuple((bright * col + (1 - bright) * pix)
  37. for col, pix in zip(color, other_col))
  38. # FIXME what should happen if only one, color or surf_col, has alpha?
  39. surf.set_at((x, y), new_color)
  40. def _drawhorzline(surf, color, x_from, y, x_to):
  41. if x_from == x_to:
  42. surf.set_at((x_from, y), color)
  43. return
  44. start, end = (x_from, x_to) if x_from <= x_to else (x_to, x_from)
  45. for x in range(start, end + 1):
  46. surf.set_at((x, y), color)
  47. def _drawvertline(surf, color, x, y_from, y_to):
  48. if y_from == y_to:
  49. surf.set_at((x, y_from), color)
  50. return
  51. start, end = (y_from, y_to) if y_from <= y_to else (y_to, y_from)
  52. for y in range(start, end + 1):
  53. surf.set_at((x, y), color)
  54. # 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 #
  55. def _clip_and_draw_horzline(surf, color, x_from, y, x_to):
  56. '''draw clipped horizontal line.'''
  57. # check Y inside surf
  58. clip = surf.get_clip()
  59. if y < clip.y or y >= clip.y + clip.h:
  60. return
  61. x_from = max(x_from, clip.x)
  62. x_to = min(x_to, clip.x + clip.w - 1)
  63. # check any x inside surf
  64. if x_to < clip.x or x_from >= clip.x + clip.w:
  65. return
  66. _drawhorzline(surf, color, x_from, y, x_to)
  67. def _clip_and_draw_vertline(surf, color, x, y_from, y_to):
  68. '''draw clipped vertical line.'''
  69. # check X inside surf
  70. clip = surf.get_clip()
  71. if x < clip.x or x >= clip.x + clip.w:
  72. return
  73. y_from = max(y_from, clip.y)
  74. y_to = min(y_to, clip.y + clip.h - 1)
  75. # check any y inside surf
  76. if y_to < clip.y or y_from >= clip.y + clip.h:
  77. return
  78. _drawvertline(surf, color, x, y_from, y_to)
  79. # These constans xxx_EDGE are "outside-the-bounding-box"-flags
  80. LEFT_EDGE = 0x1
  81. RIGHT_EDGE = 0x2
  82. BOTTOM_EDGE = 0x4
  83. TOP_EDGE = 0x8
  84. def encode(x, y, left, top, right, bottom):
  85. '''returns a code that defines position with respect to a bounding box'''
  86. # we use the fact that python interprets booleans (the inqualities)
  87. # as 0/1, and then multiply them with the xxx_EDGE flags
  88. return ((x < left) * LEFT_EDGE +
  89. (x > right) * RIGHT_EDGE +
  90. (y < top) * TOP_EDGE +
  91. (y > bottom) * BOTTOM_EDGE)
  92. INSIDE = lambda a: not a
  93. ACCEPT = lambda a, b: not (a or b)
  94. REJECT = lambda a, b: a and b
  95. def clip_line(line, left, top, right, bottom, use_float=False):
  96. '''Algorithm to calculate the clipped line.
  97. We calculate the coordinates of the part of the line segment within the
  98. bounding box (defined by left, top, right, bottom). The we write
  99. the coordinates of the line segment into "line", much like the C-algorithm.
  100. With `use_float` True, clip_line is usable for float-clipping.
  101. Returns: true if the line segment cuts the bounding box (false otherwise)
  102. '''
  103. assert isinstance(line, list)
  104. x1, y1, x2, y2 = line
  105. dtype = float if use_float else int
  106. while True:
  107. # the coordinates are progressively modified with the codes,
  108. # until they are either rejected or correspond to the final result.
  109. code1 = encode(x1, y1, left, top, right, bottom)
  110. code2 = encode(x2, y2, left, top, right, bottom)
  111. if ACCEPT(code1, code2):
  112. # write coordinates into "line" !
  113. line[:] = x1, y1, x2, y2
  114. return True
  115. if REJECT(code1, code2):
  116. return False
  117. # We operate on the (x1, y1) point, and swap if it is inside the bbox:
  118. if INSIDE(code1):
  119. x1, x2 = x2, x1
  120. y1, y2 = y2, y1
  121. code1, code2 = code2, code1
  122. if (x2 != x1):
  123. m = (y2 - y1) / float(x2 - x1)
  124. else:
  125. m = 1.0
  126. # Each case, if true, means that we are outside the border:
  127. # calculate x1 and y1 to be the "first point" inside the bbox...
  128. if code1 & LEFT_EDGE:
  129. y1 += dtype((left - x1) * m)
  130. x1 = left
  131. elif code1 & RIGHT_EDGE:
  132. y1 += dtype((right - x1) * m)
  133. x1 = right
  134. elif code1 & BOTTOM_EDGE:
  135. if x2 != x1:
  136. x1 += dtype((bottom - y1) / m)
  137. y1 = bottom
  138. elif code1 & TOP_EDGE:
  139. if x2 != x1:
  140. x1 += dtype((top - y1) / m)
  141. y1 = top
  142. def _draw_line(surf, color, x1, y1, x2, y2):
  143. '''draw a non-horizontal line (without anti-aliasing).'''
  144. # Variant of https://en.wikipedia.org/wiki/Bresenham's_line_algorithm
  145. #
  146. # This strongly differs from craw.c implementation, because we use a
  147. # "slope" variable (instead of delta_x and delta_y) and a "error" variable.
  148. # And we can not do pointer-arithmetic with "BytesPerPixel", like in
  149. # the C-algorithm.
  150. if x1 == x2:
  151. # This case should not happen...
  152. raise ValueError
  153. slope = abs((y2 - y1) / (x2 - x1))
  154. error = 0.0
  155. if slope < 1:
  156. # Here, it's a rather horizontal line
  157. # 1. check in which octants we are & set init values
  158. if x2 < x1:
  159. x1, x2 = x2, x1
  160. y1, y2 = y2, y1
  161. y = y1
  162. dy_sign = 1 if (y1 < y2) else -1
  163. # 2. step along x coordinate
  164. for x in range(x1, x2 + 1):
  165. set_at(surf, x, y, color)
  166. error += slope
  167. if error >= 0.5:
  168. y += dy_sign
  169. error -= 1
  170. else:
  171. # Case of a rather vertical line
  172. # 1. check in which octants we are & set init values
  173. if y1 > y2:
  174. x1, x2 = x2, x1
  175. y1, y2 = y2, y1
  176. x = x1
  177. slope = 1 / slope
  178. dx_sign = 1 if (x1 < x2) else -1
  179. # 2. step along y coordinate
  180. for y in range(y1, y2 + 1):
  181. set_at(surf, x, y, color)
  182. error += slope
  183. if error >= 0.5:
  184. x += dx_sign
  185. error -= 1
  186. def _draw_aaline(surf, color, from_x, from_y, to_x, to_y, blend):
  187. '''draw an anti-aliased line.
  188. The algorithm yields identical results with _draw_line for horizontal,
  189. vertical or diagonal lines, and results changes smoothly when changing
  190. any of the endpoint coordinates.
  191. Note that this yields strange results for very short lines, eg
  192. a line from (0, 0) to (0, 1) will draw 2 pixels, and a line from
  193. (0, 0) to (0, 1.1) will blend 10 % on the pixel (0, 2).
  194. '''
  195. # The different requirements that we have on an antialiasing algorithm
  196. # implies to make some compromises:
  197. # 1. We want smooth evolution wrt to the 4 endpoint coordinates
  198. # (this means also that we want a smooth evolution when the angle
  199. # passes +/- 45°
  200. # 2. We want the same behavior when swapping the endpoints
  201. # 3. We want understandable results for the endpoint values
  202. # (eg we want to avoid half-integer values to draw a simple plain
  203. # horizontal or vertical line between two integer l endpoints)
  204. #
  205. # This implies to somehow make the line artificially 1 pixel longer
  206. # and to draw a full pixel when we have the endpoints are identical.
  207. dx = to_x - from_x
  208. dy = to_y - from_y
  209. if dx == 0 and dy == 0:
  210. # For smoothness reasons, we could also do some blending here,
  211. # but it seems overshoot...
  212. set_at(surf, int(from_x), int(from_y), color)
  213. return
  214. if abs(dx) >= abs(dy):
  215. if from_x > to_x:
  216. from_x, to_x = to_x, from_x
  217. from_y, to_y = to_y, from_y
  218. dx = -dx
  219. dy = -dy
  220. slope = dy / dx
  221. def draw_two_pixel(x, float_y, factor):
  222. y = floor(float_y)
  223. draw_pixel(surf, x, y, color, factor * inv_frac(float_y), blend)
  224. draw_pixel(surf, x, y + 1, color, factor * frac(float_y), blend)
  225. # A and G are respectively left and right to the "from" point, but
  226. # with integer-x-coordinate, (and only if from_x is not integer).
  227. # Hence they appear in following order on the line in general case:
  228. # A from-pt G . . . to-pt S
  229. # |------*-------|--- . . . ---|-----*------|-
  230. G_x = ceil(from_x)
  231. G_y = from_y + (G_x - from_x) * slope
  232. # 1. Draw start of the segment if we have a non-integer-part
  233. if from_x < G_x:
  234. # this corresponds to the point "A"
  235. draw_two_pixel(floor(from_x), G_y - slope, inv_frac(from_x))
  236. # 2. Draw end of the segment: we add one pixel for homogenity reasons
  237. rest = frac(to_x)
  238. S_x = ceil(to_x)
  239. if rest > 0:
  240. # Again we draw only if we have a non-integer-part
  241. S_y = from_y + slope * (dx + 1 - rest)
  242. draw_two_pixel(S_x, S_y, rest)
  243. else:
  244. S_x += 1
  245. # 3. loop for other points
  246. for x in range(G_x, S_x):
  247. y = G_y + slope * (x - G_x)
  248. draw_two_pixel(x, y, 1)
  249. else:
  250. if from_y > to_y:
  251. from_x, to_x = to_x, from_x
  252. from_y, to_y = to_y, from_y
  253. dx = -dx
  254. dy = -dy
  255. slope = dx / dy
  256. def draw_two_pixel(float_x, y, factor):
  257. x = floor(float_x)
  258. draw_pixel(surf, x, y, color, factor * inv_frac(float_x), blend)
  259. draw_pixel(surf, x + 1, y, color, factor * frac(float_x), blend)
  260. G_y = ceil(from_y)
  261. G_x = from_x + (G_y - from_y) * slope
  262. # 1. Draw start of the segment
  263. if from_y < G_y:
  264. draw_two_pixel(G_x - slope, floor(from_y), inv_frac(from_y))
  265. # 2. Draw end of the segment
  266. rest = frac(to_y)
  267. S_y = ceil(to_y)
  268. if rest > 0:
  269. S_x = from_x + slope * (dy + 1 - rest)
  270. draw_two_pixel(S_x, S_y, rest)
  271. else:
  272. S_y += 1
  273. # 3. loop for other points
  274. for y in range(G_y, S_y):
  275. x = G_x + slope * (y - G_y)
  276. draw_two_pixel(x, y, 1)
  277. # C L I P A N D D R A W L I N E F U N C T I O N S #
  278. def _clip_and_draw_line(surf, rect, color, pts):
  279. '''clip the line into the rectangle and draw if needed.
  280. Returns true if anything has been drawn, else false.'''
  281. # "pts" is a list with the four coordinates of the two endpoints
  282. # of the line to be drawn : pts = x1, y1, x2, y2.
  283. # The data format is like that to stay closer to the C-algorithm.
  284. if not clip_line(pts, rect.x, rect.y, rect.x + rect.w - 1,
  285. rect.y + rect.h - 1):
  286. # The line segment defined by "pts" is not crossing the rectangle
  287. return 0
  288. if pts[1] == pts[3]: # eg y1 == y2
  289. _drawhorzline(surf, color, pts[0], pts[1], pts[2])
  290. elif pts[0] == pts[2]: # eg x1 == x2
  291. _drawvertline(surf, color, pts[0], pts[1], pts[3])
  292. else:
  293. _draw_line(surf, color, pts[0], pts[1], pts[2], pts[3])
  294. return 1
  295. def _clip_and_draw_line_width(surf, rect, color, line, width):
  296. yinc = xinc = 0
  297. if abs(line[0] - line[2]) > abs(line[1] - line[3]):
  298. yinc = 1
  299. else:
  300. xinc = 1
  301. newpts = line[:]
  302. if _clip_and_draw_line(surf, rect, color, newpts):
  303. anydrawn = 1
  304. frame = newpts[:]
  305. else:
  306. anydrawn = 0
  307. frame = [10000, 10000, -10000, -10000]
  308. for loop in range(1, width // 2 + 1):
  309. newpts[0] = line[0] + xinc * loop
  310. newpts[1] = line[1] + yinc * loop
  311. newpts[2] = line[2] + xinc * loop
  312. newpts[3] = line[3] + yinc * loop
  313. if _clip_and_draw_line(surf, rect, color, newpts):
  314. anydrawn = 1
  315. frame[0] = min(newpts[0], frame[0])
  316. frame[1] = min(newpts[1], frame[1])
  317. frame[2] = max(newpts[2], frame[2])
  318. frame[3] = max(newpts[3], frame[3])
  319. if loop * 2 < width:
  320. newpts[0] = line[0] - xinc * loop
  321. newpts[1] = line[1] - yinc * loop
  322. newpts[2] = line[2] - xinc * loop
  323. newpts[3] = line[3] - yinc * loop
  324. if _clip_and_draw_line(surf, rect, color, newpts):
  325. anydrawn = 1
  326. frame[0] = min(newpts[0], frame[0])
  327. frame[1] = min(newpts[1], frame[1])
  328. frame[2] = max(newpts[2], frame[2])
  329. frame[3] = max(newpts[3], frame[3])
  330. return anydrawn
  331. def _clip_and_draw_aaline(surf, rect, color, line, blend):
  332. '''draw anti-aliased line between two endpoints.'''
  333. if not clip_line(line, rect.x - 1, rect.y -1, rect.x + rect.w,
  334. rect.y + rect.h, use_float=True):
  335. return # TODO Rect(rect.x, rect.y, 0, 0)
  336. _draw_aaline(surf, color, line[0], line[1], line[2], line[3], blend)
  337. return # TODO Rect(-- affected area --)
  338. # D R A W L I N E F U N C T I O N S #
  339. def draw_aaline(surf, color, from_point, to_point, blend=True):
  340. '''draw anti-aliased line between two endpoints.'''
  341. line = [from_point[0], from_point[1], to_point[0], to_point[1]]
  342. return _clip_and_draw_aaline(surf, surf.get_clip(), color, line, blend)
  343. def draw_line(surf, color, from_point, to_point, width=1):
  344. '''draw anti-aliased line between two endpoints.'''
  345. line = [from_point[0], from_point[1], to_point[0], to_point[1]]
  346. return _clip_and_draw_line_width(surf, surf.get_clip(), color, line, width)
  347. # M U L T I L I N E F U N C T I O N S #
  348. def _multi_lines(surf, color, closed, points, width=1, blend=False, aaline=False):
  349. '''draw several lines, either anti-aliased or not.'''
  350. # The code for anti-aliased or not is almost identical, so it's factorized
  351. length = len(points)
  352. if length <= 2:
  353. raise TypeError
  354. line = [0] * 4 # store x1, y1 & x2, y2 of the lines to be drawn
  355. xlist = [pt[0] for pt in points]
  356. ylist = [pt[1] for pt in points]
  357. left = right = line[0] = xlist[0]
  358. top = bottom = line[1] = ylist[0]
  359. for x, y in points[1:]:
  360. left = min(left, x)
  361. right = max(right, x)
  362. top = min(top, y)
  363. bottom = max(right, x)
  364. rect = surf.get_clip()
  365. for loop in range(1, length):
  366. line[0] = xlist[loop - 1]
  367. line[1] = ylist[loop - 1]
  368. line[2] = xlist[loop]
  369. line[3] = ylist[loop]
  370. if aaline:
  371. _clip_and_draw_aaline(surf, rect, color, line, blend)
  372. else:
  373. _clip_and_draw_line_width(surf, rect, color, line, width)
  374. if closed:
  375. line[0] = xlist[length - 1]
  376. line[1] = ylist[length - 1]
  377. line[2] = xlist[0]
  378. line[3] = ylist[0]
  379. if aaline:
  380. _clip_and_draw_aaline(surf, rect, color, line, blend)
  381. else:
  382. _clip_and_draw_line_width(surf, rect, color, line, width)
  383. return # TODO Rect(...)
  384. def draw_lines(surf, color, closed, points, width=1):
  385. '''draw several lines connected through the points.'''
  386. return _multi_lines(surf, color, closed, points, width, aaline=False)
  387. def draw_aalines(surf, color, closed, points, blend=True):
  388. '''draw several anti-aliased lines connected through the points.'''
  389. return _multi_lines(surf, color, closed, points, blend=blend, aaline=True)
  390. def draw_polygon(surface, color, points, width):
  391. if width:
  392. draw_lines(surface, color, 1, points, width)
  393. return # TODO Rect(...)
  394. num_points = len(points)
  395. point_x = [x for x, y in points]
  396. point_y = [y for x, y in points]
  397. miny = min(point_y)
  398. maxy = max(point_y)
  399. if miny == maxy:
  400. minx = min(point_x)
  401. maxx = max(point_x)
  402. _clip_and_draw_horzline(surface, color, minx, miny, maxx)
  403. return # TODO Rect(...)
  404. for y in range(miny, maxy + 1):
  405. x_intersect = []
  406. for i in range(num_points):
  407. i_prev = i - 1 if i else num_points - 1
  408. y1 = point_y[i_prev]
  409. y2 = point_y[i]
  410. if y1 < y2:
  411. x1 = point_x[i_prev]
  412. x2 = point_x[i]
  413. elif y1 > y2:
  414. y2 = point_y[i_prev]
  415. y1 = point_y[i]
  416. x2 = point_x[i_prev]
  417. x1 = point_x[i]
  418. else: # special case handled below
  419. continue
  420. if ( ((y >= y1) and (y < y2)) or ((y == maxy) and (y <= y2))) :
  421. x_sect = (y - y1) * (x2 - x1) // (y2 - y1) + x1
  422. x_intersect.append(x_sect)
  423. x_intersect.sort()
  424. for i in range(0, len(x_intersect), 2):
  425. _clip_and_draw_horzline(surface, color, x_intersect[i], y,
  426. x_intersect[i + 1])
  427. # special case : horizontal border lines
  428. for i in range(num_points):
  429. i_prev = i - 1 if i else num_points - 1
  430. y = point_y[i]
  431. if miny < y == point_y[i_prev] < maxy:
  432. _clip_and_draw_horzline(surface, color, point_x[i], y, point_x[i_prev])
  433. return # TODO Rect(...)