Class: Pixelart::Image

Inherits:
Object
  • Object
show all
Defined in:
lib/pixelart/silhouette.rb,
lib/pixelart/led.rb,
lib/pixelart/blur.rb,
lib/pixelart/image.rb,
lib/pixelart/spots.rb,
lib/pixelart/circle.rb,
lib/pixelart/invert.rb,
lib/pixelart/sample.rb,
lib/pixelart/sketch.rb,
lib/pixelart/convert.rb,
lib/pixelart/stripes.rb,
lib/pixelart/ukraine.rb,
lib/pixelart/transparent.rb

Overview

todo/check:

use a different name for silhouette
 - why not  - outline ???
        or  - shadow  ???
        or  - profile ???
        or  - figure  ???
        or  - shape   ???
        or  - form    ???

Direct Known Subclasses

ImageColorBar, ImageComposite, ImagePalette8bit

Constant Summary collapse

CHARS =

todo/check: rename to default chars or such? why? why not?

'.@xo^~%*+=:'
PALETTE8BIT =

predefined palette8bit color maps

   (grayscale to sepia/blue/false/etc.)
- todo/check - keep "shortcut" convenience predefined map - why? why not?
{
  sepia: Palette8bit::GRAYSCALE.zip( Palette8bit::SEPIA ).to_h,
  blue:  Palette8bit::GRAYSCALE.zip( Palette8bit::BLUE ).to_h,
  false: Palette8bit::GRAYSCALE.zip( Palette8bit::FALSE ).to_h,
}
RAINBOW_RED =

todo/check: move colors to (reusable) constants int Color !!!! why? why not?

Color.parse( '#E40303' )
RAINBOW_ORANGE =
Color.parse( '#FF8C00' )
RAINBOW_YELLOW =
Color.parse( '#FFED00' )
RAINBOW_GREEN =
Color.parse( '#008026' )
RAINBOW_BLUE =
Color.parse( '#004DFF' )
RAINBOW_VIOLET =
Color.parse( '#750787' )
UKRAINE_BLUE =

todo/check: move colors to (reusable) constants int Color !!!! why? why not?

Color.parse( '#0057b7' )
UKRAINE_YELLOW =
Color.parse( '#ffdd00' )

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(width, height, initial = Color::TRANSPARENT) ⇒ Image

Returns a new instance of Image.



99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/pixelart/image.rb', line 99

def initialize( width, height, initial=Color::TRANSPARENT )
   ### todo/fix:
   ##  change params to *args only - why? why not?
   ##     make width/height optional if image passed in?

  if initial.is_a?( ChunkyPNG::Image )
    @img = initial
  else
    ## todo/check - initial - use parse_color here too e.g. allow "#fff" too etc.
    @img = ChunkyPNG::Image.new( width, height, initial )
  end
end

Class Method Details

.blob(blob) ⇒ Object Also known as: from_blob



37
38
39
40
# File 'lib/pixelart/image.rb', line 37

def self.blob( blob )
  img_inner = ChunkyPNG::Image.from_blob( blob )
  new( img_inner.width, img_inner.height, img_inner )
end

.calc_sample_steps(width, new_width, center: true, debug: false) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/pixelart/sample.rb', line 5

def self.calc_sample_steps( width, new_width,
                            center: true,
                            debug: false )
  ## todo/fix: assert new_width is smaller than width
  if debug
    puts
    puts "==> from: #{width}px  to: #{new_width}px"
  end

  indexes = []

  base_step = width / new_width    ## pixels per pixel

  err_step = (width % new_width) * 2   ## multiply by 2
  denominator = new_width * 2   # denominator (in de - nenner  e.g. 1/nenner 4/nenner)

  overflow = err_step*new_width/denominator  ## todo/check - assert that div is always WITHOUT remainder!!!!!

  if debug
    puts
    puts "base_step (pixels per pixel):"
    puts "  #{base_step}     -  #{base_step} * #{new_width}px = #{base_step*new_width}px"
    puts "err_step  (in 1/#{width}*2):"
    puts "  #{err_step} / #{denominator}      - #{err_step*new_width} / #{denominator} = +#{err_step*new_width/denominator}px overflow"
    puts
  end

  # initial pixel offset
  index = 0
  err   = err_step/2   ##  note: start off with +err_step/2 to add overflow pixel in the "middle"


  index +=  if center.is_a?( Integer )
              center
            elsif center
              base_step/2
            else
               0   #  use 0px offset
            end


  new_width.times do |i|
    if err >= denominator ## overflow
      puts "    -- overflow #{err}/#{denominator} - add +1 pixel offset to #{i}"  if debug
      index += 1
      err   -= denominator
    end

    puts "  #{i} => #{index}  -- #{err} / #{denominator}"  if debug


    indexes[i] = index

    index += base_step
    err   += err_step
  end

  indexes
