lib/chunky_png/canvas/drawing.rb in chunky_png-1.0.0.beta2 vs lib/chunky_png/canvas/drawing.rb in chunky_png-1.0.0.rc1

- old
+ new

@@ -1,86 +1,227 @@ module ChunkyPNG class Canvas + # Module that adds some primitive drawing methods to {ChunkyPNG::Canvas}. + # + # All of these methods change the current canvas instance and do not create a new one, + # even though the method names do not end with a bang. + # + # @note Drawing operations will not fail when something is drawn outside of the bounds + # of the canvas; these pixels will simply be ignored. + # @see ChunkyPNG::Canvas module Drawing - # Sets a point on the canvas by composing a pixel with its background color. - def point(x, y, color) - set_pixel(x, y, ChunkyPNG::Color.compose(color, get_pixel(x, y))) + # Composes a pixel on the canvas by alpha blending a color with its background color. + # @overload compose_pixel(x, y, color) + # @param [Integer] x The x-coordinate of the pixel to blend. + # @param [Integer] y The y-coordinate of the pixel to blend. + # @param [Integer] color The foreground color to blend with + # @overload compose_pixel(point, color) + # @param [ChunkyPNG::Point, ...] point The point on the canvas to blend. + # @param [Integer] color The foreground color to blend with + def compose_pixel(*args) + point = args.length == 2 ? ChunkyPNG::Point(args.first) : ChunkyPNG::Point(args[0], args[1]) + return unless include?(point) + set_pixel(point.x, point.y, ChunkyPNG::Color.compose(args.last, get_pixel(point.x, point.y))) end # Draws an anti-aliased line using Xiaolin Wu's algorithm. # - def line_xiaolin_wu(x0, y0, x1, y1, color) - y0, y1, x0, x1 = y1, y0, x1, x0 if y0 > y1 + # @param [Integer] x0 The x-coordinate of the first control point. + # @param [Integer] y0 The y-coordinate of the first control point. + # @param [Integer] x1 The x-coordinate of the second control point. + # @param [Integer] y1 The y-coordinate of the second control point. + # @param [Integer] stroke_color The color to use for this line. + # @param [true, false] inclusive Whether to draw the last pixel. + # Set to false when drawing multiplelines in a path. + # @return [ChunkyPNG::Canvas] Itself, with the line drawn. + def line_xiaolin_wu(x0, y0, x1, y1, stroke_color, inclusive = true) dx = x1 - x0 sx = dx < 0 ? -1 : 1 dx *= sx dy = y1 - y0 + sy = dy < 0 ? -1 : 1 + dy *= sy if dy == 0 # vertical line - Range.new(*[x0,x1].sort).each do |x| - point(x, y0, color) + x0.step(inclusive ? x1 : x1 - sx, sx) do |x| + compose_pixel(x, y0, stroke_color) end + elsif dx == 0 # horizontal line - (y0..y1).each do |y| - point(x0, y, color) + y0.step(inclusive ? y1 : y1 - sy, sy) do |y| + compose_pixel(x0, y, stroke_color) end + elsif dx == dy # diagonal - x0.step(x1, sx) do |x| - point(x, y0, color) - y0 += 1 + x0.step(inclusive ? x1 : x1 - sx, sx) do |x| + compose_pixel(x, y0, stroke_color) + y0 += sy end elsif dy > dx # vertical displacement - point(x0, y0, color) - e_acc = 0 + compose_pixel(x0, y0, stroke_color) + e_acc = 0 e = ((dx << 16) / dy.to_f).round - (y0...y1-1).each do |i| + (dy - 1).downto(0) do |i| e_acc_temp, e_acc = e_acc, (e_acc + e) & 0xffff - x0 = x0 + sx if (e_acc <= e_acc_temp) + x0 += sx if (e_acc <= e_acc_temp) w = 0xff - (e_acc >> 8) - point(x0, y0, ChunkyPNG::Color.fade(color, w)) if include_xy?(x0, y0) - y0 = y0 + 1 - point(x0 + sx, y0, ChunkyPNG::Color.fade(color, 0xff - w)) if include_xy?(x0 + sx, y0) + compose_pixel(x0, y0, ChunkyPNG::Color.fade(stroke_color, w)) + compose_pixel(x0 + sx, y0 + sy, ChunkyPNG::Color.fade(stroke_color, 0xff - w)) if inclusive || i > 0 + y0 += sy end - point(x1, y1, color) + compose_pixel(x1, y1, stroke_color) if inclusive else # horizontal displacement - point(x0, y0, color) + compose_pixel(x0, y0, stroke_color) e_acc = 0 - e = (dy << 16) / dx + e = ((dy << 16) / dx.to_f).round (dx - 1).downto(0) do |i| e_acc_temp, e_acc = e_acc, (e_acc + e) & 0xffff - y0 += 1 if (e_acc <= e_acc_temp) + y0 += sy if (e_acc <= e_acc_temp) w = 0xff - (e_acc >> 8) - point(x0, y0, ChunkyPNG::Color.fade(color, w)) if include_xy?(x0, y0) + compose_pixel(x0, y0, ChunkyPNG::Color.fade(stroke_color, w)) + compose_pixel(x0 + sx, y0 + sy, ChunkyPNG::Color.fade(stroke_color, 0xff - w)) if inclusive || i > 0 x0 += sx - point(x0, y0 + 1, ChunkyPNG::Color.fade(color, 0xff - w)) if include_xy?(x0, y0 + 1) end - point(x1, y1, color) + compose_pixel(x1, y1, stroke_color) if inclusive end return self end alias_method :line, :line_xiaolin_wu - def rect(x0, y0, x1, y1, line_color, fill_color = ChunkyPNG::Color::TRANSPARENT) + # Draws a polygon on the canvas using the stroke_color, filled using the fill_color if any. + # + # @param [Array, String] The control point vector. Accepts everything {ChunkyPNG.Vector} accepts. + # @param [Integer] stroke_color The stroke color to use for this polygon. + # @param [Integer] fill_color The fill color to use for this polygon. + # @return [ChunkyPNG::Canvas] Itself, with the polygon drawn. + def polygon(path, stroke_color = ChunkyPNG::Color::BLACK, fill_color = ChunkyPNG::Color::TRANSPARENT) + + vector = ChunkyPNG::Vector(*path) + raise ChunkyPNG::ExpectationFailed, "A polygon requires at least 3 points" if path.length < 3 + # Fill - [x0, x1].min.upto([x0, x1].max) do |x| - [y0, y1].min.upto([y0, y1].max) do |y| - point(x, y, fill_color) + unless fill_color == ChunkyPNG::Color::TRANSPARENT + vector.y_range.each do |y| + intersections = [] + vector.edges.each do |p1, p2| + if (p1.y < y && p2.y >= y) || (p2.y < y && p1.y >= y) + intersections << (p1.x + (y - p1.y).to_f / (p2.y - p1.y) * (p2.x - p1.x)).round + end + end + + intersections.sort! + 0.step(intersections.length - 1, 2) do |i| + intersections[i].upto(intersections[i + 1]) do |x| + compose_pixel(x, y, fill_color) + end + end end end # Stroke - line(x0, y0, x0, y1, line_color) - line(x0, y1, x1, y1, line_color) - line(x1, y1, x1, y0, line_color) - line(x1, y0, x0, y0, line_color) + vector.each_edge do |(from_x, from_y), (to_x, to_y)| + line(from_x, from_y, to_x, to_y, stroke_color, false) + end + + return self + end + + # Draws a rectangle on the canvas, using two control points. + # + # @param [Integer] x0 The x-coordinate of the first control point. + # @param [Integer] y0 The y-coordinate of the first control point. + # @param [Integer] x1 The x-coordinate of the second control point. + # @param [Integer] y1 The y-coordinate of the second control point. + # @param [Integer] stroke_color The line color to use for this rectangle. + # @param [Integer] fill_color The fill color to use for this rectangle. + # @return [ChunkyPNG::Canvas] Itself, with the rectangle drawn. + def rect(x0, y0, x1, y1, stroke_color = ChunkyPNG::Color::BLACK, fill_color = ChunkyPNG::Color::TRANSPARENT) + + # Fill + unless fill_color == ChunkyPNG::Color::TRANSPARENT + [x0, x1].min.upto([x0, x1].max) do |x| + [y0, y1].min.upto([y0, y1].max) do |y| + compose_pixel(x, y, fill_color) + end + end + end + # Stroke + line(x0, y0, x0, y1, stroke_color, false) + line(x0, y1, x1, y1, stroke_color, false) + line(x1, y1, x1, y0, stroke_color, false) + line(x1, y0, x0, y0, stroke_color, false) + + return self + end + + # Draws a circle on the canvas. + # + # @param [Integer] x0 The x-coordinate of the center of the circle. + # @param [Integer] y0 The y-coordinate of the center of the circle. + # @param [Integer] radius The radius of the circle from the center point. + # @param [Integer] stroke_color The color to use for the line. + # @param [Integer] fill_color The color to use that fills the circle. + # @return [ChunkyPNG::Canvas] Itself, with the circle drawn. + def circle(x0, y0, radius, stroke_color = ChunkyPNG::Color::BLACK, fill_color = ChunkyPNG::Color::TRANSPARENT) + + f = 1 - radius + ddF_x = 1 + ddF_y = -2 * radius + x = 0 + y = radius + + compose_pixel(x0, y0 + radius, stroke_color) + compose_pixel(x0, y0 - radius, stroke_color) + compose_pixel(x0 + radius, y0, stroke_color) + compose_pixel(x0 - radius, y0, stroke_color) + + lines = [radius - 1] unless fill_color == ChunkyPNG::Color::TRANSPARENT + + while x < y + + if f >= 0 + y -= 1 + ddF_y += 2 + f += ddF_y + end + + x += 1 + ddF_x += 2 + f += ddF_x + + unless fill_color == ChunkyPNG::Color::TRANSPARENT + lines[y] = lines[y] ? [lines[y], x - 1].min : x - 1 + lines[x] = lines[x] ? [lines[x], y - 1].min : y - 1 + end + + compose_pixel(x0 + x, y0 + y, stroke_color) + compose_pixel(x0 - x, y0 + y, stroke_color) + compose_pixel(x0 + x, y0 - y, stroke_color) + compose_pixel(x0 - x, y0 - y, stroke_color) + + unless x == y + compose_pixel(x0 + y, y0 + x, stroke_color) + compose_pixel(x0 - y, y0 + x, stroke_color) + compose_pixel(x0 + y, y0 - x, stroke_color) + compose_pixel(x0 - y, y0 - x, stroke_color) + end + end + + unless fill_color == ChunkyPNG::Color::TRANSPARENT + lines.each_with_index do |length, y| + line(x0 - length, y0 - y, x0 + length, y0 - y, fill_color) if length > 0 + line(x0 - length, y0 + y, x0 + length, y0 + y, fill_color) if length > 0 && y > 0 + end + end + return self end end end end