nameplate 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +122 -0
  4. data/Rakefile +39 -0
  5. data/lib/nameplate/avatar/cache.rb +59 -0
  6. data/lib/nameplate/avatar/generator.rb +237 -0
  7. data/lib/nameplate/avatar/identity.rb +41 -0
  8. data/lib/nameplate/avatar.rb +37 -0
  9. data/lib/nameplate/colors/palette.rb +45 -0
  10. data/lib/nameplate/colors/palettes/custom.rb +20 -0
  11. data/lib/nameplate/colors/palettes/dracula.rb +29 -0
  12. data/lib/nameplate/colors/palettes/google.rb +55 -0
  13. data/lib/nameplate/colors/palettes/iwanthue.rb +234 -0
  14. data/lib/nameplate/colors/palettes/jedi_light.rb +26 -0
  15. data/lib/nameplate/colors/palettes/monokai.rb +27 -0
  16. data/lib/nameplate/colors/palettes/pastel.rb +24 -0
  17. data/lib/nameplate/colors.rb +42 -0
  18. data/lib/nameplate/configuration.rb +109 -0
  19. data/lib/nameplate/fonts/Lato-Light.ttf +0 -0
  20. data/lib/nameplate/fonts/Roboto-Medium +0 -0
  21. data/lib/nameplate/has_avatar.rb +33 -0
  22. data/lib/nameplate/image/resize.rb +111 -0
  23. data/lib/nameplate/results/failure_result.rb +35 -0
  24. data/lib/nameplate/results/success_result.rb +27 -0
  25. data/lib/nameplate/utils/path_helper.rb +18 -0
  26. data/lib/nameplate/version.rb +5 -0
  27. data/lib/nameplate/view_helpers/avatar_helper.rb +60 -0
  28. data/lib/nameplate.rb +57 -0
  29. data/spec/fixtures/in.png +0 -0
  30. data/spec/nameplate/avatar/cache_spec.rb +35 -0
  31. data/spec/nameplate/avatar/generator_spec.rb +108 -0
  32. data/spec/nameplate/avatar/identity_spec.rb +27 -0
  33. data/spec/nameplate/colors_spec.rb +34 -0
  34. data/spec/nameplate/image/resize_spec.rb +96 -0
  35. data/spec/nameplate/utils/path_helper_spec.rb +16 -0
  36. data/spec/nameplate/view_helpers/avatar_helper_spec.rb +38 -0
  37. data/spec/nameplate_spec.rb +102 -0
  38. data/spec/spec_helper.rb +13 -0
  39. metadata +111 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 71033d48e71e5ea461b075ae0d1d6b5f359eb9dfcd39885ec2994ec4c1475641