end

.calc_stripes(length, n: 2, debug: false) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/pixelart/stripes.rb', line 5

def self.calc_stripes( length, n: 2, debug: false )
  stripes = []

  base_step = length / n    ## pixels per pixel

  err_step = (length % n) * 2   ## multiply by 2
  denominator =  n * 2   # denominator (in de - nenner  e.g. 1/nenner 4/nenner)

  overflow = err_step*n/denominator  ## todo/check - assert that div is always WITHOUT remainder!!!!!

  if debug
    puts
    puts "base_step (pixels per stripe):"
    puts "  #{base_step}     -  #{base_step}px * #{n} = #{base_step*n}px"
    puts "err_step  (in 1/#{length}*2):"
    puts "  #{err_step} / #{denominator}      - #{err_step*n} / #{denominator} = +#{err_step*n/denominator}px overflow"
    puts
  end

  err    = 0
  stripe = 0

  n.times do |i|
    stripe  = base_step
    err    += err_step

    if err >= denominator ## overflow
      puts "    -- overflow #{err}/#{denominator} - add +1 pixel to stripe #{i}"  if debug

      stripe += 1
      err   -= denominator
    end


    puts "  #{i} => #{stripe}  -- #{err} / #{denominator}"  if debug

    stripes[i] = stripe
  end

  ## note: assert calculation - sum of stripes MUST be equal length
  sum = stripes.sum
  puts "  sum: #{sum}"  if debug

  if sum != length
    puts "!! ERROR - stripes sum #{sum} calculation failed; expected #{length}:"
    pp stripes
    exit 1
  end

  stripes
end

.convert(dir, from: 'jpg', to: 'png', outdir: nil, overwrite: true) ⇒ Object

helper to convert (all) image in directory

  chech: move to ImageUtils.convert  or such - why? why not?

what about the name e.g. rename to convert_dir or
                                  batch_convert such - why? why not?


11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/pixelart/convert.rb', line 11

def self.convert( dir, from: 'jpg',
                       to: 'png',
                       outdir: nil,
                       overwrite: true )

    outdir = dir   if outdir.nil?

    files = Dir.glob( "#{dir}/*.#{from}" )
    puts "==> found #{files.size} image(s) to convert from #{from} to #{to} (overwrite mode set to: #{overwrite})"

    files.each_with_index do |file,i|
      dirname   = File.dirname( file )
      extname   = File.extname( file )
      basename  = File.basename( file, extname )

      ## skip convert if target / dest file already exists
      next  if overwrite == false && File.exist?( "#{outdir}/#{basename}.#{to}" )

      ##  note: make sure outdir exists (magic will not create it??)
      FileUtils.mkdir_p( outdir )  unless Dir.exist?( outdir )

      cmd = "magick convert #{dirname}/#{basename}.#{from} #{outdir}/#{basename}.#{to}"

      puts "   [#{i+1}/#{files.size}] - #{cmd}"
      ## todo/fix:   check return value!!! magick comand not available (in path) and so on!!!
      system( cmd )

      if from == 'gif'
        ## check for multi-images for gif
        ##   save  image-0.png  to  image.png
        path0 = "#{outdir}/#{basename}-0.#{to}"
        path  = "#{outdir}/#{basename}.#{to}"

        ##  note:  image-0.png only exists (gets generated) for multi-images
        if File.exist?( path0 )
          puts "   saving #{path0} to #{path}..."

          blob = File.open( path0, 'rb' ) { |f| f.read }
          File.open( path, 'wb' ) { |f| f.write( blob ) }
        end
      end
    end
end

.inherited(subclass) ⇒ Object



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

def self.inherited( subclass )
  subclasses << subclass
end

.parse(pixels, colors:, background: Color::TRANSPARENT, chars: CHARS) ⇒ Object

todo/check: support default chars encoding auto-of-the-box always

or require user-defined chars to be passed in - why? why not?


54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/pixelart/image.rb', line 54

