pixelart 1.3.0 → 1.3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Manifest.txt +2 -3
- data/README.md +6 -0
- data/Rakefile +1 -0
- data/lib/pixelart/base.rb +12 -31
- data/lib/pixelart/generator.rb +202 -0
- data/lib/pixelart/sample.rb +120 -0
- data/lib/pixelart/version.rb +1 -1
- data/lib/pixelart.rb +10 -0
- metadata +18 -5
- data/lib/pixelart/color.rb +0 -131
- data/lib/pixelart/gradient.rb +0 -106
- data/lib/pixelart/palette.rb +0 -72
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0724e3e1f83da6ef84325d79706a2f59d773eb31478f1e4d797ff582dad64a4a
|
4
|
+
data.tar.gz: 7d75ebe3aef0abb3e3d142bf787995bbcdf797416912a387601110a3547d08fe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6afe87fa92ec1de03bad71cd5f2ba298f78287b77f514a6c74cae6747c1e1d62790d722c07fb0a5d049dcf9966d4fa0a473688e71c717de534420f4746b711f2
|
7
|
+
data.tar.gz: 0b1a7cf13cdffa7410a6c0246f894d3ca5f1e3b1347fd90b9f7cbe9ed3cff4b4fc87c6704d80854b5337aa5f17e24631c28bc8188e4c6f86422e862d22949d43
|
data/Manifest.txt
CHANGED
@@ -6,14 +6,13 @@ lib/pixelart.rb
|
|
6
6
|
lib/pixelart/base.rb
|
7
7
|
lib/pixelart/blur.rb
|
8
8
|
lib/pixelart/circle.rb
|
9
|
-
lib/pixelart/color.rb
|
10
9
|
lib/pixelart/composite.rb
|
11
|
-
lib/pixelart/
|
10
|
+
lib/pixelart/generator.rb
|
12
11
|
lib/pixelart/image.rb
|
13
12
|
lib/pixelart/led.rb
|
14
13
|
lib/pixelart/misc.rb
|
15
|
-
lib/pixelart/palette.rb
|
16
14
|
lib/pixelart/pixelator.rb
|
15
|
+
lib/pixelart/sample.rb
|
17
16
|
lib/pixelart/silhouette.rb
|
18
17
|
lib/pixelart/sketch.rb
|
19
18
|
lib/pixelart/spots.rb
|
data/README.md
CHANGED
@@ -288,3 +288,9 @@ Just install the gem:
|
|
288
288
|
|
289
289
|
The scripts are dedicated to the public domain.
|
290
290
|
Use it as you please with no restrictions whatsoever.
|
291
|
+
|
292
|
+
|
293
|
+
|
294
|
+
## Questions? Comments?
|
295
|
+
|
296
|
+
Post them on the [D.I.Y. Punk (Pixel) Art reddit](https://old.reddit.com/r/DIYPunkArt). Thanks.
|
data/Rakefile
CHANGED
data/lib/pixelart/base.rb
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
###
|
2
|
+
# base module
|
3
|
+
require 'pixelart/colors'
|
4
|
+
|
5
|
+
|
6
|
+
|
1
7
|
###############
|
2
8
|
# 3rd party
|
3
9
|
require 'chunky_png'
|
@@ -11,32 +17,24 @@ require 'mini_magick'
|
|
11
17
|
require 'csvreader'
|
12
18
|
|
13
19
|
|
14
|
-
## stdlib
|
15
|
-
require 'pp'
|
16
|
-
require 'time'
|
17
|
-
require 'date'
|
18
|
-
require 'fileutils'
|
19
|
-
|
20
|
-
require 'json'
|
21
|
-
require 'yaml'
|
22
|
-
|
23
|
-
|
24
|
-
|
25
20
|
|
26
21
|
## our own code
|
27
22
|
require 'pixelart/version' # note: let version always go first
|
28
|
-
require 'pixelart/color'
|
29
|
-
require 'pixelart/gradient'
|
30
|
-
require 'pixelart/palette'
|
31
23
|
require 'pixelart/image'
|
32
24
|
require 'pixelart/composite'
|
33
25
|
|
26
|
+
require 'pixelart/sample' ## (down)sample / pixelate
|
27
|
+
|
34
28
|
|
35
29
|
require 'pixelart/pixelator'
|
36
30
|
|
37
31
|
require 'pixelart/misc' ## misc helpers
|
38
32
|
require 'pixelart/stripes'
|
39
33
|
|
34
|
+
|
35
|
+
require 'pixelart/generator' ## generate images from text via spritesheets
|
36
|
+
|
37
|
+
|
40
38
|
#########################
|
41
39
|
# (special) effects / filters / etc
|
42
40
|
require 'pixelart/circle'
|
@@ -67,21 +65,4 @@ require 'pixelart/blur'
|
|
67
65
|
|
68
66
|
|
69
67
|
|
70
|
-
|
71
|
-
|
72
|
-
##########
|
73
|
-
# add some spelling convenience variants
|
74
|
-
PixelArt = Pixelart
|
75
|
-
|
76
|
-
module Pixelart
|
77
|
-
Palette256 = Palette8Bit = Palette8bit
|
78
|
-
|
79
|
-
Palette256Image = Palette8BitImage = Palette8bitImage =
|
80
|
-
ImagePalette256 = ImagePalette8Bit = ImagePalette8bit
|
81
|
-
|
82
|
-
CompositeImage = ImageComposite
|
83
|
-
end
|
84
|
-
|
85
|
-
|
86
|
-
|
87
68
|
puts Pixelart.banner # say hello
|
@@ -0,0 +1,202 @@
|
|
1
|
+
####
|
2
|
+
# "simple" generator (no different sizes, genders, etc.)
|
3
|
+
# uses built-in spritesheet for (archetypes &) attributes
|
4
|
+
|
5
|
+
|
6
|
+
module Pixelart
|
7
|
+
|
8
|
+
class Metadata
|
9
|
+
class Sprite
|
10
|
+
attr_reader :id, :name, :type, :more_names
|
11
|
+
|
12
|
+
def initialize( id:,
|
13
|
+
name:,
|
14
|
+
type:,
|
15
|
+
more_names: [] )
|
16
|
+
@id = id # zero-based index eg. 0,1,2,3, etc.
|
17
|
+
@name = name
|
18
|
+
@type = type
|
19
|
+
@more_names = more_names
|
20
|
+
end
|
21
|
+
end # class Metadata::Sprite
|
22
|
+
end # class Metadata
|
23
|
+
|
24
|
+
|
25
|
+
|
26
|
+
|
27
|
+
class Generator
|
28
|
+
|
29
|
+
######
|
30
|
+
# static helpers - (turn into "true" static self.class methods - why? why not?)
|
31
|
+
#
|
32
|
+
def self.normalize_key( str )
|
33
|
+
## add & e.g. B&W
|
34
|
+
## add ' e.g. McDonald's Red
|
35
|
+
str.downcase.gsub(/[ ()&°'_-]/, '').strip
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.normalize_name( str )
|
39
|
+
## normalize spaces in more names
|
40
|
+
str.strip.gsub( /[ ]{2,}/, ' ' )
|
41
|
+
end
|
42
|
+
|
43
|
+
def normalize_key( str ) self.class.normalize_key( str ); end
|
44
|
+
def normalize_name( str ) self.class.normalize_name( str ); end
|
45
|
+
|
46
|
+
|
47
|
+
|
48
|
+
def build_attributes_by_name( recs )
|
49
|
+
h = {}
|
50
|
+
recs.each_with_index do |rec|
|
51
|
+
names = [rec.name] + rec.more_names
|
52
|
+
|
53
|
+
names.each do |name|
|
54
|
+
key = normalize_key( name )
|
55
|
+
|
56
|
+
if h[ key ]
|
57
|
+
puts "!!! ERROR - attribute name is not unique:"
|
58
|
+
pp rec
|
59
|
+
puts "duplicate:"
|
60
|
+
pp h[key]
|
61
|
+
exit 1
|
62
|
+
end
|
63
|
+
h[ key ] = rec
|
64
|
+
end
|
65
|
+
end
|
66
|
+
## pp h
|
67
|
+
h
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
def build_recs( recs ) ## build and normalize (meta data) records
|
72
|
+
## sort by id
|
73
|
+
recs = recs.sort do |l,r|
|
74
|
+
l['id'].to_i( 10 ) <=> r['id'].to_i( 10 ) # use base10 (decimal)
|
75
|
+
end
|
76
|
+
|
77
|
+
## assert all recs are in order by id (0 to size)
|
78
|
+
recs.each_with_index do |rec, exp_id|
|
79
|
+
id = rec['id'].to_i(10)
|
80
|
+
if id != exp_id
|
81
|
+
puts "!! ERROR - meta data record ids out-of-order - expected id #{exp_id}; got #{id}"
|
82
|
+
exit 1
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
## convert to "wrapped / immutable" kind-of struct
|
87
|
+
recs = recs.map do |rec|
|
88
|
+
id = rec['id'].to_i( 10 )
|
89
|
+
name = normalize_name( rec['name'] )
|
90
|
+
type = rec['type']
|
91
|
+
|
92
|
+
more_names = (rec['more_names'] || '').split( '|' )
|
93
|
+
more_names = more_names.map {|str| normalize_name( str ) }
|
94
|
+
|
95
|
+
Metadata::Sprite.new(
|
96
|
+
id: id,
|
97
|
+
name: name,
|
98
|
+
type: type,
|
99
|
+
more_names: more_names )
|
100
|
+
end
|
101
|
+
recs
|
102
|
+
end # method build_recs
|
103
|
+
|
104
|
+
|
105
|
+
|
106
|
+
def initialize( image_path="./spritesheet.png",
|
107
|
+
meta_path="./spritesheet.csv",
|
108
|
+
width: 24,
|
109
|
+
height: 24 )
|
110
|
+
@width = width
|
111
|
+
@height = height
|
112
|
+
|
113
|
+
@sheet = ImageComposite.read( image_path, width: @width, height: @height )
|
114
|
+
recs = CsvHash.read( meta_path )
|
115
|
+
|
116
|
+
@recs = build_recs( recs )
|
117
|
+
|
118
|
+
## lookup by "case/space-insensitive" name / key
|
119
|
+
@attributes_by_name = build_attributes_by_name( @recs )
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
def spritesheet() @sheet; end
|
124
|
+
alias_method :sheet, :spritesheet
|
125
|
+
|
126
|
+
|
127
|
+
def records() @recs; end
|
128
|
+
alias_method :meta, :records
|
129
|
+
|
130
|
+
|
131
|
+
|
132
|
+
|
133
|
+
def find_meta( q )
|
134
|
+
key = normalize_key( q ) ## normalize q(uery) string/symbol
|
135
|
+
|
136
|
+
rec = @attributes_by_name[ key ]
|
137
|
+
if rec
|
138
|
+
puts " lookup >#{key}< => #{rec.id}: #{rec.name} / #{rec.type}"
|
139
|
+
else
|
140
|
+
puts "!! WARN - no lookup found for key >#{key}<"
|
141
|
+
end
|
142
|
+
|
143
|
+
rec
|
144
|
+
end
|
145
|
+
|
146
|
+
def find( q )
|
147
|
+
rec = find_meta( q )
|
148
|
+
|
149
|
+
## return image if record found
|
150
|
+
rec ? @sheet[ rec.id ] : nil
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
def to_recs( *values )
|
155
|
+
recs = []
|
156
|
+
|
157
|
+
attribute_names = values
|
158
|
+
|
159
|
+
attribute_names.each do |attribute_name|
|
160
|
+
attribute = find_meta( attribute_name )
|
161
|
+
if attribute.nil?
|
162
|
+
puts "!! ERROR - attribute >#{attribute_name}< not found; sorry"
|
163
|
+
exit 1
|
164
|
+
end
|
165
|
+
recs << attribute
|
166
|
+
end
|
167
|
+
|
168
|
+
recs
|
169
|
+
end
|
170
|
+
|
171
|
+
|
172
|
+
|
173
|
+
def generate_image( *values, background: nil, before: nil )
|
174
|
+
## note: generate_image NO longer supports
|
175
|
+
## - generate by integer number (indexes), sorry
|
176
|
+
|
177
|
+
recs = to_recs( *values )
|
178
|
+
|
179
|
+
## note: first construct/generate image on transparent background
|
180
|
+
## add background if present as LAST step
|
181
|
+
img = Image.new( @width, @height )
|
182
|
+
|
183
|
+
recs.each do |rec|
|
184
|
+
## note: before call(back) MUST change image INPLACE!!!!
|
185
|
+
before.call( img, rec ) if before
|
186
|
+
img.compose!( @sheet[ rec.id ] )
|
187
|
+
end
|
188
|
+
|
189
|
+
if background ## for now assume background is (simply) color
|
190
|
+
img2 = Image.new( @width, @height )
|
191
|
+
img2.compose!( Image.new( @width, @height, background ) )
|
192
|
+
img2.compose!( img )
|
193
|
+
img = img2
|
194
|
+
end
|
195
|
+
|
196
|
+
img
|
197
|
+
end
|
198
|
+
alias_method :generate, :generate_image
|
199
|
+
|
200
|
+
end # class Generator
|
201
|
+
|
202
|
+
end # module Pixelart
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Pixelart
|
2
|
+
|
3
|
+
class Image
|
4
|
+
|
5
|
+
def self.calc_sample_steps( width, new_width,
|
6
|
+
center: true,
|
7
|
+
debug: false )
|
8
|
+
## todo/fix: assert new_width is smaller than width
|
9
|
+
if debug
|
10
|
+
puts
|
11
|
+
puts "==> from: #{width}px to: #{new_width}px"
|
12
|
+
end
|
13
|
+
|
14
|
+
indexes = []
|
15
|
+
|
16
|
+
base_step = width / new_width ## pixels per pixel
|
17
|
+
|
18
|
+
err_step = (width % new_width) * 2 ## multiply by 2
|
19
|
+
denominator = new_width * 2 # denominator (in de - nenner e.g. 1/nenner 4/nenner)
|
20
|
+
|
21
|
+
overflow = err_step*new_width/denominator ## todo/check - assert that div is always WITHOUT remainder!!!!!
|
22
|
+
|
23
|
+
if debug
|
24
|
+
puts
|
25
|
+
puts "base_step (pixels per pixel):"
|
26
|
+
puts " #{base_step} - #{base_step} * #{new_width}px = #{base_step*new_width}px"
|
27
|
+
puts "err_step (in 1/#{width}*2):"
|
28
|
+
puts " #{err_step} / #{denominator} - #{err_step*new_width} / #{denominator} = +#{err_step*new_width/denominator}px overflow"
|
29
|
+
puts
|
30
|
+
end
|
31
|
+
|
32
|
+
# initial pixel offset
|
33
|
+
index = 0
|
34
|
+
err = err_step/2 ## note: start off with +err_step/2 to add overflow pixel in the "middle"
|
35
|
+
|
36
|
+
|
37
|
+
index += if center.is_a?( Integer )
|
38
|
+
center
|
39
|
+
elsif center
|
40
|
+
base_step/2
|
41
|
+
else
|
42
|
+
0 # use 0px offset
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
new_width.times do |i|
|
47
|
+
if err >= denominator ## overflow
|
48
|
+
puts " -- overflow #{err}/#{denominator} - add +1 pixel offset to #{i}" if debug
|
49
|
+
index += 1
|
50
|
+
err -= denominator
|
51
|
+
end
|
52
|
+
|
53
|
+
puts " #{i} => #{index} -- #{err} / #{denominator}" if debug
|
54
|
+
|
55
|
+
|
56
|
+
indexes[i] = index
|
57
|
+
|
58
|
+
index += base_step
|
59
|
+
err += err_step
|
60
|
+
end
|
61
|
+
|
62
|
+
indexes
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
|
67
|
+
## todo/check: rename to sample to resample or downsample - why? why not?
|
68
|
+
def sample( steps_x, steps_y=steps_x,
|
69
|
+
top_x: 0, top_y: 0 )
|
70
|
+
width = steps_x.size
|
71
|
+
height = steps_y.size
|
72
|
+
puts " downsampling from #{self.width}x#{self.height} to #{width}x#{height}..."
|
73
|
+
|
74
|
+
dest = Image.new( width, height )
|
75
|
+
|
76
|
+
steps_x.each_with_index do |step_x, x|
|
77
|
+
steps_y.each_with_index do |step_y, y|
|
78
|
+
pixel = self[top_x+step_x, top_y+step_y]
|
79
|
+
|
80
|
+
dest[x,y] = pixel
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
dest
|
85
|
+
end
|
86
|
+
alias_method :pixelate, :sample
|
87
|
+
|
88
|
+
|
89
|
+
def sample_debug( steps_x, steps_y=steps_x,
|
90
|
+
color: Color.parse( '#ffff00' ),
|
91
|
+
top_x: 0,
|
92
|
+
top_y: 0) ## add a yellow pixel
|
93
|
+
|
94
|
+
## todo/fix: get a clone of the image (DO NOT modify in place)
|
95
|
+
|
96
|
+
img = self
|
97
|
+
|
98
|
+
steps_x.each_with_index do |step_x, x|
|
99
|
+
steps_y.each_with_index do |step_y, y|
|
100
|
+
base_x = top_x+step_x
|
101
|
+
base_y = top_y+step_y
|
102
|
+
|
103
|
+
img[base_x,base_y] = color
|
104
|
+
|
105
|
+
## add more colors
|
106
|
+
img[base_x+1,base_y] = color
|
107
|
+
img[base_x+2,base_y] = color
|
108
|
+
|
109
|
+
img[base_x,base_y+1] = color
|
110
|
+
img[base_x,base_y+2] = color
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
self
|
115
|
+
end
|
116
|
+
alias_method :pixelate_debug, :sample_debug
|
117
|
+
|
118
|
+
end # class Image
|
119
|
+
end # module Pixelart
|
120
|
+
|
data/lib/pixelart/version.rb
CHANGED
data/lib/pixelart.rb
CHANGED
@@ -9,4 +9,14 @@ require 'pixelart/base' # aka "strict(er)" version
|
|
9
9
|
include Pixelart
|
10
10
|
|
11
11
|
|
12
|
+
##########
|
13
|
+
# add some spelling convenience variants
|
14
|
+
|
15
|
+
|
16
|
+
module Pixelart
|
17
|
+
Palette256Image = Palette8BitImage = Palette8bitImage =
|
18
|
+
ImagePalette256 = ImagePalette8Bit = ImagePalette8bit
|
19
|
+
|
20
|
+
CompositeImage = ImageComposite
|
21
|
+
end
|
12
22
|
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pixelart
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.3.
|
4
|
+
version: 1.3.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gerald Bauer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-07-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: pixelart-colors
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: chunky_png
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -104,14 +118,13 @@ files:
|
|
104
118
|
- lib/pixelart/base.rb
|
105
119
|
- lib/pixelart/blur.rb
|
106
120
|
- lib/pixelart/circle.rb
|
107
|
-
- lib/pixelart/color.rb
|
108
121
|
- lib/pixelart/composite.rb
|
109
|
-
- lib/pixelart/
|
122
|
+
- lib/pixelart/generator.rb
|
110
123
|
- lib/pixelart/image.rb
|
111
124
|
- lib/pixelart/led.rb
|
112
125
|
- lib/pixelart/misc.rb
|
113
|
-
- lib/pixelart/palette.rb
|
114
126
|
- lib/pixelart/pixelator.rb
|
127
|
+
- lib/pixelart/sample.rb
|
115
128
|
- lib/pixelart/silhouette.rb
|
116
129
|
- lib/pixelart/sketch.rb
|
117
130
|
- lib/pixelart/spots.rb
|
data/lib/pixelart/color.rb
DELETED
@@ -1,131 +0,0 @@
|
|
1
|
-
module Pixelart
|
2
|
-
|
3
|
-
|
4
|
-
class Color
|
5
|
-
TRANSPARENT = 0 # rgba( 0, 0, 0, 0)
|
6
|
-
BLACK = 0xff # rgba( 0, 0, 0,255)
|
7
|
-
WHITE = 0xffffffff # rgba(255,255,255,255)
|
8
|
-
|
9
|
-
|
10
|
-
def self.parse( color )
|
11
|
-
if color.is_a?( Integer ) ## e.g. assumes ChunkyPNG::Color.rgb() or such
|
12
|
-
color ## pass through as is 1:1
|
13
|
-
elsif color.is_a?( Array ) ## assume array of hsl(a) e. g. [180, 0.86, 0.88]
|
14
|
-
from_hsl( *color )
|
15
|
-
elsif color.is_a?( String )
|
16
|
-
if color.downcase == 'transparent' ## special case for builtin colors
|
17
|
-
TRANSPARENT
|
18
|
-
else
|
19
|
-
## note: return an Integer !!! (not a Color class or such!!! )
|
20
|
-
from_hex( color )
|
21
|
-
end
|
22
|
-
else
|
23
|
-
raise ArgumentError, "unknown color format; cannot parse - expected rgb hex string e.g. d3d3d3"
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
def self.from_hex( hex )
|
28
|
-
## Creates a color by converting it from a string in hex notation.
|
29
|
-
##
|
30
|
-
## It supports colors with (#rrggbbaa) or without (#rrggbb)
|
31
|
-
## alpha channel as well as the 3-digit short format (#rgb)
|
32
|
-
## for those without. Color strings may include
|
33
|
-
## the prefix "0x" or "#"".
|
34
|
-
ChunkyPNG::Color.from_hex( hex )
|
35
|
-
end
|
36
|
-
|
37
|
-
def self.from_hsl( hue, saturation, lightness, alpha=255)
|
38
|
-
ChunkyPNG::Color.from_hsl( hue,
|
39
|
-
saturation,
|
40
|
-
lightness,
|
41
|
-
alpha )
|
42
|
-
end
|
43
|
-
|
44
|
-
|
45
|
-
def self.to_hex( color, include_alpha: true )
|
46
|
-
ChunkyPNG::Color.to_hex( color, include_alpha )
|
47
|
-
end
|
48
|
-
|
49
|
-
def self.to_hsl( color, include_alpha: true )
|
50
|
-
# Returns an array with the separate HSL components of a color.
|
51
|
-
ChunkyPNG::Color.to_hsl( color, include_alpha )
|
52
|
-
end
|
53
|
-
|
54
|
-
def self.r( color ) ChunkyPNG::Color.r( color ); end
|
55
|
-
def self.g( color ) ChunkyPNG::Color.g( color ); end
|
56
|
-
def self.b( color ) ChunkyPNG::Color.b( color ); end
|
57
|
-
|
58
|
-
def self.rgb( r, g, b ) ChunkyPNG::Color.rgb( r, g, b); end
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
## known built-in color names
|
63
|
-
def self.build_names
|
64
|
-
names = {
|
65
|
-
'#00000000' => 'TRANSPARENT',
|
66
|
-
'#000000ff' => 'BLACK',
|
67
|
-
'#ffffffff' => 'WHITE',
|
68
|
-
}
|
69
|
-
|
70
|
-
## auto-add grayscale 1 to 254
|
71
|
-
(1..254).each do |n|
|
72
|
-
hex = "#" + ('%02x' % n)*3
|
73
|
-
hex << "ff" ## add alpha channel (255)
|
74
|
-
names[ hex ] = "8-BIT GRAYSCALE ##{n}"
|
75
|
-
end
|
76
|
-
|
77
|
-
names
|
78
|
-
end
|
79
|
-
|
80
|
-
NAMES = build_names
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
def self.format( color )
|
85
|
-
rgb = [r(color),
|
86
|
-
g(color),
|
87
|
-
b(color)]
|
88
|
-
|
89
|
-
# rgb in hex (string format)
|
90
|
-
# note: do NOT include alpha channel for now - why? why not?
|
91
|
-
hex = "#" + rgb.map{|num| '%02x' % num }.join
|
92
|
-
|
93
|
-
hsl = to_hsl( color )
|
94
|
-
## get alpha channel (transparency) for hsla
|
95
|
-
alpha = hsl[3]
|
96
|
-
|
97
|
-
|
98
|
-
buf = ''
|
99
|
-
buf << hex
|
100
|
-
buf << " / "
|
101
|
-
buf << "rgb("
|
102
|
-
buf << "%3d " % rgb[0]
|
103
|
-
buf << "%3d " % rgb[1]
|
104
|
-
buf << "%3d)" % rgb[2]
|
105
|
-
buf << " - "
|
106
|
-
buf << "hsl("
|
107
|
-
buf << "%3d° " % (hsl[0] % 360)
|
108
|
-
buf << "%3d%% " % (hsl[1]*100+0.5).to_i
|
109
|
-
buf << "%3d%%)" % (hsl[2]*100+0.5).to_i
|
110
|
-
|
111
|
-
if alpha != 255
|
112
|
-
buf << " - α(%3d%%)" % (alpha*100/255+0.5).to_i
|
113
|
-
else
|
114
|
-
buf << " " ## add empty for 255 (full opacity)
|
115
|
-
end
|
116
|
-
|
117
|
-
## note: add alpha channel to hex
|
118
|
-
alpha_hex = '%02x' % alpha
|
119
|
-
name = NAMES[ hex+alpha_hex ]
|
120
|
-
buf << " - #{name}" if name
|
121
|
-
|
122
|
-
buf
|
123
|
-
end
|
124
|
-
class << self
|
125
|
-
alias_method :fmt, :format
|
126
|
-
end
|
127
|
-
|
128
|
-
end # class Color
|
129
|
-
end # module Pixelart
|
130
|
-
|
131
|
-
|
data/lib/pixelart/gradient.rb
DELETED
@@ -1,106 +0,0 @@
|
|
1
|
-
|
2
|
-
## inspired / helped by
|
3
|
-
## https://en.wikipedia.org/wiki/List_of_software_palettes#Color_gradient_palettes
|
4
|
-
## https://github.com/mistic100/tinygradient
|
5
|
-
## https://mistic100.github.io/tinygradient/
|
6
|
-
## https://bsouthga.dev/posts/color-gradients-with-python
|
7
|
-
|
8
|
-
|
9
|
-
module Pixelart
|
10
|
-
|
11
|
-
class Gradient
|
12
|
-
|
13
|
-
def initialize( *stops )
|
14
|
-
## note: convert stop colors to rgb triplets e.g.
|
15
|
-
## from #ffffff to [255,255,255]
|
16
|
-
## #000000 to [0,0,0] etc.
|
17
|
-
@stops = stops.map do |stop|
|
18
|
-
stop = Color.parse( stop )
|
19
|
-
[Color.r(stop), Color.g(stop), Color.b(stop)]
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
|
24
|
-
def colors( steps )
|
25
|
-
segments = @stops.size - 1
|
26
|
-
|
27
|
-
## note: gradient will include start (first)
|
28
|
-
## AND stop color (last) - stop color is NOT excluded for now!!
|
29
|
-
if segments == 1
|
30
|
-
start = @stops[0]
|
31
|
-
stop = @stops[1]
|
32
|
-
|
33
|
-
gradient = linear_gradient( start, stop, steps,
|
34
|
-
include_stop: true )
|
35
|
-
else
|
36
|
-
steps_per_segment, mod = steps.divmod( segments )
|
37
|
-
raise ArgumentError, "steps (#{steps}) must be divisible by # of segments (#{segments}); expected mod of 0 but got #{mod}" if mod != 0
|
38
|
-
|
39
|
-
gradient = []
|
40
|
-
segments.times do |segment|
|
41
|
-
start = @stops[segment]
|
42
|
-
stop = @stops[segment+1]
|
43
|
-
include_stop = (segment == segments-1) ## note: only include stop if last segment!!
|
44
|
-
|
45
|
-
# print " segment #{segment+1}/#{segments} #{steps_per_segment} color(s) - "
|
46
|
-
# print " start: #{start.inspect} "
|
47
|
-
# print include_stop ? 'include' : 'exclude'
|
48
|
-
# print " stop: #{stop.inspect}"
|
49
|
-
# print "\n"
|
50
|
-
|
51
|
-
gradient += linear_gradient( start, stop, steps_per_segment,
|
52
|
-
include_stop: include_stop )
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
## convert to color (Integer)
|
57
|
-
gradient.map do |color|
|
58
|
-
Color.rgb( *color )
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
def interpolate( start, stop, steps, n )
|
65
|
-
## note: n - expected to start with 1,2,3,etc.
|
66
|
-
color = []
|
67
|
-
3.times do |i|
|
68
|
-
stepize = Float(stop[i] - start[i]) / Float(steps-1)
|
69
|
-
value = stepize * n
|
70
|
-
## convert back to Integer from Float
|
71
|
-
## add 0.5 for rounding up (starting with 0.5) - why? why not?
|
72
|
-
value = (value+0.5).to_i
|
73
|
-
value = start[i] + value
|
74
|
-
|
75
|
-
color << value
|
76
|
-
end
|
77
|
-
color
|
78
|
-
end
|
79
|
-
|
80
|
-
|
81
|
-
def linear_gradient( start, stop, steps,
|
82
|
-
include_stop: true )
|
83
|
-
|
84
|
-
gradient = [start] ## auto-add start color (first color in gradient)
|
85
|
-
|
86
|
-
if include_stop
|
87
|
-
1.upto( steps-2 ).each do |n| ## sub two (-2), that is, start and stop color
|
88
|
-
gradient << interpolate( start, stop, steps, n )
|
89
|
-
end
|
90
|
-
# note: use original passed in stop color (should match calculated)
|
91
|
-
gradient << stop
|
92
|
-
else
|
93
|
-
1.upto( steps-1 ).each do |n| ## sub one (-1), that is, start color only
|
94
|
-
## note: add one (+1) to steps because stop color gets excluded (not included)!!
|
95
|
-
gradient << interpolate( start, stop, steps+1, n )
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
gradient
|
100
|
-
end
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
end # class Gradient
|
105
|
-
end # module Pixelart
|
106
|
-
|
data/lib/pixelart/palette.rb
DELETED
@@ -1,72 +0,0 @@
|
|
1
|
-
module Pixelart
|
2
|
-
|
3
|
-
|
4
|
-
class Palette8bit # or use Palette256 alias?
|
5
|
-
|
6
|
-
|
7
|
-
## auto-add grayscale 0 to 255
|
8
|
-
## e.g. rgb(0,0,0)
|
9
|
-
## rgb(1,1,1)
|
10
|
-
## rgb(2,2,2)
|
11
|
-
## ...
|
12
|
-
## rgb(255,255,255)
|
13
|
-
GRAYSCALE = (0..255).map { |n| Color.rgb( n, n, n ) }
|
14
|
-
|
15
|
-
|
16
|
-
## 8x32 gradient color stops
|
17
|
-
## see https://en.wikipedia.org/wiki/List_of_software_palettes#Color_gradient_palettes
|
18
|
-
|
19
|
-
SEPIA_STOPS = [
|
20
|
-
['080400', '262117'],
|
21
|
-
['272218', '453E2F'],
|
22
|
-
['463F30', '645C48'],
|
23
|
-
['655D48', '837A60'],
|
24
|
-
|
25
|
-
['847A60', 'A29778'],
|
26
|
-
['A39878', 'C1B590'],
|
27
|
-
['C2B691', 'E0D2A8'],
|
28
|
-
['E1D3A9', 'FEEFBF'],
|
29
|
-
]
|
30
|
-
|
31
|
-
BLUE_STOPS = [
|
32
|
-
['000000', '001F3E'],
|
33
|
-
['002040', '003F7E'],
|
34
|
-
['004080', '005FBD'],
|
35
|
-
['0060BF', '007FFD'],
|
36
|
-
|
37
|
-
['0080FF', '009FFF'],
|
38
|
-
['00A0FF', '00BFFF'],
|
39
|
-
['00C0FF', '00DFFF'],
|
40
|
-
['00E0FF', '00FEFF'],
|
41
|
-
]
|
42
|
-
|
43
|
-
FALSE_STOPS = [
|
44
|
-
['FF00FF', '6400FF'],
|
45
|
-
['5F00FF', '003CFF'],
|
46
|
-
['0041FF', '00DCFF'],
|
47
|
-
['00E1FF', '00FF82'],
|
48
|
-
|
49
|
-
['00FF7D', '1EFF00'],
|
50
|
-
['23FF00', 'BEFF00'],
|
51
|
-
['C3FF00', 'FFA000'],
|
52
|
-
['FF9B00', 'FF0000'],
|
53
|
-
]
|
54
|
-
|
55
|
-
|
56
|
-
def self.build_palette( gradients )
|
57
|
-
colors_per_gradient, mod = 256.divmod( gradients.size )
|
58
|
-
raise ArgumentError, "8bit palette - 256 must be divisible by # of gradients (#{gradients.size}; expected mod of 0 but got #{mod}" if mod != 0
|
59
|
-
|
60
|
-
colors = []
|
61
|
-
gradients.each do |stops|
|
62
|
-
colors += Gradient.new( *stops ).colors( colors_per_gradient )
|
63
|
-
end
|
64
|
-
colors
|
65
|
-
end
|
66
|
-
|
67
|
-
SEPIA = build_palette( SEPIA_STOPS )
|
68
|
-
BLUE = build_palette( BLUE_STOPS )
|
69
|
-
FALSE = build_palette( FALSE_STOPS )
|
70
|
-
end # class Palette8bit
|
71
|
-
end # module Pixelart
|
72
|
-
|