[devdocsgjs/main: 392/1867] Automatically generate spritesheets




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]