def self.parse( pixels, colors:,
                        background: Color::TRANSPARENT,
                        chars: CHARS )
  has_keys  = colors.is_a?(Hash)   ## check if passed-in user-defined keys (via hash table)?

  colors = parse_colors( colors )
  pixels = parse_pixels( pixels )

  width  = pixels.reduce(1) {|width,row| row.size > width ? row.size : width }
  height = pixels.size

   background = Color.parse( background )   unless background.is_a?( Integer )

  img = new( width, height )

  pixels.each_with_index do |row,y|
    row.each_with_index do |color,x|
      pixel = if has_keys     ## if passed-in user-defined keys check only the user-defined keys
                colors[color]
              else
                ## try map ascii art char (.@xo etc.) to color index (0,1,2)
                ##   if no match found - fallback on assuming draw by number (0 1 2 etc.) encoding
                pos = chars.index( color )
                if pos
                  colors[ pos.to_s ]
                else ## assume nil (not found)
                  colors[ color ]
                end
              end


      img[x,y] = if background && background != Color::TRANSPARENT &&
                                  pixel == Color::TRANSPARENT
                   background   ## note: auto-fill transparent with background color
                 else
                   pixel
                 end
    end # each row
  end # each data

  img
end

.parse_base64(str) ⇒ Object



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

def self.parse_base64( str )
  blob = Base64.decode64( str )
  img_inner = ChunkyPNG::Image.from_blob( blob )
  new( img_inner.width, img_inner.height, img_inner )
end

.parse_colors(colors) ⇒ Object



350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# File 'lib/pixelart/image.rb', line 350

def self.parse_colors( colors )
  if colors.is_a?( Array )   ## convenience shortcut
    ## note: always auto-add color 0 as pre-defined transparent - why? why not?
    h = { '0' => Color::TRANSPARENT }
    colors.each_with_index do |color, i|
      h[ (i+1).to_s ] = Color.parse( color )
    end
    h
  else  ## assume hash table with color map
    ## convert into ChunkyPNG::Color
    colors.map do |key,color|
      ## always convert key to string why? why not?  use symbol?
      [ key.to_s, Color.parse( color ) ]
    end.to_h
  end
end

.parse_pixels(pixels) ⇒ Object

helpers



323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# File 'lib/pixelart/image.rb', line 323

def self.parse_pixels( pixels )
  if pixels.is_a?( Array )  ## assume array of string (lines)
      data = []
      pixels.each do |line|
        ##  convert (string) line into indidual chars
        data << line.each_char.reduce( [] ) { |mem, c| mem << c; mem }
      end
      data
  else  ## assume it's a (multi-line) string (with newlines)
        ##  assert and throw ArgumentError if not? - why? why not?
      data = []
      pixels.each_line do |line|
        line = line.strip
        next if line.start_with?( '#' ) || line.empty?   ## note: allow comments and empty lines

        ## note: allow multiple spaces or tabs
        ##   to separate pixel codes
        ##  e.g.   o o o o o o o o o o o o dg lg w w lg w lg lg dg dg w w  lg dg o o o o o o o o o o o
        ##    or
        data << line.split( /[ \t]+/)
     end
    data
  end
end

.read(path) ⇒ Object

convenience helper



26
27
28
29
# File 'lib/pixelart/image.rb', line 26

def self.read( path )   ## convenience helper
  img_inner = ChunkyPNG::Image.from_file( path )
  new( img_inner.width, img_inner.height, img_inner )
end

.subclassesObject

keep track of all (inherited) subclasses via inherited hook

change/rename to descendants - why? why not?

note about rails (activesupport?)

If you use rails >= 3, you have two options in place.
  Use .descendants if you want more than one level depth of children classes,
  or use .subclasses for the first level of child classes.


15
16
17
# File 'lib/pixelart/image.rb', line 15

def self.subclasses
  @subclasses ||= []
end

Instance Method Details

#[](x, y) ⇒ Object



306
# File 'lib/pixelart/image.rb', line 306

def []( x, y )          @img[x,y]; end

#[]=(x, y, value) ⇒ Object



307
# File 'lib/pixelart/image.rb', line 307

def []=( x, y, value )  @img[x,y]=value; end

#_change_colors!(img, color_map) ⇒ Object



270
271
272
273
274
275
276
277
278
# File 'lib/pixelart/image.rb', line 270

def _change_colors!( img, color_map )
  img.width.times do |x|
    img.height.times do |y|
      color = img[x,y]
      new_color = color_map[color]
      img[x,y] = new_color  if new_color
    end
  end
end

#_parse_color_map(color_map) ⇒ Object



