[devdocsgjs/main: 392/1867] Automatically generate spritesheets
- From: Andy Holmes <andyholmes src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [devdocsgjs/main: 392/1867] Automatically generate spritesheets
- Date: Fri, 19 Nov 2021 23:47:14 +0000 (UTC)
commit 90123a3679b1e61b6675f9c82c25e8ed6561b113
Author: Jasper van Merle <jaspervmerle gmail com>
Date: Sun Sep 16 23:15:20 2018 +0200
Automatically generate spritesheets
.gitignore | 2 +
Gemfile | 2 +
Gemfile.lock | 7 +-
assets/images/docs-1.png | Bin 46181 -> 0 bytes
assets/images/docs-1 2x png | Bin 122108 -> 0 bytes
assets/images/docs-2.png | Bin 19346 -> 0 bytes
assets/images/docs-2 2x png | Bin 47420 -> 0 bytes
assets/stylesheets/application-dark.css.scss | 7 +-
assets/stylesheets/application.css.scss | 7 +-
assets/stylesheets/global/_icons.scss | 180 --------------------------
assets/stylesheets/global/_icons.scss.erb | 43 +++++++
lib/app.rb | 5 +
lib/tasks/assets.thor | 1 +
lib/tasks/sprites.thor | 185 +++++++++++++++++++++++++++
public/icons/docs-1.pxm | Bin 3352748 -> 0 bytes
public/icons/docs-1 2x pxm | Bin 3669552 -> 0 bytes
public/icons/docs-2.pxm | Bin 1343093 -> 0 bytes
public/icons/docs-2 2x pxm | Bin 1456026 -> 0 bytes
public/icons/docs/bluebird/16 2x png | Bin 1018 -> 2273 bytes
19 files changed, 250 insertions(+), 189 deletions(-)
---
diff --git a/.gitignore b/.gitignore
index 8b222826..bf4994e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,5 @@ public/fonts
public/docs/**/*
!public/docs/docs.json
!public/docs/**/index.json
+log/
+assets/images/sprites
diff --git a/Gemfile b/Gemfile
index 567d4c09..cf709487 100644
--- a/Gemfile
+++ b/Gemfile
@@ -18,6 +18,8 @@ group :app do
gem 'browser'
gem 'sass'
gem 'coffee-script'
+ gem 'chunky_png'
+ gem 'sprockets-sass'
end
group :production do
diff --git a/Gemfile.lock b/Gemfile.lock
index 9708a713..3fdb414f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -12,6 +12,7 @@ GEM
erubi (>= 1.0.0)
rack (>= 0.9.0)
browser (2.5.3)
+ chunky_png (1.3.10)
coderay (1.1.2)
coffee-script (2.4.1)
coffee-script-source
@@ -93,6 +94,8 @@ GEM
rack (> 1, < 3)
sprockets-helpers (1.2.1)
sprockets (>= 2.2)
+ sprockets-sass (2.0.0.beta2)
+ sprockets (>= 2.0, < 4.0)
strings (0.1.1)
unicode-display_width (~> 1.3.0)
unicode_utils (~> 1.4.0)
@@ -127,6 +130,7 @@ DEPENDENCIES
activesupport (~> 5.2)
better_errors
browser
+ chunky_png
coffee-script
erubi
html-pipeline
@@ -146,6 +150,7 @@ DEPENDENCIES
sinatra-contrib
sprockets
sprockets-helpers
+ sprockets-sass
thin
thor
tty-pager
@@ -158,4 +163,4 @@ RUBY VERSION
ruby 2.5.1p57
BUNDLED WITH
- 1.16.1
+ 1.16.4
diff --git a/assets/stylesheets/application-dark.css.scss b/assets/stylesheets/application-dark.css.scss
index 821ebc36..a1676513 100644
--- a/assets/stylesheets/application-dark.css.scss
+++ b/assets/stylesheets/application-dark.css.scss
@@ -1,7 +1,6 @@
-//= depend_on docs-1.png
-//= depend_on docs-1 2x png
-//= depend_on docs-2.png
-//= depend_on docs-2 2x png
+//= depend_on sprites/docs.png
+//= depend_on sprites/docs 2x png
+//= depend_on sprites/docs.json
/*!
* Copyright 2013-2018 Thibaut Courouble and other contributors
diff --git a/assets/stylesheets/application.css.scss b/assets/stylesheets/application.css.scss
index 245a8012..9720f0c4 100644
--- a/assets/stylesheets/application.css.scss
+++ b/assets/stylesheets/application.css.scss
@@ -1,7 +1,6 @@
-//= depend_on docs-1.png
-//= depend_on docs-1 2x png
-//= depend_on docs-2.png
-//= depend_on docs-2 2x png
+//= depend_on sprites/docs.png
+//= depend_on sprites/docs 2x png
+//= depend_on sprites/docs.json
/*!
* Copyright 2013-2018 Thibaut Courouble and other contributors
diff --git a/assets/stylesheets/global/_icons.scss.erb b/assets/stylesheets/global/_icons.scss.erb
new file mode 100644
index 00000000..555b526a
--- /dev/null
+++ b/assets/stylesheets/global/_icons.scss.erb
@@ -0,0 +1,43 @@
+<% manifest = JSON.parse(File.read('assets/images/sprites/docs.json')) %>
+
+%svg-icon {
+ display: inline-block;
+ vertical-align: top;
+ width: 1rem;
+ height: 1rem;
+ pointer-events: none;
+ fill: currentColor;
+}
+
+%doc-icon {
+ content: '';
+ display: block;
+ width: 1rem;
+ height: 1rem;
+ background-image: image-url('sprites/docs.png');
+ background-size: <%= manifest['icons_per_row'] %>rem <%= manifest['icons_per_row'] %>rem;
+}
+
+@media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) {
+ %doc-icon { background-image: image-url('sprites/docs 2x png'); }
+}
+
+%darkIconFix {
+ @if $style == 'dark' {
+ filter: invert(100%) grayscale(100%);
+ -webkit-filter: invert(100%) grayscale(100%);
+ }
+}
+
+<%=
+ items = []
+
+ manifest['icons'].each do |icon|
+ rules = []
+ rules << "background-position: -#{icon['col']}rem -#{icon['row']}rem;"
+ rules << "@extend %darkIconFix !optional;" if icon['dark_icon_fix']
+ items << "._icon-#{icon['type']}:before { #{rules.join(' ')} }"
+ end
+
+ items.join('')
+ %>
diff --git a/lib/app.rb b/lib/app.rb
index b5015b3a..af9387bb 100644
--- a/lib/app.rb
+++ b/lib/app.rb
@@ -48,6 +48,11 @@ class App < Sinatra::Application
end
configure :test, :development do
+ require 'thor'
+ load 'tasks/sprites.thor'
+
+ SpritesCLI.new.invoke(:generate)
+
require 'active_support/per_thread_registry'
require 'active_support/cache'
sprockets.cache = ActiveSupport::Cache.lookup_store :file_store, root.join('tmp', 'cache', 'assets',
environment.to_s)
diff --git a/lib/tasks/assets.thor b/lib/tasks/assets.thor
index c3a4caf5..d49efd7d 100644
--- a/lib/tasks/assets.thor
+++ b/lib/tasks/assets.thor
@@ -14,6 +14,7 @@ class AssetsCLI < Thor
option :keep, type: :numeric, default: 0, desc: 'Number of old assets to keep'
option :verbose, type: :boolean
def compile
+ invoke 'sprites:generate', [], :verbose => options[:verbose]
manifest.compile App.assets_compile
manifest.clean(options[:keep]) if options[:clean]
end
diff --git a/lib/tasks/sprites.thor b/lib/tasks/sprites.thor
new file mode 100644
index 00000000..ea0a8b16
--- /dev/null
+++ b/lib/tasks/sprites.thor
@@ -0,0 +1,185 @@
+class SpritesCLI < Thor
+ def self.to_s
+ 'Sprites'
+ end
+
+ def initialize(*args)
+ require 'docs'
+ require 'chunky_png'
+ require 'fileutils'
+ super
+ end
+
+ desc 'generate [--verbose]', 'Generate the documentation icon spritesheets'
+ option :verbose, type: :boolean
+ def generate
+ icons = get_icons
+ icons_per_row = Math.sqrt(icons.length).ceil
+
+ bg_color = get_sidebar_background
+
+ icons.each_with_index do |icon, index|
+ icon[:row] = (index / icons_per_row).floor
+ icon[:col] = index - icon[:row] * icons_per_row
+
+ icon[:icon_16] = get_icon(icon[:path_16], 16)
+ icon[:icon_32] = get_icon(icon[:path_32], 32)
+
+ icon[:dark_icon_fix] = needs_dark_icon_fix(icon[:icon_32], bg_color)
+ end
+
+ log_details(icons, icons_per_row)
+
+ generate_spritesheet(16, icons, 'assets/images/sprites/docs.png') {|icon| icon[:icon_16]}
+ generate_spritesheet(32, icons, 'assets/images/sprites/docs 2x png') {|icon| icon[:icon_32]}
+
+ save_manifest(icons, icons_per_row, 'assets/images/sprites/docs.json')
+ end
+
+ private
+
+ def get_icons
+ items = Docs.all.map do |doc|
+ base_path = "public/icons/docs/#{doc.slug}"
+ {
+ :type => doc.slug,
+ :path_16 => "#{base_path}/16.png",
+ :path_32 => "#{base_path}/16 2x png"
+ }
+ end
+
+ # Checking paths against an array of possible paths is faster than 200+ File.exist? calls
+ files = Dir.glob('public/icons/docs/**/*.png')
+ items.select {|item| files.include?(item[:path_16]) && files.include?(item[:path_32])}
+ end
+
+ def get_icon(path, max_size)
+ icon = ChunkyPNG::Image.from_file(path)
+
+ # Check if the icon is too big
+ # If it is, resize the image without changing the aspect ratio
+ if icon.width > max_size || icon.height > max_size
+ ratio = icon.width.to_f / icon.height
+ new_width = (icon.width >= icon.height ? max_size : max_size * ratio).floor
+ new_height = (icon.width >= icon.height ? max_size / ratio : max_size).floor
+
+ logger.warn("Icon #{path} is too big: max size is #{max_size} x #{max_size}, icon is #{icon.width} x
#{icon.height}, resizing to #{new_width} x #{new_height}")
+
+ icon.resample_nearest_neighbor!(new_width, new_height)
+ end
+
+ icon
+ end
+
+ def get_sidebar_background
+ # This is a hacky way to get the background color of the sidebar
+ # Unfortunately, it's not possible to get the value of a SCSS variable from a Thor task
+ # Because hard-coding the value is even worse, we extract it using some regex
+ path = 'assets/stylesheets/global/_variables-dark.scss'
+ regex = /\$sidebarBackground:\s+([^;]+);/
+ ChunkyPNG::Color.parse(File.read(path)[regex, 1])
+ end
+
+ def needs_dark_icon_fix(icon, bg_color)
+ # Determine whether the icon needs to be grayscaled if the user has enabled the dark theme
+ # The logic comes from https://www.w3.org/TR/2008/REC-WCAG20-20081211/#visual-audio-contrast
+ contrast = icon.pixels.map do |pixel|
+ get_contrast(bg_color, pixel)
+ end
+
+ contrast.max < 7
+ end
+
+ def get_contrast(base, other)
+ l1 = get_luminance(base) + 0.05
+ l2 = get_luminance(other) + 0.05
+ ratio = l1 / l2
+ l2 > l1 ? 1 / ratio : ratio
+ end
+
+ def get_luminance(color)
+ rgba = [
+ ChunkyPNG::Color.r(color).to_f,
+ ChunkyPNG::Color.g(color).to_f,
+ ChunkyPNG::Color.b(color).to_f,
+ ChunkyPNG::Color.a(color).to_f
+ ]
+
+ rgba.map! do |rgb|
+ rgb /= 255
+ rgb < 0.03928 ? rgb / 12.92 : ((rgb + 0.055) / 1.055) ** 2.4
+ end
+
+ 0.2126 * rgba[0] + 0.7152 * rgba[1] + 0.0722 * rgba[2]
+ end
+
+ def generate_spritesheet(size, icons, output_path, &icon_to_img)
+ logger.info("Generating spritesheet #{output_path} with icons of size #{size} x #{size}")
+
+ icons_per_row = Math.sqrt(icons.length).ceil
+ spritesheet = ChunkyPNG::Image.new(size * icons_per_row, size * icons_per_row)
+
+ icons.each do |icon|
+ img = icon_to_img.call(icon)
+
+ # Calculate the base coordinates
+ base_x = icon[:col] * size
+ base_y = icon[:row] * size
+
+ # Center the icon if it's not a perfect rectangle
+ x = base_x + ((size - img.width) / 2).floor
+ y = base_y + ((size - img.height) / 2).floor
+
+ spritesheet.compose!(img, x, y)
+ end
+
+ FileUtils.mkdir_p(File.dirname(output_path))
+ spritesheet.save(output_path)
+ end
+
+ def save_manifest(icons, icons_per_row, path)
+ logger.info("Saving spritesheet details to #{path}")
+
+ FileUtils.mkdir_p(File.dirname(path))
+
+ # Only save the details that the scss file needs
+ manifest_icons = icons.map do |icon|
+ {
+ :type => icon[:type],
+ :row => icon[:row],
+ :col => icon[:col],
+ :dark_icon_fix => icon[:dark_icon_fix]
+ }
+ end
+
+ manifest = {:icons_per_row => icons_per_row, :icons => manifest_icons}
+
+ File.open(path, 'w') do |f|
+ f.write(JSON.generate(manifest))
+ end
+ end
+
+ def log_details(icons, icons_per_row)
+ logger.debug("Amount of icons: #{icons.length}")
+ logger.debug("Icons per row: #{icons_per_row}")
+
+ max_type_length = icons.map { |icon| icon[:type].length }.max
+ border = "+#{'-' * (max_type_length + 2)}+#{'-' * 5}+#{'-' * 8}+#{'-' * 15}+"
+ logger.debug(border)
+ logger.debug("| #{'Type'.ljust(max_type_length)} | Row | Column | Dark icon fix |")
+ logger.debug(border)
+
+ icons.each do |icon|
+ logger.debug("| #{icon[:type].ljust(max_type_length)} | #{icon[:row].to_s.ljust(3)} |
#{icon[:col].to_s.ljust(6)} | #{(icon[:dark_icon_fix] ? 'Yes' : 'No').ljust(13)} |")
+ end
+
+ logger.debug(border)
+ end
+
+ def logger
+ @logger ||= Logger.new($stdout).tap do |logger|
+ logger.level = options[:verbose] ? Logger::DEBUG : Logger::INFO
+ logger.formatter = proc { |severity, datetime, progname, msg| "#{msg}\n" }
+ end
+ end
+end
diff --git a/public/icons/docs/bluebird/16 2x png b/public/icons/docs/bluebird/16 2x png
index 9ffae075..64ad5903 100644
Binary files a/public/icons/docs/bluebird/16 2x png and b/public/icons/docs/bluebird/16 2x png differ
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]