4
+ data.tar.gz: bc180c2c1ebee448c4c19ee3ed16e20cf4f20cde96f131963f12451e0b9e0bde
5
+ SHA512:
6
+ metadata.gz: 4647667e97c3ab81e10b2730048c8dd3098524eb2d67e635c3ee60652145b8c5d52ab10cb3b5a3cefa7d2ea3f0266eada2596dd459832d978972f15b76a7125b
7
+ data.tar.gz: 2b88c146ad0373b3069723d962b964e432ccd2f84d46abc49d9fb4034c477fe99be90c5c2dc58d00fec8508d2c46b9fff37f83b0a5e62e0f7f7df77f8369aa9b
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 t0nylombardi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # NamePlate
2
+
3
+ **NamePlate** is a Ruby gem for generating simple, Google-style avatar images from names or usernames. It’s a modernized successor to [letter_avatar](https://github.com/ksz2k/letter_avatar), built with maintainability, SOLID design, and Rails-friendly integration in mind.
4
+
5
+ Think of it as a quick way to give everyone in your app a unique, consistent avatar — without storing profile photos.
6
+
7
+ ---
8
+
9
+ ## Features
10
+
11
+ - 🔠 Generates avatars with initials (e.g., **Tony Lombardi → TL**)
12
+ - 🎨 Deterministic background colors from input strings
13
+ - 📐 Flexible sizing (from tiny icons to large images)
14
+ - 🖼 Transparent padding and proper centering with MiniMagick
15
+ - ⚡ Caching support for faster repeated lookups
16
+ - ✅ Rails helpers for easy view integration
17
+ - 🧪 Fully tested and type-annotated with RBS
18
+
19
+ ---
20
+
21
+ ## Installation
22
+
23
+ Add this line to your application’s Gemfile:
24
+
25
+ ```ruby
26
+ gem "nameplate"
27
+ ```
28
+
29
+ And then execute:
30
+
31
+ ```bash
32
+ bundle install
33
+ ```
34
+
35
+ Or install it yourself with:
36
+
37
+ ```bash
38
+ gem install nameplate
39
+ ```
40
+
41
+ ---
42
+
43
+ ## Usage
44
+
45
+ ### Quick Start
46
+
47
+ ```ruby
48
+ require "nameplate"
49
+
50
+ # Generate a 128x128 avatar for "Tony"
51
+ path = NamePlate::Avatar::Generator.call("Tony", 128)
52
+
53
+ # => "tmp/generated/128.png"
54
+ ```
55
+
56
+ ### With Rails View Helper
57
+
58
+ ```erb
59
+ <%= nameplate_avatar("Tony", size: 64, class: "avatar") %>
60
+ ```
61
+
62
+ Outputs an `<img>` tag pointing to the cached avatar file.
63
+
64
+ ---
65
+
66
+ ## Configuration
67
+
68
+ You can configure fonts, colors, and weights globally:
69
+
70
+ ```ruby
71
+ NamePlate.configure do |config|
72
+ config.font = Rails.root.join("app/assets/fonts/YourFont.ttf")
73
+ config.weight = 400
74
+ config.pointsize = 64
75
+ end
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Async Generation
81
+
82
+ For heavy workloads (e.g., pre-warming caches):
83
+
84
+ ```ruby
85
+ future = NamePlate::Avatar::Generator.async_call("Tony", 128)
86
+ future.value! # blocks until done
87
+ ```
88
+
89
+ ---
90
+
91
+ ## Development
92
+
93
+ Clone the repo and run:
94
+
95
+ ```bash
96
+ bin/setup
97
+ bundle exec rake spec
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Roadmap
103
+
104
+ - SVG support
105
+ - Rails engine integration (asset pipeline)
106
+ - Configurable color palettes
107
+
108
+ ---
109
+
110
+ ## Contributing
111
+
112
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/t0nylombardi/nameplate](https://github.com/t0nylombardi/nameplate).
113
+
114
+ ---
115
+
116
+ ## License
117
+
118
+ MIT License. See `LICENSE.txt` for details.
119
+
120
+ ---
121
+
122
+ ⚡ **NamePlate** — because every name deserves a face.
data/Rakefile ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "standard/rake"
5
+ require "pry"
6
+ require "pry-byebug"
7
+ require_relative "lib/nameplate"
8
+
9
+ SIG_DIR = "sig"
10
+
11
+ task default: %i[specstandard]
12
+ task :standard do
13
+ Standard::RakeTask.new.execute
14
+ end
15
+
16
+ desc "Iterate over [A..Z] letters in every theme"
17
+ task :iterate_letters do
18
+ output_dir = File.expand_path("demo_images", __dir__)
19
+ FileUtils.mkdir_p(output_dir)
20
+
21
+ size = 128
22
+
23
+ NamePlate::Colors.registry.each do |key, palette|
24
+ puts "== Theme: #{key} =="
25
+
26
+ NamePlate.colors_palette = key
27
+ theme_dir = File.join(output_dir, key.to_s)
28
+ FileUtils.mkdir_p(theme_dir)
29
+
30
+ ("A".."Z").each do |letter|
31
+ path = NamePlate::Avatar.generate(letter, size)
32
+ dest = File.join(theme_dir, "#{letter}.png")
33
+ FileUtils.cp(path, dest)
34
+ puts " #{letter} → #{dest}"
35
+ end
36
+ end
37
+
38
+ puts "All images generated under #{output_dir}/"
39
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent-ruby"
4
+ require "fileutils"
5
+
6
+ module NamePlate
7
+ class Avatar
8
+ # Thread-safe cache manager for avatar file paths.
9
+ class Cache
10
+ @cache = Concurrent::Map.new
11
+
12
+ class << self
13
+ # Returns the base path for cached avatars.
14
+ #
15
+ # @return [String] Base cache path.
16
+ def base_path
17
+ "#{NamePlate.cache_base_path || "public/system"}/nameplate/#{Avatar::VERSION}"
18
+ end
19
+
20
+ # Builds or fetches the file path for a cached avatar.
21
+ #
22
+ # Uses a Concurrent::Map to ensure thread-safe memoization.
23
+ #
24
+ # @param [Identity] identity The identity object representing the user.
25
+ # @param [Integer] size The size of the avatar.
26
+ # @return [String] The file path for the cached avatar.
27
+ def path(identity, size)
28
+ key = cache_key(identity, size)
29
+
30
+ @cache.compute_if_absent(key) do
31
+ dir = File.join(base_path, identity.letters, identity.color.join("_"))
32
+ FileUtils.mkdir_p(dir)
33
+ File.join(dir, "#{size}.png")
34
+ end
35
+ end
36
+
37
+ # Check if a cached avatar exists for the given identity and size.
38
+ #
39
+ # @param [Identity] identity The identity object representing the user.
40
+ # @param [Integer] size The size of the avatar.
41
+ # @return [Boolean] True if a cached avatar exists, false otherwise.
42
+ def cached?(identity, size)
43
+ File.exist?(path(identity, size))
44
+ end
45
+
46
+ private
47
+
48
+ # Builds a unique cache key from identity + size.
49
+ #
50
+ # @param [Identity] identity
51
+ # @param [Integer] size
52
+ # @return [String]
53
+ def cache_key(identity, size)
54
+ "#{identity.letters}-#{identity.color.join("_")}-#{size}"
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mini_magick"
4
+ require "concurrent-ruby"
5
+
6
+ module NamePlate
7
+ class Avatar
8
+ # Generates PNG avatars from usernames using MiniMagick/ImageMagick.
9
+ #
10
+ # - Derives an {Identity} (initial letters + background color) from a username.
11
+ # - Produces a square PNG at the requested size (capped at {Avatar::FULLSIZE}).
12
+ # - Uses {Avatar::Cache} to reuse existing files and to build deterministic paths.
13
+ #
14
+ # The high-level API for consumers is {NamePlate::Avatar.generate}. This class
15
+ # exists as the lower-level, orchestration entry point that handles logging,
16
+ # validation, caching, and MiniMagick invocation.
17
+ #
18
+ # Requirements:
19
+ # - ImageMagick must be installed and accessible on the PATH (MiniMagick uses `convert`).
20
+ # - A valid font file must be configured at {Avatar::FONT_FILE}.
21
+ #
22
+ # Configuration sources used during generation:
23
+ # - `NamePlate.pointsize`, `NamePlate.weight`, `NamePlate.annotate_position`
24
+ # - {Avatar::FILL_COLOR} (foreground text/letters color)
25
+ # - {Avatar::FONT_FILE} (font used by ImageMagick)
26
+ # - `NamePlate.cache_base_path` (root for cached files; see {Avatar::Cache})
27
+ # - `ENV["NAMEPLATE_LOG_LEVEL"]` (set to `DEBUG` for verbose logging)
28
+ #
29
+ # @example Generate and return a cached 128px avatar path
30
+ # path = NamePlate::Avatar::Generator.call("Tony Baloney", 128)
31
+ # # => "public/system/nameplate/1/TB/163_163_163/128.png"
32
+ #
33
+ # @example Disable cache and provide a custom logger
34
+ # logger = Logger.new($stderr)
35
+ # path = NamePlate::Avatar::Generator.call("Ada Lovelace", 256, cache: false, logger: logger)
36
+ # # Generates a fresh 256px PNG even if a cached one exists
37
+ #
38
+ # @see NamePlate::Avatar.generate User-facing convenience API
39
+ # @see NamePlate::Avatar::Cache Path building and cache helpers
40
+ class Generator
41
+ # Base class for avatar generation errors
42
+ class GenerationError < StandardError; end
43
+
44
+ # Raised when MiniMagick/ImageMagick fails to render an avatar.
45
+ class ImageMagickError < GenerationError; end
46
+
47
+ # Raised when filesystem operations (write/verify) fail.
48
+ class FileSystemError < GenerationError; end
49
+
50
+ # Raised when inputs or configuration are invalid.
51
+ class ConfigurationError < GenerationError; end
52
+
53
+ # Instantiate a new generator.
54
+ #
55
+ # Prefer {::call} unless you need a long-lived instance.
56
+ #
57
+ # @param username [String] The source name used to derive initials and color.
58
+ # @param size [Integer] Target size in pixels (> 0). Capped at {Avatar::FULLSIZE}.
59
+ # @param cache [Boolean] Reuse existing cached PNG when present. Defaults to `true`.
60
+ # @param logger [Logger, nil] Optional logger; defaults to a simple STDOUT logger.
61
+ # @raise [ConfigurationError] If parameters are invalid or required assets are missing.
62
+ def initialize(username, size, cache: true, logger: nil)
63
+ @username = username
64
+ @size = size
65
+ @cache = cache
66
+ @font = Avatar::FONT_FILE
67
+ @fill = Avatar::FILL_COLOR
68
+ @logger = logger || default_logger
69
+
70
+ validate_inputs!
71
+ end
72
+
73
+ def self.call(username, size, cache: true, logger: nil)
74
+ new(username, size, cache: cache, logger: logger).execute!
75
+ end
76
+
77
+ # Same API as {.call}, but returns a Future so callers can decide
78
+ # when to block.
79
+ #
80
+ # @return [Concurrent::Future<String>] Future that resolves to the avatar path.
81
+ # @see .call
82
+ def self.async_call(username, size, cache: true, logger: nil)
83
+ Concurrent::Future.execute do
84
+ new(username, size, cache: cache, logger: logger).execute!
85
+ end
86
+ end
87
+
88
+ # Run the avatar generation pipeline: build identity, resolve cache path,
89
+ # and generate if needed.
90
+ #
91
+ # @return [String] Filesystem path to the generated or cached avatar PNG.
92
+ # @raise [GenerationError] If anything goes wrong during generation.
93
+ def execute!
94
+ with_error_handling do
95
+ logger.info "Starting avatar generation for '#{username}' at size #{size}px"
96
+ path = generate
97
+ logger.info "Avatar generation completed successfully: #{path}"
98
+ path
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ attr_reader :username, :size, :cache, :font, :fill, :logger
105
+
106
+ # Generate or reuse an avatar at the requested size.
107
+ #
108
+ # Builds an identity from `username`, computes the cache path,
109
+ # renders the PNG if not already cached, and returns the file path.
110
+ #
111
+ # @return [String] Filesystem path to the generated or cached avatar PNG.
112
+ def generate
113
+ identity = build_identity
114
+ target_size = normalize_size
115
+ target_path = Avatar::Cache.path(identity, target_size)
116
+ return use_cached(target_path) if cached?(target_path)
117
+
118
+ Concurrent::Future.execute { generate_avatar(identity, target_size, target_path) }.value!
119
+
120
+ target_path
121
+ end
122
+
123
+ # Build an avatar identity from the configured username.
124
+ #
125
+ # @return [NamePlate::Avatar::Identity] Derived initials and background color.
126
+ def build_identity
127
+ Avatar::Identity.from_username(username).tap do |identity|
128
+ logger.debug "Generated identity: #{identity.inspect}"
129
+ end
130
+ end
131
+
132
+ # Clamp the requested size to the maximum full size.
133
+ #
134
+ # @return [Integer] Target size in pixels (<= {Avatar::FULLSIZE}).
135
+ def normalize_size
136
+ [size, Avatar::FULLSIZE].min.tap do |s|
137
+ logger.debug "Target size: #{s}"
138
+ end
139
+ end
140
+
141
+ # Check if a cached avatar exists at the given path.
142
+ #
143
+ # @param [String] path The expected avatar file path.
144
+ # @return [Boolean] True if a cached file exists, false otherwise.
145
+ def cached?(path)
146
+ cache && File.exist?(path)
147
+ end
148
+
149
+ # Use an existing cached avatar path.
150
+ #
151
+ # @param [String] path The cached avatar file path.
152
+ # @return [String] The same cached path that was provided.
153
+ def use_cached(path)
154
+ path
155
+ end
156
+
157
+ # Render the avatar image using MiniMagick/ImageMagick.
158
+ #
159
+ # @param identity [Identity] Letters and background color.
160
+ # @param size [Integer] Target size in pixels.
161
+ # @param filename [String, Pathname] Output file path.
162
+ # @raise [ImageMagickError] If MiniMagick raises during conversion.
163
+ def generate_avatar(identity, size, filename)
164
+ MiniMagick.convert do |c|
165
+ c.size "#{size}x#{size}"
166
+ c << "xc:#{to_rgb(identity.color)}"
167
+ c.pointsize NamePlate.pointsize.to_s
168
+ c.font font.to_s
169
+ c.weight NamePlate.weight.to_s
170
+ c.fill fill.to_s.gsub(/\s+/, "")
171
+ c.gravity "Center"
172
+ c.annotate NamePlate.annotate_position.to_s, identity.letters.to_s
173
+ c << filename.to_s
174
+ end
175
+ rescue => e
176
+ raise ImageMagickError, "MiniMagick failed to generate avatar: #{e.message}"
177
+ end
178
+
179
+ # Convert `[r, g, b]` array to `rgb(r,g,b)` string accepted by ImageMagick.
180
+ #
181
+ # @param color [Array<Integer>] RGB values in the 0..255 range.
182
+ # @return [String] `rgb(r,g,b)` formatted string.
183
+ # @raise [ConfigurationError] If the color is not a valid triplet.
184
+ def to_rgb(color)
185
+ "rgb(#{color.join(",")})"
186
+ rescue => e
187
+ raise ConfigurationError, "Invalid color format: #{color.inspect} - #{e.message}"
188
+ end
189
+
190
+ # Validate constructor inputs and required configuration.
191
+ #
192
+ # @return [void]
193
+ # @raise [ConfigurationError] When any validation fails.
194
+ def validate_inputs!
195
+ raise ConfigurationError, "Username cannot be empty" if username.to_s.strip.empty?
196
+ raise ConfigurationError, "Size must be positive integer" unless size.is_a?(Integer) && size.positive?
197
+ raise ConfigurationError, "Font file not found: #{font}" unless File.exist?(font.to_s)
198
+ raise ConfigurationError, "Fill color not configured" if fill.to_s.empty?
199
+ end
200
+
201
+ # Runs a block, wrapping/logging any errors in GenerationError subclasses.
202
+ #
203
+ # @yield The block to execute safely.
204
+ # @return [Object] The block's return value if successful.
205
+ # @raise [GenerationError] A normalized error if anything fails.
206
+ def with_error_handling
207
+ yield
208
+ rescue GenerationError => e
209
+ logger.error "#{e.class}: #{e.message}"
210
+ raise
211
+ rescue Errno::ENOENT, Errno::EACCES, Errno::EIO => e
212
+ logger.error "File system error: #{e.class} - #{e.message}"
213
+ raise FileSystemError, e.message
214
+ rescue => e
215
+ logger.error "Unexpected error: #{e.class} - #{e.message}"
216
+ logger.debug e.backtrace.join("\n")
217
+ raise GenerationError, "Unexpected error: #{e.message}"
218
+ end
219
+
220
+ # Build a default logger that writes to STDOUT.
221
+ #
222
+ # Log level defaults to `INFO`; set `ENV["NAMEPLATE_LOG_LEVEL"] = "DEBUG"`
223
+ # to enable verbose output during generation.
224
+ #
225
+ # @return [Logger]
226
+ def default_logger
227
+ require "logger"
228
+ Logger.new($stdout).tap do |log|
229
+ log.level = (ENV["NAMEPLATE_LOG_LEVEL"]&.upcase == "DEBUG") ? Logger::DEBUG : Logger::INFO
230
+ log.formatter = proc do |severity, datetime, _progname, msg|
231
+ "[#{datetime.strftime("%H:%M:%S")}] #{severity}: #{msg}\n"
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NamePlate
4
+ class Avatar
5
+ # Represents a derived avatar identity: letters and color.
6
+ class Identity
7
+ attr_reader :color, :letters
8
+
9
+ def initialize(color, letters)
10
+ @color = color
11
+ @letters = letters
12
+ end
13
+
14
+ # Build an identity from a username.
15
+ #
16
+ # @param [String] username The input name.
17
+ # @return [Identity] The derived avatar identity.
18
+ def self.from_username(username)
19
+ color = NamePlate::Colors.for(username)
20
+ letters = initials(username, count(username))
21
+ new(color, letters)
22
+ end
23
+
24
+ class << self
25
+ private
26
+
27
+ def initials(username, count)
28
+ username
29
+ .split(/\s+/)
30
+ .map { |word| word[0] }
31
+ .join
32
+ .upcase[0..count - 1]
33
+ end
34
+
35
+ def count(username)
36
+ (username.strip.split(/\s+/).size >= 2) ? 2 : 1
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require_relative "avatar/generator"
5
+ require_relative "avatar/cache"
6
+ require_relative "avatar/identity"
7
+
8
+ module NamePlate
9
+ # Avatar generation from usernames.
10
+ #
11
+ # Responsibilities are split into:
12
+ # - {Identity}: maps username → initials + color
13
+ # - {Cache}: builds consistent cache paths
14
+ # - {Generator}: orchestrates avatar creation and resizing
15
+ #
16
+ # Public API:
17
+ # NamePlate::Avatar.generate("John Doe", 128)
18
+ #
19
+ class Avatar
20
+ VERSION = 1 # bump on any change to avatar generation
21
+ FULLSIZE = 600
22
+ FILL_COLOR = "rgba(0, 0, 0, 0.65)" # black at 65% opacity
23
+ # Use __dir__ and fall back to the current working directory for Steep
24
+ # which can type __dir__ as String | nil.
25
+ FONT_FILE = File.expand_path("fonts/Roboto-Medium", __dir__ || Dir.pwd)
26
+
27
+ # Public API entry point
28
+ #
29
+ # @param username [String]
30
+ # @param size [Integer]
31
+ # @param opts [Hash] options, e.g. { cache: true }
32
+ # @return [String] path to avatar file
33
+ def self.generate(username, size, opts = {})
34
+ Generator.call(username, size, **opts)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module NamePlate
6
+ module Colors
7
+ class Palette
8
+ attr_reader :colors
9
+
10
+ # Initialize a new color palette.
11
+ #
12
+ # @param [Array<Array<Integer>>] colors The array of RGB triplets.
13
+ # If nil, uses the default {COLORS} constant defined in subclasses.
14
+ # @raise [NotImplementedError] If the subclass does not define COLORS constant.
15
+ # @raise [ArgumentError] If COLORS is not a valid array of RGB triplets
16
+ def initialize(colors = nil)
17
+ colors ||= self.class.const_get(:COLORS)
18
+ @colors = colors.freeze
19
+ end
20
+
21
+ def self.key
22
+ raise NotImplementedError, "#{self}.key must return a Symbol"
23
+ end
24
+
25
+ # Select a color based on a username string.
26
+ #
27
+ # @param [String] username The username to base the color selection on.
28
+ # @return [Array<Integer>] RGB triplet
29
+ def pick(username)
30
+ index = hash_index(username)
31
+ colors[index % colors.length]
32
+ end
33
+
34
+ private
35
+
36
+ # Generate a hash index from a username.
37
+ #
38
+ # @param [String] username The username to hash.
39
+ # @return [Integer] The hash index.
40
+ def hash_index(username)
41
+ Digest::MD5.hexdigest(username)[0...15].to_i(16)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NamePlate
4
+ module Colors
5
+ module Palettes
6
+ class Custom < Palette
7
+ def self.key = :custom
8
+
9
+ def initialize
10
+ super(NamePlate.custom_palette || [])
11
+ end
12
+
13
+ def pick(username)
14
+ raise "Custom palette not set" if colors.empty?
15
+ super
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NamePlate
4
+ module Colors
5
+ module Palettes
6
+ # Inspired by Dracula theme
7
+ class Dracula < Palette
8
+ COLORS = [
9
+ [40, 42, 54], # background
10
+ [68, 71, 90], # current line
11
+ [98, 114, 164], # comment
12
+ [139, 233, 253], # cyan
13
+ [80, 250, 123], # green
14
+ [255, 184, 108], # orange
15
+ [255, 121, 198], # pink
16
+ [189, 147, 249], # purple
17
+ [241, 250, 140], # yellow
18
+ [255, 85, 85] # red
19
+ ].freeze
20
+
21
+ def self.key = :dracula
22
+
23
+ def initialize
24
+ super(COLORS)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end