264
265
266
267
268
# File 'lib/pixelart/image.rb', line 264

def _parse_color_map( color_map )
  color_map.map do |k,v|
    [Color.parse(k),  Color.parse(v)]
  end.to_h
end

#_parse_colors(colors) ⇒ Object

private helpers



260
261
262
# File 'lib/pixelart/image.rb', line 260

def _parse_colors( colors )
  colors.map {|color| Color.parse( color ) }
end

#blur(blur = 2) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
# File 'lib/pixelart/blur.rb', line 5

def blur( blur=2 )
  @img.save( MAGICK_INPUT )

  MiniMagick::Tool::Magick.new do |magick|
    magick << MAGICK_INPUT
    magick.blur( "#{blur}x#{blur}" )
    magick << MAGICK_OUTPUT
  end

  Image.read( MAGICK_OUTPUT )
end

#change_colors(color_map) ⇒ Object Also known as: recolor

add replace_colors alias too? - why? why not?



215
216
217
218
219
220
221
222
223
# File 'lib/pixelart/image.rb', line 215

def change_colors( color_map )
  color_map = _parse_color_map( color_map )

  img = @img.dup  ## note: make a deep copy!!!
  _change_colors!( img, color_map )

  ## wrap into Pixelart::Image - lets you use zoom() and such
  Image.new( img.width, img.height, img )
end

#change_palette8bit(palette) ⇒ Object Also known as: change_palette256



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/pixelart/image.rb', line 237

def change_palette8bit( palette )
  ## step 0: mapping from grayscale to new 8bit palette (256 colors)
  color_map = if palette.is_a?( String ) || palette.is_a?( Symbol )
                 PALETTE8BIT[ palette.to_sym ]
                 ## todo/fix: check for missing/undefined palette not found - why? why not?
              else
                 ##  make sure we have colors all in Integer not names, hex, etc.
                 palette = _parse_colors( palette )
                 Palette8bit::GRAYSCALE.zip( palette ).to_h
              end

  ## step 1: convert to grayscale (256 colors)
  img = @img.grayscale
  _change_colors!( img, color_map )

  ## wrap into Pixelart::Image - lets you use zoom() and such
  Image.new( img.width, img.height, img )
end

#circleObject



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/pixelart/circle.rb', line 10

def circle
  ### for radius use min of width / height
  r = [@img.width, @img.height].min / 2

  center_x = width  / 2
  center_y = height / 2

  ################
  #  try with 96x96
  #    center_x:  96 / 2 = 48
  #    center_y:  96 / 2 = 48
  #
  #     r:    96 / 2 = 48

  img = Image.new( @img.width, @img.height )

  @img.width.times do |x|
    @img.height.times do |y|

      ## change to float calcuation (instead of ints) - why? why not?
      xx, yy, rr = x - center_x,
                   y - center_y,
                   r

      img[ x, y] = if xx*xx+yy*yy < rr*rr
                         @img[ x, y ]
                   else
                         0  ## transparent - alpha(0)
                   end
    end
  end

  img
end

#compose!(other, x = 0, y = 0) ⇒ Object Also known as: paste!



297
298
299
# File 'lib/pixelart/image.rb', line 297

def compose!( other, x=0, y=0 )
  @img.compose!( other.image, x, y )    ## note: "unwrap" inner image ref
end

#crop(x, y, crop_width, crop_height) ⇒ Object



139
140
141
142
# File 'lib/pixelart/image.rb', line 139

def crop( x, y, crop_width, crop_height )
  Image.new( nil, nil,
              image.crop( x,y, crop_width, crop_height ) )
end

#flip_horizontallyObject Also known as: flop

flip horizontally on x-axis (top-to-bottom/bottom-to-top)

  e.g. pixels on the top will now be pixels on the bottom

todo/check:   commom use is reverse?
   e.g. flip_vertically is top-to-bottom!!!
    use flip_y_axis, flip_x_axis or something else - why? why not?
  check photoshop and gimp terminology and update later if different - why? why not?


177
178
179
180
# File 'lib/pixelart/image.rb', line 177

def flip_horizontally
  img = @img.flip_horizontally
  Image.new( img.width, img.height, img )
end

#grayscaleObject Also known as: greyscale

filter / effects



162
163
164
165
# File 'lib/pixelart/image.rb', line 162

def grayscale
  img = @img.grayscale
  Image.new( img.width, img.height, img )
end

#heightObject



304
# File 'lib/pixelart/image.rb', line 304

