proofsheet 0.1.0 → 0.2.0
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/CHANGELOG.md +5 -0
- data/README.md +50 -0
- data/lib/proofsheet/cli.rb +21 -6
- data/lib/proofsheet/composer.rb +132 -0
- data/lib/proofsheet/composition_config.rb +70 -0
- data/lib/proofsheet/cropper.rb +34 -0
- data/lib/proofsheet/manifest.rb +10 -0
- data/lib/proofsheet/rounded_corners.rb +47 -0
- data/lib/proofsheet/version.rb +1 -1
- data/lib/proofsheet.rb +4 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 32d08a5e2dc65281e9d912c3c1fbf1d99c62b6dbeada3292f1460d4f9a03f866
|
|
4
|
+
data.tar.gz: 9eed758acf8fbba11febe56cb65ccd4c880f851301eaac1e5f1e82bd7845d7b4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a4691597bd56de3061f2767887c05b8ad3d23f8460b5d6ccce808c2162f5a2c70bfeb31d82b08c64389b9edf454010beb1244b493f4f4c8bb4ee6f475faae200
|
|
7
|
+
data.tar.gz: f398d39c015136879e4b0b4aad78ba34907e67713bae18d0dedf23392466993fc0e5652d97cc8bfb018c4d4e35043950f375c0c137144580eed7594b34075ee9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-09-19
|
|
4
|
+
|
|
5
|
+
- Add the `compose` command for arranging captured screenshots on solid, transparent, gradient, or image backgrounds
|
|
6
|
+
- Add per-layer cropping, proportional resizing, rounded corners, rotation, and shadows
|
|
7
|
+
|
|
3
8
|
## [0.1.0] - 2026-09-09
|
|
4
9
|
|
|
5
10
|
- Initial release
|
data/README.md
CHANGED
|
@@ -106,6 +106,56 @@ proofsheet list
|
|
|
106
106
|
|
|
107
107
|
Relative output paths are resolved from the directory where the command runs.
|
|
108
108
|
|
|
109
|
+
## Compositions
|
|
110
|
+
|
|
111
|
+
Compositions arrange captured shots on a new PNG canvas. A background can be a solid color, an angled two-color gradient, or an image. Layers can be positioned, resized proportionally, rounded, rotated, and given a shadow.
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
compositions:
|
|
115
|
+
launch-hero:
|
|
116
|
+
canvas: [1600, 1000]
|
|
117
|
+
background:
|
|
118
|
+
gradient: ["#211002", "#5aa579"]
|
|
119
|
+
angle: 135
|
|
120
|
+
layers:
|
|
121
|
+
- shot: dashboard
|
|
122
|
+
x: 120
|
|
123
|
+
y: 140
|
|
124
|
+
crop:
|
|
125
|
+
x: 80
|
|
126
|
+
y: 40
|
|
127
|
+
width: 1200
|
|
128
|
+
height: 700
|
|
129
|
+
width: 980
|
|
130
|
+
radius: 32
|
|
131
|
+
rotate: -4
|
|
132
|
+
shadow:
|
|
133
|
+
x: 8
|
|
134
|
+
y: 24
|
|
135
|
+
blur: 32
|
|
136
|
+
color: "#00000066"
|
|
137
|
+
- shot: activity
|
|
138
|
+
x: 900
|
|
139
|
+
y: 460
|
|
140
|
+
width: 560
|
|
141
|
+
rotate: 5
|
|
142
|
+
shadow: true
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Solid colors can be written directly as `background: "#fff8f5"`. For a transparent canvas, use `background: "#00000000"`. For an image background, use `background: { image: "assets/background.png" }`; Proofsheet scales and center-crops it to cover the canvas. Colors use `#RRGGBB` or `#RRGGBBAA` notation.
|
|
146
|
+
|
|
147
|
+
Layer crop coordinates are measured in pixels from the original captured shot. Proofsheet crops first, then resizes proportionally using `width`, rounds corners using `radius`, rotates, adds the shadow, and places the result on the canvas. The radius is measured in pixels after resizing and is clamped to half the layer's smaller dimension.
|
|
148
|
+
|
|
149
|
+
Setting `shadow: true` uses `x: 0`, `y: 16`, `blur: 24`, and `color: "#00000055"`. Use the expanded shadow mapping shown above to override any of those values.
|
|
150
|
+
|
|
151
|
+
Capture the source shots, then compose every configured image or selected compositions:
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
proofsheet capture
|
|
155
|
+
proofsheet compose
|
|
156
|
+
proofsheet compose launch-hero
|
|
157
|
+
```
|
|
158
|
+
|
|
109
159
|
## Development
|
|
110
160
|
|
|
111
161
|
Run `bin/setup`, then `bundle exec rake` to run the specs and linter. To install this gem locally, run `bundle exec rake install`.
|
data/lib/proofsheet/cli.rb
CHANGED
|
@@ -4,29 +4,30 @@ require "optparse"
|
|
|
4
4
|
|
|
5
5
|
module Proofsheet
|
|
6
6
|
class CLI
|
|
7
|
-
USAGE = "Usage: proofsheet <capture [SHOT ...] |
|
|
7
|
+
USAGE = "Usage: proofsheet <capture [SHOT ...] | compose [COMPOSITION ...] | list> " \
|
|
8
|
+
"[--config PATH] [--host HOST]"
|
|
8
9
|
|
|
9
|
-
def initialize(argv, out: $stdout, err: $stderr, root: Dir.pwd, manifest_loader: Manifest.method(:load),
|
|
10
|
-
capturer_class: Capturer)
|
|
10
|
+
def initialize(argv, out: $stdout, err: $stderr, root: Dir.pwd, manifest_loader: Manifest.method(:load), **classes)
|
|
11
11
|
@argv = argv.dup
|
|
12
12
|
@out = out
|
|
13
13
|
@err = err
|
|
14
14
|
@root = root
|
|
15
15
|
@manifest_loader = manifest_loader
|
|
16
|
-
@capturer_class = capturer_class
|
|
16
|
+
@capturer_class = classes.fetch(:capturer_class, Capturer)
|
|
17
|
+
@composer_class = classes.fetch(:composer_class, Composer)
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def run
|
|
20
21
|
command = @argv.shift
|
|
21
22
|
return help(0) if %w[help --help -h].include?(command)
|
|
22
23
|
return version if %w[--version -v].include?(command)
|
|
23
|
-
return help(1) unless %w[capture list].include?(command)
|
|
24
|
+
return help(1) unless %w[capture compose list].include?(command)
|
|
24
25
|
|
|
25
26
|
options = { config: Manifest::DEFAULT_PATH }
|
|
26
27
|
parser(options).parse!(@argv)
|
|
27
28
|
manifest = @manifest_loader.call(options[:config])
|
|
28
29
|
|
|
29
|
-
command
|
|
30
|
+
dispatch(command, manifest, options)
|
|
30
31
|
0
|
|
31
32
|
rescue OptionParser::ParseError, Error, KeyError => e
|
|
32
33
|
@err.puts "proofsheet: #{e.message}"
|
|
@@ -35,6 +36,13 @@ module Proofsheet
|
|
|
35
36
|
|
|
36
37
|
private
|
|
37
38
|
|
|
39
|
+
def dispatch(command, manifest, options)
|
|
40
|
+
return list(manifest) if command == "list"
|
|
41
|
+
return compose(manifest) if command == "compose"
|
|
42
|
+
|
|
43
|
+
capture(manifest, options)
|
|
44
|
+
end
|
|
45
|
+
|
|
38
46
|
def parser(options)
|
|
39
47
|
OptionParser.new do |opts|
|
|
40
48
|
opts.on("-c", "--config PATH") { |path| options[:config] = path }
|
|
@@ -61,6 +69,13 @@ module Proofsheet
|
|
|
61
69
|
@out.puts "Done."
|
|
62
70
|
end
|
|
63
71
|
|
|
72
|
+
def compose(manifest)
|
|
73
|
+
names = @argv.empty? ? manifest.composition_names : @argv
|
|
74
|
+
@out.puts "Composing #{names.size} #{names.size == 1 ? "image" : "images"}"
|
|
75
|
+
@composer_class.new(manifest: manifest, root: @root, out: @out).compose(names)
|
|
76
|
+
@out.puts "Done."
|
|
77
|
+
end
|
|
78
|
+
|
|
64
79
|
def help(status)
|
|
65
80
|
(status.zero? ? @out : @err).puts USAGE
|
|
66
81
|
status
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "vips"
|
|
6
|
+
|
|
7
|
+
module Proofsheet
|
|
8
|
+
class Composer
|
|
9
|
+
def initialize(manifest:, root: Dir.pwd, out: $stdout)
|
|
10
|
+
@manifest = manifest
|
|
11
|
+
@root = Pathname(root)
|
|
12
|
+
@out = out
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def compose(names = @manifest.composition_names)
|
|
16
|
+
names.each { |name| compose_one(@manifest.composition(name)) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def compose_one(composition)
|
|
22
|
+
@out.puts composition.name
|
|
23
|
+
image = background_image(composition)
|
|
24
|
+
composition.layers.each { |layer| image = add_layer(image, layer) }
|
|
25
|
+
write(composition, image)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def background_image(composition)
|
|
29
|
+
spec = composition.background
|
|
30
|
+
return solid(composition.width, composition.height, spec.color) if spec.color
|
|
31
|
+
return gradient(composition.width, composition.height, spec.gradient, spec.angle) if spec.gradient
|
|
32
|
+
return cover_image(spec.image, composition.width, composition.height) if spec.image
|
|
33
|
+
|
|
34
|
+
raise Error, "#{composition.name}: background must set color, gradient, or image"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def add_layer(canvas, layer)
|
|
38
|
+
image = layer_image(layer)
|
|
39
|
+
canvas = add_shadow(canvas, image, layer) if layer.shadow
|
|
40
|
+
canvas.composite2(image, :over, x: layer.x, y: layer.y)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def layer_image(layer)
|
|
44
|
+
image = load_image(output_path("#{layer.shot}.png"))
|
|
45
|
+
image = Cropper.call(image, layer.crop) if layer.crop
|
|
46
|
+
image = image.resize(layer.width.to_f / image.width) if layer.width
|
|
47
|
+
image = RoundedCorners.call(image, layer.radius)
|
|
48
|
+
image = image.rotate(layer.rotate, background: [0, 0, 0, 0]) unless layer.rotate.zero?
|
|
49
|
+
image
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def add_shadow(canvas, image, layer)
|
|
53
|
+
shadow = layer.shadow
|
|
54
|
+
alpha, padding = shadow_mask(image, shadow.blur)
|
|
55
|
+
overlay = shadow_overlay(alpha, shadow.color)
|
|
56
|
+
canvas.composite2(overlay, :over, x: layer.x + shadow.x - padding,
|
|
57
|
+
y: layer.y + shadow.y - padding)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def shadow_mask(image, blur)
|
|
61
|
+
padding = (blur * 3).ceil
|
|
62
|
+
alpha = image.extract_band(3).embed(padding, padding, image.width + (padding * 2),
|
|
63
|
+
image.height + (padding * 2), extend: :black)
|
|
64
|
+
alpha = alpha.gaussblur(blur) if blur.positive?
|
|
65
|
+
[alpha, padding]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def shadow_overlay(alpha, value)
|
|
69
|
+
color = parse_color(value)
|
|
70
|
+
opacity = color.pop
|
|
71
|
+
rgb = solid(alpha.width, alpha.height, color).extract_band(0, n: 3)
|
|
72
|
+
rgb.bandjoin(alpha * (opacity / 255.0))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def solid(width, height, color)
|
|
76
|
+
channels = color.is_a?(Array) ? color : parse_color(color)
|
|
77
|
+
rgb = Vips::Image.black(width, height).new_from_image(channels.first(3)).copy(interpretation: :srgb)
|
|
78
|
+
rgb.bandjoin(channels.fetch(3, 255))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def gradient(width, height, colors, angle)
|
|
82
|
+
raise Error, "gradient must contain exactly two colors" unless colors&.size == 2
|
|
83
|
+
|
|
84
|
+
from, to = colors.map { |color| parse_color(color) }
|
|
85
|
+
amount = gradient_amount(width, height, angle)
|
|
86
|
+
channels = from.zip(to).map { |start, finish| (amount * (finish - start)) + start }
|
|
87
|
+
channels.shift.bandjoin(channels).cast(:uchar).copy(interpretation: :srgb)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def gradient_amount(width, height, degrees)
|
|
91
|
+
coordinates = Vips::Image.xyz(width, height)
|
|
92
|
+
radians = degrees * Math::PI / 180
|
|
93
|
+
projection = (coordinates[0] * Math.cos(radians)) + (coordinates[1] * Math.sin(radians))
|
|
94
|
+
minimum = projection.min
|
|
95
|
+
maximum = projection.max
|
|
96
|
+
return Vips::Image.black(width, height) if minimum == maximum
|
|
97
|
+
|
|
98
|
+
(projection - minimum) / (maximum - minimum)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def cover_image(path, width, height)
|
|
102
|
+
image = load_image(@root.join(path))
|
|
103
|
+
scale = [width.to_f / image.width, height.to_f / image.height].max
|
|
104
|
+
image = image.resize(scale)
|
|
105
|
+
image.crop((image.width - width) / 2, (image.height - height) / 2, width, height)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def load_image(path)
|
|
109
|
+
raise Error, "image not found: #{path}" unless path.exist?
|
|
110
|
+
|
|
111
|
+
image = Vips::Image.new_from_file(path.to_s, access: :sequential)
|
|
112
|
+
image = image.colourspace(:srgb) unless image.interpretation == :srgb
|
|
113
|
+
image.has_alpha? ? image : image.bandjoin(255)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def parse_color(value)
|
|
117
|
+
match = /\A#([0-9a-f]{6})([0-9a-f]{2})?\z/i.match(value.to_s)
|
|
118
|
+
raise Error, "invalid color #{value.inspect}; use #RRGGBB or #RRGGBBAA" unless match
|
|
119
|
+
|
|
120
|
+
(match[1].scan(/../) + [match[2] || "ff"]).map { |channel| channel.to_i(16) }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def output_path(filename) = @root.join(@manifest.output_dir, filename)
|
|
124
|
+
|
|
125
|
+
def write(composition, image)
|
|
126
|
+
path = output_path(composition.filename)
|
|
127
|
+
FileUtils.mkdir_p(path.dirname)
|
|
128
|
+
image.write_to_file(path.to_s)
|
|
129
|
+
@out.puts " wrote #{path.relative_path_from(@root)}"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proofsheet
|
|
4
|
+
class CompositionConfig
|
|
5
|
+
Background = Data.define(:color, :gradient, :image, :angle)
|
|
6
|
+
Shadow = Data.define(:x, :y, :blur, :color)
|
|
7
|
+
Crop = Data.define(:x, :y, :width, :height)
|
|
8
|
+
Layer = Data.define(:shot, :x, :y, :crop, :width, :radius, :rotate, :shadow)
|
|
9
|
+
Composition = Data.define(:name, :width, :height, :background, :layers) do
|
|
10
|
+
def filename
|
|
11
|
+
"#{name}.png"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(data, path:)
|
|
16
|
+
@data = data
|
|
17
|
+
@path = path
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def names
|
|
21
|
+
@data.keys
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fetch(name)
|
|
25
|
+
raw = @data[name]
|
|
26
|
+
raise Error, unknown_message(name) if raw.nil?
|
|
27
|
+
|
|
28
|
+
width, height = raw.fetch("canvas")
|
|
29
|
+
Composition.new(name: name, width: width, height: height,
|
|
30
|
+
background: background(raw.fetch("background", "#ffffff")),
|
|
31
|
+
layers: raw.fetch("layers").map { |layer| layer_spec(layer) })
|
|
32
|
+
rescue KeyError => e
|
|
33
|
+
raise Error, "#{name}: #{e.message} in #{@path}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def unknown_message(name)
|
|
39
|
+
"unknown composition #{name.inspect} — known compositions: #{names.join(", ")}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def background(raw)
|
|
43
|
+
return Background.new(color: raw, gradient: nil, image: nil, angle: 0) if raw.is_a?(String)
|
|
44
|
+
|
|
45
|
+
Background.new(color: raw["color"], gradient: raw["gradient"], image: raw["image"],
|
|
46
|
+
angle: raw.fetch("angle", 0))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def layer_spec(raw)
|
|
50
|
+
Layer.new(shot: raw.fetch("shot"), x: raw.fetch("x", 0), y: raw.fetch("y", 0),
|
|
51
|
+
crop: crop(raw["crop"]), width: raw["width"], radius: raw.fetch("radius", 0),
|
|
52
|
+
rotate: raw.fetch("rotate", 0), shadow: shadow(raw["shadow"]))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def crop(raw)
|
|
56
|
+
return if raw.nil?
|
|
57
|
+
|
|
58
|
+
Crop.new(x: raw.fetch("x"), y: raw.fetch("y"),
|
|
59
|
+
width: raw.fetch("width"), height: raw.fetch("height"))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def shadow(raw)
|
|
63
|
+
return if raw.nil? || raw == false
|
|
64
|
+
|
|
65
|
+
raw = {} if raw == true
|
|
66
|
+
Shadow.new(x: raw.fetch("x", 0), y: raw.fetch("y", 16), blur: raw.fetch("blur", 24),
|
|
67
|
+
color: raw.fetch("color", "#00000055"))
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proofsheet
|
|
4
|
+
class Cropper
|
|
5
|
+
def self.call(image, spec)
|
|
6
|
+
new(image, spec).call
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def initialize(image, spec)
|
|
10
|
+
@image = image
|
|
11
|
+
@spec = spec
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
raise Error, "crop #{@spec.to_h} exceeds source image #{@image.width}×#{@image.height}" unless valid?
|
|
16
|
+
|
|
17
|
+
@image.crop(@spec.x, @spec.y, @spec.width, @spec.height)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def valid?
|
|
23
|
+
@spec.x >= 0 && @spec.y >= 0 && positive_dimensions? && within_bounds?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def positive_dimensions?
|
|
27
|
+
@spec.width.positive? && @spec.height.positive?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def within_bounds?
|
|
31
|
+
@spec.x + @spec.width <= @image.width && @spec.y + @spec.height <= @image.height
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/proofsheet/manifest.rb
CHANGED
|
@@ -73,6 +73,12 @@ module Proofsheet
|
|
|
73
73
|
names.map { |name| shot(name) }
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
def composition_names = composition_config.names
|
|
77
|
+
|
|
78
|
+
def composition(name)
|
|
79
|
+
composition_config.fetch(name)
|
|
80
|
+
end
|
|
81
|
+
|
|
76
82
|
def shot(name)
|
|
77
83
|
raw = shot_data[name]
|
|
78
84
|
raise Error, "unknown shot #{name.inspect} — known shots: #{names.join(", ")}" if raw.nil?
|
|
@@ -124,5 +130,9 @@ module Proofsheet
|
|
|
124
130
|
rescue KeyError
|
|
125
131
|
raise Error, "shots is missing from #{@path}"
|
|
126
132
|
end
|
|
133
|
+
|
|
134
|
+
def composition_config
|
|
135
|
+
@composition_config ||= CompositionConfig.new(@data.fetch("compositions", {}), path: @path)
|
|
136
|
+
end
|
|
127
137
|
end
|
|
128
138
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proofsheet
|
|
4
|
+
class RoundedCorners
|
|
5
|
+
def self.call(image, requested_radius)
|
|
6
|
+
return image unless requested_radius.positive?
|
|
7
|
+
|
|
8
|
+
new(image, requested_radius).call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(image, requested_radius)
|
|
12
|
+
@image = image
|
|
13
|
+
@radius = [requested_radius.to_f, image.width / 2.0, image.height / 2.0].min
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
alpha = (@image.extract_band(3) * (coverage / 255.0)).cast(:uchar)
|
|
18
|
+
@image.extract_band(0, n: 3).bandjoin(alpha)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def coverage
|
|
24
|
+
distance = ((x_distance * x_distance) + (y_distance * y_distance))**0.5
|
|
25
|
+
value = (((distance * -1) + @radius + 0.5) * 255)
|
|
26
|
+
value = value.relational_const(:more, [255]).ifthenelse(255, value)
|
|
27
|
+
value.relational_const(:less, [0]).ifthenelse(0, value)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def x_distance
|
|
31
|
+
corner_distance(coordinates[0], @image.width)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def y_distance
|
|
35
|
+
corner_distance(coordinates[1], @image.height)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def corner_distance(coordinate, length)
|
|
39
|
+
distance = (coordinate - ((length - 1) / 2.0)).abs - ((length / 2.0) - @radius)
|
|
40
|
+
distance.relational_const(:more, [0]).ifthenelse(distance, 0)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def coordinates
|
|
44
|
+
@coordinates ||= Vips::Image.xyz(@image.width, @image.height)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/proofsheet/version.rb
CHANGED
data/lib/proofsheet.rb
CHANGED
|
@@ -10,6 +10,10 @@ end
|
|
|
10
10
|
require_relative "proofsheet/browser"
|
|
11
11
|
require_relative "proofsheet/clip"
|
|
12
12
|
require_relative "proofsheet/credentials"
|
|
13
|
+
require_relative "proofsheet/composition_config"
|
|
13
14
|
require_relative "proofsheet/manifest"
|
|
14
15
|
require_relative "proofsheet/capturer"
|
|
16
|
+
require_relative "proofsheet/cropper"
|
|
17
|
+
require_relative "proofsheet/rounded_corners"
|
|
18
|
+
require_relative "proofsheet/composer"
|
|
15
19
|
require_relative "proofsheet/cli"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: proofsheet
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Marc Heiligers
|
|
@@ -70,8 +70,12 @@ files:
|
|
|
70
70
|
- lib/proofsheet/capturer.rb
|
|
71
71
|
- lib/proofsheet/cli.rb
|
|
72
72
|
- lib/proofsheet/clip.rb
|
|
73
|
+
- lib/proofsheet/composer.rb
|
|
74
|
+
- lib/proofsheet/composition_config.rb
|
|
73
75
|
- lib/proofsheet/credentials.rb
|
|
76
|
+
- lib/proofsheet/cropper.rb
|
|
74
77
|
- lib/proofsheet/manifest.rb
|
|
78
|
+
- lib/proofsheet/rounded_corners.rb
|
|
75
79
|
- lib/proofsheet/version.rb
|
|
76
80
|
- sig/proofsheet.rbs
|
|
77
81
|
homepage: https://github.com/FASCINATION-works/proofsheet
|