Class: Gifenc::Image

Inherits:
Object
  • Object
show all
Defined in:
lib/image.rb

Overview

Represents a single image. A GIF may contain multiple images, and they need not be animation frames (they could simply be tiles of a static image). Crucially, images can be smaller than the GIF logical screen (canvas), thus being placed at an offset of it, saving space and time, and allowing for more complex compositions. How each image interacts with the previous ones depends on properties like the disposal method (#disposal) and the transparency (#trans_color).

Most methods modifying the image return the image itself, so that they can be chained properly.

Defined Under Namespace

Classes: Brush

Constant Summary collapse

IMAGE_SEPARATOR =

1-byte field indicating the beginning of an image block.

','.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(width = nil, height = nil, x = nil, y = nil, bbox: nil, color: DEFAULT_COLOR, gce: nil, delay: nil, trans_color: nil, disposal: nil, interlace: DEFAULT_INTERLACE, lct: nil) ⇒ Image

Create a new image or frame. The minimum information required is the width and height, which may be supplied directly, or by providing the bounding box, which also contains the offset of the image in the logical screen.

Parameters:

  • width (Integer) (defaults to: nil)

    Width of the image in pixels.

  • height (Integer) (defaults to: nil)

    Height of the image in pixels.

  • x (Integer) (defaults to: nil)

    Horizontal offset of the image in the logical screen.

  • y (Integer) (defaults to: nil)

    Vertical offset of the image in the logical screen.

  • bbox (Array<Integer>) (defaults to: nil)

    The image's bounding box, which is a tuple in the form [X, Y, W, H], where [X, Y] are the coordinates of its upper left corner, and [W, H] are its width and height, respectively. This can be provided instead of the first 4 parameters.

  • color (Integer) (defaults to: DEFAULT_COLOR)

    The initial color of the canvas.

  • gce (Extension::GraphicControl) (defaults to: nil)

    An optional Graphic Control Extension for the image. This extension controls mainly 3 things: the image's delay onscreen, the color to use for transparency, and the disposal method to employ before displaying the next image. These things can instead be supplied individually in their corresponding parameters: delay, trans_color and disposal. Each individually passed parameter will override the corresponding value in the GCE, if supplied. If neither a GCE nor any of the 3 individual parameters is used, then a GCE will not be built, unless the attributes are written to later.

  • delay (Integer) (defaults to: nil)

    Time, in 1/100ths of a second, to wait before displaying the next image (see #delay for details).

  • trans_color (Integer) (defaults to: nil)

    Index of the color to use for transparency (see #trans_color for details)

  • disposal (Integer) (defaults to: nil)

    The disposal method to use after displaying this image and before displaying the next one (see #disposal for details).

  • interlace (Boolean) (defaults to: DEFAULT_INTERLACE)

    Whether the pixel data of this image is interlaced or not.

  • lct (ColorTable) (defaults to: nil)

    Add a Local Color Table to this image, overriding the global one.

Raises:



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/image.rb', line 92

def initialize(
    width      = nil,
    height     = nil,
    x          = nil,
    y          = nil,
    bbox:        nil,
    color:       DEFAULT_COLOR,
    gce:         nil,
    delay:       nil,
    trans_color: nil,
    disposal:    nil,
    interlace:   DEFAULT_INTERLACE,
    lct:         nil
  )
  # Image attributes
  if bbox
    @x      = bbox[0]
    @y      = bbox[1]
    @width  = bbox[2]
    @height = bbox[3]
  end
  @width      = width  if width
  @height     = height if height
  @x          = x      if x
  @y          = y      if y
  @lct        = lct
  @interlace  = interlace
  @compressed = false

  # Checks
  raise Exception::CanvasError, "The width of the image must be supplied" if !@width
  raise Exception::CanvasError, "The height of the image must be supplied" if !@height
  @x = 0 if !@x
  @y = 0 if !@y

  # Image data
  @color  = color
  @pixels = [@color] * (@width * @height)

  # Extended features
  if gce || delay || trans_color || disposal
    @gce = gce ? gce.dup : Extension::GraphicControl.new
    @gce.delay       = delay       if delay
    @gce.trans_color = trans_color if trans_color
    @gce.disposal    = disposal    if disposal
  end
end

Instance Attribute Details

#colorInteger

Default color of the canvas. This is the initial color of the image, as well as the color that appears in the new regions when the canvas is is enlarged.

Returns:

  • (Integer)

    Index of the canvas color in the color table.



45
46
47
# File 'lib/image.rb', line 45

def color
  @color
end

#heightInteger (readonly)

Height of the image in pixels. Use the #resize method to change it.

Returns:

  • (Integer)

    Image height.

See Also:



25
26
27
# File 'lib/image.rb', line 25

def height
  @height
end

#lctColorTable

The local color table to use for this image. If left unspecified (nil), the global color table will be used.

Returns:



50
51
52
# File 'lib/image.rb', line 50

def lct
  @lct
end

#pixelsArray<Integer> (readonly)

Contains the table based image data (the color indexes for each pixel). Use the #replace method to bulk change the pixel data.

Returns:

  • (Array<Integer>)

    Pixel data.

See Also:



56
57
58
# File 'lib/image.rb', line 56

def pixels
  @pixels
end

#widthInteger (readonly)

Width of the image in pixels. Use the #resize method to change it.

Returns:

  • (Integer)

    Image width.

See Also:



20
21
22
# File 'lib/image.rb', line 20

def width
  @width
end

#xInteger

The image's horizontal offset in the GIF's logical screen. Note that the image will be cropped if it overflows the logical screen's boundary.

Returns:

  • (Integer)

    Image X offset.

See Also:



32
33
34
# File 'lib/image.rb', line 32

def x
  @x
end

#yInteger

The image's vertical offset in the GIF's logical screen. Note that the image will be cropped if it overflows the logical screen's boundary.

Returns:

  • (Integer)

    Image Y offset.

See Also:



39
40
41
# File 'lib/image.rb', line 39

def y
  @y
end

Instance Method Details

#[](x, y) ⇒ Integer

Get the value (color index) of a pixel fast (i.e. without bound checks). See also #get.

Parameters:

  • x (Integer)

    The X coordinate of the pixel.

  • y (Integer)

    The Y coordinate of the pixel.

Returns:

  • (Integer)

    The color index of the pixel.



437
438
439
# File 'lib/image.rb', line 437

def [](x, y)
  @pixels[y * @width + x]
end

#[]=(x, y, color) ⇒ Integer

Set the value (color index) of a pixel fast (i.e. without bound checks). See also #set.

Parameters:

  • x (Integer)

    The X coordinate of the pixel.

  • y (Integer)

    The Y coordinate of the pixel.

  • color (Integer)

    The new color index of the pixel.

Returns:

  • (Integer)

    The new color index of the pixel.



447
448
449
# File 'lib/image.rb', line 447

def []=(x, y, color)
  @pixels[y * @width + x] = color & 0xFF
end

#bboxArray

Returns the bounding box of the image. This is a tuple of the form [X, Y, W, H], where [X, Y] are the coordinates of its upper left corner - i.e., it's offset in the logical screen - and [W, H] are its width and height, respectively, in pixels.

Returns:

  • (Array)

    The image's bounding box in the format described above.



428
429
430
# File 'lib/image.rb', line 428

def bbox
  [@x, @y, @width, @height]
end

#bound_check(point, silent = true) ⇒ Object

Ensure the given point is within the image's bounds.

Parameters:

  • point (Point)

    The point to check. Can be provided as a tuple of coordinates [X, Y], or as a Geometry::Point object.

  • silent (Boolean) (defaults to: true)

    Whether to raise an exception or simply return false if the bound check fails.



1220
1221
1222
# File 'lib/image.rb', line 1220

def bound_check(point, silent = true)
  Geometry.bound_check([point], self, silent)
end

#circle(c, r, stroke = nil, fill = nil, weight: 1, style: :smooth) ⇒ Image

Draw a circle with the given properties.

Parameters:

  • c (Array<Integer>)

    The X and Y coordinates of the circle's center.

  • r (Float)

    The radius of the circle, in pixels. It can be non-integer, which will affect the intermediate calculations and result in a different final shape, which is in-between the ones corresponding to the integer values below and above for the radius.

  • stroke (Integer) (defaults to: nil)

    Index of the color of the border. The border is drawn inside the circle, i.e., the supplied radius is not enlarged for the border. Leave nil for no border.

  • fill (Integer) (defaults to: nil)

    Index of the color for the filling of the circle. Leave nil for no filling.

  • weight (Integer) (defaults to: 1)

    Thickness of the border, in pixels.

  • style (Symbol) (defaults to: :smooth)

    Style of the border. If :smooth, the border will approximate a circular shape as much as possible. If :grid, each additional unit of weight is added by simply drawing an additional layer of points inside the circle with the border's color.

Returns:

Raises:



807
808
809
# File 'lib/image.rb', line 807

def circle(c, r, stroke = nil, fill = nil, weight: 1, style: :smooth)
  ellipse(c, [r, r], stroke, fill, weight: weight, style: style)
end

#clearImage

Paint the whole canvas with the base image color.

Returns:



286
287
288
289
# File 'lib/image.rb', line 286

def clear
  @pixels = [@color] * (@width * @height)
  self
end

#col(col) ⇒ Array<Integer>

Fetch one column of pixels from the image.

Parameters:

  • col (Integer)

    The index of the column to fetch.

Returns:

  • (Array<Integer>)

    The column of pixels.

Raises:



251
252
253
254
255
256
# File 'lib/image.rb', line 251

def col(col)
  if col < 0 || col >= @width
    raise Exception::CanvasError, "Column out of bounds."
  end
  @height.times.map{ |r| @pixels[col, r] }
end

#compressObject



417
418
419
420
421
# File 'lib/image.rb', line 417

def compress
  raise Exception::CanvasError, "Image is already compressed." if @compressed
  @pixels = Util.lzw_encode(@pixels.pack('C*'))
  @compressed = true
end

#copy(src: nil, offset: [0, 0], dim: nil, dest: [0, 0], trans: false, bbox: nil) ⇒ Image

Note:

The two images are assumed to have the same color table, since what is copied is the color indexes.

Copy a rectangular region from another image to this one. The dimension of the region, as well as the source offset and the destination offset, can be provided. If the region would go out of bounds, the function will just gracefully crop it rather than failing. Additionally, a more restrictive bounding box (smaller than the full image) can also be provided, to where the copying will be confined.

Parameters:

  • src (Image) (defaults to: nil)

    The source image to copy the contents from.

  • offset (Array<Integer>) (defaults to: [0, 0])

    The coordinates of the offset of the region in the source image.

  • dim (Array<Integer>) (defaults to: nil)

    The dimensions of the region, in the form [W, H], where W is the width and H is the height of the rectangle to copy. If unspecified (nil), the whole source image will be copied.

  • dest (Array<Integer>) (defaults to: [0, 0])

    The coordinates of the destination offset of the region in this image.

  • trans (Boolean) (defaults to: false)

    If enabled, the pixels from the source image whose color is the transparent one (for the source image) won't be copied over, effectively achieving the usual GIF composition with basic transparency. It's a bit slower as a consequence.

  • bbox (Array<Integer>) (defaults to: nil)

    The bounding box (with respect to this image) where the copying should be restricted to. Everything outside this region will be left untouched. If unspecified (nil), this will be the whole image. As usual, the format is [X, Y, W, H], where [X, Y] are the coordinates of the upper left pixel, and [W, H] are the pixel width and height, respectively.

Returns:

Raises:



319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# File 'lib/image.rb', line 319

def copy(src: nil, offset: [0, 0], dim: nil, dest: [0, 0], trans: false, bbox: nil)
  raise Exception::CanvasError, "Cannot copy, no source provided." if !src

  # Parse parameters
  bbox   = [0, 0, @width, @height] unless bbox
  dim    = [src.width, src.height] unless dim
  offset = Geometry::Point.parse(offset).round
  dim    = Geometry::Point.parse(dim).round
  dest   = Geometry::Point.parse(dest).round

  # Normalize main bbox
  bbox = Geometry.rect_overlap(bbox, [0, 0, @width, @height])
  return if !bbox
  bbox.map!(&:round)

  # Normalize source bbox
  src_bbox  = [offset.x, offset.y, dim.x, dim.y]
  src_bbox  = Geometry.rect_overlap(src_bbox, [0, 0, src.width, src.height])
  return if !src_bbox
  offset    = Geometry::Point.parse(src_bbox[0, 2])
  dim       = Geometry::Point.parse(src_bbox[2, 2])

  # Normalize destination bbox
  dest_bbox = [dest.x, dest.y, dim.x, dim.y]
  overlap = Geometry.rect_overlap(dest_bbox, bbox)
  return if !dest_bbox
  dest      = Geometry::Point.parse(overlap[0, 2])
  dim       = Geometry::Point.parse(overlap[2, 2])

  # Transform coordinates of source to coordinates of destination
  offset += Gifenc::Geometry.transform([dest], dest_bbox)[0]

  # Handy fetch
  dx, dy = dest.x.round,   dest.y.round
  ox, oy = offset.x.round, offset.y.round
  lx, ly = dim.x.round,    dim.y.round
  bg = src.trans_color

  # Copy pixel data. We use a different, slightly faster, algorithm if we
  # don't have a bg color check to make, by directly copying full rows.
  if trans && bg
    c = nil
    ly.times.each{ |y|
      lx.times.each{ |x|
        c = src[ox + x, oy + y]
        self[dx + x, dy + y] = c unless c == bg
      }
    }
  else
    ly.times.each{ |y|
      @pixels[(dy + y) * @width + dx, lx] = src.pixels[(oy + y) * src.width + ox, lx]
    }
  end

  self
end

#curve(func, from, to, step: nil, dots: nil, line_color: 0, line_weight: 1, node_color: nil, node_weight: 0) ⇒ Image

TODO:

Add a way to automatically compute the time step with a reasonable value, without having to explicitly send the step or the dots.

Draw a 2D parameterized curve. A lambda function containing the mathematical expression for each coordinate must be passed.

Parameters:

  • func (Lambda)

    A lambda function that takes in a single floating point parameter (the time) and outputs the pair of coordinates [X, Y] corresponding to the curve at that given instant.

  • from (Float)

    The starting time to begin plotting the curve, i.e., the initial value of the time parameter for the lambda.

  • to (Float)

    The ending time to finish plotting the curve, i.e., the final value of the time parameter for the lambda.

  • step (Float) (defaults to: nil)

    The time step to use. The points of the curve resulting from this time step will be joined via straight lines. The smaller the, time step, the smoother the curve will look, resolution permitting. Alternatively, one may supply the dots argument.

  • dots (Integer) (defaults to: nil)

    The amount of points to plot. The plotting interval will be divided into this many segments of equal size, and the resulting points will be joined via straight lines. The more dots, the smoother the curve will look. Alternatively, one may supply the step argument.

  • line_color (Integer) (defaults to: 0)

    The index of the color to use for the trace.

  • line_weight (Float) (defaults to: 1)

    The size of the brush to use for the trace.

  • node_color (Integer) (defaults to: nil)

    The index of the color to use for the node circles. If nil (default), the line color will be used.

  • node_weight (Float) (defaults to: 0)

    The radius of the node circles. If 0 (default), nodes joining each segment of the curve will not be drawn.

Returns:

Raises:



866
867
868
869
870
871
872
873
874
875
876
877
# File 'lib/image.rb', line 866

def curve(func, from, to, step: nil, dots: nil, line_color: 0, line_weight: 1,
  node_color: nil, node_weight: 0)
  if !step && !dots
    raise Exception::GeometryError, "Cannot infer the curve's drawing density,|
      please specify either the step or the dots argument."
  end
  step = (to - from).abs.to_f / (dots + 1) if !step
  points = (from .. to).step(step).map{ |t| func.call(t) }
  node_color = line_color unless node_color
  polygonal(points, line_color: line_color, line_weight: line_weight,
    node_color: node_color, node_weight: node_weight)
end

#delayInteger

Get current delay, in 1/100ths of a second, to display this image before moving on to the next one. Note that very small delays are typically not supported, see Extension::GraphicControl#delay for more details.

Returns:

  • (Integer)

    Time to display the image.

See Also:



179
180
181
# File 'lib/image.rb', line 179

def delay
  @gce ? @gce.delay : nil
end

#delay=(value) ⇒ Integer

Set current delay, in 1/100ths of a second, to display this image before moving on to the next one. Note that very small delays are typically not supported, see Extension::GraphicControl#delay for more details.

Returns:

  • (Integer)

    Time to display the image.

See Also:



188
189
190
191
192
# File 'lib/image.rb', line 188

def delay=(value)
  return if !value
  @gce = Extension::GraphicControl.new if !@gce
  @gce.delay = value
end

#destroyImage

Destroy the pixel data of the image. This simply substitutes the contents of the array, hoping that the underlying data will go out of scope and be collected by the garbage collector. This is intended for freeing space and to simulate "destroying" the image.

Returns:



279
280
281
282
# File 'lib/image.rb', line 279

def destroy
  @pixels = nil
  self
end

#disposalInteger

Get the disposal method of the image, which specifies how to handle the disposal of this image before displaying the next one in the GIF. See Extension::GraphicControl#disposal for details about the different disposal methods available.

Returns:

  • (Integer)

    The current disposal method.

See Also:



200
201
202
# File 'lib/image.rb', line 200

def disposal
  @gce ? @gce.disposal : nil
end

#disposal=(value) ⇒ Integer

Set the disposal method of the image, which specifies how to handle the disposal of this image before displaying the next one in the GIF. See Extension::GraphicControl#disposal for details about the different disposal methods available.

Returns:

  • (Integer)

    The current disposal method.

See Also:



210
211
212
213
214
# File 'lib/image.rb', line 210

def disposal=(value)
  return if !value
  @gce = Extension::GraphicControl.new if !@gce
  @gce.disposal = value
end

#dupImage

Create a duplicate copy of this image.

Returns:

  • (Image)

    The new image.



163
164
165
166
167
168
169
170
171
172
# File 'lib/image.rb', line 163

def dup
  lct = @lct ? @lct.dup : nil
  gce = @gce ? @gce.dup : nil
  image = Image.new(
    @width, @height, @x, @y,
    color: @color, gce: gce, delay: @delay, trans_color: @trans_color,
    disposal: @disposal, interlace: @interlace, lct: lct
  ).replace(@pixels.dup)
  image
end

#ellipse(c, r, stroke = nil, fill = nil, weight: 1, style: :smooth) ⇒ Image

Draw an ellipse with the given properties.

Parameters:

  • c (Array<Integer>)

    The X and Y coordinates of the ellipse's center.

  • r (Array<Float>)

    The semi axes (major and minor) of the ellipse, in pixels. They can be non-integer, which will affect the intermediate calculations and result in a different, in-between, shape.

  • stroke (Integer) (defaults to: nil)

    Index of the color of the border. The border is drawn inside the ellipse, i.e., the supplied axes are not enlarged for the border. Leave nil for no border.

  • fill (Integer) (defaults to: nil)

    Index of the color for the filling of the ellipse. Leave nil for no filling.

  • weight (Integer) (defaults to: 1)

    Thickness of the border, in pixels.

  • style (Symbol) (defaults to: :smooth)

    Style of the border. If :smooth, the border will approximate an elliptical shape as much as possibe. If :grid, each additional unit of weight is added by simply drawing an additional layer of points inside the ellipse with the border's color.

Returns:

Raises:



721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
# File 'lib/image.rb', line 721

def ellipse(c, r, stroke = nil, fill = nil, weight: 1, style: :smooth)
  # Parse data
  return self if !stroke && !fill
  a = r[0]
  b = r[1]
  c = Geometry::Point.parse(c).round
  e1 = Geometry::E1
  e2 = Geometry::E2
  upper = (c - e2 * b).round
  lower = (c + e2 * b).round
  left  = (c - e1 * a).round
  right = (c + e1 * a).round
  if !Geometry.bound_check([upper, lower, left, right], self, true)
    raise Exception::CanvasError, "Ellipse out of bounds."
  end
  if stroke
    weight = [weight.to_i, 1].max
    if weight > [a, b].min
      fill = stroke
      stroke = nil
    end
  end
  f = (a.to_f / b) ** 2

  # Fill
  if fill
    b.round.downto(0).each{ |y|
      midpoint1 = ((c.y - y) * @width + c.x).round
      midpoint2 = ((c.y + y) * @width + c.x).round if y > 0
      partial_r = (y > 0 ? (a ** 2 - f * (y - 0.5) ** 2) ** 0.5 : a).round
      @pixels[midpoint1 - partial_r, 2 * partial_r + 1] = [fill] * (2 * partial_r + 1)
      @pixels[midpoint2 - partial_r, 2 * partial_r + 1] = [fill] * (2 * partial_r + 1) if y > 0
    }
  end

  # Stroke
  if stroke
    prev_r = 0
    b.round.downto(0).each{ |y|
      midpoint1 = ((c.y - y) * @width + c.x).round
      midpoint2 = ((c.y + y) * @width + c.x).round if y > 0
      partial_r = (y > 0 ? (a ** 2 - f * (y - 0.5) ** 2) ** 0.5 : a).round
      if style == :grid
        border = [weight + partial_r - prev_r, 1 + partial_r].min
        (0 ... [weight, y + 1].min).each{ |w|
          @pixels[midpoint1 - partial_r                + w * @width, border] = [stroke] * border
          @pixels[midpoint1 + partial_r - (border - 1) + w * @width, border] = [stroke] * border
          @pixels[midpoint2 - partial_r                - w * @width, border] = [stroke] * border if y > 0
          @pixels[midpoint2 + partial_r - (border - 1) - w * @width, border] = [stroke] * border if y > 0
        }
        prev_r = partial_r
      elsif style == :smooth
        a2 = [a - weight, 0].max
        b2 = [b - weight, 0].max
        f2 = (a2.to_f / b2) ** 2
        partial_r2 = (y > 0 ? (a2 ** 2 >= f2 * (y - 0.5) ** 2 ? (a2 ** 2 - f2 * (y - 0.5) ** 2) ** 0.5 : -1) : a2).round
        border = partial_r - partial_r2
        @pixels[midpoint1 - partial_r               , border] = [stroke] * border
        @pixels[midpoint1 + partial_r - (border - 1), border] = [stroke] * border
        @pixels[midpoint2 - partial_r               , border] = [stroke] * border if y > 0
        @pixels[midpoint2 + partial_r - (border - 1), border] = [stroke] * border if y > 0
      end
    }
  end

  self
end

#encode(stream) ⇒ Object

TODO:

Add support for interlaced images.

Encode the image data to GIF format and write it to a stream.

Parameters:

  • stream (IO)

    Stream to write the data to.



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/image.rb', line 143

def encode(stream)
  # Optional Graphic Control Extension before image data
  @gce.encode(stream) if @gce

  # Image descriptor
  stream << IMAGE_SEPARATOR
  stream << [@x, @y, @width, @height].pack('S<4'.freeze)
  flags = (@interlace ? 1 : 0) << 6
  flags |= @lct.local_flags if @lct
  stream << [flags].pack('C'.freeze)

  # Local Color Table
  @lct.encode(stream) if @lct

  # LZW-compressed image data
  stream << (@compressed ? @pixels : Util.lzw_encode(@pixels.pack('C*'.freeze)))
end

#fill(x, y, color) ⇒ Image

Fill a contiguous region with a new color. This implements the classic flood fill algorithm used in bucket tools.

Parameters:

  • x (Integer)

    X coordinate of the starting pixel.

  • y (Integer)

    Y coordinate of the starting pixel.

  • color (Integer)

    Index of the color to fill the region with.

Returns:

Raises:



492
493
494
495
496
497
# File 'lib/image.rb', line 492

def fill(x, y, color)
  bound_check([x, y], false)
  return self if self[x, y] == color
  fill_span(x, y, self[x, y], color)
  self
end

#get(points) ⇒ Array<Integer>

Get the values (color index) of a list of pixels safely (i.e. with bound checks). For the fast version, see #[].

Parameters:

  • points (Array<Array<Integer>>)

    The list of points whose color should be retrieved. Must be an array of pairs of coordinates.

Returns:

  • (Array<Integer>)

    The list of colors, in the same order.

Raises:



457
458
459
460
461
462
463
# File 'lib/image.rb', line 457

def get(points)
  bound_check([points.min_by(&:first)[0], points.min_by(&:last)[1]], false)
  bound_check([points.max_by(&:first)[0], points.max_by(&:last)[1]], false)
  points.map{ |p|
    @pixels[p[1] * @width + p[0]]
  }
end

#graph(func, x_from, x_to, y_from, y_to, pos: [0, 0], center: nil, x_scale: 1, y_scale: 1, color: 0, weight: 1, grid: false, grid_color: 0, grid_weight: 1, grid_sep_x: nil, grid_sep_y: nil, grid_steps_x: nil, grid_steps_y: nil, grid_style: :dotted, grid_density: :normal, axes: true, axes_color: 0, axes_weight: 1, axes_style: :solid, axes_density: :normal, origin: false, origin_color: 0, origin_weight: 2, background: false, background_color: 0, background_padding: 1, frame: false, frame_color: 0, frame_weight: 1, frame_style: :solid, frame_density: :normal) ⇒ Image

TODO:

Desirable features:

  • Calculate the Y range automatically based on the function and the X range, unless an explicit Y range is supplied.
  • Change plot's orientation (horizontal, or even arbitrary angle).
  • Add other elements, such as text labels, or axes tips (e.g. arrows).
  • Specify drawing precision (in dots or steps), or even calculate it based on the function and the supplied range.
  • Allow to have arbitrary line styles for the graph line too.

Plot the graph of a function. In the following, we will distinguish between "function" coordinates (i.e., the values the function actually takes), and "pixel" coordinates (i.e., the actual coordinates of the pixels that will be drawn).

Parameters:

  • func (Lambda)

    The function to plot. Must take a single Float parameter, which represents the function's variable, and return a single Float value, the function's value at that point. Both in function coordinates.

  • x_from (Float)

    The start of the X range to plot, in function coordinates.

  • x_to (Float)

    The end of the X range to plot, in function coordinates.

  • y_from (Float)

    The start of the Y range to plot, in function coordinates.

  • y_to (Float)

    The end of the Y range to plot, in function coordinates.

  • pos (Point) (defaults to: [0, 0])

    The position of the graph. These are the pixel coordinates of the upper left corner of the graph. See also :origin.

  • center (Point) (defaults to: nil)

    The position of the origin. These are the pixel coordinates of the origin of the graph, even if the origin isn't actually in the plotting range (it'll still be used internally to calculate all the other pixel coords relative to this). Takes preference over :pos.

  • color (Integer) (defaults to: 0)

    The index of the color to use for the function's graph.

  • weight (Integer) (defaults to: 1)

    The width of the graph's line, in pixels.

  • grid (Boolean) (defaults to: false)

    Whether to draw a grid or not.

  • grid_color (Integer) (defaults to: 0)

    Index of the color to be used for the grid lines.

  • grid_weight (Integer) (defaults to: 1)

    The width of the grid lines in pixels.

  • grid_sep_x (Float) (defaults to: nil)

    The separation between the vertical grid lines, in function coordinates. Takes preference over :grid_steps_x.

  • grid_sep_y (Float) (defaults to: nil)

    The separation between the horizontal grid lines, in function coordinates. Takes preference over :grid_steps_y.

  • grid_steps_x (Float) (defaults to: nil)

    How many grid intervals to use for the X range. Only used if no explicit separation has been supplied with :grid_sep_x.

  • grid_steps_y (Float) (defaults to: nil)

    How many grid intervals to use for the Y range. Only used if no explicit separation has been supplied with :grid_sep_y.

  • grid_style (Symbol) (defaults to: :dotted)

    Style of the grid lines (:solid, :dashed, :dotted). See style option in #line.

  • grid_density (Symbol) (defaults to: :normal)

    Density of the grid pattern (:normal, :dense, :loose). See density option in #line.

  • axes (Boolean) (defaults to: true)

    Whether to draw to axes or not.

  • axes_color (Integer) (defaults to: 0)

    Index of the color to use for the axes.

  • axes_weight (Integer) (defaults to: 1)

    Width of the axes line in pixels.

  • axes_style (Symbol) (defaults to: :solid)

    Style of the axes lines (:solid, :dashed, :dotted). See style option in #line.

  • axes_density (Symbol) (defaults to: :normal)

    Density of the axes pattern (:normal, :dense, :loose). See density option in #line.

  • origin (Boolean) (defaults to: false)

    Whether to draw the origin or not.

  • origin_color (Integer) (defaults to: 0)

    The index of the color to use for the origin dot.

  • origin_weight (Float) (defaults to: 2)

    The radius of the circle to use for the origin, in pixels.

  • background (Boolean) (defaults to: false)

    Whether to draw a solid rectangle as a background for the whole plot.

  • background_color (Integer) (defaults to: 0)

    Index of the color to use for the background rectangle.

  • background_padding (Integer) (defaults to: 1)

    Amount of extra padding pixels between the rectangle's boundary and the elements it surrounds.

  • frame (Boolean) (defaults to: false)

    Whether to draw a frame around the plot. If specified, it will be the border of the background's rectangle.

  • frame_color (Integer) (defaults to: 0)

    Index of the color to use for the frame.

  • frame_weight (Integer) (defaults to: 1)

    Width of the frame lines in pixels.

  • frame_style (Symbol) (defaults to: :solid)

    Style of the frame lines (:solid, :dashed, :dotted). See style option in #line.

  • frame_density (Symbol) (defaults to: :normal)

    Density of the frame pattern (:normal, :dense, :loose). See density option in #line.

Returns:

Raises:



1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
# File 'lib/image.rb', line 1011

def graph(func, x_from, x_to, y_from, y_to, pos: [0, 0], center: nil, x_scale: 1,
  y_scale: 1, color: 0, weight: 1, grid: false, grid_color: 0,
  grid_weight: 1, grid_sep_x: nil, grid_sep_y: nil, grid_steps_x: nil,
  grid_steps_y: nil, grid_style: :dotted, grid_density: :normal, axes: true,
  axes_color: 0, axes_weight: 1, axes_style: :solid, axes_density: :normal,
  origin: false, origin_color: 0, origin_weight: 2, background: false,
  background_color: 0, background_padding: 1, frame: false, frame_color: 0,
  frame_weight: 1, frame_style: :solid, frame_density: :normal
)
  # Normalize parameters
  center, origin = origin, center
  if !origin
    pos = Geometry::Point.parse(pos || [0, 0])
    origin = [pos.x - x_scale * x_from, pos.y + y_scale * y_to]
  end
  origin = Geometry::Point.parse(origin)

  # Background
  if background
    rect(
      # Calculate real position (upper left corner)
      origin.x + x_scale * x_from - background_padding,
      origin.y - y_scale * y_to - background_padding,

      # Calculate real dimensions
      (x_to - x_from).abs * x_scale + 2 * background_padding + 1,
      (y_to - y_from).abs * y_scale + 2 * background_padding + 1,

      # Stroke and fill color
      nil, background_color
    )
  end

  # Grid
  if grid
    grid_sep_x = grid_steps_x ? (x_to - x_from).abs.to_f / grid_steps_x : 10.0 / x_scale if !grid_sep_x
    grid_sep_y = grid_steps_y ? (y_to - y_from).abs.to_f / grid_steps_y : 10.0 / y_scale if !grid_sep_y
    grid(
      # Calculate real position (upper left corner)
      origin.x + x_scale * x_from,
      origin.y - y_scale * y_to,

      # Calculate real dimensions
      (x_to - x_from).abs * x_scale,
      (y_to - y_from).abs * y_scale,

      # Calculate real separation between grid lines
      x_scale * grid_sep_x,
      y_scale * grid_sep_y,

      # Offset the grid lines so that they're centered at the origin
      (x_from.abs.to_f % grid_sep_x) * x_scale,
      (y_to.abs.to_f % grid_sep_y) * y_scale,

      # Grid aspect
      color:   grid_color,
      weight:  grid_weight,
      style:   grid_style,
      density: grid_density
    )
  end

  # Axes
  if axes
    # X axis
    line(
      p1: [origin.x + x_scale * x_from, origin.y],
      p2: [origin.x + x_scale * x_to, origin.y],
      color: axes_color,
      weight: axes_weight,
      style: axes_style,
      density: axes_density
    ) if 0.between?(y_from, y_to)

    # Y axis
    line(
      p1: [origin.x, origin.y - y_scale * y_from],
      p2: [origin.x, origin.y - y_scale * y_to],
      color: axes_color,
      weight: axes_weight,
      style: axes_style,
      density: axes_density
    ) if 0.between?(x_from, x_to)
  end

  # Origin
  if center
    circle(origin, origin_weight, nil, origin_color)
  end

  # Graph
  curve(
    -> (t) {
      x = origin.x + x_scale * t
      y = origin.y - y_scale * func.call(t)
      func.call(t).between?(y_from, y_to) ? [x, y] : nil
    },
    x_from, x_to, dots: 100, line_color: color, line_weight: weight
  )

  # Frame
  if frame
    rect(
      # Calculate real position (upper left corner)
      origin.x + x_scale * x_from - background_padding,
      origin.y - y_scale * y_to - background_padding,

      # Calculate real dimensions
      (x_to - x_from).abs * x_scale + 2 * background_padding + 1,
      (y_to - y_from).abs * y_scale + 2 * background_padding + 1,

      # Stroke and fill color
      frame_color, nil,

      # Aspect
      weight:  frame_weight,
      style:   frame_style,
      density: frame_density
    )
  end

  self
end

#grid(x, y, w, h, step_x, step_y, off_x = 0, off_y = 0, color: 0, weight: 1, style: :solid, density: :normal, pattern: nil, pattern_offsets: [0, 0]) ⇒ Image

Draw a rectangular grid of straight lines.

Parameters:

  • x (Integer)

    The X offset in the image to begin the grid.

  • y (Integer)

    The Y offset in the image to begin the grid.

  • w (Integer)

    The width of the grid in pixels.

  • h (Integer)

    The height of the grid in pixels.

  • step_x (Integer)

    The separation of the vertical lines in pixels.

  • step_y (Integer)

    The separation of the horizontal lines in pixels.

  • off_x (Integer) (defaults to: 0)

    The initial shift of the vertical lines with respect to the grid start, x.

  • off_y (Integer) (defaults to: 0)

    The initial shift of the horizontal lines with respect to the grid start, y.

  • color (Integer) (defaults to: 0)

    The index of the color in the color table to use for the grid lines.

  • weight (Integer) (defaults to: 1)

    The size of the brush to use for the grid lines.

  • style (Symbol) (defaults to: :solid)

    Line style (:solid, :dashed, :dotted). See style param in #line).

  • density (Symbol) (defaults to: :normal)

    Line pattern density (:normal, :dense, :loose). See density param in #line.

  • pattern (Array<Integer>) (defaults to: nil)

    Specifies the pattern of the line style. See pattern param in #line.

  • pattern_offsets (Array<Integer>) (defaults to: [0, 0])

    Specifies the offsets of the patterns on each direction. See pattern_offset param in #line.

Returns:

Raises:



677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
# File 'lib/image.rb', line 677

def grid(x, y, w, h, step_x, step_y, off_x = 0, off_y = 0, color: 0, weight: 1,
  style: :solid, density: :normal, pattern: nil, pattern_offsets: [0, 0])

  if !Geometry.bound_check([[x, y], [x + w - 1, y + h - 1]], self, true)
    raise Exception::CanvasError, "Grid out of bounds."
  end
  pattern = parse_line_pattern(style, density, weight) unless pattern

  # Draw vertical lines
  (x + off_x ... x + w).step(step_x).each{ |j|
    line(
      p1: [j, y], p2: [j, y + h - 1], color: color, weight: weight,
      anchor: -1, pattern: pattern, pattern_offset: pattern_offsets[0]
    )
  }

  # Draw horizontal lines
  (y + off_y... y + h).step(step_y).each{ |i|
    line(
      p1: [x, i], p2: [x + w - 1, i], color: color, weight: weight,
      anchor: 1, pattern: pattern, pattern_offset: pattern_offsets[1]
    )
  }

  self
end

#line(p1: nil, p2: nil, vector: nil, angle: nil, direction: nil, length: nil, color: 0, weight: 1, anchor: 0, bbox: nil, avoid: [], style: :solid, density: :normal, pattern: nil, pattern_offset: 0) ⇒ Image

TODO:

Add support for anchors and anti-aliasing, better brushes, etc.

Draw a straight line connecting 2 points. It requires the startpoint p1 and either of the following:

  • The endpoint (p2).
  • The displacement vector (vector).
  • The direction vector (direction) and the length (length).
  • The angle (angle) and the length (length).

Parameters:

  • p1 (Array<Integer>) (defaults to: nil)

    The [X, Y] coordinates of the startpoint.

  • p2 (Array<Integer>) (defaults to: nil)

    The [X, Y] coordinates of the endpoint.

  • vector (Array<Integer>) (defaults to: nil)

    The coordinates of the displacement vector.

  • direction (Array<Integer>) (defaults to: nil)

    The coordinates of the direction vector. If this method is chosen, the length must be provided as well. Note that this vector will be normalized automatically.

  • angle (Float) (defaults to: nil)

    Angle of the line in radians (0-2Pi). If this method is chosen, the length must be provided as well.

  • length (Float) (defaults to: nil)

    The length of the line. Must be provided if either the direction or the angle method is being used.

  • color (Integer) (defaults to: 0)

    Index of the color of the line.

  • weight (Integer) (defaults to: 1)

    Width of the line in pixels.

  • anchor (Float) (defaults to: 0)

    Since the weight can be multiple pixels, this argument indicates the position of the line with respect to the coordinates. It must be in the interval [-1, 1]. A value of 0 centers the line in its width, a value of -1 draws it on one side, and 1 on the other.

  • bbox (Array<Integer>) (defaults to: nil)

    Bounding box determining the region to which the drawing will be restricted, in the format [X, Y, W, H], where [X, Y] are the coordinates of the upper left corner of the box, and [W, H] are the pixel dimensions. If unspecified (nil), this defaults to the whole image.

  • avoid (Array<Integer>) (defaults to: [])

    List of colors over which the line should NOT be drawn.

  • style (Symbol) (defaults to: :solid)

    Named style / pattern of the line. Can be :solid (default), :dashed and :dotted. Fine grained control can be obtained with the pattern option.

  • density (Symbol) (defaults to: :normal)

    Density of the line style/ pattern. Can be :normal (default), :dense and :loose. Only relevant when the style option is used (and only makes a difference for dashed and dotted patterns). Fine grained control can be obtained with the pattern option.

  • pattern (Array<Integer>) (defaults to: nil)

    A pair of integers specifying what portion of the line should be ON (i.e. drawn) and OFF (i.e. spacing). With this option, any arbitrary pattern can be achieved. Common patterns, such as dotted and dashed, can be achieved more simply using the style and density options instead.

  • pattern_offset (Integer) (defaults to: 0)

    If the line style is not solid, this integer will offset /shift the pattern a fixed number of pixels forward (or backwards).

Returns:

Raises:



545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
# File 'lib/image.rb', line 545

def line(p1: nil, p2: nil, vector: nil, angle: nil, direction: nil,
  length: nil, color: 0, weight: 1, anchor: 0, bbox: nil, avoid: [],
  style: :solid, density: :normal, pattern: nil, pattern_offset: 0)
  # Determine start and end points
  raise Exception::CanvasError, "The line start must be specified." if !p1
  p1 = Geometry::Point.parse(p1)
  if p2
    p2 = Geometry::Point.parse(p2)
  else
    p2 = Geometry.endpoint(
      point: p1, vector: vector, direction: direction,
      angle: angle, length: length
    )
  end
  pattern = parse_line_pattern(style, density, weight) unless pattern

  if (p2 - p1).norm < Geometry::PRECISION
    a = Geometry::ORIGIN
  else
    a = (p2 - p1).normal_right.normalize_inf
    a -= a * (1 - anchor)
  end
  steps = (p2 - p1).norm_inf.ceil + 1
  delta = (p2 - p1) / [(steps - 1), 1].max
  point = p1
  brush = Brush.square(weight, color, [a.x, a.y])
  steps.times.each_with_index{ |s|
    if (s - pattern_offset) % pattern.sum < pattern[0]
      brush.draw(point.x.round, point.y.round, self, bbox: bbox, avoid: avoid)
    end
    point += delta
  }

  self
end

#move(x, y) ⇒ Image

TODO:

We're only checking negative out of bounds, what about positive ones?

Move the image relative to the current position.

Parameters:

  • x (Integer)

    X displacement.

  • y (Integer)

    Y displacement.

Returns:

Raises:

See Also:



410
411
412
413
414
415
# File 'lib/image.rb', line 410

def move(x, y)
  raise Exception::CanvasError, "Cannot move image, out of bounds." if @x < -x || @y < -y
  @x += x
  @y += y
  self
end

#place(x, y) ⇒ Image

TODO:

We're only checking negative out of bounds, what about positive ones?

Place the image at a different origin of coordinates.

Parameters:

  • x (Integer)

    New origin horizontal coordinate.

  • y (Integer)

    New origin vertical coordinate.

Returns:

Raises:

See Also:



396
397
398
399
400
401
# File 'lib/image.rb', line 396

def place(x, y)
  raise Exception::CanvasError, "Cannot move image, out of bounds." if @x < 0 || @y < 0
  @x = x
  @y = y
  self
end

#polygonal(points, line_color: 0, line_weight: 1, node_color: nil, node_weight: 0) ⇒ Image

Draw a polygonal chain connecting a sequence of points. This simply consists in joining them in order with straight lines.

Parameters:

  • points (Array<Point>)

    The list of points, in order, to join.

  • line_color (Integer) (defaults to: 0)

    The index of the color to use for the lines.

  • line_weight (Float) (defaults to: 1)

    The size of the line stroke, in pixels.

  • node_color (Integer) (defaults to: nil)

    The index of the color to use for the nodes. Default (nil) is the same as the line color.

  • node_weight (Float) (defaults to: 0)

    The radius of the node circles, in pixels. If 0, no nodes will be drawn.

Returns:

Raises:

  • (Exception::CanvasError)

    If the chain would go out of bounds. The segments that are within bounds will be drawn, even if they come after an out of bounds segment.



824
825
826
827
828
829
830
831
832
833
834
835
836
837
# File 'lib/image.rb', line 824

def polygonal(points, line_color: 0, line_weight: 1, node_color: nil,
    node_weight: 0
  )
  node_color = line_color unless node_color
  0.upto(points.size - 2).each{ |i|
    next if !points[i] || !points[i + 1]
    line(p1: points[i], p2: points[i + 1], color: line_color, weight: line_weight) rescue nil
  }
  points.each{ |p|
    next if !p
    circle(p, node_weight, nil, node_color)
  }
  self
end

#rect(x, y, w, h, stroke = nil, fill = nil, weight: 1, anchor: 1, style: :solid, density: :normal, bbox: nil) ⇒ Image

Draw a rectangle with border and optional fill.

Parameters:

  • x (Integer)

    X coordinate of the top-left vertex.

  • y (Integer)

    Y coordinate of the top-left vertex.

  • w (Integer)

    Width of the rectangle in pixels.

  • h (Integer)

    Height of the rectangle in pixels.

  • stroke (Integer) (defaults to: nil)

    Index of the border color.

  • fill (Integer) (defaults to: nil)

    Index of the fill color (nil for no fill).

  • weight (Integer) (defaults to: 1)

    Stroke width of the border in pixels.

  • anchor (Float) (defaults to: 1)

    Indicates the position of the border with respect to the rectangle's boundary. Must be between -1 and 1. For example:

    • For 0 the border is centered around the boundary.
    • For 1 the border is entirely contained within the boundary.
    • For -1 the border is entirely surrounding the boundary.
  • style (Symbol) (defaults to: :solid)

    Border line style (:solid, :dashed, :dotted). See style param in #line).

  • density (Symbol) (defaults to: :normal)

    Border line pattern density (:normal, :dense, :loose). See density param in #line.

  • bbox (Array<Integer>) (defaults to: nil)

    Bounding box determining the region to which the drawing will be restricted. See bbox param in #line.

Returns:

Raises:



602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
# File 'lib/image.rb', line 602

def rect(x, y, w, h, stroke = nil, fill = nil, weight: 1, anchor: 1,
  style: :solid, density: :normal, bbox: nil)
  # Normalize bbox
  bbox = [0, 0, @width, @height] unless bbox
  bbox = Geometry.rect_overlap(bbox, [0, 0, @width, @height])
  return if !bbox
  bbox.map!(&:round)

  # Intersect bbox with rectangle
  rect_bbox = Geometry.rect_overlap(bbox, [x, y, w, h])
  return if !rect_bbox
  x0 = rect_bbox[0].round
  y0 = rect_bbox[1].round
  x1 = (rect_bbox[0] + rect_bbox[2]).round - 1
  y1 = (rect_bbox[1] + rect_bbox[3]).round - 1

  # Fill rectangle, if provided
  if fill
    (x0 .. x1).each{ |x|
      (y0 .. y1).each{ |y|
        @pixels[y * @width + x] = fill
      }
    }
  end

  # Rectangle border
  if stroke
    if anchor != 0
      o = ((weight - 1) / 2.0 * anchor).round
      w -= 2 * o - (weight % 2 == 0 ? 1 : 0)
      h -= 2 * o - (weight % 2 == 0 ? 1 : 0)
      rect(x0 + o, y0 + o, w, h, stroke, weight: weight, anchor: 0)
    else
      points = [[x0, y0], [x1, y0], [x1, y1], [x0, y1]]
      4.times.each{ |i|
        line(
          p1:      points[i],
          p2:      points[(i + 1) % 4],
          color:   stroke,
          weight:  weight,
          anchor:  anchor,
          style:   style,
          density: density
        )
      }
    end
  end

  self
end

#replace(pixels) ⇒ Image

Change the pixel data (color indices) of the image. The size of the array must match the current dimensions of the canvas, otherwise a manual resize is first required.

Parameters:

  • pixels (Array<Integer>)

    The new pixel data to fill the canvas.

Returns:

Raises:

  • (Exception::CanvasError)

    If the supplied pixel data length doesn't match the canvas's current dimensions.



265
266
267
268
269
270
271
272
# File 'lib/image.rb', line 265

def replace(pixels)
  if pixels.size != @width * @height
    raise Exception::CanvasError, "Pixel data doesn't match image dimensions. Please\
      resize the image first."
  end
  @pixels = pixels
  self
end

#resize(width, height) ⇒ Image

Change the image's width and height. If the provided values are smaller, the image is cropped. If they are larger, the image is padded with the color specified by #color.

Returns:



380
381
382
383
384
385
386
387
# File 'lib/image.rb', line 380

def resize(width, height)
  @pixels = @pixels.each_slice(@width).map{ |row|
    width > @width ? row + [@color] * (width - @width) : row.take(width)
  }
  @pixels = height > @height ? @pixels + ([@color] * width) * (height - @height) : @pixels.take(height)
  @pixels.flatten!
  self
end

#row(row) ⇒ Array<Integer>

Fetch one row of pixels from the image.

Parameters:

  • row (Integer)

    The index of the row to fetch.

Returns:

  • (Array<Integer>)

    The row of pixels.

Raises:



240
241
242
243
244
245
# File 'lib/image.rb', line 240

def row(row)
  if row < 0 || row >= @height
    raise Exception::CanvasError, "Row out of bounds."
  end
  @pixels[row * @width, @width]
end

#set(points, colors) ⇒ Image

Set the values (color index) of a list of pixels safely (i.e. with bound checks). For the fast version, see #[]=.

Parameters:

  • points (Array<Array<Integer>>)

    The list of points whose color to change. Must be an array of pairs of coordinates.

  • colors (Integer, Array<Integer>)

    The color(s) to assign. If an integer is passed, then all pixels will be set to the same color. Alternatively, an array with the same length as the points list must be passed, and each point will be set to the respective color in the list.

Returns:

Raises:



475
476
477
478
479
480
481
482
483
# File 'lib/image.rb', line 475

def set(points, colors)
  bound_check([points.min_by(&:first)[0], points.min_by(&:last)[1]], false)
  bound_check([points.max_by(&:first)[0], points.max_by(&:last)[1]], false)
  single = colors.is_a?(Integer)
  points.each_with_index{ |p, i|
    @pixels[p[1] * @width + p[0]] = single ? color & 0xFF : colors[i] & 0xFF
  }
  self
end

#spiral(center, step, loops, angle: 0, color: 0, weight: 1) ⇒ Image

Draw an Archimedean spiral. This type of spiral is the simplest case, which grows at a constant rate on either direction.

Parameters:

  • center (Array<Integer>)

    The coordinates of the center of the spiral.

  • step (Float)

    Distance between spiral's loops.

  • loops (Float)

    How many loops to draw.

  • angle (Float) (defaults to: 0)

    Initial spiral angle.

  • color (Integer) (defaults to: 0)

    Index of the line's color.

  • weight (Float) (defaults to: 1)

    Size of the line.

Returns:

Raises:



932
933
934
935
936
937
938
939
# File 'lib/image.rb', line 932

def spiral(center, step, loops, angle: 0, color: 0, weight: 1)
  spiral_general(
    0, loops * 2 * Math::PI, center: center, angle: angle,
    scale_x: -> (t) {step * t / (2 * Math::PI) },
    scale_y: -> (t) {step * t / (2 * Math::PI) },
    color: color, weight: weight
  )
end

#spiral_general(from, to, center: [@width / 2, @height / 2], angle: 0, scale_x: -> (t) { t }, scale_y: -> (t) { t }, speed: 1, color: 0, weight: 1, control_points: 64) ⇒ Image

Draw a general spiral given by its scale functions in either direction. These functions specify, in terms of the time, how the spiral grows horizontally and vertically. For instance, a linear function would yield a spiral of constant growth, i.e., an Archimedean spiral.

Parameters:

  • from (Float)

    The starting time to begin plotting the curve.

  • to (Float)

    The final time to end the plot.

  • center (Array<Integer>) (defaults to: [@width / 2, @height / 2])

    The coordinates of the center of the spiral.

  • angle (Float) (defaults to: 0)

    Initial angle of the spiral.

  • scale_x (Lambda(Float)) (defaults to: -> (t) { t })

    The function that specifies the spiral's growth in the X direction in terms of time.

  • scale_y (Lambda(Float)) (defaults to: -> (t) { t })

    The function that specifies the spiral's growth in the Y direction in terms of time.

  • speed (Float) (defaults to: 1)

    Speed at which the spiral is traversed.

  • color (Integer) (defaults to: 0)

    Index of the line's color.

  • weight (Float) (defaults to: 1)

    Size of the line.

  • control_points (Integer) (defaults to: 64)

    The amount of control points to use per quadrant when drawing the spiral. The higher, the smoother the curve.

Returns:

Raises:



898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
# File 'lib/image.rb', line 898

def spiral_general(
    from, to,
    center: [@width / 2, @height / 2],
    angle: 0,
    scale_x: -> (t) { t },
    scale_y: -> (t) { t },
    speed: 1,
    color: 0,
    weight: 1,
    control_points: 64
  )
  center = Geometry::Point.parse(center)
  curve(
    -> (t) {
      [
        center.x + scale_x.call(t) * Math.cos(angle + speed * t),
        center.y + scale_y.call(t) * Math.sin(angle + speed * t)
      ]
    },
    from, to, step: 2 * Math::PI / control_points,
    line_color: color, line_weight: weight
  )
end

#trans_colorInteger

Get the index (in the color table) of the transparent color. Pixels with this color aren't rendered, and instead the background shows through them. See Extension::GraphicControl#trans_color for more details.

Returns:

  • (Integer)

    Index of the transparent color.

See Also:



221
222
223
# File 'lib/image.rb', line 221

def trans_color
  @gce ? @gce.trans_color : nil
end

#trans_color=(value) ⇒ Integer

Set the index (in the color table) of the transparent color. Pixels with this color aren't rendered, and instead the background shows through them. See Extension::GraphicControl#trans_color for more details.

Returns:

  • (Integer)

    Index of the transparent color.

See Also:



230
231
232
233
234
# File 'lib/image.rb', line 230

def trans_color=(value)
  return if !value
  @gce = Extension::GraphicControl.new if !@gce
  @gce.trans_color = value
end