def height()       @img.height; end

#imageObject

return image ref - use a different name - why? why not?

change to to_image  - why? why not?


316
# File 'lib/pixelart/image.rb', line 316

def image()        @img; end

#invertObject

note: invert will only invert r/g/b - and NOT the a(lpha) channel

the a(lpha) channel get passed on as is (1:1)


12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/pixelart/invert.rb', line 12

def invert
    img = Image.new( @img.width, @img.height )

    @img.width.times do |x|
      @img.height.times do |y|
        pixel = @img[x,y]

        ## note: xor (^) with 0 returns the original value unmodified.
        ##       xor (^) with 0xff flips the bits.
        ##         that is we are flipping/inverting r, g and b.
        ##             and keep the a(lpha) channel as is.

        ## hack - why? why not?
        ##   if transparent e.g. 0x0 than keep as is
        ##                 do not use 0xffffff00  - makes a difference?

        img[x,y] = if pixel == Color::TRANSPARENT  # transparent (0)
                       Color::TRANSPARENT
                   else
                       pixel ^ 0xffffff00
                   end
    end
  end
  img
end

#led(led = 8, spacing: 2, round_corner: false) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/pixelart/led.rb', line 5

def led( led=8, spacing: 2, round_corner: false )

  width  = @img.width*led  + (@img.width-1)*spacing
  height = @img.height*led + (@img.height-1)*spacing

  puts "  #{width}x#{height}"

  img = Image.new( width, height, Color::BLACK )

  @img.width.times do |x|
    @img.height.times do |y|
      pixel = @img[x,y]
      pixel = Color::BLACK  if pixel == Color::TRANSPARENT
      led.times do |n|
        led.times do |m|
          ## round a little - drop all four corners for now
          next  if round_corner &&
                  [[0,0],[0,1],[1,0],[1,1],[0,2],[2,0],
                   [0,led-1],[0,led-2],[1,led-1],[1,led-2],[0,led-3],[2,led-1],
                   [led-1,0],[led-1,1],[led-2,0],[led-2,1],[led-1,2],[led-3,0],
                   [led-1,led-1],[led-1,led-2],[led-2,led-1],[led-2,led-2],[led-1,led-3],[led-3,led-1],
                  ].include?( [n,m] )
          img[x*led+n + spacing*x,
              y*led+m + spacing*y] = pixel
        end
      end
    end
  end
  img
end

#left(left) ⇒ Object

shift image n-pixels to the left (NOT changing width/height)



146
147
148
149
150
# File 'lib/pixelart/image.rb', line 146

def left( left )
  img = Image.new( width, height )
  img.compose!( crop( 0, 0, width-left, height ), left, 0 )
  img
end

#mirrorObject Also known as: flip_vertically, flip, phlip, hand_phlip

flip vertially on y-axis (right-to-left/left-to-right)

e.g. pixels on the left will now be pixels on the right


190
191
192
193
# File 'lib/pixelart/image.rb', line 190

def mirror
  img = @img.mirror
  Image.new( img.width, img.height, img )
end

#pixelsObject



309
# File 'lib/pixelart/image.rb', line 309

def pixels()       @img.pixels; end

#rainbowObject Also known as: pride



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/pixelart/stripes.rb', line 97

def rainbow
  ##
  # the most common variant consists of six stripes:
  #   red, orange, yellow, green, blue, and violet.
  # The flag is typically flown horizontally,
  #  with the red stripe on top, as it would be in a natural rainbow
  #
  #  see https://en.wikipedia.org/wiki/Rainbow_flag_(LGBT)
  stripes( RAINBOW_RED,
           RAINBOW_ORANGE,
           RAINBOW_YELLOW,
           RAINBOW_GREEN,
           RAINBOW_BLUE,
           RAINBOW_VIOLET )
end

#rotate_clockwiseObject Also known as: rotate_right

90 degrees



206
207
208
209
# File 'lib/pixelart/image.rb', line 206

def rotate_clockwise      # 90 degrees
  img = @img.rotate_clockwise
  Image.new( img.width, img.height, img )
end

#rotate_counter_clockwiseObject Also known as: rotate_left

90 degrees



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

def rotate_counter_clockwise   # 90 degrees
  img = @img.rotate_counter_clockwise
  Image.new( img.width, img.height, img )
end

#sample(steps_x, steps_y = steps_x, top_x: 0, top_y: 0) ⇒ Object Also known as: pixelate

