foggy-mirror 1.0.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 +7 -0
- data/.github/workflows/main.yml +24 -0
- data/.gitignore +10 -0
- data/.rspec +1 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +56 -0
- data/LICENSE.txt +21 -0
- data/README.md +77 -0
- data/Rakefile +4 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/exe/foggy-mirror +6 -0
- data/foggy-mirror.gemspec +33 -0
- data/lib/foggy-mirror/adapters/image_magick.rb +29 -0
- data/lib/foggy-mirror/adapters/vips.rb +53 -0
- data/lib/foggy-mirror/cli.rb +90 -0
- data/lib/foggy-mirror/exporter.rb +19 -0
- data/lib/foggy-mirror/exporters/css.rb +25 -0
- data/lib/foggy-mirror/exporters/svg.rb +41 -0
- data/lib/foggy-mirror/processor.rb +101 -0
- data/lib/foggy-mirror/utils.rb +11 -0
- data/lib/foggy-mirror/version.rb +5 -0
- data/lib/foggy-mirror.rb +25 -0
- metadata +138 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1cc9dc0f12ce500219fb0488fb7b8e54c24819a611645a0072a56695ced1336a
|
4
|
+
data.tar.gz: 883c91418982a115fd45411d80159260c78bb0798078ee6b7a0efa1f85934c63
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 01bcab41ac44cc5305863dc22486cea86cccf94558f1d397942d1a214fbf2ad6fcc6e420312e2cacc9efe4b07064b349fc43769c42ad51aca4623e361d647dd4
|
7
|
+
data.tar.gz: 788d0af0500e35d613a9108ba04f0b22da5dc33d9ea3cebb69cea1fdeda7e226a57766be6623ae48eff088dc4349c225bdc639a6c9f49257fbfcb76ed4291ab1
|
@@ -0,0 +1,24 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on: [push, pull_request]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
specs:
|
7
|
+
strategy:
|
8
|
+
fail-fast: false
|
9
|
+
matrix:
|
10
|
+
ruby: [2.7, '3.0', 3.1]
|
11
|
+
|
12
|
+
runs-on: ubuntu-latest
|
13
|
+
|
14
|
+
steps:
|
15
|
+
- uses: actions/checkout@v2
|
16
|
+
- name: Install ImageMagick and libvips
|
17
|
+
run: sudo apt install imagemagick libvips42
|
18
|
+
- name: Set up Ruby
|
19
|
+
uses: ruby/setup-ruby@v1
|
20
|
+
with:
|
21
|
+
ruby-version: ${{ matrix.ruby }}
|
22
|
+
bundler-cache: true
|
23
|
+
- name: Run specs
|
24
|
+
run: bundle exec rspec spec --backtrace
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
foggy-mirror (1.0.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
byebug (11.1.3)
|
10
|
+
coderay (1.1.3)
|
11
|
+
diff-lcs (1.5.0)
|
12
|
+
ffi (1.15.5)
|
13
|
+
method_source (1.0.0)
|
14
|
+
nokogiri (1.13.3-x86_64-darwin)
|
15
|
+
racc (~> 1.4)
|
16
|
+
nokogiri (1.13.3-x86_64-linux)
|
17
|
+
racc (~> 1.4)
|
18
|
+
pry (0.13.1)
|
19
|
+
coderay (~> 1.1)
|
20
|
+
method_source (~> 1.0)
|
21
|
+
pry-byebug (3.9.0)
|
22
|
+
byebug (~> 11.0)
|
23
|
+
pry (~> 0.13.0)
|
24
|
+
racc (1.6.0)
|
25
|
+
rake (13.0.6)
|
26
|
+
rspec (3.11.0)
|
27
|
+
rspec-core (~> 3.11.0)
|
28
|
+
rspec-expectations (~> 3.11.0)
|
29
|
+
rspec-mocks (~> 3.11.0)
|
30
|
+
rspec-core (3.11.0)
|
31
|
+
rspec-support (~> 3.11.0)
|
32
|
+
rspec-expectations (3.11.0)
|
33
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
34
|
+
rspec-support (~> 3.11.0)
|
35
|
+
rspec-mocks (3.11.0)
|
36
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
37
|
+
rspec-support (~> 3.11.0)
|
38
|
+
rspec-support (3.11.0)
|
39
|
+
ruby-vips (2.1.4)
|
40
|
+
ffi (~> 1.12)
|
41
|
+
|
42
|
+
PLATFORMS
|
43
|
+
x86_64-darwin-19
|
44
|
+
x86_64-darwin-21
|
45
|
+
x86_64-linux
|
46
|
+
|
47
|
+
DEPENDENCIES
|
48
|
+
foggy-mirror!
|
49
|
+
nokogiri
|
50
|
+
pry-byebug
|
51
|
+
rake (~> 13.0)
|
52
|
+
rspec (~> 3.1)
|
53
|
+
ruby-vips (~> 2.1)
|
54
|
+
|
55
|
+
BUNDLED WITH
|
56
|
+
2.3.8
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Pedro Carbajal and Beezwax Datatools, Inc.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# foggy-mirror
|
2
|
+
|
3
|
+

|
4
|
+
|
5
|
+
foggy-mirror is a small Ruby tool that creates faux blurry versions of raster
|
6
|
+
images using radial gradients, exported as either SVG or CSS (using
|
7
|
+
`background-image`).
|
8
|
+
|
9
|
+
This is useful as a poor man's replacement for CSS's `backdrop-filter: blur()`,
|
10
|
+
as that CSS feature isn't fully supported by browsers, and sometimes you want
|
11
|
+
an element with a "frosted glass" effect on top of a crispy background.
|
12
|
+
|
13
|
+
Using gradients we can achieve very smooth and infinitely scalable graphics at
|
14
|
+
very low file size (e.g. the example SVG below is only 814 bytes after gzip).
|
15
|
+
If you need a less blurry version of the original picture, then a regular blur
|
16
|
+
effect saved to JPEG/WebP may be a better value proposition.
|
17
|
+
|
18
|
+
## Example
|
19
|
+
|
20
|
+
Original raster image:
|
21
|
+
|
22
|
+

|
23
|
+
|
24
|
+
SVG:
|
25
|
+
|
26
|
+
<img src="/img/unsplash.svg" alt="foggy-mirror SVG" width="500" height="500" />
|
27
|
+
|
28
|
+
## Installation
|
29
|
+
|
30
|
+
In your Gemfile:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
gem 'foggy-mirror'
|
34
|
+
```
|
35
|
+
|
36
|
+
## Usage
|
37
|
+
|
38
|
+
Within Ruby:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
require 'foggy-mirror'
|
42
|
+
|
43
|
+
# All keyword arguments are optional, only the filename is required
|
44
|
+
p = FoggyMirror::Processor.new("/path/to/image.jpg",
|
45
|
+
resolution: 5,
|
46
|
+
overlap: 0.5,
|
47
|
+
distribution: :shuffle,
|
48
|
+
random_offset: 0.5,
|
49
|
+
random: Random.new,
|
50
|
+
adapter: FoggyMirror::ImageMagick,
|
51
|
+
adapter_options: {}
|
52
|
+
)
|
53
|
+
|
54
|
+
p.to_svg # Outputs SVG
|
55
|
+
|
56
|
+
p.to_css # Outputs CSS properties
|
57
|
+
```
|
58
|
+
|
59
|
+
Options are:
|
60
|
+
|
61
|
+
* `resolution` (Integer): How many radial gradients to use per dimension (X/Y).
|
62
|
+
Defaults to 5.
|
63
|
+
* `overlap` (Float): How much radial gradients overlap each other (0 means no
|
64
|
+
overlap). Defaults to 0.5.
|
65
|
+
* `distribution` (Symbol/String): How to distribute the radial gradients. Since
|
66
|
+
they can overlap each other, their position on the Z-axis affects the final
|
67
|
+
result. Accepted values are `:scan` (default), `:scan_reverse`, `:suffle`,
|
68
|
+
`:spiral_in` and `:spiral_out`.
|
69
|
+
* `random_offset` (Float): How much to randomly offset the center of each
|
70
|
+
radial gradient, which can create a more natural looking result (0 means no
|
71
|
+
offset). Defaults to 0.5.
|
72
|
+
* `random` (Random instance): The Random instance to use for generating random
|
73
|
+
values, in case you need it to be deterministic.
|
74
|
+
* `adapter` (Class): the adapter to use for reading and processing image files.
|
75
|
+
Supported adapters are `FoggyMirror::ImageMagick` (uses CLI commands) and
|
76
|
+
`FoggyMirror::Vips` (fastest, but requires installing `ruby-vips` gem).
|
77
|
+
* `adapter_options` (Hash): options to pass to the adapter on initialization.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "foggy-mirror"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require "irb"
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/exe/foggy-mirror
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/foggy-mirror/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "foggy-mirror"
|
7
|
+
spec.version = FoggyMirror::VERSION
|
8
|
+
spec.authors = ["Pedro Carbajal"]
|
9
|
+
spec.email = ["pedro_c@beezwax.net"]
|
10
|
+
|
11
|
+
spec.summary = "Tool to create gradient-based blurred versions of raster images"
|
12
|
+
spec.description = "foggy-mirror takes a raster image and outputs a blurred version of it using CSS or SVG radial-gradients"
|
13
|
+
spec.homepage = "https://github.com/beezwax/foggy-mirror"
|
14
|
+
spec.required_ruby_version = ">= 2.4.0"
|
15
|
+
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
17
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
18
|
+
|
19
|
+
# Specify which files should be added to the gem when it is released.
|
20
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
21
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
22
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features|img)/}) }
|
23
|
+
end
|
24
|
+
spec.bindir = "exe"
|
25
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
26
|
+
spec.require_paths = ["lib"]
|
27
|
+
|
28
|
+
spec.add_development_dependency "ruby-vips", "~> 2.1"
|
29
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
30
|
+
spec.add_development_dependency "rspec", "~> 3.1"
|
31
|
+
spec.add_development_dependency "nokogiri"
|
32
|
+
spec.add_development_dependency "pry-byebug"
|
33
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'shellwords'
|
3
|
+
|
4
|
+
module FoggyMirror
|
5
|
+
class ImageMagick
|
6
|
+
def initialize(path, command: 'convert')
|
7
|
+
@path = path
|
8
|
+
@command = command
|
9
|
+
end
|
10
|
+
|
11
|
+
def dominant_color
|
12
|
+
out = `#{@command} #{@path.shellescape} -depth 8 -colors 1 -resize 1x1 txt:-`
|
13
|
+
validate_pixel_enumeration!(out)
|
14
|
+
out.match(HTML_COLOR_MATCHER)[0]
|
15
|
+
end
|
16
|
+
|
17
|
+
def color_samples(res)
|
18
|
+
out = `#{@command} #{@path.shellescape} -depth 8 -colors 256 -resize #{res}x#{res}! txt:-`
|
19
|
+
validate_pixel_enumeration!(out)
|
20
|
+
out.scan(HTML_COLOR_MATCHER)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def validate_pixel_enumeration!(output)
|
26
|
+
raise Error, "convert command didn't return as expected" unless output.start_with?("# ImageMagick pixel")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
begin
|
3
|
+
require 'vips'
|
4
|
+
rescue LoadError
|
5
|
+
raise LoadError, "Couldn't load 'vips' library, do you have ruby-vips in your Gemfile?"
|
6
|
+
end
|
7
|
+
|
8
|
+
module FoggyMirror
|
9
|
+
class Vips
|
10
|
+
include Utils
|
11
|
+
|
12
|
+
def initialize(path)
|
13
|
+
@path = path
|
14
|
+
end
|
15
|
+
|
16
|
+
def dominant_color
|
17
|
+
html_color *color_avg(load_image(DEFAULT_RESOLUTION))
|
18
|
+
end
|
19
|
+
|
20
|
+
def color_samples(res)
|
21
|
+
square_image(res).to_a.flatten(1).map { |rgb| html_color *rgb }
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def square_image(res)
|
27
|
+
# We load the image as a thumbnail, shrinking it to double the resolution
|
28
|
+
# requested, making use of libvips' shrink-on-load optimization, which
|
29
|
+
# should be way faster than loading the entire image and shrinking.
|
30
|
+
#
|
31
|
+
# This optimization assumes the original image isn't more than twice as
|
32
|
+
# wide as it is high, or vice-versa.
|
33
|
+
im = load_image(res * 2)
|
34
|
+
|
35
|
+
# Make the image square
|
36
|
+
if im.width > im.height
|
37
|
+
im = im.reduceh(im.width.to_f / im.height)
|
38
|
+
elsif im.height > im.width
|
39
|
+
im = im.reducev(im.height.to_f / im.width)
|
40
|
+
end
|
41
|
+
|
42
|
+
im.resize(res.to_f / im.width)
|
43
|
+
end
|
44
|
+
|
45
|
+
def load_image(res)
|
46
|
+
::Vips::Image.thumbnail(@path, res)
|
47
|
+
end
|
48
|
+
|
49
|
+
def color_avg(image)
|
50
|
+
image.stats.to_a[1..-1].map { |b| b[4].first.to_i }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
module FoggyMirror
|
5
|
+
class CLI
|
6
|
+
DEFAULT_EXTENSION = '.foggy.svg'
|
7
|
+
|
8
|
+
def initialize(args = ARGV)
|
9
|
+
@args = args.dup
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
@options = {}
|
14
|
+
@extension = DEFAULT_EXTENSION
|
15
|
+
@stdout = false
|
16
|
+
@target_dir = nil
|
17
|
+
|
18
|
+
parser.parse!(@args)
|
19
|
+
|
20
|
+
# OptionParser.parse removes options from the args, so we're left with
|
21
|
+
# filenames
|
22
|
+
@args.each do |path|
|
23
|
+
p = Processor.new(path, **@options)
|
24
|
+
|
25
|
+
unless @stdout
|
26
|
+
foggy_file = File.join(@target_dir || File.dirname(path), File.basename(path, '.*') + @extension)
|
27
|
+
IO.write(foggy_file, p.to_svg)
|
28
|
+
else
|
29
|
+
puts p.to_svg
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def parser
|
37
|
+
@parser ||=
|
38
|
+
OptionParser.new do |opts|
|
39
|
+
opts.banner = 'Usage: foggy-mirror [options] [--] image_file ...'
|
40
|
+
|
41
|
+
opts.on('--res=RESOLUTION', Integer, 'The output resolution (how many radial gradients per dimension)') do |r|
|
42
|
+
@options[:resolution] = r
|
43
|
+
end
|
44
|
+
|
45
|
+
opts.on('--overlap=OVERLAP', Float, 'How much to overlap radial gradients') do |o|
|
46
|
+
@options[:overlap] = o
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.on('--dist=DISTRIBUTION', %w[shuffle spiral_in spiral_out scan scan_reverse], 'Distribution strategy for radial gradients') do |d|
|
50
|
+
@options[:distribution] = d.to_s
|
51
|
+
end
|
52
|
+
|
53
|
+
opts.on('--random-offset=OFFSET', Float, 'Upper limit for how much to randomly offset each radial gradient') do |r|
|
54
|
+
@options[:random_offset] = r.to_f
|
55
|
+
end
|
56
|
+
|
57
|
+
opts.on('--random-seed=SEED', Integer, 'The random seed to use (for deterministic results)') do |s|
|
58
|
+
@options[:random] = Random.new(s)
|
59
|
+
end
|
60
|
+
|
61
|
+
opts.on('--adapter=ADAPTER', ADAPTERS.keys, 'Which graphics library adapter to use') do |a|
|
62
|
+
@options[:adapter] = a
|
63
|
+
end
|
64
|
+
|
65
|
+
opts.on('--extension=EXTENSION', String, "The extension to use for created files (default: #{DEFAULT_EXTENSION})") do |e|
|
66
|
+
@extension = e
|
67
|
+
end
|
68
|
+
|
69
|
+
opts.on('--stdout', 'Output to STDOUT instead of writing to files') do
|
70
|
+
@stdout = true
|
71
|
+
end
|
72
|
+
|
73
|
+
opts.on('--target-dir=DIR', String, 'Directory to write files to (defaults to same as input files)') do |d|
|
74
|
+
@target_dir = d
|
75
|
+
end
|
76
|
+
|
77
|
+
opts.on_tail('-h', '--help', 'Print help') do
|
78
|
+
puts opts
|
79
|
+
exit
|
80
|
+
end
|
81
|
+
|
82
|
+
opts.on_tail("--version", "Show version") do
|
83
|
+
require 'foggy-mirror/version'
|
84
|
+
puts VERSION
|
85
|
+
exit
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module FoggyMirror
|
6
|
+
class Exporter
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegators :@processor, :base_color, :resolution, :blobs
|
10
|
+
|
11
|
+
def initialize(processor)
|
12
|
+
@processor = processor
|
13
|
+
end
|
14
|
+
|
15
|
+
def render
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FoggyMirror
|
4
|
+
class CSS < Exporter
|
5
|
+
def render(hash: false)
|
6
|
+
if hash
|
7
|
+
return {
|
8
|
+
'background-color' => base_color,
|
9
|
+
'background-image' => radial_gradients.join(', ')
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
render(hash: true).map { |k, v| "#{k}: #{v}" }.join(";\n") + ';'
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def radial_gradients
|
19
|
+
blobs.map do |b|
|
20
|
+
x, y, r = [b.x, b.y, b.r].map { |c| (c * 100).round(1) }
|
21
|
+
"radial-gradient(circle at #{x}% #{y}%, #{b.color} 0%, #{b.color}00 #{r}%)"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FoggyMirror
|
4
|
+
class SVG < Exporter
|
5
|
+
XML_HEADER = '<?xml version="1.0" encoding="UTF-8"?>'
|
6
|
+
|
7
|
+
ID_START = 'a'
|
8
|
+
|
9
|
+
VIEWBOX_SIZE = 1000
|
10
|
+
|
11
|
+
def render
|
12
|
+
"#{header}<defs>#{radial_gradients.join}</defs>#{circles.join}</svg>"
|
13
|
+
end
|
14
|
+
|
15
|
+
def header
|
16
|
+
%{#{XML_HEADER}<svg viewBox="0 0 #{VIEWBOX_SIZE} #{VIEWBOX_SIZE}" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#{base_color}">}
|
17
|
+
end
|
18
|
+
|
19
|
+
def radial_gradients
|
20
|
+
color_ids.map do |color, id|
|
21
|
+
%{<radialGradient id="#{id}"><stop offset="0" stop-color="#{color}"/><stop offset="100%" stop-color="#{color}" stop-opacity="0"/></radialGradient>}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def circles
|
26
|
+
blobs.map.with_index do |b, i|
|
27
|
+
id = color_ids[b.color]
|
28
|
+
x, y, r = [b.x, b.y, b.r].map { |c| (c * VIEWBOX_SIZE).to_i }
|
29
|
+
%{<circle cx="#{x}" cy="#{y}" r="#{r}" fill="url(##{id})"/>}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def color_ids
|
34
|
+
@color_ids ||=
|
35
|
+
begin
|
36
|
+
id = String.new(ID_START)
|
37
|
+
Hash[blobs.map(&:color).uniq.map { |c| [c, id.dup].tap { id.succ! } }]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FoggyMirror
|
4
|
+
class Processor
|
5
|
+
attr_reader :image_path, :resolution, :overlap, :random_offset, :random, :adapter
|
6
|
+
|
7
|
+
def initialize(image_path, resolution: DEFAULT_RESOLUTION, overlap: 0.5, distribution: nil, random_offset: 0.5, random: Random.new, adapter: default_adapter, adapter_options: {})
|
8
|
+
@image_path = image_path
|
9
|
+
|
10
|
+
@resolution = resolution
|
11
|
+
@overlap = overlap
|
12
|
+
@distribution = distribution
|
13
|
+
@random_offset = random_offset
|
14
|
+
|
15
|
+
@random = random
|
16
|
+
@adapter = resolve_adapter(adapter).new(image_path, **adapter_options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def base_color
|
20
|
+
@base_color ||= adapter.dominant_color
|
21
|
+
end
|
22
|
+
|
23
|
+
def color_samples(res)
|
24
|
+
@color_samples ||= adapter.color_samples(res)
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_css(hash: false)
|
28
|
+
CSS.new(self).render(hash: hash)
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_svg
|
32
|
+
SVG.new(self).render
|
33
|
+
end
|
34
|
+
|
35
|
+
def blobs
|
36
|
+
samples = color_samples(resolution)
|
37
|
+
|
38
|
+
increment = 1.0 / (resolution - 1)
|
39
|
+
|
40
|
+
blobs = resolution.times.with_object([]) do |y, blobs|
|
41
|
+
resolution.times do |x|
|
42
|
+
xp = x * increment + increment * random_offset * (random.rand - 0.5)
|
43
|
+
yp = y * increment + increment * random_offset * (random.rand - 0.5)
|
44
|
+
r = increment * (1 + overlap)
|
45
|
+
color = samples[y * resolution + x]
|
46
|
+
|
47
|
+
blobs << Blob.new(x: xp, y: yp, r: r, color: color)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
case @distribution.to_s.downcase
|
52
|
+
when 'shuffle'
|
53
|
+
blobs.shuffle(random: random)
|
54
|
+
when 'spiral_in'
|
55
|
+
spiral_in(blobs)
|
56
|
+
when 'spiral_out'
|
57
|
+
spiral_in(blobs).reverse
|
58
|
+
when 'scan_reverse'
|
59
|
+
blobs.reverse
|
60
|
+
else
|
61
|
+
blobs
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def spiral_in(array)
|
68
|
+
matrix = array.each_slice(Math.sqrt(array.size).to_i).to_a
|
69
|
+
|
70
|
+
actions = [
|
71
|
+
-> { matrix.shift }, # top
|
72
|
+
-> { matrix.map { |f| f.pop } }, # right
|
73
|
+
-> { matrix.pop.reverse }, # bottom
|
74
|
+
-> { matrix.map { |f| f.shift }.reverse } # left
|
75
|
+
]
|
76
|
+
|
77
|
+
peel = actions.cycle
|
78
|
+
|
79
|
+
[].tap do |r|
|
80
|
+
r.concat(peel.next.call) until matrix.empty?
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def default_adapter
|
85
|
+
require 'vips'
|
86
|
+
Vips
|
87
|
+
rescue LoadError
|
88
|
+
ImageMagick
|
89
|
+
end
|
90
|
+
|
91
|
+
def resolve_adapter(adapter)
|
92
|
+
if adapter.kind_of?(Symbol) || adapter.kind_of?(String)
|
93
|
+
return ADAPTERS.fetch(adapter.to_sym).call
|
94
|
+
end
|
95
|
+
|
96
|
+
return adapter if adapter.kind_of?(Class)
|
97
|
+
|
98
|
+
raise Error, "`adapter' must be an adapter class or an adapter name (#{ADAPTERS.keys.join(', ')})"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/lib/foggy-mirror.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FoggyMirror
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
HTML_COLOR_MATCHER = /#[A-F0-9]{6}/.freeze
|
7
|
+
|
8
|
+
DEFAULT_RESOLUTION = 5
|
9
|
+
|
10
|
+
ADAPTERS = {
|
11
|
+
vips: -> { FoggyMirror::Vips },
|
12
|
+
magick: -> { FoggyMirror::ImageMagick }
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
Blob = Struct.new(:x, :y, :r, :color, keyword_init: true)
|
16
|
+
|
17
|
+
autoload :Processor, 'foggy-mirror/processor'
|
18
|
+
autoload :CLI, 'foggy-mirror/cli'
|
19
|
+
autoload :Utils, 'foggy-mirror/utils'
|
20
|
+
autoload :ImageMagick, 'foggy-mirror/adapters/image_magick'
|
21
|
+
autoload :Vips, 'foggy-mirror/adapters/vips'
|
22
|
+
autoload :Exporter, 'foggy-mirror/exporter'
|
23
|
+
autoload :CSS, 'foggy-mirror/exporters/css'
|
24
|
+
autoload :SVG, 'foggy-mirror/exporters/svg'
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: foggy-mirror
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Pedro Carbajal
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-03-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: ruby-vips
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.1'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '13.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '13.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.1'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.1'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: nokogiri
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry-byebug
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: foggy-mirror takes a raster image and outputs a blurred version of it
|
84
|
+
using CSS or SVG radial-gradients
|
85
|
+
email:
|
86
|
+
- pedro_c@beezwax.net
|
87
|
+
executables:
|
88
|
+
- foggy-mirror
|
89
|
+
extensions: []
|
90
|
+
extra_rdoc_files: []
|
91
|
+
files:
|
92
|
+
- ".github/workflows/main.yml"
|
93
|
+
- ".gitignore"
|
94
|
+
- ".rspec"
|
95
|
+
- Gemfile
|
96
|
+
- Gemfile.lock
|
97
|
+
- LICENSE.txt
|
98
|
+
- README.md
|
99
|
+
- Rakefile
|
100
|
+
- bin/console
|
101
|
+
- bin/setup
|
102
|
+
- exe/foggy-mirror
|
103
|
+
- foggy-mirror.gemspec
|
104
|
+
- lib/foggy-mirror.rb
|
105
|
+
- lib/foggy-mirror/adapters/image_magick.rb
|
106
|
+
- lib/foggy-mirror/adapters/vips.rb
|
107
|
+
- lib/foggy-mirror/cli.rb
|
108
|
+
- lib/foggy-mirror/exporter.rb
|
109
|
+
- lib/foggy-mirror/exporters/css.rb
|
110
|
+
- lib/foggy-mirror/exporters/svg.rb
|
111
|
+
- lib/foggy-mirror/processor.rb
|
112
|
+
- lib/foggy-mirror/utils.rb
|
113
|
+
- lib/foggy-mirror/version.rb
|
114
|
+
homepage: https://github.com/beezwax/foggy-mirror
|
115
|
+
licenses: []
|
116
|
+
metadata:
|
117
|
+
homepage_uri: https://github.com/beezwax/foggy-mirror
|
118
|
+
source_code_uri: https://github.com/beezwax/foggy-mirror
|
119
|
+
post_install_message:
|
120
|
+
rdoc_options: []
|
121
|
+
require_paths:
|
122
|
+
- lib
|
123
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: 2.4.0
|
128
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
requirements: []
|
134
|
+
rubygems_version: 3.3.3
|
135
|
+
signing_key:
|
136
|
+
specification_version: 4
|
137
|
+
summary: Tool to create gradient-based blurred versions of raster images
|
138
|
+
test_files: []
|