vissen-output 0.6.1
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 +7 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +64 -0
- data/.travis.yml +5 -0
- data/CHANGELOG.md +61 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +46 -0
- data/LICENSE.txt +21 -0
- data/README.md +40 -0
- data/Rakefile +17 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/vissen/output/buffer.rb +82 -0
- data/lib/vissen/output/color.rb +118 -0
- data/lib/vissen/output/context/circle.rb +70 -0
- data/lib/vissen/output/context/cloud.rb +130 -0
- data/lib/vissen/output/context/grid.rb +103 -0
- data/lib/vissen/output/context.rb +134 -0
- data/lib/vissen/output/context_error.rb +14 -0
- data/lib/vissen/output/error.rb +10 -0
- data/lib/vissen/output/filter/gamma.rb +38 -0
- data/lib/vissen/output/filter/quantizer.rb +64 -0
- data/lib/vissen/output/filter.rb +35 -0
- data/lib/vissen/output/palette.rb +112 -0
- data/lib/vissen/output/palettes.rb +12 -0
- data/lib/vissen/output/pixel.rb +26 -0
- data/lib/vissen/output/pixel_buffer.rb +71 -0
- data/lib/vissen/output/point.rb +57 -0
- data/lib/vissen/output/version.rb +8 -0
- data/lib/vissen/output/vixel.rb +64 -0
- data/lib/vissen/output/vixel_buffer.rb +56 -0
- data/lib/vissen/output/vixel_stack.rb +89 -0
- data/lib/vissen/output.rb +31 -0
- data/vissen-output.gemspec +31 -0
- metadata +148 -0
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vissen
|
4
|
+
module Output
|
5
|
+
module Context
|
6
|
+
# The cloud context imposes no structure on the placement of its elements
|
7
|
+
# but instead accepts coordinates for each individual point.
|
8
|
+
#
|
9
|
+
# == Usage
|
10
|
+
# The following example creates a context with three points, placed on a
|
11
|
+
# straight line.
|
12
|
+
#
|
13
|
+
# placement = [[0.1, 0.1],
|
14
|
+
# [0.5, 0.5],
|
15
|
+
# [0.9, 0.9]]
|
16
|
+
# context = Cloud.new placement
|
17
|
+
#
|
18
|
+
class Cloud
|
19
|
+
include Context
|
20
|
+
|
21
|
+
# @return [Array<Point>] the points in the context.
|
22
|
+
attr_reader :points
|
23
|
+
|
24
|
+
# @param points [Array<Point>, Array<Array<Integer>>] the position of
|
25
|
+
# the points in the context.
|
26
|
+
# @param width [Float] the width of the context.
|
27
|
+
# @param height [Float] the height of the context.
|
28
|
+
# @param args (see Context).
|
29
|
+
def initialize(points, width: 1.0, height: 1.0, **args)
|
30
|
+
super(width, height, **args)
|
31
|
+
|
32
|
+
factor = @width / width
|
33
|
+
|
34
|
+
@points = points.map { |point| Point.from point, scale: factor }
|
35
|
+
freeze
|
36
|
+
end
|
37
|
+
|
38
|
+
# Prevents any more points from being added.
|
39
|
+
#
|
40
|
+
# @return [self]
|
41
|
+
def freeze
|
42
|
+
@points.freeze
|
43
|
+
super
|
44
|
+
end
|
45
|
+
|
46
|
+
# See `Context#point_count`.
|
47
|
+
#
|
48
|
+
# @return [Integer] the number of grid points.
|
49
|
+
def point_count
|
50
|
+
@points.length
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Array<Integer>] the x and y coordinates of a point.
|
54
|
+
def position(index)
|
55
|
+
@points[index].position
|
56
|
+
end
|
57
|
+
|
58
|
+
class << self
|
59
|
+
# Randomly places points separated by a minimum distance. Note that
|
60
|
+
# the algorithm is nondeterministic in the time it takes to find the
|
61
|
+
# points.
|
62
|
+
#
|
63
|
+
# Draws a random point from the space of valid coordinates and
|
64
|
+
# calculates the distance to all previously selected points. If the
|
65
|
+
# distances are all greater than the given value the point is accepted
|
66
|
+
# and the process repeats until all points have been found.
|
67
|
+
#
|
68
|
+
# @raise [RangeError] if distance is too great and the allocation
|
69
|
+
# algorithm therefore is unlikely to converge.
|
70
|
+
#
|
71
|
+
# @param point_count [Integer] the number of points to scatter.
|
72
|
+
# @param width [Numeric] the width of the context.
|
73
|
+
# @param height [Numeric] the height of the context.
|
74
|
+
# @param distance [Numeric] the minimum distance between each point.
|
75
|
+
# @param args (see Context).
|
76
|
+
def scatter(point_count,
|
77
|
+
width: 1.0,
|
78
|
+
height: 1.0,
|
79
|
+
distance: nil,
|
80
|
+
**args)
|
81
|
+
if distance
|
82
|
+
d2 = (distance**2)
|
83
|
+
raise RangeError if 2 * d2 * point_count > width * height
|
84
|
+
else
|
85
|
+
d2 = (width * height) / (2.0 * point_count)
|
86
|
+
end
|
87
|
+
|
88
|
+
points = place_points point_count,
|
89
|
+
position_scrambler(width, height),
|
90
|
+
distance_condition(d2)
|
91
|
+
|
92
|
+
new(points, width: width, height: height, **args)
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
# @param x_max [#to_f] the largest value x is allowed to take.
|
98
|
+
# @param y_max [#to_f] the largest value y is allowed to take.
|
99
|
+
# @return [Proc] a proc that ranomizes the first and second element of
|
100
|
+
# the given array.
|
101
|
+
def position_scrambler(x_max, y_max)
|
102
|
+
x_range = 0.0..x_max.to_f
|
103
|
+
y_range = 0.0..y_max.to_f
|
104
|
+
|
105
|
+
proc do |pos|
|
106
|
+
pos[0] = rand x_range
|
107
|
+
pos[1] = rand y_range
|
108
|
+
pos
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def place_points(point_count, scrambler, condition)
|
113
|
+
points = Array.new(point_count) { [0.0, 0.0] }
|
114
|
+
points.each_with_index do |point, i|
|
115
|
+
loop do
|
116
|
+
scrambler.call point
|
117
|
+
break if points[0...i].all?(&condition.curry[point])
|
118
|
+
end
|
119
|
+
end
|
120
|
+
points
|
121
|
+
end
|
122
|
+
|
123
|
+
def distance_condition(distance_squared)
|
124
|
+
->(p, q) { (p[0] - q[0])**2 + (p[1] - q[1])**2 > distance_squared }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vissen
|
4
|
+
module Output
|
5
|
+
module Context
|
6
|
+
# The grid structure stores the number of rows and columns as well as its
|
7
|
+
# width and height. The dimensions are normalized to fit within a 1x1
|
8
|
+
# square.
|
9
|
+
#
|
10
|
+
# Aspect ratio is defined as width/height. If it is not given each grid
|
11
|
+
# cell is assumed to be square, meaning the aspect_ratio will equal
|
12
|
+
# columns/rows.
|
13
|
+
class Grid
|
14
|
+
include Context
|
15
|
+
|
16
|
+
# @return [Integer] the number of rows in the grid.
|
17
|
+
attr_reader :rows
|
18
|
+
|
19
|
+
# @return [Integer] the number of columns in the grid.
|
20
|
+
attr_reader :columns
|
21
|
+
|
22
|
+
# @param rows [Integer] the number of rows in the grid.
|
23
|
+
# @param columns [Integer] the number of columns in the grid.
|
24
|
+
# @param width [Numeric] (see Context)
|
25
|
+
# @param height [Numeric] (see Context)
|
26
|
+
# @param args (see Context)
|
27
|
+
def initialize(rows,
|
28
|
+
columns,
|
29
|
+
width: (columns - 1).to_f,
|
30
|
+
height: (rows - 1).to_f,
|
31
|
+
**args)
|
32
|
+
raise RangeError if rows <= 0 || columns <= 0
|
33
|
+
|
34
|
+
@rows = rows
|
35
|
+
@columns = columns
|
36
|
+
|
37
|
+
super(width, height, **args)
|
38
|
+
|
39
|
+
define_position
|
40
|
+
end
|
41
|
+
|
42
|
+
# See `Context#point_count`.
|
43
|
+
#
|
44
|
+
# @return [Integer] the number of grid points.
|
45
|
+
def point_count
|
46
|
+
@rows * @columns
|
47
|
+
end
|
48
|
+
|
49
|
+
# See `Context#index_from`.
|
50
|
+
#
|
51
|
+
# WARNING: no range check is performed.
|
52
|
+
#
|
53
|
+
# @param row [Integer] the row of the point of interest.
|
54
|
+
# @param column [Integer] the column of the point of interest.
|
55
|
+
# @return [Integer] the index of a row and column.
|
56
|
+
def index_from(row, column)
|
57
|
+
column * @rows + row
|
58
|
+
end
|
59
|
+
|
60
|
+
# Calculates the row and column of the the point stored at the given
|
61
|
+
# index.
|
62
|
+
#
|
63
|
+
# @param index [Integer] the index of the point of interest.
|
64
|
+
# @return [Array<Integer>] the row and column of a given index.
|
65
|
+
def row_column_from(index)
|
66
|
+
row = (index % @rows)
|
67
|
+
column = (index / @rows)
|
68
|
+
[row, column]
|
69
|
+
end
|
70
|
+
|
71
|
+
# Iterates over each point in the grid and yields the index, row and
|
72
|
+
# column.
|
73
|
+
#
|
74
|
+
# @return [Integer] the number of points in the grid.
|
75
|
+
def each_row_and_column
|
76
|
+
return to_enum(__callee__) unless block_given?
|
77
|
+
|
78
|
+
point_count.times { |i| yield(i, *row_column_from(i)) }
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def define_position
|
84
|
+
# Define #position dynamically based on the
|
85
|
+
# calculated width and height
|
86
|
+
x_factor = @columns == 1 ? 0.0 : width / (@columns - 1)
|
87
|
+
y_factor = @rows == 1 ? 0.0 : height / (@rows - 1)
|
88
|
+
|
89
|
+
# Position
|
90
|
+
#
|
91
|
+
# Returns the x and y coordinates of the grid point at the given
|
92
|
+
# index.
|
93
|
+
define_singleton_method(:position) do |index|
|
94
|
+
[
|
95
|
+
x_factor * (index / @rows),
|
96
|
+
y_factor * (index % @rows)
|
97
|
+
].freeze
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vissen
|
4
|
+
module Output
|
5
|
+
# The output context gives the points that relate to it their position and
|
6
|
+
# color. Contexts can come in different forms and offer different
|
7
|
+
# functionality but they must be able to answer three questions:
|
8
|
+
#
|
9
|
+
# 1. How many points are in the context,
|
10
|
+
# 2. what is the absolute position of each point and
|
11
|
+
# 3. what color palette corresponds to a palette index.
|
12
|
+
#
|
13
|
+
module Context
|
14
|
+
# @return [Float] the normalized width of the context.
|
15
|
+
attr_reader :width
|
16
|
+
|
17
|
+
# @return [Float] the normalized width of the context.
|
18
|
+
attr_reader :height
|
19
|
+
|
20
|
+
# @return [Array<Palette>] the array of palettes used to render the
|
21
|
+
# context.
|
22
|
+
attr_reader :palettes
|
23
|
+
|
24
|
+
# Output contexts give the two things to the vixels within them: a
|
25
|
+
# position and a color.
|
26
|
+
#
|
27
|
+
# The width and height are always normalized to fit within a 1 x 1 square.
|
28
|
+
#
|
29
|
+
# @param width [Numeric] the width of the context.
|
30
|
+
# @param height [Numeric] the height of the context.
|
31
|
+
# @param palettes [Array<Palette>] the color palettes to use when
|
32
|
+
# rendering the context.
|
33
|
+
def initialize(width, height, palettes: PALETTES)
|
34
|
+
if width.negative? || height.negative? || (width.zero? && height.zero?)
|
35
|
+
raise RangeError, 'Contexts needs a size in at least one dimension'
|
36
|
+
end
|
37
|
+
|
38
|
+
# Normalize width and height
|
39
|
+
normalizing_factor = 1.0 / [width, height].max
|
40
|
+
|
41
|
+
@width = width * normalizing_factor
|
42
|
+
@height = height * normalizing_factor
|
43
|
+
@palettes = palettes
|
44
|
+
end
|
45
|
+
|
46
|
+
# This method must be implemented by any class that includes this module.
|
47
|
+
def point_count
|
48
|
+
raise NotImplementedError
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [true, false] true if the context has only one dimension.
|
52
|
+
def one_dimensional?
|
53
|
+
@width == 0.0 || @height == 0.0
|
54
|
+
end
|
55
|
+
|
56
|
+
# Allocates, for each grid point, one object of the given class.
|
57
|
+
# Optionally takes a block that is expected to return each new object. The
|
58
|
+
# index of the element is passed to the given block.
|
59
|
+
#
|
60
|
+
# @raise [ArgumentError] if both a class and a block are given.
|
61
|
+
#
|
62
|
+
# @param klass [Class] the class of the allocated objects.
|
63
|
+
# @param block [Proc] the block to call for allocating each object.
|
64
|
+
# @return [Array<klass>] an array of new objects.
|
65
|
+
def alloc_points(klass = nil, &block)
|
66
|
+
if klass
|
67
|
+
raise ArgumentError if block_given?
|
68
|
+
block = proc { klass.new }
|
69
|
+
end
|
70
|
+
|
71
|
+
Array.new(point_count, &block)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Iterates over the context points. The index of the the point is passed
|
75
|
+
# to the given block.
|
76
|
+
#
|
77
|
+
# @return [Enumerator, Integer] an `Enumerator` if no block is given,
|
78
|
+
# otherwise the number of points that was iterated over.
|
79
|
+
def each
|
80
|
+
point_count.times
|
81
|
+
end
|
82
|
+
|
83
|
+
# This method must be implemented by a class that includes this module and
|
84
|
+
# should return the x and y coordinates of the point with the given index.
|
85
|
+
#
|
86
|
+
# @param _index [Integer] the index of a point in the context.
|
87
|
+
# @return [Array<Float>] an array containing the x and y coordinates of
|
88
|
+
# the point associated with the given intex.
|
89
|
+
def position(_index)
|
90
|
+
raise NotImplementedError
|
91
|
+
end
|
92
|
+
|
93
|
+
# Iterates over each point in the grid and yields the point index and x
|
94
|
+
# and y coordinates.
|
95
|
+
#
|
96
|
+
# @return (see #each)
|
97
|
+
def each_position
|
98
|
+
return to_enum(__callee__) unless block_given?
|
99
|
+
|
100
|
+
point_count.times do |index|
|
101
|
+
yield(index, *position(index))
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Context specific method to convert any domain specifc property (like row
|
106
|
+
# and column) to an index. The Cloud module calls this method when
|
107
|
+
# resolving the index given to its #[] method.
|
108
|
+
#
|
109
|
+
# @param index [Object] the object or objects that are used to refer to
|
110
|
+
# points in this context.
|
111
|
+
# @return [Integer] the index of the point.
|
112
|
+
def index_from(index)
|
113
|
+
index
|
114
|
+
end
|
115
|
+
|
116
|
+
# This utility method traverses the given target array and calculates for
|
117
|
+
# each corresponding grid point index the squared distance between the
|
118
|
+
# point and the given coordinate.
|
119
|
+
#
|
120
|
+
# @param x [Numeric] the x coordinate to calculate distances from.
|
121
|
+
# @param y [Numeric] the y coordinate to calculate distances from.
|
122
|
+
# @param target [Array<Float>] the target array to populate with
|
123
|
+
# distances.
|
124
|
+
def distance_squared(x, y, target)
|
125
|
+
target.each_with_index do |_, i|
|
126
|
+
x_i, y_i = position i
|
127
|
+
dx = x_i - x
|
128
|
+
dy = y_i - y
|
129
|
+
target[i] = (dx * dx) + (dy * dy)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vissen
|
4
|
+
module Output
|
5
|
+
# Context errors occur when two components of the output stack are used
|
6
|
+
# together even though they do not share the same context.
|
7
|
+
class ContextError < Error
|
8
|
+
# @param msg [String] the error messge.
|
9
|
+
def initialize(msg = 'The context of the given object does not match')
|
10
|
+
super
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vissen
|
4
|
+
module Output
|
5
|
+
module Filter
|
6
|
+
# Applies gamma correction to the given PixelGrid.
|
7
|
+
class Gamma
|
8
|
+
include Filter
|
9
|
+
|
10
|
+
# @return [Float] the gamma correction value.
|
11
|
+
attr_reader :value
|
12
|
+
|
13
|
+
# @param args (see Filter)
|
14
|
+
# @param value [Float] the gamma correction value.
|
15
|
+
def initialize(*args, value: 2.2)
|
16
|
+
super(*args)
|
17
|
+
|
18
|
+
@value = value
|
19
|
+
|
20
|
+
freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
# Applies the filter to the given pixel cloud.
|
24
|
+
#
|
25
|
+
# @see Filter
|
26
|
+
# @param pixel_buffer [PixelBuffer] the pixel buffer to perform the
|
27
|
+
# filter operation on.
|
28
|
+
def apply(pixel_buffer)
|
29
|
+
pixel_buffer.each do |pixel|
|
30
|
+
pixel.r = pixel.r**@value
|
31
|
+
pixel.g = pixel.g**@value
|
32
|
+
pixel.b = pixel.b**@value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vissen
|
4
|
+
module Output
|
5
|
+
module Filter
|
6
|
+
# Scales and rounds the color components to only take discrete values.
|
7
|
+
class Quantizer
|
8
|
+
include Filter
|
9
|
+
|
10
|
+
# @raise [RangeError] if steps < 2.
|
11
|
+
# @raise [ArgumentError] if the range is exclusive and has a floating
|
12
|
+
# point end value.
|
13
|
+
#
|
14
|
+
# @param args (see Filter)
|
15
|
+
# @param steps [Integer] the number of quantized steps.
|
16
|
+
# @param range [Range] the range in which the quantized values should
|
17
|
+
# lie.
|
18
|
+
def initialize(*args, steps: 256, range: 0...steps)
|
19
|
+
super(*args)
|
20
|
+
|
21
|
+
raise RangeError if steps < 2
|
22
|
+
|
23
|
+
from = range.begin
|
24
|
+
to = if range.exclude_end?
|
25
|
+
raise ArgumentError if range.end.is_a?(Float)
|
26
|
+
range.end - 1
|
27
|
+
else
|
28
|
+
range.end
|
29
|
+
end
|
30
|
+
|
31
|
+
design_function from, to, steps
|
32
|
+
|
33
|
+
freeze
|
34
|
+
end
|
35
|
+
|
36
|
+
# Applies the filter to the given pixel cloud.
|
37
|
+
#
|
38
|
+
# @see Filter
|
39
|
+
# @param pixel_buffer [PixelBuffer] the pixel Buffer to perform the
|
40
|
+
# filter operation on.
|
41
|
+
def apply(pixel_buffer)
|
42
|
+
pixel_buffer.each do |pixel|
|
43
|
+
pixel.r = @fn.call pixel.r
|
44
|
+
pixel.g = @fn.call pixel.g
|
45
|
+
pixel.b = @fn.call pixel.b
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def design_function(from, to, steps)
|
52
|
+
steps -= 1
|
53
|
+
@fn =
|
54
|
+
if from.zero? && to == steps
|
55
|
+
->(v) { (v * to).round }
|
56
|
+
else
|
57
|
+
factor = (to - from).to_f / steps
|
58
|
+
->(v) { from + (v * steps).round * factor }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vissen
|
4
|
+
module Output
|
5
|
+
# An output filter is defined as a time invariant operation on a pixel
|
6
|
+
# cloud. Upon initialization the filter is given the output context as a
|
7
|
+
# chance to precompute some results. The rest of the work is done in
|
8
|
+
# `#apply` and should not depend on time.
|
9
|
+
module Filter
|
10
|
+
# @return [Context] the filter context.
|
11
|
+
attr_reader :context
|
12
|
+
|
13
|
+
# @raise [TypeError] if the context is not a `Context`.
|
14
|
+
#
|
15
|
+
# @param context [Context] the context within which the filter will be
|
16
|
+
# applied.
|
17
|
+
def initialize(context)
|
18
|
+
raise TypeError unless context.is_a? Context
|
19
|
+
|
20
|
+
@context = context
|
21
|
+
end
|
22
|
+
|
23
|
+
# This method should apply the filter to the given `PixelBuffer`.
|
24
|
+
#
|
25
|
+
# @raise NotImplementedError if the method is not implemented in the
|
26
|
+
# specific `Filter` implementation.
|
27
|
+
#
|
28
|
+
# @param _pixel_buffer [PixelBuffer] the pixel cloud to which the filter
|
29
|
+
# should be applied.
|
30
|
+
def apply(_pixel_buffer)
|
31
|
+
raise NotImplementedError
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vissen
|
4
|
+
module Output
|
5
|
+
# The Palette is, at its core, a transformation between a position (0..1)
|
6
|
+
# and a color value \\{(0..1) x 3\\}. It can either be continous or be based
|
7
|
+
# on a pre-allocated lookup table.
|
8
|
+
#
|
9
|
+
# == Usage
|
10
|
+
# The following example creates a continuous palette and acesses the color
|
11
|
+
# at index 0.42.
|
12
|
+
#
|
13
|
+
# palette = Palette.new 0x11998e, 0x38ef7d, label: 'Quepal'
|
14
|
+
# palette[0.42] => #21BD87
|
15
|
+
#
|
16
|
+
# A discrete palette can also be created by specifying the number of steps
|
17
|
+
# to use.
|
18
|
+
#
|
19
|
+
# palette = Palette.new 0x11998e, 0x38ef7d, steps: 5
|
20
|
+
# palette[0.42] => #1BAF8A
|
21
|
+
#
|
22
|
+
class Palette
|
23
|
+
# @return [String, nil] the optional palette label.
|
24
|
+
attr_reader :label
|
25
|
+
|
26
|
+
# @param colors [Array<Color>, Array<#to_a>] the colors to use in the
|
27
|
+
# palette.
|
28
|
+
# @param steps [Integer] the number of discrete palette values. The
|
29
|
+
# palette will be continuous if nil.
|
30
|
+
# @param label [String] the optional label to use when identifying the
|
31
|
+
# palette.
|
32
|
+
def initialize(*colors, steps: nil, label: nil)
|
33
|
+
@colors = colors.map { |c| Color.from(c).freeze }
|
34
|
+
@label = label
|
35
|
+
|
36
|
+
if steps
|
37
|
+
define_discrete_accessor steps
|
38
|
+
else
|
39
|
+
define_continous_accessor
|
40
|
+
end
|
41
|
+
|
42
|
+
freeze
|
43
|
+
end
|
44
|
+
|
45
|
+
# Prevents both the palette colors and the label from changing.
|
46
|
+
#
|
47
|
+
# @return [self]
|
48
|
+
def freeze
|
49
|
+
@colors.freeze
|
50
|
+
@label.freeze
|
51
|
+
|
52
|
+
super
|
53
|
+
end
|
54
|
+
|
55
|
+
# Discretize the palette into the given number of values. Palettes defined
|
56
|
+
# with a step count are sampled as if they where continuous.
|
57
|
+
#
|
58
|
+
# @param n [Integer] the number of discrete values to produce.
|
59
|
+
# @return [Array<Color>] an array of colors sampled from the palette.
|
60
|
+
def to_a(n)
|
61
|
+
Array.new(n) { |i| color_at(i.to_f / (n - 1)).freeze }
|
62
|
+
end
|
63
|
+
|
64
|
+
# Example output:
|
65
|
+
# "#42BEAF -> #020180 (rainbow)"
|
66
|
+
#
|
67
|
+
# @return [String] a string representation of the palette made up of the
|
68
|
+
# palette colors as well as the (optional) label.
|
69
|
+
def inspect
|
70
|
+
@colors.map(&:inspect).join(' -> ').tap do |base|
|
71
|
+
break "#{base} (#{@label})" if @label
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def define_discrete_accessor(steps)
|
78
|
+
@palette = to_a steps
|
79
|
+
define_singleton_method :[] do |index|
|
80
|
+
index = if index >= 1.0 then @palette.length - 1
|
81
|
+
elsif index < 0.0 then 0
|
82
|
+
else (index * (@palette.length - 1)).floor
|
83
|
+
end
|
84
|
+
|
85
|
+
@palette[index]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def define_continous_accessor
|
90
|
+
define_singleton_method(:[]) { |pos| color_at pos }
|
91
|
+
end
|
92
|
+
|
93
|
+
def color_bin(pos)
|
94
|
+
num_bins = (@colors.length - 1)
|
95
|
+
bin = (pos * num_bins).floor
|
96
|
+
mixing_ratio = pos * num_bins - bin
|
97
|
+
|
98
|
+
[bin, mixing_ratio]
|
99
|
+
end
|
100
|
+
|
101
|
+
def color_at(pos)
|
102
|
+
return @colors[0] if pos <= 0
|
103
|
+
return @colors[-1] if pos >= 1
|
104
|
+
|
105
|
+
# Find the two colors we are between
|
106
|
+
bin, r = color_bin pos
|
107
|
+
a, b = @colors[bin, 2]
|
108
|
+
a.mix_with b, r
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vissen
|
4
|
+
module Output
|
5
|
+
# Default palette collection.
|
6
|
+
PALETTES = {
|
7
|
+
'Argon' => [0x03001e, 0x7303c0, 0xec38bc, 0xfdeff9],
|
8
|
+
'Red Sunset' => [0x355c7d, 0x6c5b7b, 0xc06c84],
|
9
|
+
'Quepal' => [0x11998e, 0x38ef7d]
|
10
|
+
}.map { |l, c| Palette.new(*c, label: l) }.freeze
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vissen
|
4
|
+
module Output
|
5
|
+
# The `Pixel` object is used by the `PixelBuffer` to store the colors of its
|
6
|
+
# points.
|
7
|
+
#
|
8
|
+
# TODO: How do we want the pixel to saturate? When written or when read?
|
9
|
+
class Pixel < Color
|
10
|
+
# Reset the pixel color to black (0, 0, 0).
|
11
|
+
#
|
12
|
+
# @return [self]
|
13
|
+
def clear!
|
14
|
+
self.r = 0
|
15
|
+
self.g = 0
|
16
|
+
self.b = 0
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [String] a string representation of the pixel.
|
21
|
+
def inspect
|
22
|
+
format '(%.1f, %.1f, %.1f)', r, g, b
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|