todo/check: rename to sample to resample or downsample - why? why not?



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/pixelart/sample.rb', line 68

def sample( steps_x, steps_y=steps_x,
            top_x: 0, top_y: 0 )
  width   = steps_x.size
  height  = steps_y.size
  puts "    downsampling from #{self.width}x#{self.height} to #{width}x#{height}..."

  dest = Image.new( width, height )

  steps_x.each_with_index do |step_x, x|
    steps_y.each_with_index do |step_y, y|
       pixel = self[top_x+step_x, top_y+step_y]

       dest[x,y] =  pixel
    end
  end

  dest
end

#sample_debug(steps_x, steps_y = steps_x, color: Color.parse( '#ffff00' ), top_x: 0, top_y: 0) ⇒ Object Also known as: pixelate_debug



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/pixelart/sample.rb', line 89

def sample_debug( steps_x, steps_y=steps_x,
              color:  Color.parse( '#ffff00' ),
              top_x: 0,
              top_y: 0)  ## add a yellow pixel

   ## todo/fix:  get a clone of the image (DO NOT modify in place)

    img = self

  steps_x.each_with_index do |step_x, x|
    steps_y.each_with_index do |step_y, y|
        base_x = top_x+step_x
        base_y = top_y+step_y

        img[base_x,base_y] = color

       ## add more colors
       img[base_x+1,base_y] = color
       img[base_x+2,base_y] = color

       img[base_x,base_y+1] = color
       img[base_x,base_y+2] = color
      end
  end

  self
end

#save(path, constraints = {}) ⇒ Object Also known as: write

(image) delegates

todo/check: add some more??


286
287
288
289
290
291
292
293
# File 'lib/pixelart/image.rb', line 286

def save( path, constraints = {} )
  # step 1: make sure outdir exits
  outdir = File.dirname( path )
  FileUtils.mkdir_p( outdir )  unless Dir.exist?( outdir )

  # step 2: save
  @img.save( path, constraints )
end

#silhouette(color = '#000000') ⇒ Object



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/pixelart/silhouette.rb', line 14

def silhouette( color='#000000' )
    color = Color.parse( color )

    img = Image.new( @img.width, @img.height )

    @img.width.times do |x|
      @img.height.times do |y|
        pixel = @img[x,y]

        img[x,y] = if pixel == Color::TRANSPARENT  # transparent (0)
                       Color::TRANSPARENT
                   else
                       color
                   end
    end
  end
  img
end

#sketch(sketch = 4, line: 1, line_color: Color::BLACK, colorize: false) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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
# File 'lib/pixelart/sketch.rb', line 6

def sketch( sketch=4, line: 1,
                      line_color: Color::BLACK,
                      colorize: false )
    ## todo/check: rename color option to fill or such - why? why not?

     # todo: line - find a better name eg. line_strenght/width or such?
    width  = @img.width*sketch  + (@img.width+1)*line
    height = @img.height*sketch + (@img.height+1)*line

    puts "  #{width}x#{height}"


    background_color = colorize ? Color::TRANSPARENT
                                : Color::WHITE


    img = Image.new( width, height, background_color )


    @img.width.times do |x|
      @img.height.times do |y|
        pixel = @img[x,y]

        ## get surrounding pixels - if "out-of-bound" use transparent (0)
        left   =  x == 0  ? Color::TRANSPARENT : @img[x-1,y]
        top    =  y == 0  ? Color::TRANSPARENT : @img[x  ,y-1]
        diag   =  (x==0 || y== 0) ? Color::TRANSPARENT : @img[x-1,y-1]

        if pixel != left   ## draw vertical line
          line.times do |n|
            (sketch+line*2).times do |m|
                img[    x*sketch + line*x + n,
                    m + y*sketch + line*y] = line_color
              end
            end
        end

        if pixel != top   ## draw horizontal line
           (sketch+line*2).times do |n|
             line.times do |m|
               img[n + x*sketch + line*x,
                       y*sketch + line*y + m] = line_color
              end
           end
        end


        ## check special edge case for x and y to add "finish-up" right and bottom line
        if x == @img.width-1 && pixel != Color::TRANSPARENT
           ## draw vertical line
           line.times do |n|
            (sketch+line*2).times do |m|
              img[    (x+1)*sketch + line*(x+1) + n,
                  m + y*sketch + line*y] = line_color
            end
           end
        end

        if y== @img.height-1 && pixel != Color::TRANSPARENT
          ## draw horizontal line
          (sketch+line*2).times do |n|
            line.times do |m|
              img[n + x*sketch + line*x,
                      (y+1)*sketch + line*(y+1) + m] = line_color
            end
          end
        end

        ###############
        ## fill with pixel color if color true (default is false)
        if colorize && pixel != Color::TRANSPARENT
          sketch.times do |n|
            sketch.times do |m|
              img[x*sketch + line*(x+1) + n,
                  y*sketch + line*(y+1) + m] = pixel
            end
          end

          if pixel == left   ## draw vertical line
            line.times do |n|
              sketch.times do |m|
                img[x*sketch + line*x + n,
                    y*sketch + line*(y+1) + m]  = pixel
                    # (y%2==0 ? 0x0000ffff : 0x000088ff )  # (for debugging)
              end
            end
          end

          if pixel == top   ## draw horizontal line
            sketch.times do |n|
              line.times do |m|
                img[x*sketch + line*(x+1) + n,
                    y*sketch + line*y + m]   = pixel
                    # (x%2==0 ? 0xff0000ff : 0x880000ff ) # (for debugging)
               end
            end
         end

         ## check all four same color (00,01)
         ##                           (10, x)  - bingo!
         if pixel == left && pixel == top && pixel == diag
            line.times do |n|
              line.times do |m|
                img[x*sketch + line*x + n,
                    y*sketch + line*y + m]  = pixel
                # 0xffff00ff  # (for debugging)
               end
            end
         end
        end  # colorize?

      end #  height.times
    end   #  width.times

    img
