├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── .rspec ├── .simplecov ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── fixtures └── generated_patterns │ ├── chevrons.svg │ ├── concentric_circles.svg │ ├── diamonds.svg │ ├── diamonds_with_base_color.svg │ ├── diamonds_with_color.svg │ ├── hexagons.svg │ ├── mosaic_squares.svg │ ├── nested_squares.svg │ ├── octagons.svg │ ├── overlapping_circles.svg │ ├── overlapping_rings.svg │ ├── plaid.svg │ ├── plus_signs.svg │ ├── sine_waves.svg │ ├── squares.svg │ ├── tessellation.svg │ ├── triangles.svg │ └── xes.svg ├── geo_pattern.gemspec ├── lib ├── geo_pattern.rb └── geo_pattern │ ├── background.rb │ ├── background_generators │ └── solid_generator.rb │ ├── color.rb │ ├── color_generators │ ├── base_color_generator.rb │ └── simple_generator.rb │ ├── color_preset.rb │ ├── errors.rb │ ├── geo_pattern_task.rb │ ├── helpers.rb │ ├── pattern.rb │ ├── pattern_generator.rb │ ├── pattern_helpers.rb │ ├── pattern_preset.rb │ ├── pattern_sieve.rb │ ├── pattern_store.rb │ ├── pattern_validator.rb │ ├── rake_task.rb │ ├── roles │ ├── comparable_metadata.rb │ └── named_generator.rb │ ├── seed.rb │ ├── structure.rb │ ├── structure_generators │ ├── base_generator.rb │ ├── chevrons_generator.rb │ ├── concentric_circles_generator.rb │ ├── diamonds_generator.rb │ ├── hexagons_generator.rb │ ├── mosaic_squares_generator.rb │ ├── nested_squares_generator.rb │ ├── octagons_generator.rb │ ├── overlapping_circles_generator.rb │ ├── overlapping_rings_generator.rb │ ├── plaid_generator.rb │ ├── plus_signs_generator.rb │ ├── sine_waves_generator.rb │ ├── squares_generator.rb │ ├── tessellation_generator.rb │ ├── triangles_generator.rb │ └── xes_generator.rb │ ├── svg_image.rb │ └── version.rb ├── script ├── bootstrap ├── console └── test └── spec ├── background_generators └── solid_generator_spec.rb ├── background_spec.rb ├── color_generators ├── base_color_generator_spec.rb └── simple_generator_spec.rb ├── color_preset_spec.rb ├── color_spec.rb ├── geo_pattern_spec.rb ├── helpers_spec.rb ├── pattern_preset_spec.rb ├── pattern_sieve_spec.rb ├── pattern_spec.rb ├── pattern_store_spec.rb ├── pattern_validator_spec.rb ├── seed_spec.rb ├── spec_helper.rb ├── structure_generators ├── chevrons_generator_spec.rb ├── concentric_circles_generator_spec.rb ├── diamonds_generator_spec.rb ├── hexagons_generator_spec.rb ├── mosaic_squares_generator_spec.rb ├── nested_squares_generator_spec.rb ├── octagons_generator_spec.rb ├── overlapping_circles_generator_spec.rb ├── overlapping_rings_generator_spec.rb ├── plaid_generator_spec.rb ├── plus_signs_generator_spec.rb ├── sine_waves_generator_spec.rb ├── squares_generator_spec.rb ├── tessellation_generator_spec.rb ├── triangles_generator_spec.rb └── xes_generator_spec.rb ├── structure_spec.rb ├── support ├── aruba.rb ├── helpers │ └── fixtures.rb ├── kernel.rb ├── matchers │ ├── image.rb │ └── name.rb ├── rspec.rb ├── shared_examples │ ├── generator.rb │ ├── pattern.rb │ ├── pattern_name.rb │ └── structure.rb └── string.rb └── svg_spec.rb /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | # Due to https://github.com/actions/runner/issues/849, we have to use quotes for '3.0' 15 | ruby: [2.6, 2.7, '3.0', 3.1, 3.2, head] 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | 22 | - name: Set up Ruby 23 | uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: ${{ matrix.ruby }} 26 | rubygems: latest 27 | bundler-cache: true 28 | 29 | - name: Run tests 30 | run: bundle exec rake 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | test.rb 19 | .ruby-version 20 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format Fuubar 2 | --order rand 3 | --color 4 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | class ExcludeRegexFilter < SimpleCov::Filter 5 | def matches?(source_file) 6 | source_file.filename !~ filter_argument 7 | end 8 | end 9 | 10 | class IncludeRegexFilter < SimpleCov::Filter 11 | def matches?(source_file) 12 | source_file.filename =~ filter_argument 13 | end 14 | end 15 | 16 | SimpleCov.start do 17 | add_filter "/features/" 18 | add_filter "/fixtures/" 19 | add_filter "/spec/" 20 | add_filter "/tmp" 21 | add_filter "/vendor" 22 | 23 | generator_filter = %r{/background_generators/|/structure_generators/} 24 | add_group "lib", ExcludeRegexFilter.new(generator_filter) 25 | add_group "generators", IncludeRegexFilter.new(generator_filter) 26 | end 27 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in geopatterns.gemspec 6 | gemspec 7 | 8 | group :development, :test do 9 | gem "activesupport", "~> 6" 10 | gem "aruba" 11 | gem "fuubar" 12 | gem "inch" 13 | gem "pry" 14 | gem "pry-rescue" 15 | gem "pry-stack_explorer" 16 | gem "rake" 17 | gem "rspec" 18 | gem "standard" 19 | gem "simplecov" 20 | 21 | gem "byebug" 22 | gem "irb" 23 | gem "pry-byebug" 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 GitHub, Inc. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### This project is largely unmaintained now. 2 | 3 | #### I'll happily accept PRs to keep things in working order, but I no longer plan to make updates. 4 | 5 | ---- 6 | 7 | [![](https://img.shields.io/gem/v/geo_pattern.svg?style=flat)](http://rubygems.org/gems/geo_pattern) 8 | ![Ruby](https://github.com/jasonlong/geo_pattern/workflows/Ruby/badge.svg) 9 | [![](https://img.shields.io/gem/dt/geo_pattern.svg?style=flat)](http://rubygems.org/gems/geo_pattern) 10 | [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard) 11 | 12 | # GeoPattern 13 | 14 | Generate beautiful tiling SVG patterns from a string. The string is converted 15 | into a SHA and a color and pattern are determined based on the values in the 16 | hash. The color is determined by shifting the hue and saturation from a default 17 | (or passed in) base color. One of 16 patterns is used (or you can specify one) 18 | and the sizing of the pattern elements is also determined by the hash values. 19 | 20 | You can use the generated pattern as the `background-image` for a container. 21 | Using the `base64` representation of the pattern still results in SVG 22 | rendering, so it looks great on retina displays. 23 | 24 | See the [GitHub Guides](https://guides.github.com/) site and the [Explore section 25 | of GitHub](https://github.com/explore) are examples of this library in action. 26 | Brandon Mills has put together an awesome [live preview 27 | page](http://btmills.github.io/geopattern/geopattern.html) that's built on his 28 | Javascript port. 29 | 30 | ## Installation 31 | 32 | **Note:** as of version `1.4.0`, Ruby version 2 or greater is required. 33 | 34 | Add this line to your application's Gemfile: 35 | 36 | gem 'geo_pattern' 37 | 38 | And then execute: 39 | 40 | $ bundle 41 | 42 | Or install it yourself as: 43 | 44 | $ gem install geo_pattern 45 | 46 | ## Usage 47 | 48 | Make a new pattern: 49 | 50 | ```ruby 51 | pattern = GeoPattern.generate('Mastering Markdown') 52 | ``` 53 | 54 | To specify a base background color (with a hue and saturation that adjusts depending on the string): 55 | 56 | ```ruby 57 | pattern = GeoPattern.generate('Mastering Markdown', base_color: '#fc0') 58 | ``` 59 | 60 | To use a specific background color (w/o any hue or saturation adjustments): 61 | 62 | ```ruby 63 | pattern = GeoPattern.generate('Mastering Markdown', color: '#fc0') 64 | ``` 65 | 66 | To use a specific [pattern generator](#available-patterns): 67 | 68 | ```ruby 69 | pattern = GeoPattern.generate('Mastering Markdown', patterns: :sine_waves) 70 | ``` 71 | 72 | To use a subset of the [available patterns](#available-patterns): 73 | 74 | ```ruby 75 | pattern = GeoPattern.generate('Mastering Markdown', patterns: [:sine_waves, :xes]) 76 | ``` 77 | 78 | Get the SVG string: 79 | 80 | ```ruby 81 | puts pattern.to_svg 82 | # => PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC... 90 | ``` 91 | 92 | You can then use this string to set the background: 93 | 94 | ```html 95 |
96 | ``` 97 | 98 | ## Available patterns 99 | 100 | *Note: As of version `1.3.0`, string references (e.g. `overlapping_circles`) 101 | are deprecated in favor of symbol references (e.g. `:overlapping_circles`).* 102 | 103 | ### :chevrons 104 | 105 | ![](http://jasonlong.github.io/geo_pattern/examples/chevrons.png) 106 | 107 | 108 | ### :octagons 109 | 110 | ![](http://jasonlong.github.io/geo_pattern/examples/octogons.png) 111 | 112 | ### :overlapping_circles 113 | 114 | ![](http://jasonlong.github.io/geo_pattern/examples/overlapping_circles.png) 115 | 116 | ### :plus_signs 117 | 118 | ![](http://jasonlong.github.io/geo_pattern/examples/plus_signs.png) 119 | 120 | ### :xes 121 | 122 | ![](http://jasonlong.github.io/geo_pattern/examples/xes.png) 123 | 124 | ### :sine_waves 125 | 126 | ![](http://jasonlong.github.io/geo_pattern/examples/sine_waves.png) 127 | 128 | ### :hexagons 129 | 130 | ![](http://jasonlong.github.io/geo_pattern/examples/hexagons.png) 131 | 132 | ### :overlapping_rings 133 | 134 | ![](http://jasonlong.github.io/geo_pattern/examples/overlapping_rings.png) 135 | 136 | ### :plaid 137 | 138 | ![](http://jasonlong.github.io/geo_pattern/examples/plaid.png) 139 | 140 | ### :triangles 141 | 142 | ![](http://jasonlong.github.io/geo_pattern/examples/triangles.png) 143 | 144 | ### :squares 145 | 146 | ![](http://jasonlong.github.io/geo_pattern/examples/squares.png) 147 | 148 | ### :nested_squares 149 | 150 | ![](http://jasonlong.github.io/geo_pattern/examples/nested_squares.png) 151 | 152 | ### :mosaic_squares 153 | 154 | ![](http://jasonlong.github.io/geo_pattern/examples/mosaic_squares.png) 155 | 156 | ### :concentric_circles 157 | 158 | ![](http://jasonlong.github.io/geo_pattern/examples/concentric_circles.png) 159 | 160 | ### :diamonds 161 | 162 | ![](http://jasonlong.github.io/geo_pattern/examples/diamonds.png) 163 | 164 | ### :tessellation 165 | 166 | ![](http://jasonlong.github.io/geo_pattern/examples/tessellation.png) 167 | 168 | 169 | ## Inspection of pattern 170 | 171 | If you want to get some more information about a pattern, please use the 172 | following methods. 173 | 174 | ```ruby 175 | pattern = GeoPattern.generate('Mastering Markdown', patterns: [:sine_waves, :xes]) 176 | 177 | # The color of the background in html notation 178 | pattern.background.color.to_html 179 | 180 | # The color of the background in svg notation 181 | pattern.background.color.to_svg 182 | 183 | 184 | # The input colors 185 | pattern.background.preset.color 186 | pattern.background.preset.base_color 187 | 188 | # The generator 189 | pattern.background.generator 190 | ``` 191 | 192 | To get more information about the structure of the pattern, please use the following methods: 193 | 194 | ```ruby 195 | pattern = GeoPattern.generate('Mastering Markdown', patterns: [:sine_waves, :xes]) 196 | 197 | # The name of the structure 198 | pattern.structure.name 199 | 200 | # The generator of the structure 201 | pattern.structure.generator 202 | ``` 203 | 204 | ## Rake Support 205 | 206 | ```ruby 207 | string = 'Mastering markdown' 208 | 209 | require 'geo_pattern/geo_pattern_task' 210 | 211 | GeoPattern::GeoPatternTask.new( 212 | name: 'generate', 213 | description: 'Generate patterns to make them available as fixtures', 214 | data: { 215 | 'fixtures/generated_patterns/diamonds_with_color.svg' => { input: string, patterns: [:diamonds], color: '#00ff00' }, 216 | 'fixtures/generated_patterns/diamonds_with_base_color.svg' => { input: string, patterns: [:diamonds], base_color: '#00ff00' } 217 | } 218 | ) 219 | ``` 220 | 221 | ## Developing 222 | 223 | ### Generate Fixtures 224 | 225 | ```ruby 226 | rake fixtures:generate 227 | ``` 228 | 229 | ### Run tests 230 | 231 | ```ruby 232 | rake test 233 | ``` 234 | 235 | ## Contributing 236 | 237 | 1. Fork it ( https://github.com/jasonlong/geo_pattern/fork ) 238 | 2. Create your feature branch (`git checkout -b my-new-feature`) 239 | 3. Commit your changes (`git commit -am 'Add some feature'`) 240 | 4. Push to the branch (`git push origin my-new-feature`) 241 | 5. Create new Pull Request 242 | 243 | ## Development 244 | 245 | Prefix rspec-commandline with `RSPEC_PROFILE=1` to output the ten slowest 246 | examples of the test suite. 247 | 248 | ```bash 249 | RSPEC_PROFILE=1 bundle exec rspec 250 | ``` 251 | 252 | ## Ports & related projects 253 | 254 | JavaScript port by Brandon Mills: 255 | https://github.com/btmills/geopattern 256 | 257 | TypeScript port by MooYeol Lee: 258 | https://github.com/mooyoul/geo-pattern 259 | 260 | Python port by Bryan Veloso: 261 | https://github.com/bryanveloso/geopatterns 262 | 263 | Elixir port by Anne Johnson: 264 | https://github.com/annejohnson/geo_pattern 265 | 266 | PHP port by Anand Capur: 267 | https://github.com/redeyeventures/geopattern-php 268 | 269 | Go port by Pravendra Singh: 270 | https://github.com/pravj/geopattern 271 | 272 | CoffeeScript port by Aleks (muchweb): 273 | https://github.com/muchweb/geo-pattern-coffee 274 | 275 | Cocoa port by Matt Faluotico: 276 | https://github.com/mattfxyz/GeoPattern-Cocoa 277 | 278 | Middleman extension by @maxmeyer: 279 | https://github.com/fedux-org/middleman-geo_pattern 280 | 281 | Dart(Flutter) port by @suyash: 282 | https://github.com/suyash/geopattern 283 | 284 | Lua port by Ivan Azoyan: 285 | https://github.com/azoyan/geopattern 286 | 287 | Java port by Jason Selzer: 288 | https://github.com/jselzer/geopattern 289 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path("lib", __dir__) 4 | 5 | desc "Default task running Tests" 6 | task default: :test 7 | 8 | desc "Run test suite" 9 | task test: ["test:standard", "test:rspec"] 10 | task "test:ci" => ["bootstrap:gem_requirements", :test] 11 | namespace :test do 12 | task :rspec do 13 | sh "rspec" 14 | end 15 | 16 | task "inch" do 17 | sh "inch" 18 | end 19 | 20 | require "standard/rake" 21 | end 22 | 23 | namespace :gem do 24 | require "bundler/gem_tasks" 25 | end 26 | 27 | unless ENV.key?("CI") 28 | require "geo_pattern/geo_pattern_task" 29 | 30 | namespace :fixtures do 31 | string = "Mastering Markdown" 32 | 33 | GeoPattern::GeoPatternTask.new( 34 | name: "generate", 35 | description: "Generate patterns to make them available as fixtures", 36 | data: { 37 | "fixtures/generated_patterns/chevrons.svg" => {input: string, patterns: [:chevrons]}, 38 | "fixtures/generated_patterns/concentric_circles.svg" => {input: string, patterns: [:concentric_circles]}, 39 | "fixtures/generated_patterns/diamonds.svg" => {input: string, patterns: [:diamonds]}, 40 | "fixtures/generated_patterns/hexagons.svg" => {input: string, patterns: [:hexagons]}, 41 | "fixtures/generated_patterns/mosaic_squares.svg" => {input: string, patterns: [:mosaic_squares]}, 42 | "fixtures/generated_patterns/nested_squares.svg" => {input: string, patterns: [:nested_squares]}, 43 | "fixtures/generated_patterns/octagons.svg" => {input: string, patterns: [:octagons]}, 44 | "fixtures/generated_patterns/overlapping_circles.svg" => {input: string, patterns: [:overlapping_circles]}, 45 | "fixtures/generated_patterns/overlapping_rings.svg" => {input: string, patterns: [:overlapping_rings]}, 46 | "fixtures/generated_patterns/plaid.svg" => {input: string, patterns: [:plaid]}, 47 | "fixtures/generated_patterns/plus_signs.svg" => {input: string, patterns: [:plus_signs]}, 48 | "fixtures/generated_patterns/sine_waves.svg" => {input: string, patterns: [:sine_waves]}, 49 | "fixtures/generated_patterns/squares.svg" => {input: string, patterns: [:squares]}, 50 | "fixtures/generated_patterns/tessellation.svg" => {input: string, patterns: [:tessellation]}, 51 | "fixtures/generated_patterns/triangles.svg" => {input: string, patterns: [:triangles]}, 52 | "fixtures/generated_patterns/xes.svg" => {input: string, patterns: [:xes]}, 53 | "fixtures/generated_patterns/diamonds_with_color.svg" => {input: string, patterns: [:diamonds], color: "#00ff00"}, 54 | "fixtures/generated_patterns/diamonds_with_base_color.svg" => {input: string, patterns: [:diamonds], base_color: "#00ff00"} 55 | } 56 | ) 57 | end 58 | end 59 | 60 | desc "Bootstrap project" 61 | task bootstrap: %w[bootstrap:bundler] 62 | 63 | desc "Bootstrap project for ci" 64 | task "bootstrap:ci" do 65 | Rake::Task["bootstrap"].invoke 66 | end 67 | 68 | namespace :bootstrap do 69 | desc "Bootstrap bundler" 70 | task :bundler do |t| 71 | puts t.comment 72 | sh "gem install bundler" 73 | sh "bundle install" 74 | end 75 | 76 | desc "Require gems" 77 | task :gem_requirements do |t| 78 | puts t.comment 79 | require "bundler" 80 | Bundler.require 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /fixtures/generated_patterns/concentric_circles.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/generated_patterns/octagons.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/generated_patterns/overlapping_circles.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/generated_patterns/overlapping_rings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/generated_patterns/plaid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/generated_patterns/plus_signs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/generated_patterns/squares.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/generated_patterns/tessellation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/generated_patterns/triangles.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/generated_patterns/xes.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /geo_pattern.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path("lib", __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require "geo_pattern/version" 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "geo_pattern" 9 | spec.version = GeoPattern::VERSION 10 | spec.authors = ["Jason Long"] 11 | spec.email = ["jason@jasonlong.me"] 12 | spec.summary = "Generate SVG beautiful patterns" 13 | spec.description = "Generate SVG beautiful patterns" 14 | spec.homepage = "https://github.com/jasonlong/geo_pattern" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0") 18 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 19 | spec.require_paths = ["lib"] 20 | 21 | spec.required_ruby_version = ">= 2.6" 22 | 23 | spec.add_dependency "color", "~> 1.5" 24 | spec.add_development_dependency "bundler", "~> 2.2" 25 | end 26 | -------------------------------------------------------------------------------- /lib/geo_pattern.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "base64" 4 | require "digest/sha1" 5 | require "color" 6 | require "forwardable" 7 | 8 | require "geo_pattern/version" 9 | 10 | require "geo_pattern/roles/named_generator" 11 | require "geo_pattern/roles/comparable_metadata" 12 | 13 | require "geo_pattern/errors" 14 | require "geo_pattern/color" 15 | require "geo_pattern/svg_image" 16 | require "geo_pattern/pattern_helpers" 17 | require "geo_pattern/helpers" 18 | require "geo_pattern/pattern_store" 19 | require "geo_pattern/pattern_validator" 20 | require "geo_pattern/pattern_sieve" 21 | require "geo_pattern/pattern" 22 | require "geo_pattern/seed" 23 | require "geo_pattern/pattern_preset" 24 | require "geo_pattern/color_preset" 25 | 26 | require "geo_pattern/structure" 27 | require "geo_pattern/background" 28 | 29 | require "geo_pattern/color_generators/simple_generator" 30 | require "geo_pattern/color_generators/base_color_generator" 31 | 32 | require "geo_pattern/background_generators/solid_generator" 33 | 34 | require "geo_pattern/structure_generators/base_generator" 35 | require "geo_pattern/structure_generators/chevrons_generator" 36 | require "geo_pattern/structure_generators/concentric_circles_generator" 37 | require "geo_pattern/structure_generators/diamonds_generator" 38 | require "geo_pattern/structure_generators/hexagons_generator" 39 | require "geo_pattern/structure_generators/mosaic_squares_generator" 40 | require "geo_pattern/structure_generators/nested_squares_generator" 41 | require "geo_pattern/structure_generators/octagons_generator" 42 | require "geo_pattern/structure_generators/overlapping_circles_generator" 43 | require "geo_pattern/structure_generators/overlapping_rings_generator" 44 | require "geo_pattern/structure_generators/plaid_generator" 45 | require "geo_pattern/structure_generators/plus_signs_generator" 46 | require "geo_pattern/structure_generators/sine_waves_generator" 47 | require "geo_pattern/structure_generators/squares_generator" 48 | require "geo_pattern/structure_generators/tessellation_generator" 49 | require "geo_pattern/structure_generators/triangles_generator" 50 | require "geo_pattern/structure_generators/xes_generator" 51 | 52 | require "geo_pattern/pattern_generator" 53 | 54 | module GeoPattern 55 | def self.generate(string = Time.now, opts = {}) 56 | GeoPattern::PatternGenerator.new(string.to_s, **opts).generate 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/geo_pattern/background.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | class Background 5 | include Roles::ComparableMetadata 6 | 7 | extend Forwardable 8 | 9 | attr_reader :image, :preset, :color, :generator 10 | 11 | def_delegators :@preset, :base_color 12 | 13 | def initialize(options) 14 | @image = options[:image] 15 | @preset = options[:preset] 16 | @color = options[:color] 17 | @generator = options[:generator] 18 | 19 | raise ArgumentError, "Argument color is missing" if @color.nil? 20 | raise ArgumentError, "Argument image is missing" if @image.nil? 21 | raise ArgumentError, "Argument preset is missing" if @preset.nil? 22 | raise ArgumentError, "Argument generator is missing" if @generator.nil? 23 | end 24 | 25 | def_comparators :base_color, :color 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/geo_pattern/background_generators/solid_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | # Generating backgrounds 5 | module BackgroundGenerators 6 | # Generating a solid background 7 | class SolidGenerator 8 | include Roles::NamedGenerator 9 | 10 | private 11 | 12 | attr_reader :color, :seed, :preset 13 | 14 | public 15 | 16 | # New generator 17 | # 18 | # @param [Seed] seed 19 | # The seed used during generation the background 20 | # 21 | # @param [ColorPreset] preset 22 | # A preset of values which are used during generating the background 23 | def initialize(seed, preset) 24 | @color = color_for(seed, preset) 25 | @preset = preset 26 | end 27 | 28 | # Generate the background for pattern 29 | # 30 | # @param [#background=] pattern 31 | # The pattern for which the background should be generated 32 | def generate(pattern) 33 | pattern.background = Background.new(image: generate_background, preset: preset, color: color, generator: self.class) 34 | 35 | self 36 | end 37 | 38 | private 39 | 40 | def generate_background 41 | svg = SvgImage.new 42 | svg.rect(0, 0, "100%", "100%", "fill" => color.to_svg) 43 | 44 | svg 45 | end 46 | 47 | def color_for(seed, preset) 48 | return ColorGenerators::BaseColorGenerator.new(preset.base_color, seed).generate if preset.mode? :base_color 49 | 50 | ColorGenerators::SimpleGenerator.new(preset.color).generate 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/geo_pattern/color.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | class Color 5 | private 6 | 7 | attr_accessor :color 8 | 9 | public 10 | 11 | def initialize(html_color) 12 | @color = ::Color::RGB.from_html html_color 13 | end 14 | 15 | def to_svg 16 | r = (color.r * 255).round 17 | g = (color.g * 255).round 18 | b = (color.b * 255).round 19 | 20 | format("rgb(%d, %d, %d)", r: r, g: g, b: b) 21 | end 22 | 23 | def to_html 24 | color.html 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/geo_pattern/color_generators/base_color_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | # Color generators 5 | module ColorGenerators 6 | # Generate color based on Base Color and seed 7 | class BaseColorGenerator 8 | private 9 | 10 | attr_reader :creator, :seed, :color 11 | 12 | public 13 | 14 | # New 15 | # 16 | # @param [String] color 17 | # HTML color string, #0a0a0a 18 | def initialize(color, seed, creator = Color) 19 | @color = color 20 | @seed = seed 21 | @creator = creator 22 | end 23 | 24 | # Generator color 25 | def generate 26 | creator.new(transform(color, seed)) 27 | end 28 | 29 | private 30 | 31 | def transform(color, seed) 32 | hue_offset = map(seed.to_i(14, 3), 0, 4095, 0, 359) 33 | sat_offset = seed.to_i(17, 1) 34 | new_color = ::Color::RGB.from_html(color).to_hsl 35 | new_color.hue = new_color.hue - hue_offset 36 | 37 | new_color.saturation = if sat_offset % 2 == 0 38 | new_color.saturation + sat_offset 39 | else 40 | new_color.saturation - sat_offset 41 | end 42 | new_color.html 43 | end 44 | 45 | # Ruby implementation of Processing's map function 46 | # http://processing.org/reference/map_.html 47 | # v for value, d for desired 48 | def map(value, v_min, v_max, d_min, d_max) 49 | v_value = value.to_f # so it returns float 50 | 51 | v_range = v_max - v_min 52 | d_range = d_max - d_min 53 | (v_value - v_min) * d_range / v_range + d_min 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/geo_pattern/color_generators/simple_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | # Color generators 5 | module ColorGenerators 6 | # Simple one 7 | class SimpleGenerator 8 | private 9 | 10 | attr_reader :color, :creator 11 | 12 | public 13 | 14 | # New 15 | # 16 | # @param [String] color 17 | # HTML color string, #0a0a0a 18 | def initialize(color, creator = Color) 19 | @color = color 20 | @creator = creator 21 | end 22 | 23 | # Generator color 24 | def generate 25 | creator.new(color) 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/geo_pattern/color_preset.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # rubocop:disable Lint/DuplicateMethods 4 | 5 | module GeoPattern 6 | class ColorPreset 7 | attr_accessor :color, :base_color, :mode 8 | 9 | def initialize(color: nil, base_color: nil) 10 | @color = color 11 | @base_color = base_color 12 | end 13 | 14 | # Return mode 15 | # 16 | # @return [Symbol] 17 | # The color mode 18 | def mode 19 | if color.nil? || color.empty? 20 | :base_color 21 | else 22 | :color 23 | end 24 | end 25 | 26 | def mode?(m) 27 | mode == m 28 | end 29 | end 30 | end 31 | 32 | # rubocop:enable Lint/DuplicateMethods 33 | -------------------------------------------------------------------------------- /lib/geo_pattern/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | # user errors 5 | class UserError < StandardError; end 6 | 7 | # raised if an invalid pattern is requested 8 | class InvalidPatternError < UserError; end 9 | end 10 | -------------------------------------------------------------------------------- /lib/geo_pattern/geo_pattern_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "geo_pattern" 4 | require "geo_pattern/rake_task" 5 | 6 | module GeoPattern 7 | # GeoPatternTask 8 | # 9 | # @see Rakefile 10 | class GeoPatternTask < RakeTask 11 | # @!attribute [r] data 12 | # The data which should be used to generate patterns 13 | attr_reader :data 14 | 15 | # @!attribute [r] allowed_patterns 16 | # The the patterns which are allowed to be used 17 | attr_reader :allowed_patterns 18 | 19 | # Create a new geo pattern task 20 | # 21 | # @param [Hash] data 22 | # The data which should be used to generate patterns 23 | # 24 | # @param [Array] allowed_patterns 25 | # The allowed_patterns 26 | # 27 | # @see RakeTask 28 | # For other arguments accepted 29 | def initialize(opts = {}) 30 | super 31 | 32 | raise ArgumentError, :data if @options[:data].nil? 33 | 34 | @data = @options[:data] 35 | @allowed_patterns = @options[:allowed_patterns] 36 | end 37 | 38 | # @private 39 | def run_task(_verbose) 40 | data.each do |path, string| 41 | opts = {} 42 | path = File.expand_path(path) 43 | 44 | if string.is_a?(Hash) 45 | input = string[:input] 46 | opts[:patterns] = string[:patterns] if string.key? :patterns 47 | opts[:color] = string[:color] if string.key? :color 48 | opts[:base_color] = string[:base_color] if string.key? :base_color 49 | else 50 | raise "Invalid data structure for Rake Task" 51 | end 52 | 53 | pattern = GeoPattern.generate(input, opts) 54 | 55 | logger.info "Creating pattern at \"#{path}\"." 56 | FileUtils.mkdir_p File.dirname(path) 57 | File.write(path, pattern.to_svg) 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/geo_pattern/helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | module Helpers 5 | def require_files_matching_pattern(pattern) 6 | Dir.glob(pattern).sort.each { |f| require_relative f } 7 | end 8 | 9 | # Makes an underscored, lowercase form from the expression in the string. 10 | # 11 | # @see ActiveSupport 12 | # It's MIT-Licensed 13 | # 14 | def underscore(camel_cased_word) 15 | return camel_cased_word unless /[A-Z-]/.match?(camel_cased_word) 16 | 17 | word = camel_cased_word.to_s.dup 18 | 19 | word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2') 20 | word.gsub!(/([a-z\d])([A-Z])/, '\1_\2') 21 | 22 | word.downcase! 23 | 24 | word 25 | end 26 | 27 | # Removes the module part from the expression in the string. 28 | # 29 | # @see ActiveSupport 30 | # It's MIT-Licensed 31 | # 32 | # @exmple Use demodulize 33 | # 34 | # 'ActiveRecord::CoreExtensions::String::Inflections'.demodulize # => "Inflections" 35 | # 'Inflections'.demodulize # => "Inflections" 36 | # '::Inflections'.demodulize # => "Inflections" 37 | # ''.demodulize # => "" 38 | # 39 | # See also +deconstantize+. 40 | def demodulize(path) 41 | path = path.to_s 42 | 43 | if (i = path.rindex("::")) 44 | path[(i + 2)..] 45 | else 46 | path 47 | end 48 | end 49 | 50 | def build_arguments(*methods) 51 | methods.flatten.map { |m| [m, "#{m}?"] }.flatten 52 | end 53 | 54 | module_function :underscore, :demodulize, :build_arguments, :require_files_matching_pattern 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/geo_pattern/pattern.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | class Pattern 5 | private 6 | 7 | attr_reader :svg_image 8 | 9 | public 10 | 11 | attr_accessor :background, :structure, :height, :width 12 | 13 | def initialize(svg_image = SvgImage.new) 14 | @svg_image = svg_image 15 | end 16 | 17 | # Check if string is included in pattern 18 | # 19 | # @param [String] string 20 | # The checked string 21 | def include?(string) 22 | image.include?(string) 23 | end 24 | 25 | # Generate things for the pattern 26 | # 27 | # @param [#generate) generator 28 | # The generator which should do things with this pattern - e.g. adding 29 | # background or a structure 30 | def generate_me(generator) 31 | generator.generate self 32 | end 33 | 34 | # Convert pattern to svg 35 | def to_svg 36 | image.to_s 37 | end 38 | alias_method :to_s, :to_svg 39 | 40 | # Convert to base64 41 | def to_base64 42 | Base64.strict_encode64(to_svg) 43 | end 44 | 45 | # Convert to data uri 46 | def to_data_uri 47 | "url(data:image/svg+xml;base64,#{to_base64});" 48 | end 49 | 50 | # @see #to_data_uri 51 | # @deprecated 52 | def uri_image 53 | warn 'Using "#uri_image" is deprecated as of 1.3.1. Please use "#to_data_uri" instead.' 54 | 55 | to_data_uri 56 | end 57 | 58 | # @see #to_svg 59 | # @deprecated 60 | def svg_string 61 | warn 'Using "#svg_string" is deprecated as of 1.3.1. Please use "#to_svg" instead.' 62 | 63 | to_svg 64 | end 65 | 66 | # @see #to_base64 67 | # @deprecated 68 | def base64_string 69 | warn 'Using "#base64_string" is deprecated as of 1.3.1. Please use "#to_base64" instead.' 70 | 71 | to_base64 72 | end 73 | 74 | private 75 | 76 | def image 77 | svg_image.height = height 78 | svg_image.width = width 79 | 80 | svg_image << background.image if background 81 | svg_image << structure.image if structure 82 | 83 | svg_image 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/geo_pattern/pattern_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | class PatternGenerator 5 | private 6 | 7 | attr_reader :background_generator, :structure_generator 8 | 9 | public 10 | 11 | def initialize(string, generator: nil, patterns: nil, base_color: nil, color: nil) 12 | warn "Using generator key is deprecated as of 1.3.1" if generator 13 | 14 | requested_patterns = (Array(generator) | Array(patterns)).flatten.compact 15 | 16 | pattern_preset = PatternPreset.new( 17 | fill_color_dark: "#222", 18 | fill_color_light: "#ddd", 19 | stroke_color: "#000", 20 | stroke_opacity: 0.02, 21 | opacity_min: 0.02, 22 | opacity_max: 0.15 23 | ) 24 | 25 | color_preset = ColorPreset.new( 26 | base_color: "#933c3c" 27 | ) 28 | color_preset.color = color if color 29 | color_preset.base_color = base_color if base_color 30 | 31 | seed = Seed.new(string) 32 | 33 | pattern_validator = PatternValidator.new 34 | pattern_validator.validate(requested_patterns) 35 | 36 | pattern_sieve = PatternSieve.new(requested_patterns, seed) 37 | 38 | @background_generator = BackgroundGenerators::SolidGenerator.new(seed, color_preset) 39 | @structure_generator = begin 40 | generator_klass = pattern_sieve.fetch 41 | generator_klass.new(seed, pattern_preset) 42 | end 43 | end 44 | 45 | def generate 46 | pattern = Pattern.new 47 | 48 | pattern.generate_me background_generator 49 | pattern.generate_me structure_generator 50 | 51 | pattern 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/geo_pattern/pattern_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | module PatternHelpers 5 | def hex_val(hash, index, length) 6 | hash[index, length || 1].to_i(16) 7 | end 8 | 9 | # Ruby implementation of Processing's map function 10 | # http://processing.org/reference/map_.html 11 | # v for value, d for desired 12 | def map(value, v_min, v_max, d_min, d_max) 13 | v_value = value.to_f # so it returns float 14 | 15 | v_range = v_max - v_min 16 | d_range = d_max - d_min 17 | (v_value - v_min) * d_range / v_range + d_min 18 | end 19 | 20 | def html_to_rgb(color) 21 | generate_rgb_string(::Color::RGB.from_html(color)) 22 | end 23 | 24 | def html_to_rgb_for_string(seed, base_color) 25 | hue_offset = map(seed.to_i(14, 3), 0, 4095, 0, 359) 26 | sat_offset = seed.to_i(17, 1) 27 | base_color = ::Color::RGB.from_html(base_color).to_hsl 28 | base_color.hue = base_color.hue - hue_offset 29 | 30 | base_color.saturation = if sat_offset % 2 == 0 31 | base_color.saturation + sat_offset 32 | else 33 | base_color.saturation - sat_offset 34 | end 35 | 36 | generate_rgb_string(base_color.to_rgb) 37 | end 38 | 39 | def generate_rgb_string(rgb) 40 | r = (rgb.r * 255).round 41 | g = (rgb.g * 255).round 42 | b = (rgb.b * 255).round 43 | 44 | format("rgb(%d, %d, %d)", r: r, g: g, b: b) 45 | end 46 | 47 | module_function :hex_val, :map, :html_to_rgb, :html_to_rgb_for_string, :generate_rgb_string 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/geo_pattern/pattern_preset.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | class PatternPreset 5 | private 6 | 7 | attr_accessor :options 8 | 9 | public 10 | 11 | def initialize(options) 12 | @options = options 13 | end 14 | 15 | %i[fill_color_dark fill_color_light stroke_color stroke_opacity opacity_min opacity_max].each do |m| 16 | define_method m do 17 | options[m] 18 | end 19 | end 20 | 21 | def update(opts) 22 | options.merge! opts 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/geo_pattern/pattern_sieve.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | class PatternSieve 5 | private 6 | 7 | attr_reader :pattern_store, :requested_patterns, :seed, :available_patterns, :index 8 | 9 | public 10 | 11 | def initialize(requested_patterns, seed, pattern_store = PatternStore.new) 12 | @requested_patterns = requested_patterns 13 | @seed = seed 14 | @pattern_store = pattern_store 15 | 16 | @available_patterns = determine_available_patterns 17 | @index = determine_index 18 | end 19 | 20 | def fetch 21 | available_patterns[index] 22 | end 23 | 24 | private 25 | 26 | def determine_index 27 | [seed.to_i(20, 1), available_patterns.length - 1].min 28 | end 29 | 30 | def determine_available_patterns 31 | patterns = Array(requested_patterns).compact 32 | 33 | return pattern_store.all if patterns.empty? 34 | 35 | patterns.map { |p| pattern_store[p] }.compact 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/geo_pattern/pattern_store.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | # rubocop:disable Naming/ConstantName 5 | ChevronPattern = :chevrons 6 | ConcentricCirclesPattern = :concentric_circles 7 | DiamondPattern = :diamonds 8 | HexagonPattern = :hexagons 9 | MosaicSquaresPattern = :mosaic_squares 10 | NestedSquaresPattern = :nested_squares 11 | OctagonPattern = :octagons 12 | OverlappingCirclesPattern = :overlapping_circles 13 | OverlappingRingsPattern = :overlapping_rings 14 | PlaidPattern = :plaid 15 | PlusSignPattern = :plus_signs 16 | SineWavePattern = :sine_waves 17 | SquarePattern = :squares 18 | TessellationPattern = :tessellation 19 | TrianglePattern = :triangles 20 | XesPattern = :xes 21 | # rubocop:enable Naming/ConstantName 22 | 23 | class PatternStore 24 | private 25 | 26 | attr_reader :store 27 | 28 | public 29 | 30 | def initialize 31 | @store = { 32 | chevrons: StructureGenerators::ChevronsGenerator, 33 | concentric_circles: StructureGenerators::ConcentricCirclesGenerator, 34 | diamonds: StructureGenerators::DiamondsGenerator, 35 | hexagons: StructureGenerators::HexagonsGenerator, 36 | mosaic_squares: StructureGenerators::MosaicSquaresGenerator, 37 | nested_squares: StructureGenerators::NestedSquaresGenerator, 38 | octagons: StructureGenerators::OctagonsGenerator, 39 | overlapping_circles: StructureGenerators::OverlappingCirclesGenerator, 40 | overlapping_rings: StructureGenerators::OverlappingRingsGenerator, 41 | plaid: StructureGenerators::PlaidGenerator, 42 | plus_signs: StructureGenerators::PlusSignsGenerator, 43 | sine_waves: StructureGenerators::SineWavesGenerator, 44 | squares: StructureGenerators::SquaresGenerator, 45 | tessellation: StructureGenerators::TessellationGenerator, 46 | triangles: StructureGenerators::TrianglesGenerator, 47 | xes: StructureGenerators::XesGenerator 48 | } 49 | end 50 | 51 | def [](pattern) 52 | warn "String pattern references are deprecated as of 1.3.0" if pattern.is_a?(String) 53 | 54 | store[pattern.to_s.to_sym] 55 | end 56 | 57 | def all 58 | store.values 59 | end 60 | 61 | def known?(pattern) 62 | store.key?(pattern.to_s.to_sym) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/geo_pattern/pattern_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | class PatternValidator 5 | private 6 | 7 | attr_reader :pattern_store 8 | 9 | public 10 | 11 | def initialize(pattern_store = PatternStore.new) 12 | @pattern_store = pattern_store 13 | end 14 | 15 | def validate(requested_patterns) 16 | message = "Error: At least one of the requested patterns \"#{requested_patterns.join(", ")}\" is invalid" 17 | 18 | raise InvalidPatternError, message unless valid?(requested_patterns) 19 | 20 | self 21 | end 22 | 23 | private 24 | 25 | def valid?(requested_patterns) 26 | requested_patterns.all? { |p| pattern_store.known? p } 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/geo_pattern/rake_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rake" 4 | require "rake/tasklib" 5 | require "logger" 6 | 7 | module GeoPattern 8 | # Rake Task 9 | # 10 | # This task can be used to generate pattern files 11 | class RakeTask < ::Rake::TaskLib 12 | include ::Rake::DSL if defined?(::Rake::DSL) 13 | 14 | # @!attribute [r] name 15 | # Name of task. 16 | attr_reader :name 17 | 18 | # @!attribute [r] description 19 | # A description for the task 20 | attr_reader :description 21 | 22 | # @!attribute [r] verbose (true) 23 | # Use verbose output. If this is set to true, the task will print the 24 | # executed spec command to stdout. 25 | attr_reader :verbose 26 | 27 | private 28 | 29 | attr_reader :task_arguments, :task_block, :logger, :working_directory 30 | 31 | # Create task instance 32 | # 33 | # @param [String] description 34 | # A description for the task 35 | # 36 | # @param [String] name 37 | # The name for the task (including namespace), e.g. namespace1:task1 38 | # 39 | # @param [Array] arguments 40 | # Arguments for the task. Look 41 | # [here](http://viget.com/extend/protip-passing-parameters-to-your-rake-tasks) 42 | # for a better description on how to use arguments in rake tasks 43 | # 44 | # @yield 45 | # A block which is called before the "run_task"-method is called. The 46 | # parameters it taskes depends on the number of parameters the block 47 | # can take. If the block is defined which two parameters, it takes two 48 | # parameters from the paramter 'arguments'. 49 | def initialize(opts = {}, &task_block) 50 | @options = { 51 | description: nil, 52 | name: GeoPattern::Helpers.underscore(self.class.to_s.split("::").slice(-2..-1).join(":").gsub(/Task$/, "")), 53 | arguments: [], 54 | logger: ::Logger.new($stderr), 55 | working_directory: Dir.getwd 56 | }.merge opts 57 | 58 | before_initialize 59 | 60 | raise ArgumentError, :description if @options[:description].nil? 61 | 62 | @description = @options[:description] 63 | @task_arguments = Array(@options[:arguments]) 64 | @task_block = task_block 65 | @logger = @options[:logger] 66 | @working_directory = @options[:working_directory] 67 | @name = @options[:name] 68 | 69 | after_initialize 70 | 71 | define_task 72 | end 73 | 74 | # Run code after initialize 75 | def after_initialize 76 | end 77 | 78 | # Run code before initialize 79 | def before_initialize 80 | end 81 | 82 | # Define task 83 | def define_task 84 | desc description unless ::Rake.application.last_description 85 | 86 | task name, *task_arguments do |_, task_args| 87 | RakeFileUtils.__send__(:verbose, verbose) do 88 | instance_exec(*[self, task_args].slice(0, task_block.arity), &task_block) if task_block.respond_to? :call 89 | run_task verbose 90 | end 91 | end 92 | end 93 | 94 | # Run code if task is executed 95 | def run_task(_verbose) 96 | end 97 | 98 | public 99 | 100 | # Binding to instance 101 | def instance_binding 102 | binding 103 | end 104 | 105 | # Include module in instance 106 | def include(modules) 107 | modules = Array(modules) 108 | 109 | modules.each { |m| self.class.include m } 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/geo_pattern/roles/comparable_metadata.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | # Roles 5 | module Roles 6 | # A comparable metadata 7 | module ComparableMetadata 8 | def self.included(base) 9 | base.extend ClassMethods 10 | end 11 | 12 | def generator?(value) 13 | return false unless value.is_a?(Class) || value.nil? 14 | return true if value.nil? && @generator 15 | 16 | return true if value == generator 17 | 18 | false 19 | end 20 | 21 | # Class Methods 22 | module ClassMethods 23 | # Define comparators 24 | def def_comparators(*methods) 25 | methods.flatten.each do |m| 26 | define_method "#{m}?".to_sym do |value| 27 | return true if value.nil? && public_send(m) 28 | return true if value == public_send(m) 29 | 30 | false 31 | end 32 | end 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/geo_pattern/roles/named_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | module Roles 5 | module NamedGenerator 6 | def name?(other_name) 7 | name == other_name.to_sym 8 | end 9 | 10 | def name 11 | Helpers.underscore(Helpers.demodulize(self.class).gsub(/Generator/, "")).to_sym 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/geo_pattern/seed.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | class Seed 5 | private 6 | 7 | attr_reader :seed 8 | 9 | public 10 | 11 | def initialize(string) 12 | @seed = Digest::SHA1.hexdigest string.to_s 13 | end 14 | 15 | def [](*args) 16 | seed[*args] 17 | end 18 | 19 | def to_i(index, length) 20 | seed[index, length || 1].to_i(16) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/geo_pattern/structure.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | class Structure 5 | include Roles::ComparableMetadata 6 | 7 | extend Forwardable 8 | 9 | attr_reader :image, :preset, :name, :generator 10 | 11 | def_delegators :@preset, :fill_color_dark, :fill_color_light, :stroke_color, :stroke_opacity, :opacity_min, :opacity_max 12 | 13 | def initialize(options) 14 | @image = options[:image] 15 | @preset = options[:preset] 16 | @name = options[:name] 17 | @generator = options[:generator] 18 | 19 | raise ArgumentError, "Argument name is missing" if @name.nil? 20 | raise ArgumentError, "Argument image is missing" if @image.nil? 21 | raise ArgumentError, "Argument preset is missing" if @preset.nil? 22 | raise ArgumentError, "Argument generator is missing" if @generator.nil? 23 | end 24 | 25 | def_comparators :name, :fill_color_dark, :fill_color_light, :stroke_color, :stroke_opacity, :opacity_min, :opacity_max 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/geo_pattern/structure_generators/base_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | module StructureGenerators 5 | class BaseGenerator 6 | include Roles::NamedGenerator 7 | 8 | private 9 | 10 | attr_reader :svg, :seed, :fill_color_dark, :fill_color_light, :stroke_color, :stroke_opacity, :opacity_min, :opacity_max, :preset 11 | attr_accessor :height, :width 12 | 13 | public 14 | 15 | def initialize(seed, preset, svg = SvgImage.new) 16 | @svg = svg 17 | @seed = seed 18 | @preset = preset 19 | 20 | @fill_color_dark = @preset.fill_color_dark 21 | @fill_color_light = @preset.fill_color_light 22 | @stroke_color = @preset.stroke_color 23 | @stroke_opacity = @preset.stroke_opacity 24 | @opacity_min = @preset.opacity_min 25 | @opacity_max = @preset.opacity_max 26 | 27 | @height = 100 28 | @width = 100 29 | 30 | after_initialize 31 | end 32 | 33 | def generate(pattern) 34 | pattern.structure = Structure.new(image: svg_image, preset: preset, generator: self.class, name: name) 35 | 36 | pattern.height = height 37 | pattern.width = width 38 | 39 | self 40 | end 41 | 42 | private 43 | 44 | # Set Svg Image 45 | def svg_image 46 | image = generate_structure 47 | image.height = height 48 | image.width = width 49 | 50 | image 51 | end 52 | 53 | # Hook for generators 54 | def after_initialize 55 | end 56 | 57 | # Generate the structure 58 | def generate_structure 59 | raise NotImplementedError 60 | end 61 | 62 | def hex_val(index, len) 63 | seed.to_i(index, len) 64 | end 65 | 66 | def fill_color(val) 67 | val.even? ? fill_color_light : fill_color_dark 68 | end 69 | 70 | def opacity(val) 71 | map(val, 0, 15, opacity_min, opacity_max) 72 | end 73 | 74 | def map(value, v_min, v_max, d_min, d_max) 75 | PatternHelpers.map(value, v_min, v_max, d_min, d_max) 76 | end 77 | 78 | protected 79 | 80 | def build_plus_shape(square_size) 81 | [ 82 | "rect(#{square_size},0,#{square_size},#{square_size * 3})", 83 | "rect(0, #{square_size},#{square_size * 3},#{square_size})" 84 | ] 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/geo_pattern/structure_generators/chevrons_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | module StructureGenerators 5 | class ChevronsGenerator < BaseGenerator 6 | private 7 | 8 | attr_reader :chevron_height, :chevron_width, :chevron 9 | 10 | def after_initialize 11 | @chevron_width = map(hex_val(0, 1), 0, 15, 30, 80) 12 | @chevron_height = map(hex_val(0, 1), 0, 15, 30, 80) 13 | @chevron = build_chevron_shape(chevron_width, chevron_height) 14 | 15 | self.height = chevron_height * 6 * 0.66 16 | self.width = chevron_width * 6 17 | end 18 | 19 | def generate_structure 20 | i = 0 21 | 6.times do |y| 22 | 6.times do |x| 23 | val = hex_val(i, 1) 24 | opacity = opacity(val) 25 | fill = fill_color(val) 26 | 27 | styles = { 28 | "stroke" => stroke_color, 29 | "stroke-opacity" => stroke_opacity, 30 | "fill" => fill, 31 | "fill-opacity" => opacity, 32 | "stroke-width" => 1 33 | } 34 | 35 | svg.group(chevron, styles.merge("transform" => "translate(#{x * chevron_width},#{y * chevron_height * 0.66 - chevron_height / 2})")) 36 | 37 | # Add an extra row at the end that matches the first row, for tiling. 38 | if y == 0 39 | svg.group(chevron, styles.merge("transform" => "translate(#{x * chevron_width},#{6 * chevron_height * 0.66 - chevron_height / 2})")) 40 | end 41 | i += 1 42 | end 43 | end 44 | 45 | svg 46 | end 47 | 48 | def build_chevron_shape(width, height) 49 | e = height * 0.66 50 | [ 51 | %{polyline("0,0,#{width / 2},#{height - e},#{width / 2},#{height},0,#{e},0,0")}, 52 | %{polyline("#{width / 2},#{height - e},#{width},0,#{width},#{e},#{width / 2},#{height},#{width / 2},#{height - e}")} 53 | ] 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/geo_pattern/structure_generators/concentric_circles_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | module StructureGenerators 5 | class ConcentricCirclesGenerator < BaseGenerator 6 | private 7 | 8 | attr_reader :scale, :ring_size, :stroke_width 9 | 10 | def after_initialize 11 | @scale = hex_val(0, 1) 12 | @ring_size = map(scale, 0, 15, 10, 60) 13 | @stroke_width = ring_size / 5 14 | 15 | self.width = self.height = (ring_size + stroke_width) * 6 16 | end 17 | 18 | def generate_structure 19 | i = 0 20 | 6.times do |y| 21 | 6.times do |x| 22 | val = hex_val(i, 1) 23 | opacity = opacity(val) 24 | fill = fill_color(val) 25 | 26 | svg.circle( 27 | x * ring_size + x * stroke_width + (ring_size + stroke_width) / 2, 28 | y * ring_size + y * stroke_width + (ring_size + stroke_width) / 2, 29 | ring_size / 2, 30 | "fill" => "none", 31 | "stroke" => fill, 32 | "style" => { 33 | "opacity" => opacity, 34 | "stroke-width" => "#{stroke_width}px" 35 | } 36 | ) 37 | 38 | val = hex_val(39 - i, 1) 39 | opacity = opacity(val) 40 | fill = fill_color(val) 41 | 42 | svg.circle( 43 | x * ring_size + x * stroke_width + (ring_size + stroke_width) / 2, 44 | y * ring_size + y * stroke_width + (ring_size + stroke_width) / 2, 45 | ring_size / 4, 46 | "fill" => fill, 47 | "fill-opacity" => opacity 48 | ) 49 | 50 | i += 1 51 | end 52 | end 53 | 54 | svg 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/geo_pattern/structure_generators/diamonds_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | module StructureGenerators 5 | class DiamondsGenerator < BaseGenerator 6 | private 7 | 8 | attr_reader :diamond_height, :diamond_width, :diamond 9 | 10 | def after_initialize 11 | @diamond_width = map(hex_val(0, 1), 0, 15, 10, 50) 12 | @diamond_height = map(hex_val(1, 1), 0, 15, 10, 50) 13 | @diamond = build_diamond_shape(diamond_width, diamond_height) 14 | 15 | self.height = diamond_height * 3 16 | self.width = diamond_width * 6 17 | end 18 | 19 | def generate_structure 20 | i = 0 21 | 6.times do |y| 22 | 6.times do |x| 23 | val = hex_val(i, 1) 24 | opacity = opacity(val) 25 | fill = fill_color(val) 26 | 27 | styles = { 28 | "fill" => fill, 29 | "fill-opacity" => opacity, 30 | "stroke" => stroke_color, 31 | "stroke-opacity" => stroke_opacity 32 | } 33 | 34 | dx = (y % 2 == 0) ? 0 : diamond_width / 2 35 | 36 | svg.polyline(diamond, styles.merge( 37 | "transform" => "translate(#{x * diamond_width - diamond_width / 2 + dx}, #{diamond_height / 2 * y - diamond_height / 2})" 38 | )) 39 | 40 | # Add an extra one at top-right, for tiling. 41 | if x == 0 42 | svg.polyline(diamond, styles.merge( 43 | "transform" => "translate(#{6 * diamond_width - diamond_width / 2 + dx}, #{diamond_height / 2 * y - diamond_height / 2})" 44 | )) 45 | end 46 | 47 | # Add an extra row at the end that matches the first row, for tiling. 48 | if y == 0 49 | svg.polyline(diamond, styles.merge( 50 | "transform" => "translate(#{x * diamond_width - diamond_width / 2 + dx}, #{diamond_height / 2 * 6 - diamond_height / 2})" 51 | )) 52 | end 53 | 54 | # Add an extra one at bottom-right, for tiling. 55 | if x == 0 && y == 0 56 | svg.polyline(diamond, styles.merge( 57 | "transform" => "translate(#{6 * diamond_width - diamond_width / 2 + dx}, #{diamond_height / 2 * 6 - diamond_height / 2})" 58 | )) 59 | end 60 | i += 1 61 | end 62 | end 63 | 64 | svg 65 | end 66 | 67 | def build_diamond_shape(width, height) 68 | "#{width / 2}, 0, #{width}, #{height / 2}, #{width / 2}, #{height}, 0, #{height / 2}" 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/geo_pattern/structure_generators/hexagons_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | module StructureGenerators 5 | class HexagonsGenerator < BaseGenerator 6 | private 7 | 8 | attr_reader :scale, :side_length, :hex_height, :hex_width, :hex 9 | 10 | def after_initialize 11 | @scale = hex_val(0, 1) 12 | @side_length = map(scale, 0, 15, 8, 60) 13 | @hex_height = side_length * Math.sqrt(3) 14 | @hex_width = side_length * 2 15 | @hex = build_hexagon_shape(side_length) 16 | 17 | self.width = hex_width * 3 + side_length * 3 18 | self.height = hex_height * 6 19 | end 20 | 21 | def generate_structure 22 | i = 0 23 | 6.times do |y| 24 | 6.times do |x| 25 | val = hex_val(i, 1) 26 | dy = (x % 2 == 0) ? y * hex_height : y * hex_height + hex_height / 2 27 | opacity = opacity(val) 28 | fill = fill_color(val) 29 | 30 | styles = { 31 | "fill" => fill, 32 | "fill-opacity" => opacity, 33 | "stroke" => stroke_color, 34 | "stroke-opacity" => stroke_opacity 35 | } 36 | 37 | svg.polyline(hex, styles.merge("transform" => "translate(#{x * side_length * 1.5 - hex_width / 2}, #{dy - hex_height / 2})")) 38 | 39 | # Add an extra one at top-right, for tiling. 40 | if x == 0 41 | svg.polyline(hex, styles.merge("transform" => "translate(#{6 * side_length * 1.5 - hex_width / 2}, #{dy - hex_height / 2})")) 42 | end 43 | 44 | # Add an extra row at the end that matches the first row, for tiling. 45 | if y == 0 46 | dy = (x % 2 == 0) ? 6 * hex_height : 6 * hex_height + hex_height / 2 47 | svg.polyline(hex, styles.merge("transform" => "translate(#{x * side_length * 1.5 - hex_width / 2}, #{dy - hex_height / 2})")) 48 | end 49 | 50 | # Add an extra one at bottom-right, for tiling. 51 | if x == 0 && y == 0 52 | svg.polyline(hex, styles.merge("transform" => "translate(#{6 * side_length * 1.5 - hex_width / 2}, #{5 * hex_height + hex_height / 2})")) 53 | end 54 | i += 1 55 | end 56 | end 57 | 58 | svg 59 | end 60 | 61 | def build_hexagon_shape(side_length) 62 | c = side_length 63 | a = c / 2 64 | b = Math.sin(60 * Math::PI / 180) * c 65 | "0,#{b},#{a},0,#{a + c},0,#{2 * c},#{b},#{a + c},#{2 * b},#{a},#{2 * b},0,#{b}" 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/geo_pattern/structure_generators/mosaic_squares_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | module StructureGenerators 5 | class MosaicSquaresGenerator < BaseGenerator 6 | private 7 | 8 | attr_reader :triangle_size 9 | 10 | def after_initialize 11 | @triangle_size = map(hex_val(0, 1), 0, 15, 15, 50) 12 | 13 | self.height = self.width = triangle_size * 8 14 | end 15 | 16 | def generate_structure 17 | i = 0 18 | 4.times do |y| 19 | 4.times do |x| 20 | if x.even? 21 | if y.even? 22 | draw_outer_mosaic_tile(x * triangle_size * 2, y * triangle_size * 2, triangle_size, hex_val(i, 1)) 23 | else 24 | draw_inner_mosaic_tile(x * triangle_size * 2, y * triangle_size * 2, triangle_size, [hex_val(i, 1), hex_val(i + 1, 1)]) 25 | end 26 | elsif y.even? 27 | draw_inner_mosaic_tile(x * triangle_size * 2, y * triangle_size * 2, triangle_size, [hex_val(i, 1), hex_val(i + 1, 1)]) 28 | else 29 | draw_outer_mosaic_tile(x * triangle_size * 2, y * triangle_size * 2, triangle_size, hex_val(i, 1)) 30 | end 31 | i += 1 32 | end 33 | end 34 | 35 | svg 36 | end 37 | 38 | def draw_inner_mosaic_tile(x, y, triangle_size, vals) 39 | triangle = build_right_triangle_shape(triangle_size) 40 | opacity = opacity(vals[0]) 41 | fill = fill_color(vals[0]) 42 | styles = { 43 | "stroke" => stroke_color, 44 | "stroke-opacity" => stroke_opacity, 45 | "fill-opacity" => opacity, 46 | "fill" => fill 47 | } 48 | svg.polyline(triangle, styles.merge("transform" => "translate(#{x + triangle_size}, #{y}) scale(-1, 1)")) 49 | svg.polyline(triangle, styles.merge("transform" => "translate(#{x + triangle_size}, #{y + triangle_size * 2}) scale(1, -1)")) 50 | 51 | opacity = opacity(vals[1]) 52 | fill = fill_color(vals[1]) 53 | styles = { 54 | "stroke" => stroke_color, 55 | "stroke-opacity" => stroke_opacity, 56 | "fill-opacity" => opacity, 57 | "fill" => fill 58 | } 59 | svg.polyline(triangle, styles.merge("transform" => "translate(#{x + triangle_size}, #{y + triangle_size * 2}) scale(-1, -1)")) 60 | svg.polyline(triangle, styles.merge("transform" => "translate(#{x + triangle_size}, #{y}) scale(1, 1)")) 61 | end 62 | 63 | def draw_outer_mosaic_tile(x, y, triangle_size, val) 64 | opacity = opacity(val) 65 | fill = fill_color(val) 66 | triangle = build_right_triangle_shape(triangle_size) 67 | styles = { 68 | "stroke" => stroke_color, 69 | "stroke-opacity" => stroke_opacity, 70 | "fill-opacity" => opacity, 71 | "fill" => fill 72 | } 73 | 74 | svg.polyline(triangle, styles.merge("transform" => "translate(#{x}, #{y + triangle_size}) scale(1, -1)")) 75 | svg.polyline(triangle, styles.merge("transform" => "translate(#{x + triangle_size * 2}, #{y + triangle_size}) scale(-1, -1)")) 76 | svg.polyline(triangle, styles.merge("transform" => "translate(#{x}, #{y + triangle_size}) scale(1, 1)")) 77 | svg.polyline(triangle, styles.merge("transform" => "translate(#{x + triangle_size * 2}, #{y + triangle_size}) scale(-1, 1)")) 78 | end 79 | 80 | def build_right_triangle_shape(side_length) 81 | "0, 0, #{side_length}, #{side_length}, 0, #{side_length}, 0, 0" 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/geo_pattern/structure_generators/nested_squares_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | module StructureGenerators 5 | class NestedSquaresGenerator < BaseGenerator 6 | private 7 | 8 | attr_reader :block_size, :square_size 9 | 10 | def after_initialize 11 | @block_size = map(hex_val(0, 1), 0, 15, 4, 12) 12 | @square_size = block_size * 7 13 | 14 | self.height = self.width = (square_size + block_size) * 6 + block_size * 6 15 | end 16 | 17 | def generate_structure 18 | i = 0 19 | 6.times do |y| 20 | 6.times do |x| 21 | val = hex_val(i, 1) 22 | opacity = opacity(val) 23 | fill = fill_color(val) 24 | 25 | styles = { 26 | "fill" => "none", 27 | "stroke" => fill, 28 | "style" => { 29 | "opacity" => opacity, 30 | "stroke-width" => "#{block_size}px" 31 | } 32 | } 33 | 34 | svg.rect(x * square_size + x * block_size * 2 + block_size / 2, 35 | y * square_size + y * block_size * 2 + block_size / 2, 36 | square_size, square_size, styles) 37 | 38 | val = hex_val(39 - i, 1) 39 | opacity = opacity(val) 40 | fill = fill_color(val) 41 | 42 | styles = { 43 | "fill" => "none", 44 | "stroke" => fill, 45 | "style" => { 46 | "opacity" => opacity, 47 | "stroke-width" => "#{block_size}px" 48 | } 49 | } 50 | 51 | svg.rect(x * square_size + x * block_size * 2 + block_size / 2 + block_size * 2, 52 | y * square_size + y * block_size * 2 + block_size / 2 + block_size * 2, 53 | block_size * 3, block_size * 3, styles) 54 | i += 1 55 | end 56 | end 57 | 58 | svg 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/geo_pattern/structure_generators/octagons_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | module StructureGenerators 5 | class OctagonsGenerator < BaseGenerator 6 | private 7 | 8 | attr_reader :square_size, :tile 9 | 10 | def after_initialize 11 | @square_size = map(hex_val(0, 1), 0, 15, 10, 60) 12 | @tile = build_octogon_shape(square_size) 13 | 14 | self.height = self.width = square_size * 6 15 | end 16 | 17 | def generate_structure 18 | i = 0 19 | 6.times do |y| 20 | 6.times do |x| 21 | val = hex_val(i, 1) 22 | opacity = opacity(val) 23 | fill = fill_color(val) 24 | 25 | svg.polyline(tile, 26 | "fill" => fill, 27 | "fill-opacity" => opacity, 28 | "stroke" => stroke_color, 29 | "stroke-opacity" => stroke_opacity, 30 | "transform" => "translate(#{x * square_size}, #{y * square_size})") 31 | i += 1 32 | end 33 | end 34 | 35 | svg 36 | end 37 | 38 | def build_octogon_shape(square_size) 39 | s = square_size 40 | c = s * 0.33 41 | "#{c},0,#{s - c},0,#{s},#{c},#{s},#{s - c},#{s - c},#{s},#{c},#{s},0,#{s - c},0,#{c},#{c},0" 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/geo_pattern/structure_generators/overlapping_circles_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | module StructureGenerators 5 | class OverlappingCirclesGenerator < BaseGenerator 6 | private 7 | 8 | attr_reader :scale, :diameter, :radius 9 | 10 | def after_initialize 11 | @scale = hex_val(0, 1) 12 | @diameter = map(scale, 0, 15, 25, 200) 13 | @radius = diameter / 2 14 | 15 | self.height = self.width = radius * 6 16 | end 17 | 18 | def generate_structure 19 | i = 0 20 | 6.times do |y| 21 | 6.times do |x| 22 | val = hex_val(i, 1) 23 | opacity = opacity(val) 24 | fill = fill_color(val) 25 | 26 | styles = { 27 | "fill" => fill, 28 | "style" => { 29 | "opacity" => opacity 30 | } 31 | } 32 | 33 | svg.circle(x * radius, y * radius, radius, styles) 34 | 35 | # Add an extra one at top-right, for tiling. 36 | if x == 0 37 | svg.circle(6 * radius, y * radius, radius, styles) 38 | end 39 | 40 | # Add an extra row at the end that matches the first row, for tiling. 41 | if y == 0 42 | svg.circle(x * radius, 6 * radius, radius, styles) 43 | end 44 | 45 | # Add an extra one at bottom-right, for tiling. 46 | if x == 0 && y == 0 47 | svg.circle(6 * radius, 6 * radius, radius, styles) 48 | end 49 | i += 1 50 | end 51 | end 52 | 53 | svg 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/geo_pattern/structure_generators/overlapping_rings_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | module StructureGenerators 5 | class OverlappingRingsGenerator < BaseGenerator 6 | private 7 | 8 | attr_reader :scale, :ring_size, :stroke_width 9 | 10 | def after_initialize 11 | @scale = hex_val(0, 1) 12 | @ring_size = map(scale, 0, 15, 10, 60) 13 | @stroke_width = ring_size / 4 14 | 15 | self.height = self.width = ring_size * 6 16 | end 17 | 18 | def generate_structure 19 | i = 0 20 | 6.times do |y| 21 | 6.times do |x| 22 | val = hex_val(i, 1) 23 | opacity = opacity(val) 24 | fill = fill_color(val) 25 | 26 | styles = { 27 | "fill" => "none", 28 | "stroke" => fill, 29 | "style" => { 30 | "opacity" => opacity, 31 | "stroke-width" => "#{stroke_width}px" 32 | } 33 | } 34 | 35 | svg.circle(x * ring_size, y * ring_size, ring_size - stroke_width / 2, styles) 36 | 37 | # Add an extra one at top-right, for tiling. 38 | if x == 0 39 | svg.circle(6 * ring_size, y * ring_size, ring_size - stroke_width / 2, styles) 40 | end 41 | 42 | if y == 0 43 | svg.circle(x * ring_size, 6 * ring_size, ring_size - stroke_width / 2, styles) 44 | end 45 | 46 | if x == 0 && y == 0 47 | svg.circle(6 * ring_size, 6 * ring_size, ring_size - stroke_width / 2, styles) 48 | end 49 | i += 1 50 | end 51 | end 52 | 53 | svg 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/geo_pattern/structure_generators/plaid_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | module StructureGenerators 5 | class PlaidGenerator < BaseGenerator 6 | private 7 | 8 | def generate_structure 9 | local_height = 0 10 | local_width = 0 11 | 12 | # horizontal stripes 13 | i = 0 14 | 18.times do 15 | space = hex_val(i, 1) 16 | local_height += space + 5 17 | 18 | val = hex_val(i + 1, 1) 19 | opacity = opacity(val) 20 | fill = fill_color(val) 21 | stripe_height = val + 5 22 | 23 | svg.rect(0, local_height, "100%", stripe_height, 24 | "opacity" => opacity, 25 | "fill" => fill) 26 | local_height += stripe_height 27 | i += 2 28 | end 29 | 30 | # vertical stripes 31 | i = 0 32 | 18.times do 33 | space = hex_val(i, 1) 34 | local_width += space + 5 35 | 36 | val = hex_val(i + 1, 1) 37 | opacity = opacity(val) 38 | fill = fill_color(val) 39 | stripe_width = val + 5 40 | 41 | svg.rect(local_width, 0, stripe_width, "100%", 42 | "opacity" => opacity, 43 | "fill" => fill) 44 | local_width += stripe_width 45 | i += 2 46 | end 47 | 48 | self.height = local_height 49 | self.width = local_width 50 | 51 | svg 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/geo_pattern/structure_generators/plus_signs_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | module StructureGenerators 5 | class PlusSignsGenerator < BaseGenerator 6 | private 7 | 8 | attr_reader :square_size, :plus_size, :plus_shape 9 | 10 | def after_initialize 11 | @square_size = map(hex_val(0, 1), 0, 15, 10, 25) 12 | @plus_size = square_size * 3 13 | @plus_shape = build_plus_shape(square_size) 14 | 15 | self.height = self.width = square_size * 12 16 | end 17 | 18 | def generate_structure 19 | i = 0 20 | 6.times do |y| 21 | 6.times do |x| 22 | val = hex_val(i, 1) 23 | opacity = opacity(val) 24 | fill = fill_color(val) 25 | dx = (y % 2 == 0) ? 0 : 1 26 | 27 | styles = { 28 | "fill" => fill, 29 | "stroke" => stroke_color, 30 | "stroke-opacity" => stroke_opacity, 31 | "style" => { 32 | "fill-opacity" => opacity 33 | } 34 | } 35 | 36 | svg.group(plus_shape, styles.merge( 37 | "transform" => "translate(#{x * plus_size - x * square_size + dx * square_size - square_size},#{y * plus_size - y * square_size - plus_size / 2})" 38 | )) 39 | 40 | # Add an extra column on the right for tiling. 41 | if x == 0 42 | svg.group(plus_shape, styles.merge( 43 | "transform" => "translate(#{4 * plus_size - x * square_size + dx * square_size - square_size},#{y * plus_size - y * square_size - plus_size / 2})" 44 | )) 45 | end 46 | 47 | # Add an extra row on the bottom that matches the first row, for tiling. 48 | if y == 0 49 | svg.group(plus_shape, styles.merge( 50 | "transform" => "translate(#{x * plus_size - x * square_size + dx * square_size - square_size},#{4 * plus_size - y * square_size - plus_size / 2})" 51 | )) 52 | end 53 | 54 | # Add an extra one at top-right and bottom-right, for tiling. 55 | if x == 0 && y == 0 56 | svg.group(plus_shape, styles.merge( 57 | "transform" => "translate(#{4 * plus_size - x * square_size + dx * square_size - square_size},#{4 * plus_size - y * square_size - plus_size / 2})" 58 | )) 59 | end 60 | i += 1 61 | end 62 | end 63 | 64 | svg 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/geo_pattern/structure_generators/sine_waves_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | module StructureGenerators 5 | class SineWavesGenerator < BaseGenerator 6 | private 7 | 8 | attr_reader :period, :amplitude, :wave_width 9 | 10 | def after_initialize 11 | @period = map(hex_val(0, 1), 0, 15, 100, 400).floor 12 | @amplitude = map(hex_val(1, 1), 0, 15, 30, 100).floor 13 | @wave_width = map(hex_val(2, 1), 0, 15, 3, 30).floor 14 | 15 | self.height = wave_width * 36 16 | self.width = period 17 | end 18 | 19 | def generate_structure 20 | 36.times do |i| 21 | val = hex_val(i, 1) 22 | opacity = opacity(val) 23 | fill = fill_color(val) 24 | x_offset = period / 4 * 0.7 25 | 26 | styles = { 27 | "fill" => "none", 28 | "stroke" => fill, 29 | "style" => { 30 | "opacity" => opacity, 31 | "stroke-width" => "#{wave_width}px" 32 | } 33 | } 34 | 35 | str = "M0 #{amplitude} C #{x_offset} 0, #{period / 2 - x_offset} 0, #{period / 2} #{amplitude} S #{period - x_offset} #{amplitude * 2}, #{period} #{amplitude} S #{period * 1.5 - x_offset} 0, #{period * 1.5}, #{amplitude}" 36 | 37 | svg.path(str, styles.merge("transform" => "translate(-#{period / 4}, #{wave_width * i - amplitude * 1.5})")) 38 | svg.path(str, styles.merge("transform" => "translate(-#{period / 4}, #{wave_width * i - amplitude * 1.5 + wave_width * 36})")) 39 | end 40 | 41 | svg 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/geo_pattern/structure_generators/squares_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | module StructureGenerators 5 | class SquaresGenerator < BaseGenerator 6 | private 7 | 8 | attr_reader :square_size 9 | 10 | def after_initialize 11 | @square_size = map(hex_val(0, 1), 0, 15, 10, 60) 12 | 13 | self.height = self.width = square_size * 6 14 | end 15 | 16 | def generate_structure 17 | i = 0 18 | 6.times do |y| 19 | 6.times do |x| 20 | val = hex_val(i, 1) 21 | opacity = opacity(val) 22 | fill = fill_color(val) 23 | 24 | svg.rect(x * square_size, y * square_size, square_size, square_size, 25 | "fill" => fill, 26 | "fill-opacity" => opacity, 27 | "stroke" => stroke_color, 28 | "stroke-opacity" => stroke_opacity) 29 | i += 1 30 | end 31 | end 32 | 33 | svg 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/geo_pattern/structure_generators/tessellation_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | module StructureGenerators 5 | class TessellationGenerator < BaseGenerator 6 | private 7 | 8 | attr_reader :side_length, :hex_height, :hex_width, :triangle_height, :triangle, :tile_width, :tile_height 9 | 10 | def after_initialize 11 | # 3.4.6.4 semi-regular tessellation 12 | @side_length = map(hex_val(0, 1), 0, 15, 5, 40) 13 | @hex_height = side_length * Math.sqrt(3) 14 | @hex_width = side_length * 2 15 | @triangle_height = side_length / 2 * Math.sqrt(3) 16 | @triangle = build_rotated_triangle_shape(side_length, triangle_height) 17 | @tile_width = side_length * 3 + triangle_height * 2 18 | @tile_height = (hex_height * 2) + (side_length * 2) 19 | 20 | self.height = tile_height 21 | self.width = tile_width 22 | end 23 | 24 | def generate_structure 25 | 20.times do |i| 26 | val = hex_val(i, 1) 27 | opacity = opacity(val) 28 | fill = fill_color(val) 29 | 30 | styles = { 31 | "stroke" => stroke_color, 32 | "stroke-opacity" => stroke_opacity, 33 | "fill" => fill, 34 | "fill-opacity" => opacity, 35 | "stroke-width" => 1 36 | } 37 | 38 | case i 39 | when 0 # all 4 corners 40 | svg.rect(-side_length / 2, -side_length / 2, side_length, side_length, styles) 41 | svg.rect(tile_width - side_length / 2, -side_length / 2, side_length, side_length, styles) 42 | svg.rect(-side_length / 2, tile_height - side_length / 2, side_length, side_length, styles) 43 | svg.rect(tile_width - side_length / 2, tile_height - side_length / 2, side_length, side_length, styles) 44 | when 1 # center / top square 45 | svg.rect(hex_width / 2 + triangle_height, hex_height / 2, side_length, side_length, styles) 46 | when 2 # side squares 47 | svg.rect(-side_length / 2, tile_height / 2 - side_length / 2, side_length, side_length, styles) 48 | svg.rect(tile_width - side_length / 2, tile_height / 2 - side_length / 2, side_length, side_length, styles) 49 | when 3 # center / bottom square 50 | svg.rect(hex_width / 2 + triangle_height, hex_height * 1.5 + side_length, side_length, side_length, styles) 51 | when 4 # left top / bottom triangle 52 | svg.polyline(triangle, styles.merge("transform" => "translate(#{side_length / 2}, #{-side_length / 2}) rotate(0, #{side_length / 2}, #{triangle_height / 2})")) 53 | svg.polyline(triangle, styles.merge("transform" => "translate(#{side_length / 2}, #{tile_height - -side_length / 2}) rotate(0, #{side_length / 2}, #{triangle_height / 2}) scale(1, -1)")) 54 | when 5 # right top / bottom triangle 55 | svg.polyline(triangle, styles.merge("transform" => "translate(#{tile_width - side_length / 2}, #{-side_length / 2}) rotate(0, #{side_length / 2}, #{triangle_height / 2}) scale(-1, 1)")) 56 | svg.polyline(triangle, styles.merge("transform" => "translate(#{tile_width - side_length / 2}, #{tile_height + side_length / 2}) rotate(0, #{side_length / 2}, #{triangle_height / 2}) scale(-1, -1)")) 57 | when 6 # center / top / right triangle 58 | svg.polyline(triangle, styles.merge("transform" => "translate(#{tile_width / 2 + side_length / 2}, #{hex_height / 2})")) 59 | when 7 # center / top / left triangle 60 | svg.polyline(triangle, styles.merge("transform" => "translate(#{tile_width - tile_width / 2 - side_length / 2}, #{hex_height / 2}) scale(-1, 1)")) 61 | when 8 # center / bottom / right triangle 62 | svg.polyline(triangle, styles.merge("transform" => "translate(#{tile_width / 2 + side_length / 2}, #{tile_height - hex_height / 2}) scale(1, -1)")) 63 | when 9 # center / bottom / left triangle 64 | svg.polyline(triangle, styles.merge("transform" => "translate(#{tile_width - tile_width / 2 - side_length / 2}, #{tile_height - hex_height / 2}) scale(-1, -1)")) 65 | when 10 # left / middle triangle 66 | svg.polyline(triangle, styles.merge("transform" => "translate(#{side_length / 2}, #{tile_height / 2 - side_length / 2})")) 67 | when 11 # right / middle triangle 68 | svg.polyline(triangle, styles.merge("transform" => "translate(#{tile_width - side_length / 2}, #{tile_height / 2 - side_length / 2}) scale(-1, 1)")) 69 | when 12 # left / top square 70 | svg.rect(0, 0, side_length, side_length, 71 | styles.merge("transform" => "translate(#{side_length / 2}, #{side_length / 2}) rotate(-30, 0, 0)")) 72 | when 13 # right / top square 73 | svg.rect(0, 0, side_length, side_length, 74 | styles.merge("transform" => "scale(-1, 1) translate(#{-tile_width + side_length / 2}, #{side_length / 2}) rotate(-30, 0, 0)")) 75 | when 14 # left / center-top square 76 | svg.rect(0, 0, side_length, side_length, 77 | styles.merge("transform" => "translate(#{side_length / 2}, #{tile_height / 2 - side_length / 2 - side_length}) rotate(30, 0, #{side_length})")) 78 | when 15 # right / center-top square 79 | svg.rect(0, 0, side_length, side_length, 80 | styles.merge("transform" => "scale(-1, 1) translate(#{-tile_width + side_length / 2}, #{tile_height / 2 - side_length / 2 - side_length}) rotate(30, 0, #{side_length})")) 81 | when 16 # left / center-top square 82 | svg.rect(0, 0, side_length, side_length, 83 | styles.merge("transform" => "scale(1, -1) translate(#{side_length / 2}, #{-tile_height + tile_height / 2 - side_length / 2 - side_length}) rotate(30, 0, #{side_length})")) 84 | when 17 # right / center-bottom square 85 | svg.rect(0, 0, side_length, side_length, 86 | styles.merge("transform" => "scale(-1, -1) translate(#{-tile_width + side_length / 2}, #{-tile_height + tile_height / 2 - side_length / 2 - side_length}) rotate(30, 0, #{side_length})")) 87 | when 18 # left / bottom square 88 | svg.rect(0, 0, side_length, side_length, 89 | styles.merge("transform" => "scale(1, -1) translate(#{side_length / 2}, #{-tile_height + side_length / 2}) rotate(-30, 0, 0)")) 90 | when 19 # right / bottom square 91 | svg.rect(0, 0, side_length, side_length, 92 | styles.merge("transform" => "scale(-1, -1) translate(#{-tile_width + side_length / 2}, #{-tile_height + side_length / 2}) rotate(-30, 0, 0)")) 93 | end 94 | end 95 | 96 | svg 97 | end 98 | 99 | def build_rotated_triangle_shape(side_length, width) 100 | half_height = side_length / 2 101 | "0, 0, #{width}, #{half_height}, 0, #{side_length}, 0, 0" 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/geo_pattern/structure_generators/triangles_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | module StructureGenerators 5 | class TrianglesGenerator < BaseGenerator 6 | private 7 | 8 | attr_reader :scale, :side_length, :triangle_height, :triangle 9 | 10 | def after_initialize 11 | @scale = hex_val(0, 1) 12 | @side_length = map(scale, 0, 15, 15, 80) 13 | @triangle_height = side_length / 2 * Math.sqrt(3) 14 | @triangle = build_triangle_shape(side_length, triangle_height) 15 | 16 | self.width = side_length * 3 17 | self.height = triangle_height * 6 18 | end 19 | 20 | def generate_structure 21 | i = 0 22 | 6.times do |y| 23 | 6.times do |x| 24 | val = hex_val(i, 1) 25 | opacity = opacity(val) 26 | fill = fill_color(val) 27 | 28 | styles = { 29 | "fill" => fill, 30 | "fill-opacity" => opacity, 31 | "stroke" => stroke_color, 32 | "stroke-opacity" => stroke_opacity 33 | } 34 | 35 | rotation = if y % 2 == 0 36 | (x % 2 == 0) ? 180 : 0 37 | else 38 | (x % 2 != 0) ? 180 : 0 39 | end 40 | 41 | svg.polyline(triangle, styles.merge( 42 | "transform" => "translate(#{x * side_length * 0.5 - side_length / 2}, #{triangle_height * y}) rotate(#{rotation}, #{side_length / 2}, #{triangle_height / 2})" 43 | )) 44 | 45 | # Add an extra one at top-right, for tiling. 46 | if x == 0 47 | svg.polyline(triangle, styles.merge( 48 | "transform" => "translate(#{6 * side_length * 0.5 - side_length / 2}, #{triangle_height * y}) rotate(#{rotation}, #{side_length / 2}, #{triangle_height / 2})" 49 | )) 50 | end 51 | i += 1 52 | end 53 | end 54 | 55 | svg 56 | end 57 | 58 | def build_triangle_shape(side_length, height) 59 | half_width = side_length / 2 60 | "#{half_width}, 0, #{side_length}, #{height}, 0, #{height}, #{half_width}, 0" 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/geo_pattern/structure_generators/xes_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | module StructureGenerators 5 | class XesGenerator < BaseGenerator 6 | private 7 | 8 | attr_reader :square_size, :x_shape, :x_size 9 | 10 | def after_initialize 11 | @square_size = map(hex_val(0, 1), 0, 15, 10, 25) 12 | @x_shape = build_plus_shape(square_size) # rotated later 13 | @x_size = square_size * 3 * 0.943 14 | 15 | self.height = self.width = x_size * 3 16 | end 17 | 18 | def generate_structure 19 | i = 0 20 | 6.times do |y| 21 | 6.times do |x| 22 | val = hex_val(i, 1) 23 | opacity = opacity(val) 24 | dy = (x % 2 == 0) ? y * x_size - x_size * 0.5 : y * x_size - x_size * 0.5 + x_size / 4 25 | fill = fill_color(val) 26 | 27 | styles = { 28 | "fill" => fill, 29 | "style" => { 30 | "opacity" => opacity 31 | } 32 | } 33 | 34 | svg.group(x_shape, styles.merge( 35 | "transform" => "translate(#{x * x_size / 2 - x_size / 2},#{dy - y * x_size / 2}) rotate(45, #{x_size / 2}, #{x_size / 2})" 36 | )) 37 | 38 | # Add an extra column on the right for tiling. 39 | if x == 0 40 | svg.group(x_shape, styles.merge( 41 | "transform" => "translate(#{6 * x_size / 2 - x_size / 2},#{dy - y * x_size / 2}) rotate(45, #{x_size / 2}, #{x_size / 2})" 42 | )) 43 | end 44 | 45 | # Add an extra row on the bottom that matches the first row, for tiling. 46 | if y == 0 47 | dy = (x % 2 == 0) ? 6 * x_size - x_size / 2 : 6 * x_size - x_size / 2 + x_size / 4 48 | svg.group(x_shape, styles.merge( 49 | "transform" => "translate(#{x * x_size / 2 - x_size / 2},#{dy - 6 * x_size / 2}) rotate(45, #{x_size / 2}, #{x_size / 2})" 50 | )) 51 | end 52 | 53 | # These can hang off the bottom, so put a row at the top for tiling. 54 | if y == 5 55 | svg.group(x_shape, styles.merge( 56 | "transform" => "translate(#{x * x_size / 2 - x_size / 2},#{dy - 11 * x_size / 2}) rotate(45, #{x_size / 2}, #{x_size / 2})" 57 | )) 58 | end 59 | 60 | # Add an extra one at top-right and bottom-right, for tiling. 61 | if x == 0 && y == 0 62 | svg.group(x_shape, styles.merge( 63 | "transform" => "translate(#{6 * x_size / 2 - x_size / 2},#{dy - 6 * x_size / 2}) rotate(45, #{x_size / 2}, #{x_size / 2})" 64 | )) 65 | end 66 | i += 1 67 | end 68 | end 69 | 70 | svg 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/geo_pattern/svg_image.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | class SvgImage 5 | include Comparable 6 | 7 | private 8 | 9 | attr_reader :svg_string 10 | 11 | public 12 | 13 | attr_reader :height, :width 14 | 15 | def initialize 16 | @width = 100 17 | @height = 100 18 | @svg_string = +"" 19 | end 20 | 21 | def width=(width) 22 | @width = width.floor 23 | end 24 | 25 | def height=(height) 26 | @height = height.floor 27 | end 28 | 29 | # Pattern includes string 30 | # 31 | # @param [String] string 32 | # The string which should be included in the body of the SvgImage 33 | def include?(string) 34 | body.include? string 35 | end 36 | 37 | def svg_header 38 | %() 39 | end 40 | 41 | def svg_closer 42 | "" 43 | end 44 | 45 | def to_s 46 | svg_header + svg_string + svg_closer 47 | end 48 | 49 | def body 50 | svg_string 51 | end 52 | 53 | def <<(svg) 54 | svg_string << svg.body 55 | end 56 | 57 | def rect(x, y, width, height, args = {}) 58 | svg_string << %() 59 | end 60 | 61 | def circle(cx, cy, r, args = {}) 62 | svg_string << %() 63 | end 64 | 65 | def path(str, args = {}) 66 | svg_string << %() 67 | end 68 | 69 | def polyline(str, args = {}) 70 | svg_string << %() 71 | end 72 | 73 | def group(elements, args = {}) 74 | svg_string << %() 75 | elements.each { |e| eval e } # rubocop:disable Security/Eval 76 | svg_string << %() 77 | end 78 | 79 | def write_args(args) 80 | str = +"" 81 | args.each do |key, value| 82 | if value.is_a?(Hash) 83 | str << %(#{key}=") 84 | value.each do |k, v| 85 | str << %(#{k}:#{v};) 86 | end 87 | str << %(" ) 88 | else 89 | str << %(#{key}="#{value}" ) 90 | end 91 | end 92 | str 93 | end 94 | 95 | def self.as_comment(str) 96 | "" 97 | end 98 | 99 | def <=>(other) 100 | to_s <=> other.to_s 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/geo_pattern/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GeoPattern 4 | VERSION = "1.5.0" 5 | end 6 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | info_msg="\e[0;32m[INFO]\e[0;30m" 6 | error_msg="\e[0;31mFAILED\e[0;30m" 7 | 8 | function output_error_log { 9 | [[ -f error.log ]] && ( cat error.log >&2; rm error.log) 10 | } 11 | 12 | echo -ne "$info_msg Checking if ruby installed? " 13 | which 'ruby' >/dev/null 2>error.log || ( echo -e "$error_msg\n\nCould not find \`ruby\`. Please install ruby or add it to PATH"; output_error_log; exit 1 ) 14 | echo OK 15 | 16 | echo -en "$info_msg rubygem \"rake\" " 17 | gem install rake >/dev/null 2>error.log || ( echo -e "$error_msg\n\nAn error occurred during installation of rake. Run \`gem install rake\` yourself."; output_error_log; exit 1 ) 18 | echo OK 19 | 20 | if [ -n "$CI" ]; then 21 | echo -e "$info_msg Bootstrapping in CI mode" 22 | rake_task='bootstrap:ci' 23 | else 24 | echo -e "$info_msg Bootstrapping in normal mode" 25 | rake_task='bootstrap' 26 | fi 27 | 28 | echo -en "$info_msg Running rake task \"$rake_task\" " 29 | rake --trace $rake_task >error.log 2>&1 || ( echo -e "$error_msg\n\nAn error occurred during run of rake-task \"$rake_task\". Run \`rake $rake_task\` yourself."; output_error_log; exit 1 ) 30 | echo OK 31 | -------------------------------------------------------------------------------- /script/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | $LOAD_PATH << File.expand_path("../lib", __dir__) 5 | 6 | require "pry" 7 | require "geo_pattern" 8 | 9 | Pry.start 10 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | function output_error_log { 6 | [[ -f error.log ]] && ( cat error.log >&2; rm error.log) 7 | } 8 | 9 | info_msg="\e[0;32m[INFO]\e[0;30m" 10 | error_msg="\e[0;31mFAILED\e[0;30m" 11 | 12 | if [ -n "$CI" ]; then 13 | echo -e "$info_msg Testing in CI mode" 14 | rake_task='test:ci' 15 | else 16 | echo -e "$info_msg Testing in normal mode" 17 | rake_task='test' 18 | fi 19 | 20 | echo -en "$info_msg Running rake task \"$rake_task\" " 21 | bundle exec rake --trace $rake_task 22 | -------------------------------------------------------------------------------- /spec/background_generators/solid_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe BackgroundGenerators::SolidGenerator do 6 | subject(:generator) { described_class.new(seed, preset) } 7 | 8 | let(:seed) { instance_double("GeoPattern::Seed") } 9 | let(:preset) { instance_double("GeoPattern::ColorPreset") } 10 | let(:pattern) { instance_double("GeoPattern::Pattern") } 11 | let(:background_metadata) { instance_double("GeoPattern::BackgroundMetadata") } 12 | 13 | let(:color) { "#aaaaaa" } 14 | let(:base_color) { "#bbbbbb" } 15 | let(:base_color_should_be_used) { true } 16 | 17 | before :each do 18 | allow(seed).to receive(:to_i).with(14, 3).and_return(2616) 19 | allow(seed).to receive(:to_i).with(17, 1).and_return(3) 20 | 21 | allow(preset).to receive(:mode?).with(:base_color).and_return(base_color_should_be_used) 22 | allow(preset).to receive(:color).and_return(color) 23 | allow(preset).to receive(:base_color).and_return(base_color) 24 | end 25 | 26 | it { is_expected.not_to be_nil } 27 | 28 | describe "#generate" do 29 | context "when base color is given" do 30 | let(:generated_color) { %w[187 187 187] } 31 | 32 | before :each do 33 | expect(pattern).to receive(:background=).with(have_image_with_rgb_color(generated_color)) 34 | end 35 | 36 | it { generator.generate(pattern) } 37 | end 38 | 39 | context "when color is given" do 40 | let(:base_color_should_be_used) { false } 41 | let(:generated_color) { %w[170 170 170] } 42 | 43 | before :each do 44 | expect(pattern).to receive(:background=).with(have_image_with_rgb_color(generated_color)) 45 | end 46 | 47 | it { generator.generate(pattern) } 48 | end 49 | 50 | it_behaves_like "a named generator", :solid 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/background_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Background do 6 | subject(:metadata) { described_class.new(image: svg_image, preset: preset, generator: generator, color: color) } 7 | 8 | let(:svg_image) { instance_double("GeoPattern::SvgImage") } 9 | let(:svg_image_content) { fixtures_path("generated_patterns/sine_waves.svg").read.chomp } 10 | let(:preset) { instance_double("GeoPattern::ColorPreset") } 11 | let(:generator) { stub_const("GeoPattern::BackgroundGenerators::SolidGenerator", Class.new) } 12 | 13 | let(:color) { "#aaaaaa" } 14 | let(:base_color) { "#bbbbbb" } 15 | 16 | it { is_expected.not_to be_nil } 17 | 18 | before :each do 19 | allow(preset).to receive(:color).and_return(color) 20 | allow(preset).to receive(:base_color).and_return(base_color) 21 | end 22 | 23 | it_behaves_like "a metadata argument", :color 24 | it_behaves_like "a metadata argument", :generator 25 | it_behaves_like "a forwarded metadata argument", :base_color 26 | it_behaves_like "a forwarded metadata argument", :color 27 | end 28 | -------------------------------------------------------------------------------- /spec/color_generators/base_color_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe ColorGenerators::BaseColorGenerator do 6 | subject(:generator) { described_class.new(color_string, seed) } 7 | 8 | let(:seed) { instance_double("GeoPattern::Seed") } 9 | let(:seed_value1) { 2616 } 10 | let(:seed_value2) { 2 } 11 | 12 | let(:color_string) { "#ff00ff" } 13 | 14 | it { is_expected.not_to be_nil } 15 | 16 | before :each do 17 | allow(seed).to receive(:to_i).with(14, 3).and_return(seed_value1) 18 | allow(seed).to receive(:to_i).with(17, 1).and_return(seed_value2) 19 | end 20 | 21 | describe "#generate" do 22 | let(:color) { generator.generate } 23 | 24 | context "when sat offset is % 2 == 0" do 25 | it { expect(color.to_svg).to eq "rgb(210, 255, 0)" } 26 | end 27 | 28 | context "when sat offset is not % 2 == 0" do 29 | let(:seed_value2) { 3 } 30 | it { expect(color.to_svg).to eq "rgb(207, 251, 4)" } 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/color_generators/simple_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ColorGenerators::SimpleGenerator do 4 | subject(:generator) { described_class.new(color_string) } 5 | 6 | let(:color_string) { "#ff00ff" } 7 | 8 | it { is_expected.not_to be_nil } 9 | 10 | describe "#generate" do 11 | let(:color) { generator.generate } 12 | it { expect(color.to_svg).to eq "rgb(255, 0, 255)" } 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/color_preset_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe ColorPreset do 6 | subject(:preset) { ColorPreset.new(**options) } 7 | let(:options) do 8 | { 9 | base_color: "#0f0f0f" 10 | } 11 | end 12 | 13 | it { expect(preset).not_to be nil } 14 | 15 | describe "#base_color" do 16 | it { expect(preset.base_color).to eq "#0f0f0f" } 17 | end 18 | 19 | describe "#color" do 20 | context "when set" do 21 | let(:options) do 22 | { 23 | base_color: "#0f0f0f", 24 | color: "#1f1f1f" 25 | } 26 | end 27 | 28 | it { expect(preset.color).to eq "#1f1f1f" } 29 | end 30 | end 31 | 32 | describe "#mode?" do 33 | context "when nil" do 34 | let(:options) do 35 | { 36 | base_color: "#0f0f0f", 37 | color: nil 38 | } 39 | end 40 | 41 | it { expect(preset).to be_mode :base_color } 42 | end 43 | 44 | context "when defined" do 45 | let(:options) do 46 | { 47 | base_color: "#0f0f0f", 48 | color: "#1f1f1f" 49 | } 50 | end 51 | 52 | it { expect(preset).to be_mode :color } 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/color_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe GeoPattern::Color do 6 | subject(:color) { described_class.new(color_string) } 7 | let(:color_string) { "#ff00ff" } 8 | let(:seed) { instance_double("GeoPattern::Seed") } 9 | 10 | describe "#to_svg" do 11 | it { expect(color.to_svg).to eq "rgb(255, 0, 255)" } 12 | end 13 | 14 | describe "#to_html" do 15 | it { expect(color.to_html).to eq color_string } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/geo_pattern_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe GeoPattern do 6 | subject(:pattern) { GeoPattern.generate(input) } 7 | let(:input) { "Mastering Markdown" } 8 | let(:color) { "#ffcc00" } 9 | let(:rgb_base_color) { PatternHelpers.html_to_rgb_for_string(seed, color) } 10 | let(:seed) { instance_double("GeoPattern::Seed") } 11 | 12 | before :each do 13 | allow(seed).to receive(:to_i).with(14, 3).and_return(2616) 14 | allow(seed).to receive(:to_i).with(17, 1).and_return(3) 15 | end 16 | 17 | it { expect(pattern).not_to be_nil } 18 | 19 | describe ".generate" do 20 | context "when invoked with the same input it returns the same output" do 21 | let(:other_pattern) { GeoPattern.generate(input) } 22 | it { expect(pattern.to_svg).to eq other_pattern.to_svg } 23 | end 24 | 25 | context "when an invalid option is given" do 26 | subject(:pattern) { GeoPattern.generate(input, **args) } 27 | let(:args) { {unknown: true} } 28 | 29 | it { expect { pattern }.to raise_error ArgumentError } 30 | end 31 | 32 | context "set background color of generated pattern" do 33 | let(:new_color) { "#ffcc00" } 34 | 35 | context "when a base color is set" do 36 | subject(:pattern) { GeoPattern.generate(input, base_color: color) } 37 | let(:new_color) { "#04fbf6" } 38 | 39 | it { expect(pattern.background.color.to_html).to eq(new_color) } 40 | end 41 | 42 | context "when a color is set" do 43 | subject(:pattern) { GeoPattern.generate(input, color: color) } 44 | 45 | it { expect(pattern.background.color.to_html).to eq(new_color) } 46 | end 47 | end 48 | 49 | context "specify the pattern" do 50 | subject(:pattern) { GeoPattern.generate(input, patterns: chosen_pattern) } 51 | 52 | let(:chosen_pattern) { :sine_waves } 53 | 54 | context "when the deprecated generator option is used" do 55 | subject(:pattern) { GeoPattern.generate(input, generator: chosen_pattern) } 56 | subject(:other_pattern) { GeoPattern.generate(input, patterns: chosen_pattern) } 57 | 58 | it { expect { pattern.to_svg }.to output(/deprecated/).to_stderr } 59 | it { silence(:stderr) { expect(pattern.to_svg).to eq other_pattern.to_svg } } 60 | end 61 | 62 | context "when multiple patterns are selected" do 63 | let(:chosen_pattern) { %i[sine_waves xes] } 64 | 65 | it { expect(pattern.structure.name).to eq(:sine_waves).or eq(:xes) } 66 | end 67 | 68 | context "when an old style pattern was chosen via class name" do 69 | it_behaves_like "an old style pattern", ChevronPattern, :chevrons 70 | it_behaves_like "an old style pattern", ConcentricCirclesPattern, :concentric_circles 71 | it_behaves_like "an old style pattern", DiamondPattern, :diamonds 72 | it_behaves_like "an old style pattern", HexagonPattern, :hexagons 73 | it_behaves_like "an old style pattern", MosaicSquaresPattern, :mosaic_squares 74 | it_behaves_like "an old style pattern", NestedSquaresPattern, :nested_squares 75 | it_behaves_like "an old style pattern", OctagonPattern, :octagons 76 | it_behaves_like "an old style pattern", OverlappingCirclesPattern, :overlapping_circles 77 | it_behaves_like "an old style pattern", OverlappingRingsPattern, :overlapping_rings 78 | it_behaves_like "an old style pattern", PlaidPattern, :plaid 79 | it_behaves_like "an old style pattern", PlusSignPattern, :plus_signs 80 | it_behaves_like "an old style pattern", SineWavePattern, :sine_waves 81 | it_behaves_like "an old style pattern", SquarePattern, :squares 82 | it_behaves_like "an old style pattern", TessellationPattern, :tessellation 83 | it_behaves_like "an old style pattern", TrianglePattern, :triangles 84 | it_behaves_like "an old style pattern", XesPattern, :xes 85 | end 86 | 87 | context "when an valid pattern was chosen" do 88 | it_behaves_like "a chosen pattern", :chevrons 89 | it_behaves_like "a chosen pattern", :concentric_circles 90 | it_behaves_like "a chosen pattern", :diamonds 91 | it_behaves_like "a chosen pattern", :hexagons 92 | it_behaves_like "a chosen pattern", :mosaic_squares 93 | it_behaves_like "a chosen pattern", :nested_squares 94 | it_behaves_like "a chosen pattern", :octagons 95 | it_behaves_like "a chosen pattern", :overlapping_circles 96 | it_behaves_like "a chosen pattern", :overlapping_rings 97 | it_behaves_like "a chosen pattern", :plaid 98 | it_behaves_like "a chosen pattern", :plus_signs 99 | it_behaves_like "a chosen pattern", :sine_waves 100 | it_behaves_like "a chosen pattern", :squares 101 | it_behaves_like "a chosen pattern", :tessellation 102 | it_behaves_like "a chosen pattern", :triangles 103 | it_behaves_like "a chosen pattern", :xes 104 | end 105 | 106 | context "when invalid patterns were chosen" do 107 | InvalidPatternClass = Class.new # rubocop:disable Lint/ConstantDefinitionInBlock 108 | 109 | it_behaves_like "an invalid pattern", InvalidPatternClass 110 | it_behaves_like "an invalid pattern", "invalid_pattern" 111 | it_behaves_like "an invalid pattern", :invalid_pattern 112 | it_behaves_like "an invalid pattern", [:sine_waves, "invalid_pattern"] 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /spec/helpers_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Helpers do 6 | describe ".underscore" do 7 | subject { described_class.underscore string } 8 | 9 | context "when string is empty" do 10 | let(:string) { "" } 11 | 12 | it { is_expected.to eq "" } 13 | end 14 | 15 | context "when string is in snakecase" do 16 | let(:string) { "HelloWorld" } 17 | 18 | it { is_expected.to eq "hello_world" } 19 | end 20 | 21 | context "when string is in snakecase at some places" do 22 | let(:string) { "helloWorld" } 23 | 24 | it { is_expected.to eq "hello_world" } 25 | end 26 | end 27 | 28 | describe ".demodulize" do 29 | subject { described_class.demodulize string } 30 | 31 | context "when constant" do 32 | before :each do 33 | stub_const("HelloWorld", Class.new) 34 | end 35 | 36 | let(:string) { HelloWorld } 37 | 38 | it { is_expected.to eq "HelloWorld" } 39 | end 40 | 41 | context "when string" do 42 | context "is empty" do 43 | let(:string) { "" } 44 | 45 | it { is_expected.to eq "" } 46 | end 47 | 48 | context "starts with ::" do 49 | let(:string) { "::Class" } 50 | 51 | it { is_expected.to eq "Class" } 52 | end 53 | 54 | context "has :: in betweend" do 55 | let(:string) { "HelloWorld::Class" } 56 | 57 | it { is_expected.to eq "Class" } 58 | end 59 | 60 | context "when no namespaces is given" do 61 | let(:string) { "HelloWorld" } 62 | 63 | it { is_expected.to eq "HelloWorld" } 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/pattern_preset_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe PatternPreset do 6 | subject(:preset) { PatternPreset.new(options) } 7 | let(:options) do 8 | { 9 | fill_color_dark: "#222", 10 | fill_color_light: "#ddd", 11 | stroke_color: "#000", 12 | stroke_opacity: 0.02, 13 | opacity_min: 0.02, 14 | opacity_max: 0.15 15 | } 16 | end 17 | 18 | it { expect(preset).not_to be nil } 19 | 20 | describe "#fill_color_dark" do 21 | it { expect(preset.fill_color_dark).to eq "#222" } 22 | end 23 | 24 | describe "#fill_color_light" do 25 | it { expect(preset.fill_color_light).to eq "#ddd" } 26 | end 27 | 28 | describe "#stroke_color" do 29 | it { expect(preset.stroke_color).to eq "#000" } 30 | end 31 | 32 | describe "#stroke_opacity" do 33 | it { expect(preset.stroke_opacity).to eq 0.02 } 34 | end 35 | 36 | describe "#opacity_min" do 37 | it { expect(preset.opacity_min).to eq 0.02 } 38 | end 39 | 40 | describe "#opacity_max" do 41 | it { expect(preset.opacity_max).to eq 0.15 } 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/pattern_sieve_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe PatternSieve do 6 | subject(:sieve) { PatternSieve.new(requested_patterns, seed, store) } 7 | 8 | before :each do 9 | stub_const("Pattern1", Class.new) 10 | stub_const("Pattern2", Class.new) 11 | end 12 | 13 | let(:store) { instance_double("GeoPattern::PatternStore") } 14 | let(:available_patterns) { [Pattern1, Pattern2] } 15 | let(:requested_patterns) { %i[pattern1 pattern2] } 16 | let(:seed) { instance_double("GeoPattern::Seed") } 17 | 18 | before :each do 19 | allow(seed).to receive(:to_i).with(20, 1).and_return(1) 20 | allow(store).to receive(:[]) 21 | end 22 | 23 | # Minimum valid object test 24 | it { expect(sieve).not_to be_nil } 25 | 26 | describe "#fetch" do 27 | context "when requested_patterns is empty" do 28 | let(:requested_patterns) { nil } 29 | 30 | before :each do 31 | expect(store).to receive(:all).and_return(available_patterns) 32 | expect(store).not_to receive(:[]) 33 | end 34 | 35 | it { expect(sieve.fetch).to eq Pattern2 } 36 | end 37 | 38 | context "when a valid pattern is requested" do 39 | let(:requested_patterns) { [:pattern1] } 40 | 41 | before :each do 42 | expect(store).to receive(:[]).with(:pattern1).and_return(Pattern1) 43 | end 44 | 45 | it { expect(sieve.fetch).to eq Pattern1 } 46 | end 47 | 48 | context "when an invalid pattern is requested" do 49 | let(:requested_patterns) { [:patternX] } 50 | 51 | before :each do 52 | allow(store).to receive(:[]).with(:patternX).and_return(nil) 53 | end 54 | 55 | it { expect(sieve.fetch).to be nil } 56 | end 57 | 58 | context "when a requested pattern is nil" do 59 | let(:requested_patterns) { [:pattern1, nil, :pattern2] } 60 | 61 | before :each do 62 | expect(store).not_to receive(:[]).with(nil) 63 | end 64 | 65 | it { sieve.fetch } 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/pattern_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Pattern do 6 | subject(:pattern) { Pattern.new(svg_image) } 7 | 8 | let(:svg_image) { instance_double("GeoPattern::SvgImage") } 9 | let(:svg_image_content) { fixtures_path("generated_patterns/sine_waves.svg").read.chomp } 10 | let(:background) { instance_double("GeoPattern::Background") } 11 | let(:background_image) { instance_double("GeoPattern::SvgImage") } 12 | let(:background_body) { %() } 13 | let(:structure) { instance_double("GeoPattern::Structure") } 14 | let(:structure_image) { instance_double("GeoPattern::SvgImage") } 15 | let(:structure_body) { %() } 16 | 17 | it { expect(pattern).not_to be_nil } 18 | 19 | before :each do 20 | allow(svg_image).to receive(:to_s).and_return svg_image_content 21 | allow(svg_image).to receive(:height=).and_return 100 22 | allow(svg_image).to receive(:width=).and_return 100 23 | 24 | allow(background).to receive(:image).and_return background_image 25 | 26 | allow(structure).to receive(:image).and_return structure_image 27 | end 28 | 29 | describe "#to_s" do 30 | before :each do 31 | allow(svg_image).to receive(:to_s).and_return(svg_image_content) 32 | end 33 | 34 | it { expect(pattern.to_s).to eq svg_image_content } 35 | end 36 | 37 | describe "#to_base64" do 38 | before :each do 39 | allow(svg_image).to receive(:to_s).and_return(svg_image_content) 40 | end 41 | 42 | it { expect(pattern.to_base64).to include "PHN2ZyB4bWxuc" } 43 | end 44 | 45 | describe "#to_data_uri" do 46 | before :each do 47 | allow(svg_image).to receive(:to_s).and_return(svg_image_content) 48 | end 49 | 50 | it { expect(pattern.to_data_uri).to include "PHN2ZyB4bWxuc" } 51 | end 52 | 53 | describe "#generate_me" do 54 | context "when a background is added" do 55 | let(:generator) { instance_double("GeoPattern::BackgroundGenerator") } 56 | 57 | before :each do 58 | expect(generator).to receive(:generate).with(pattern) 59 | end 60 | 61 | it do 62 | pattern.generate_me(generator) 63 | end 64 | end 65 | end 66 | 67 | describe "#include?" do 68 | it do 69 | expect(svg_image).to receive(:include?) 70 | 71 | pattern.include? "" 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/pattern_store_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe PatternStore do 6 | subject(:pattern_store) { PatternStore.new } 7 | 8 | it { expect(pattern_store).not_to be_nil } 9 | 10 | describe "#[]" do 11 | context "when a known pattern is requested" do 12 | context "as string" do 13 | let(:pattern_name) { "xes" } 14 | it_behaves_like "a known pattern name" 15 | end 16 | 17 | context "as klass" do 18 | let(:pattern_name) { XesPattern } 19 | it_behaves_like "a known pattern name" 20 | end 21 | 22 | context "as symbol" do 23 | let(:pattern_name) { :xes } 24 | it_behaves_like "a known pattern name" 25 | end 26 | end 27 | 28 | context "when an unknown pattern is requested" do 29 | context "as string" do 30 | let(:pattern_name) { "unknown" } 31 | it_behaves_like "an unknown pattern name" 32 | end 33 | 34 | context "as klass" do 35 | let(:pattern_name) do 36 | stub_const("UnknownPattern", Class.new) 37 | UnknownPattern 38 | end 39 | 40 | it_behaves_like "an unknown pattern name" 41 | end 42 | 43 | context "as symbol" do 44 | let(:pattern_name) { :unknown } 45 | it_behaves_like "an unknown pattern name" 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/pattern_validator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe PatternValidator do 6 | subject(:validator) { PatternValidator.new(store) } 7 | let(:store) { instance_double("GeoPattern::PatternStore") } 8 | let(:patterns) { [] } 9 | 10 | # Minimum valid object test 11 | it { expect(validator).not_to be_nil } 12 | 13 | describe "#validate" do 14 | context "when valid pattern is validated" do 15 | before :each do 16 | allow(store).to receive(:known?).with("pattern1").and_return(true) 17 | end 18 | 19 | it { expect { validator.validate(%w[pattern1]) }.not_to raise_error } 20 | end 21 | 22 | context "when invalid pattern is validated" do 23 | before :each do 24 | allow(store).to receive(:known?).with("pattern1").and_return(false) 25 | end 26 | 27 | it { expect { validator.validate(%w[pattern1]) }.to raise_error InvalidPatternError } 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/seed_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Seed do 6 | subject(:seed) { Seed.new(input) } 7 | let(:input) { "string" } 8 | 9 | it { expect(seed).not_to be_nil } 10 | 11 | describe "#[]" do 12 | context "when use an integer" do 13 | it { expect(seed[1]).to be_kind_of String } 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH << File.expand_path("../lib", __dir__) 4 | 5 | require "simplecov" 6 | SimpleCov.command_name "rspec" 7 | SimpleCov.start 8 | 9 | # Pull in all of the gems including those in the `test` group 10 | require "bundler" 11 | Bundler.require :default, :test, :development 12 | 13 | require "geo_pattern" 14 | 15 | # Loading support files 16 | GeoPattern::Helpers.require_files_matching_pattern ::File.expand_path("support/**/*.rb", __dir__) 17 | 18 | # No need to add the namespace to every class tested 19 | include GeoPattern # rubocop:disable Style/MixinUsage 20 | -------------------------------------------------------------------------------- /spec/structure_generators/chevrons_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe StructureGenerators::ChevronsGenerator do 6 | it_behaves_like "a structure generator", :chevrons 7 | end 8 | -------------------------------------------------------------------------------- /spec/structure_generators/concentric_circles_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe StructureGenerators::ConcentricCirclesGenerator do 6 | it_behaves_like "a structure generator", :concentric_circles 7 | end 8 | -------------------------------------------------------------------------------- /spec/structure_generators/diamonds_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe StructureGenerators::DiamondsGenerator do 6 | it_behaves_like "a structure generator", :diamonds 7 | end 8 | -------------------------------------------------------------------------------- /spec/structure_generators/hexagons_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe StructureGenerators::HexagonsGenerator do 6 | it_behaves_like "a structure generator", :hexagons 7 | end 8 | -------------------------------------------------------------------------------- /spec/structure_generators/mosaic_squares_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe StructureGenerators::MosaicSquaresGenerator do 6 | it_behaves_like "a structure generator", :mosaic_squares 7 | end 8 | -------------------------------------------------------------------------------- /spec/structure_generators/nested_squares_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe StructureGenerators::NestedSquaresGenerator do 6 | it_behaves_like "a structure generator", :nested_squares 7 | end 8 | -------------------------------------------------------------------------------- /spec/structure_generators/octagons_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe StructureGenerators::OctagonsGenerator do 6 | it_behaves_like "a structure generator", :octagons 7 | end 8 | -------------------------------------------------------------------------------- /spec/structure_generators/overlapping_circles_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe StructureGenerators::OverlappingCirclesGenerator do 6 | it_behaves_like "a structure generator", :overlapping_circles 7 | end 8 | -------------------------------------------------------------------------------- /spec/structure_generators/overlapping_rings_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe StructureGenerators::OverlappingRingsGenerator do 6 | it_behaves_like "a structure generator", :overlapping_rings 7 | end 8 | -------------------------------------------------------------------------------- /spec/structure_generators/plaid_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe StructureGenerators::PlaidGenerator do 6 | it_behaves_like "a structure generator", :plaid 7 | end 8 | -------------------------------------------------------------------------------- /spec/structure_generators/plus_signs_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe StructureGenerators::PlusSignsGenerator do 6 | it_behaves_like "a structure generator", :plus_signs 7 | end 8 | -------------------------------------------------------------------------------- /spec/structure_generators/sine_waves_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe StructureGenerators::SineWavesGenerator do 6 | it_behaves_like "a structure generator", :sine_waves 7 | end 8 | -------------------------------------------------------------------------------- /spec/structure_generators/squares_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe StructureGenerators::SquaresGenerator do 6 | it_behaves_like "a structure generator", :squares 7 | end 8 | -------------------------------------------------------------------------------- /spec/structure_generators/tessellation_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe StructureGenerators::TessellationGenerator do 6 | it_behaves_like "a structure generator", :tessellation 7 | end 8 | -------------------------------------------------------------------------------- /spec/structure_generators/triangles_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe StructureGenerators::TrianglesGenerator do 6 | it_behaves_like "a structure generator", :triangles 7 | end 8 | -------------------------------------------------------------------------------- /spec/structure_generators/xes_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe StructureGenerators::XesGenerator do 6 | it_behaves_like "a structure generator", :xes 7 | end 8 | -------------------------------------------------------------------------------- /spec/structure_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Structure do 6 | subject(:metadata) { described_class.new(image: svg_image, preset: preset, generator: generator, name: name) } 7 | 8 | let(:svg_image) { instance_double("GeoPattern::SvgImage") } 9 | let(:svg_image_content) { fixtures_path("generated_patterns/sine_waves.svg").read.chomp } 10 | let(:preset) { instance_double("GeoPattern::PatternPreset") } 11 | let(:generator) { stub_const("GeoPattern::StructureGenerators::ChevronGenerator", Class.new) } 12 | 13 | let(:name) { :chevron } 14 | let(:fill_color_dark) { "#222" } 15 | let(:fill_color_light) { "#ddd" } 16 | let(:stroke_color) { "#000" } 17 | let(:stroke_opacity) { 0.02 } 18 | let(:opacity_min) { 0.02 } 19 | let(:opacity_max) { 0.15 } 20 | 21 | it { is_expected.not_to be_nil } 22 | 23 | before :each do 24 | allow(preset).to receive(:fill_color_dark).and_return(fill_color_dark) 25 | allow(preset).to receive(:fill_color_light).and_return(fill_color_light) 26 | allow(preset).to receive(:stroke_color).and_return(stroke_color) 27 | allow(preset).to receive(:stroke_opacity).and_return(stroke_opacity) 28 | allow(preset).to receive(:opacity_min).and_return(opacity_min) 29 | allow(preset).to receive(:opacity_max).and_return(opacity_max) 30 | end 31 | 32 | it_behaves_like "a metadata argument", :name 33 | it_behaves_like "a metadata argument", :generator 34 | it_behaves_like "a forwarded metadata argument", :fill_color_dark 35 | it_behaves_like "a forwarded metadata argument", :fill_color_light 36 | it_behaves_like "a forwarded metadata argument", :stroke_color 37 | it_behaves_like "a forwarded metadata argument", :stroke_opacity 38 | it_behaves_like "a forwarded metadata argument", :opacity_min 39 | it_behaves_like "a forwarded metadata argument", :opacity_max 40 | end 41 | -------------------------------------------------------------------------------- /spec/support/aruba.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "aruba/rspec" 4 | require "aruba/api" 5 | 6 | # Spec Helpers 7 | module SpecHelper 8 | # Helpers for aruba 9 | module Aruba 10 | include ::Aruba::Api 11 | 12 | def dirs 13 | @dirs ||= %w[tmp rspec] 14 | end 15 | end 16 | end 17 | 18 | RSpec.configure do |c| 19 | c.include SpecHelper::Aruba 20 | 21 | c.before :each do 22 | setup_aruba 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/support/helpers/fixtures.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SpecHelper 4 | module Fixtures 5 | def fixtures_path(name) 6 | base_path = Pathname.new(File.expand_path("../../../fixtures", __dir__)) 7 | base_path + Pathname.new(name) 8 | end 9 | end 10 | end 11 | 12 | RSpec.configure do |c| 13 | c.include SpecHelper::Fixtures 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/kernel.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # rubocop:disable Style/EvalWithLocation 4 | 5 | module Kernel 6 | # Captures the given stream and returns it: 7 | # 8 | # @param [String] stream 9 | # 10 | # The name of the stream to be captured 11 | # 12 | # @example Capture output 13 | # 14 | # stream = capture(:stdout) { puts 'notice' } 15 | # stream # => "notice\n" 16 | # 17 | # stream = capture(:stderr) { warn 'error' } 18 | # stream # => "error\n" 19 | # 20 | # @example Capture output for subprocesses 21 | # 22 | # stream = capture(:stdout) { system('echo notice') } 23 | # stream # => "notice\n" 24 | # 25 | # stream = capture(:stderr) { system('echo error 1>&2') } 26 | # stream # => "error\n" 27 | def capture(stream) 28 | stream = stream.to_s 29 | captured_stream = Tempfile.new(stream) 30 | stream_io = eval("$#{stream}") # rubocop:disable Security/Eval 31 | origin_stream = stream_io.dup 32 | stream_io.reopen(captured_stream) 33 | 34 | yield 35 | 36 | stream_io.rewind 37 | captured_stream.read 38 | ensure 39 | captured_stream.close 40 | captured_stream.unlink 41 | stream_io.reopen(origin_stream) 42 | end 43 | alias_method :silence, :capture 44 | end 45 | 46 | # rubocop:enable Style/EvalWithLocation 47 | -------------------------------------------------------------------------------- /spec/support/matchers/image.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rspec/expectations" 4 | 5 | RSpec::Matchers.define :have_image_with_rgb_color do |*expected| 6 | expected = format("rgb(%s, %s, %s)", *expected.flatten) # rubocop:disable Style/FormatStringToken 7 | 8 | match do |actual| 9 | actual.image.include? expected 10 | end 11 | 12 | failure_message_when_negated do |actual| 13 | "expected that #{actual} includes color #{expected}" 14 | end 15 | 16 | failure_message do |actual| 17 | "expected that #{actual} not includes color #{expected}" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/matchers/name.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rspec/expectations" 4 | 5 | RSpec::Matchers.define :have_name do |expected| 6 | match do |actual| 7 | actual.name? expected 8 | end 9 | 10 | failure_message_when_negated do |actual| 11 | "expected that #{actual} does not nave name \"#{expected}\"" 12 | end 13 | 14 | failure_message do |actual| 15 | "expected that #{actual} has name \"#{expected}\"" 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/support/rspec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.configure do |config| 4 | config.expect_with :rspec do |expectations| 5 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 6 | end 7 | 8 | config.mock_with :rspec do |mocks| 9 | mocks.verify_partial_doubles = true 10 | end 11 | 12 | config.filter_run :focus 13 | config.run_all_when_everything_filtered = true 14 | 15 | config.disable_monkey_patching! 16 | config.warnings = false 17 | 18 | if config.files_to_run.one? 19 | config.default_formatter = "doc" 20 | end 21 | 22 | config.profile_examples = 10 if ENV.key? "RSPEC_PROFILE" 23 | 24 | config.order = :random 25 | Kernel.srand config.seed 26 | end 27 | -------------------------------------------------------------------------------- /spec/support/shared_examples/generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_examples "a structure generator" do |name| 4 | subject { described_class.new(seed, preset, svg_image) } 5 | 6 | let(:seed) { instance_double("GeoPattern::Seed") } 7 | let(:preset) { instance_double("GeoPattern::PatternPreset") } 8 | let(:svg_image) { SvgImage.new } 9 | let(:pattern) { instance_double("GeoPattern::Pattern") } 10 | 11 | let(:name) { name } 12 | 13 | let(:fill_color_dark) { "#222" } 14 | let(:fill_color_light) { "#ddd" } 15 | let(:stroke_color) { "#000" } 16 | let(:stroke_opacity) { 0.02 } 17 | let(:opacity_min) { 0.02 } 18 | let(:opacity_max) { 0.15 } 19 | 20 | before :each do 21 | allow(preset).to receive(:fill_color_dark).and_return(fill_color_dark) 22 | allow(preset).to receive(:fill_color_light).and_return(fill_color_light) 23 | allow(preset).to receive(:stroke_color).and_return(stroke_color) 24 | allow(preset).to receive(:stroke_opacity).and_return(stroke_opacity) 25 | allow(preset).to receive(:opacity_min).and_return(opacity_min) 26 | allow(preset).to receive(:opacity_max).and_return(opacity_max) 27 | 28 | allow(seed).to receive(:to_i).and_return(1) 29 | end 30 | 31 | it { is_expected.not_to be_nil } 32 | it { is_expected.to respond_to(:generate) } 33 | 34 | it do 35 | expect(pattern).to receive(:structure=).with(kind_of(Structure)) 36 | expect(pattern).to receive(:height=).with(kind_of(Numeric)) 37 | expect(pattern).to receive(:width=).with(kind_of(Numeric)) 38 | 39 | subject.generate(pattern) 40 | end 41 | 42 | it_behaves_like "a named generator", name 43 | end 44 | 45 | RSpec.shared_examples "a named generator" do |name| 46 | it { is_expected.to have_name name } 47 | it { is_expected.to have_name name.to_s } 48 | end 49 | -------------------------------------------------------------------------------- /spec/support/shared_examples/pattern.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_examples "a chosen pattern" do |name| 4 | subject(:pattern) { GeoPattern.generate(input, patterns: name) } 5 | 6 | let(:file_name) { "#{name}.svg" } 7 | 8 | before :each do 9 | write_file file_name, pattern.to_s 10 | end 11 | 12 | it { expect(pattern.structure).to be_name name } 13 | it { expect(file_name).to have_same_file_content_as("%/generated_patterns/#{name}.svg") } 14 | end 15 | 16 | RSpec.shared_examples "an invalid pattern" do |chosen_pattern| 17 | subject(:pattern) { GeoPattern.generate(input, patterns: chosen_pattern) } 18 | 19 | it { expect { subject }.to raise_error InvalidPatternError } 20 | end 21 | 22 | RSpec.shared_examples "an old style pattern" do |chosen_pattern, name| 23 | subject(:pattern) { GeoPattern.generate(input, patterns: chosen_pattern) } 24 | 25 | let(:file_name) { "#{name}.svg" } 26 | 27 | before :each do 28 | write_file file_name, pattern.to_s 29 | end 30 | 31 | it { expect(pattern.structure).to be_name name } 32 | it { expect(file_name).to have_same_file_content_as("%/generated_patterns/#{name}.svg") } 33 | end 34 | -------------------------------------------------------------------------------- /spec/support/shared_examples/pattern_name.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_examples "a known pattern name" do 4 | it { silence(:stderr) { expect(pattern_store[pattern_name]).not_to be_nil } } 5 | end 6 | 7 | RSpec.shared_examples "an unknown pattern name" do 8 | it { silence(:stderr) { expect(pattern_store[pattern_name]).to be_nil } } 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/shared_examples/structure.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_examples "a metadata argument" do |argument| 4 | describe "##{argument}" do 5 | it { expect(metadata.public_send(argument)).to eq public_send(argument) } 6 | end 7 | 8 | describe "##{argument}?" do 9 | context "when #{argument} is not defined" do 10 | let(argument) { nil } 11 | it { expect { metadata }.to raise_error ArgumentError, "Argument #{argument} is missing" } 12 | end 13 | 14 | context "when #{argument} is defined" do 15 | context "when argument is not given" do 16 | it { is_expected.to public_send(:"be_#{argument}", nil) } 17 | end 18 | 19 | context "when argument is the same as the defined one" do 20 | it { is_expected.to public_send(:"be_#{argument}", public_send(argument)) } 21 | end 22 | 23 | context "when argument is different from the defined one" do 24 | it { is_expected.not_to public_send(:"be_#{argument}", "blub") } 25 | end 26 | end 27 | end 28 | end 29 | 30 | RSpec.shared_examples "a forwarded metadata argument" do |argument| 31 | describe "##{argument}" do 32 | it { expect(metadata.public_send(argument)).to eq public_send(argument) } 33 | end 34 | 35 | describe "##{argument}?" do 36 | context "when #{argument} is defined" do 37 | context "when argument is not given" do 38 | it { is_expected.to public_send(:"be_#{argument}", nil) } 39 | end 40 | 41 | context "when argument is the same as the defined one" do 42 | it { is_expected.to public_send(:"be_#{argument}", public_send(argument)) } 43 | end 44 | 45 | context "when argument is different from the defined one" do 46 | it { is_expected.not_to public_send(:"be_#{argument}", "blub") } 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/support/string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_support/core_ext/string/strip" 4 | -------------------------------------------------------------------------------- /spec/svg_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe SvgImage do 6 | context "#<=>" do 7 | it "is comparable" do 8 | svg1 = SvgImage.new 9 | svg2 = SvgImage.new 10 | 11 | expect(svg1).to eq svg2 12 | end 13 | end 14 | end 15 | --------------------------------------------------------------------------------