pixelart 1.3.0 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
-
|