end

#spots(spot = 10, spacing: 0, center: nil, radius: nil, background: nil, lightness: nil, odd: false) ⇒ Object



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/pixelart/spots.rb', line 124

def spots( spot=10,
              spacing: 0,
              center: nil,
              radius: nil,
              background: nil,
              lightness: nil,
              odd: false )

  v = spots_hidef( spot,
        spacing: spacing,
        center: center,
        radius: radius,
        background: background,
        lightness: lightness,
        odd: odd )

  v.to_image
end

#spots_hidef(spot = 10, spacing: 0, center: nil, radius: nil, background: nil, lightness: nil, odd: false) ⇒ Object Also known as: spots_hd



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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
# File 'lib/pixelart/spots.rb', line 6

def spots_hidef( spot=10,
                   spacing: 0,
                   center: nil,
                   radius: nil,
                   background: nil,
                   lightness: nil,
                   odd: false )

  width  = @img.width*spot+(@img.width-1)*spacing
  height = @img.height*spot+(@img.height-1)*spacing

  ## puts "  #{width}x#{height}"

  ## settings in a hash for "pretty printing" in comments
  settings = { spot: spot
             }

  settings[ :spacing ] = spacing  if spacing
  settings[ :center ]  = center  if center
  settings[ :radius ] = radius  if radius
  settings[ :background ] = background  if background
  settings[ :lightness ] = lightness  if lightness
  settings[ :odd ] = odd   if odd


  v = Vector.new( width, height, header: <<TXT )
generated by pixelart/v#{VERSION} on #{Time.now.utc}

spots_hidef with settings:
    #{settings.to_json}
TXT


  min_center, max_center = center ? center : [0,0]
  min_radius, max_radius = radius ? radius : [0,0]

  ## note: allow passing in array of colors (will get randomally picked)
  background_colors = if background
                        ## check for array; otherwise assume single color passed in
                        background_ary = background.is_a?( Array) ? background : [background]
                        background_ary.map { |background| Color.parse( background ) }
                      else
                        [0]   # transparent (default - no background)
                      end


  min_lightness, max_lightness = lightness ? lightness : [0.0,0.0]


   @img.width.times do |x|
      @img.height.times do |y|
         color = @img[ x, y ]

         if color == 0   ## transparent
           next if background.nil?

           color = if background_colors.size == 1
                     background_colors[0]
                   else  ## pick random background color
                     background_colors[ rand( background_colors.size ) ]
                   end
         end


         if lightness
          ## todo/check: make it work with alpha too
          h,s,l = Color.to_hsl( color, include_alpha: false )

           h = h % 360    ## make sure h(ue) is always positive!!!

           ## note: rand() return between 0.0 and 1.0
           l_diff = min_lightness +
                     (max_lightness-min_lightness)*rand()

           lnew = [1.0, l+l_diff].min
           lnew = [0.0, lnew].max

           ## print " #{l}+#{l_diff}=#{lnew} "

           color = Color.from_hsl( h,
                                   [1.0, s].min,
                                   lnew )
         end

         ## note: return hexstring with leading #
         # e.g.    0 becomes #00000000
         #        and so on
         color_hex = Color.to_hex( color, include_alpha: true )

         cx_offset,
         cy_offset = if center  ## randomize (offset off center +/-)
                       [(spot/2 + min_center) + rand( max_center-min_center ),
                        (spot/2 + min_center) + rand( max_center-min_center )]
                     else
                       [spot/2,   ## center
                        spot/2]
                     end

         cx = x*spot + x*spacing + cx_offset
         cy = y*spot + y*spacing + cx_offset

         r = if radius ## randomize (radius +/-)
                       min_radius + rand( max_radius-min_radius )
                     else
                       spot/2
                     end

         cx += spot/2   if odd && (y % 2 == 1)  ## add odd offset


         v.circle( cx: cx, cy: cy, r: r, fill: color_hex)
      end
    end
  v
