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
more...

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.

[View source]

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

[View source]

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

[View source]

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

[View source]

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?
[View source]

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

[View source]

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?
[View source]

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

[View source]

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

[View source]

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

[View source]

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

[View source]

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

[View source]

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.
[View source]

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

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

Instance Method Details

#[](x, y) ⇒ Object

[View source]

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

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

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

[View source]

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

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

#_change_colors!(img, color_map) ⇒ Object

[View source]

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

[View source]

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

[View source]

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

[View source]

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?

[View source]

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

[View source]

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

[View source]

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!

[View source]

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

[View source]

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?
[View source]

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

[View source]

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

[View source]

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?
[View source]

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)
[View source]

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

[View source]

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)

[View source]

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
[View source]

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

[View source]

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

def pixels()       @img.pixels; end

#rainbowObject Also known as: pride

[View source]

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

[View source]

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

[View source]

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?

[View source]

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

[View source]

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??
[View source]

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

[View source]

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

[View source]

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

[View source]

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

[View source]

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

[View source]

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

[View source]

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

def to_blob
  @img.to_blob
end

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

[View source]

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

[View source]

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

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

#widthObject

[View source]

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

def width()        @img.width; end

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

[View source]

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