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.



127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/pixelart/image.rb', line 127

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, width: nil, height: nil) ⇒ 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
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
# File 'lib/pixelart/image.rb', line 54

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

  colors = parse_colors( colors )

  ## note: for now use strict parser only 
  ##        if colors with hash map / keys defined
  ##         will raise error / exit if unknown token found!!!
  ##   AND  pixels is a single txt / text string to parse (NOT array of string lines) 
  ##
  #
  #  note default for now is:
  #      1) tokens separated by space if not strict (e.g. has no color keys AND not array of strings)
  #      2) every char is a token  if array of strings
  pixels =  if has_keys && pixels.is_a?( String )
              keys = colors.keys.map { |key| key.to_s }
              ## todo/fix: - sort by lenght first; 
              ##           - escape for rx chars!!
              rx = /#{keys.join('|')}/
              parse_pixels_strict( rx, pixels )  
            else
              parse_pixels( pixels )
            end 

  ## note: for now only use (require) width for flattened/streamed text input
  if width   
     ## always flattern first - why? why not?
     ##   allow multi-line text inputs - allow/support why? why not?
     pixels = pixels.flatten.each_slice( width ).to_a
  else
    ## find row with max width  
    width  = pixels.reduce(1) {|width,row| row.size > width ? row.size : width }
    height = pixels.size
  end

   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



406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
# File 'lib/pixelart/image.rb', line 406

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(obj) ⇒ Object

helpers



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/pixelart/image.rb', line 351

def self.parse_pixels( obj )
  pixels = []
  if obj.is_a?( Array )  ## assume array of string (lines)
      lines = obj
      lines.each do |line|
        ##  convert (string) line into indidual chars
        pixels << line.each_char.reduce( [] ) { |mem, c| mem << c; mem }
      end
  else  ## assume it's a (multi-line) string (with newlines)
        ##  assert and throw ArgumentError if not? - why? why not?
      txt = obj
      txt.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
        pixels << line.split( /[ \t]+/ )
     end
  end
  pixels
end

.parse_pixels_strict(rx, txt) ⇒ Object



377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# File 'lib/pixelart/image.rb', line 377

def self.parse_pixels_strict( rx, txt )
    ## must match tokens in regex (rx)  e.g. /0|1|2|3../ or /A|B|C... etc./
    pixels = []

    txt.each_line do |line|
       line = line.strip
       next if line.start_with?( '#' ) || line.empty?   ## note: allow comments and empty lines

       scan = StringScanner.new( line )
       tokens = []
       loop do
         # puts "  pos: #{scan.pos} - size: #{scan.rest.size} - #{scan.rest}"  
         token = scan.scan( rx )
         if token.nil?
          ## todo/fix: raise an exception here
           puts "!! ERROR - parse error; expected match of #{rx.to_s} but got: #{scan.rest}"
           exit 1
         end      
         tokens << token
         
         scan.skip( /[ \t]+/ )    
         break if scan.eos?
       end
       pixels << tokens
    end
    pixels
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



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

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

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



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

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

#_change_colors!(img, color_map) ⇒ Object



298
299
300
301
302
303
304
305
306
# File 'lib/pixelart/image.rb', line 298

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



292
293
294
295
296
# File 'lib/pixelart/image.rb', line 292

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



288
289
290
# File 'lib/pixelart/image.rb', line 288

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?



243
244
245
246
247
248
249
250
251
# File 'lib/pixelart/image.rb', line 243

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



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/pixelart/image.rb', line 265

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!



325
326
327
# File 'lib/pixelart/image.rb', line 325

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



167
168
169
170
# File 'lib/pixelart/image.rb', line 167

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?


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

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

#grayscaleObject Also known as: greyscale

filter / effects



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

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

#heightObject



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

def height()       @img.height; end

#imageObject

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

change to to_image  - why? why not?


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

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)



174
175
176
177
178
# File 'lib/pixelart/image.rb', line 174

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


218
219
220
221
# File 'lib/pixelart/image.rb', line 218

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

#pixelsObject



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

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



234
235
236
237
# File 'lib/pixelart/image.rb', line 234

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



228
229
230
231
# File 'lib/pixelart/image.rb', line 228

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??


314
315
316
317
318
319
320
321
# File 'lib/pixelart/image.rb', line 314

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



181
182
183
# File 'lib/pixelart/image.rb', line 181

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



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

def width()        @img.width; end

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



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

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