end

#stripes_horizontal(*colors) ⇒ Object Also known as: stripes



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/pixelart/stripes.rb', line 58

def stripes_horizontal( *colors )
  colors = colors.map { |color| Color.parse( color ) }

  img = Image.new( @img.width, @img.height )

  n = colors.size
  lengths = self.class.calc_stripes( @img.height, n: n )

  i      = 0
  length = lengths[0]
  color  = colors[0]

  @img.height.times do |y|
    if y >= length
      i      += 1
      length += lengths[i]
      color   = colors[i]
    end
    @img.width.times do |x|
      img[x,y] = color
    end
  end

  img.compose!( self )  ## paste/compose image onto backgorund
  img
end

#to_blobObject Also known as: blob



153
154
155
# File 'lib/pixelart/image.rb', line 153

def to_blob
  @img.to_blob
end

#transparent(style = :solid, fuzzy: false) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/pixelart/transparent.rb', line 5

def transparent( style = :solid, fuzzy: false )
  img = Image.new( width, height )


  background = self[0,0]

  bh,bs,bl =  Color.to_hsl( background )
  bh = (bh % 360)  ## might might negative degree (always make positive)

  height.times do |y|
      if style == :linear
        background = self[0,y]

        bh,bs,bl =  Color.to_hsl( background )
        bh = (bh % 360)  ## might might negative degree (always make positive)
      end
    width.times do |x|
      pixel = self[x,y]

      if background == 0  ## special case if background is already transparent keep going
        img[x,y] =  pixel
      elsif fuzzy
        ## check for more transparents
          ##   not s  is 0.0 to 0.99  (100%)
          ##   and l  is 0.0 to 0.99  (100%)
        h,s,l =  Color.to_hsl( pixel )
        h = (h % 360)  ## might might negative degree (always make positive)

        ## try some kind-of fuzzy "heuristic" match on background color
        if ((h >= bh-5) && (h <= bh+5)) &&
           ((s >= bs-0.07) && (s <= bs+0.07)) &&
           ((l >= bl-0.07) && (l <= bl+0.07))
         img[x,y] = 0  ## Color::TRANSPARENT

         if h != bh || s != bs || l != bl
            # report fuzzy background color
            puts "  #{x}/#{y} fuzzy background #{[h,s,l]} ~= #{[bh,bs,bl]}"
         end
        else
          img[x,y] =  pixel
        end
      else
         if pixel == background
          img[x,y] = 0   ## Color::TRANSPARENT
         else
           img[x,y] =  pixel
         end
      end
    end
  end
  img
end

#ukraineObject



16
# File 'lib/pixelart/ukraine.rb', line 16

def ukraine() stripes( UKRAINE_BLUE, UKRAINE_YELLOW ); end

#widthObject



303
# File 'lib/pixelart/image.rb', line 303

def width()        @img.width; end

#zoom(zoom = 2, spacing: 0) ⇒ Object Also known as: scale



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/pixelart/image.rb', line 114

def zoom( zoom=2, spacing: 0 )
  ## create a new zoom factor x image (2x, 3x, etc.)

  width  = @img.width*zoom+(@img.width-1)*spacing
  height = @img.height*zoom+(@img.height-1)*spacing

  img = Image.new( width, height )

  @img.width.times do |x|
    @img.height.times do |y|
      pixel = @img[x,y]
      zoom.times do |n|
        zoom.times do |m|
          img[n+zoom*x+spacing*x,
              m+zoom*y+spacing*y] = pixel
        end
      end
    end # each x
  end # each y

  img
end