Automatically generate spritesheets

pull/870/head
Jasper van Merle 6 years ago
parent bec78bf6ed
commit 90123a3679

2
.gitignore vendored

@ -8,3 +8,5 @@ public/fonts
public/docs/**/*
!public/docs/docs.json
!public/docs/**/index.json
log/
assets/images/sprites

@ -18,6 +18,8 @@ group :app do
gem 'browser'
gem 'sass'
gem 'coffee-script'
gem 'chunky_png'
gem 'sprockets-sass'
end
group :production do

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

@ -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

@ -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

@ -1,180 +0,0 @@
%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('docs-1.png');
background-size: 10rem 10rem;
}
%doc-icon-2 { background-image: image-url('docs-2.png') !important; }
@media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) {
%doc-icon { background-image: image-url('docs-1@2x.png'); }
%doc-icon-2 { background-image: image-url('docs-2@2x.png') !important; }
}
%darkIconFix {
@if $style == 'dark' {
filter: invert(100%) grayscale(100%);
-webkit-filter: invert(100%) grayscale(100%);
}
}
._icon-jest:before { background-position: 0 0; }
._icon-liquid:before { background-position: -1rem 0; }
._icon-openjdk:before { background-position: -2rem 0; }
._icon-codeceptjs:before { background-position: -3rem 0; }
._icon-codeception:before { background-position: -4rem 0; }
._icon-sqlite:before { background-position: -5rem 0; @extend %darkIconFix !optional; }
._icon-async:before { background-position: -6rem 0; @extend %darkIconFix !optional; }
._icon-http:before { background-position: -7rem 0; @extend %darkIconFix !optional; }
._icon-jquery:before { background-position: -8rem 0; @extend %darkIconFix !optional; }
._icon-underscore:before { background-position: -9rem 0; @extend %darkIconFix !optional; }
._icon-html:before { background-position: 0 -1rem; }
._icon-css:before { background-position: -1rem -1rem; }
._icon-dom:before { background-position: -2rem -1rem; }
._icon-dom_events:before { background-position: -3rem -1rem; }
._icon-javascript:before { background-position: -4rem -1rem; }
._icon-backbone:before { background-position: -5rem -1rem; @extend %darkIconFix !optional; }
._icon-node:before,
._icon-node_lts:before { background-position: -6rem -1rem; }
._icon-sass:before { background-position: -7rem -1rem; }
._icon-less:before { background-position: -8rem -1rem; }
._icon-angularjs:before { background-position: -9rem -1rem; }
._icon-coffeescript:before { background-position: 0 -2rem; @extend %darkIconFix !optional; }
._icon-ember:before { background-position: -1rem -2rem; }
._icon-yarn:before { background-position: -2rem -2rem; }
._icon-immutable:before { background-position: -3rem -2rem; @extend %darkIconFix !optional; }
._icon-jqueryui:before { background-position: -4rem -2rem; }
._icon-jquerymobile:before { background-position: -5rem -2rem; }
._icon-lodash:before { background-position: -6rem -2rem; }
._icon-php:before { background-position: -7rem -2rem; }
._icon-ruby:before,
._icon-minitest:before { background-position: -8rem -2rem; }
._icon-rails:before { background-position: -9rem -2rem; }
._icon-python:before,
._icon-python2:before { background-position: 0 -3rem; }
._icon-git:before { background-position: -1rem -3rem; }
._icon-redis:before { background-position: -2rem -3rem; }
._icon-postgresql:before { background-position: -3rem -3rem; }
._icon-d3:before { background-position: -4rem -3rem; }
._icon-knockout:before { background-position: -5rem -3rem; }
._icon-moment:before { background-position: -6rem -3rem; @extend %darkIconFix !optional; }
._icon-c:before { background-position: -7rem -3rem; }
._icon-statsmodels:before { background-position: -8rem -3rem; }
._icon-yii:before,
._icon-yii1:before { background-position: -9rem -3rem; }
._icon-cpp:before { background-position: 0 -4rem; }
._icon-go:before { background-position: -1rem -4rem; }
._icon-express:before { background-position: -2rem -4rem; }
._icon-grunt:before { background-position: -3rem -4rem; }
._icon-rust:before { background-position: -4rem -4rem; @extend %darkIconFix !optional; }
._icon-laravel:before { background-position: -5rem -4rem; }
._icon-haskell:before { background-position: -6rem -4rem; }
._icon-requirejs:before { background-position: -7rem -4rem; }
._icon-chai:before { background-position: -8rem -4rem; }
._icon-sinon:before { background-position: -9rem -4rem; }
._icon-cordova:before { background-position: 0 -5rem; }
._icon-markdown:before { background-position: -1rem -5rem; @extend %darkIconFix !optional; }
._icon-django:before { background-position: -2rem -5rem; }
._icon-xslt_xpath:before { background-position: -3rem -5rem; }
._icon-nginx:before,
._icon-nginx_lua_module:before { background-position: -4rem -5rem; }
._icon-svg:before { background-position: -5rem -5rem; }
._icon-marionette:before { background-position: -6rem -5rem; }
._icon-jsdoc:before,
._icon-koa:before,
._icon-graphite:before,
._icon-mongoose:before { background-position: -7rem -5rem; }
._icon-phpunit:before { background-position: -8rem -5rem; }
._icon-nokogiri:before { background-position: -9rem -5rem; @extend %darkIconFix !optional; }
._icon-rethinkdb:before { background-position: 0 -6rem; }
._icon-react:before { background-position: -1rem -6rem; }
._icon-socketio:before { background-position: -2rem -6rem; }
._icon-modernizr:before { background-position: -3rem -6rem; }
._icon-bower:before { background-position: -4rem -6rem; }
._icon-fish:before { background-position: -5rem -6rem; @extend %darkIconFix !optional; }
._icon-scikit_image:before { background-position: -6rem -6rem; }
._icon-twig:before { background-position: -7rem -6rem; }
._icon-pandas:before { background-position: -8rem -6rem; }
._icon-scikit_learn:before { background-position: -9rem -6rem; }
._icon-bottle:before { background-position: 0 -7rem; }
._icon-docker:before { background-position: -1rem -7rem; }
._icon-cakephp:before { background-position: -2rem -7rem; }
._icon-lua:before { background-position: -3rem -7rem; @extend %darkIconFix !optional; }
._icon-clojure:before { background-position: -4rem -7rem; }
._icon-symfony:before { background-position: -5rem -7rem; }
._icon-mocha:before { background-position: -6rem -7rem; }
._icon-meteor:before { background-position: -7rem -7rem; @extend %darkIconFix !optional; }
._icon-npm:before { background-position: -8rem -7rem; }
._icon-apache_http_server:before { background-position: -9rem -7rem; }
._icon-drupal:before { background-position: 0 -8rem; }
._icon-webpack:before { background-position: -1rem -8rem; }
._icon-phaser:before { background-position: -2rem -8rem; }
._icon-vue:before { background-position: -3rem -8rem; }
._icon-opentsdb:before { background-position: -4rem -8rem; }
._icon-q:before { background-position: -5rem -8rem; }
._icon-crystal:before { background-position: -6rem -8rem; @extend %darkIconFix !optional; }
._icon-julia:before { background-position: -7rem -8rem; @extend %darkIconFix !optional; }
._icon-redux:before { background-position: -8rem -8rem; @extend %darkIconFix !optional; }
._icon-bootstrap:before { background-position: -9rem -8rem; }
._icon-react_native:before { background-position: 0 -9rem; }
._icon-phalcon:before { background-position: -1rem -9rem; }
._icon-matplotlib:before { background-position: -2rem -9rem; }
._icon-cmake:before { background-position: -3rem -9rem; }
._icon-elixir:before { background-position: -4rem -9rem; @extend %darkIconFix !optional; }
._icon-vagrant:before { background-position: -5rem -9rem; }
._icon-dojo:before { background-position: -6rem -9rem; }
._icon-flow:before { background-position: -7rem -9rem; }
._icon-relay:before { background-position: -8rem -9rem; }
._icon-phoenix:before { background-position: -9rem -9rem; }
._icon-tcl_tk:before { background-position: 0 0; @extend %doc-icon-2; }
._icon-erlang:before { background-position: -1rem 0; @extend %doc-icon-2; }
._icon-chef:before { background-position: -2rem 0; @extend %doc-icon-2; }
._icon-ramda:before { background-position: -3rem 0; @extend %doc-icon-2; @extend %darkIconFix !optional; }
._icon-codeigniter:before { background-position: -4rem 0; @extend %doc-icon-2; @extend %darkIconFix !optional; }
._icon-influxdata:before { background-position: -5rem 0; @extend %doc-icon-2; @extend %darkIconFix !optional; }
._icon-tensorflow:before { background-position: -6rem 0; @extend %doc-icon-2; }
._icon-haxe:before { background-position: -7rem 0; @extend %doc-icon-2; }
._icon-ansible:before { background-position: -8rem 0; @extend %doc-icon-2; @extend %darkIconFix !optional; }
._icon-typescript:before { background-position: -9rem 0; @extend %doc-icon-2; }
._icon-browser_support_tables:before { background-position: 0rem -1rem; @extend %doc-icon-2; }
._icon-gnu_fortran:before { background-position: -1rem -1rem; @extend %doc-icon-2; }
._icon-gcc:before { background-position: -2rem -1rem; @extend %doc-icon-2; }
._icon-perl:before { background-position: -3rem -1rem; @extend %doc-icon-2; }
._icon-apache_pig:before { background-position: -4rem -1rem; @extend %doc-icon-2; }
._icon-numpy:before { background-position: -5rem -1rem; @extend %doc-icon-2; }
._icon-kotlin:before { background-position: -6rem -1rem; @extend %doc-icon-2; }
._icon-padrino:before { background-position: -7rem -1rem; @extend %doc-icon-2; }
._icon-angular:before { background-position: -8rem -1rem; @extend %doc-icon-2; }
._icon-love:before { background-position: -9rem -1rem; @extend %doc-icon-2; }
._icon-jasmine:before { background-position: 0 -2rem; @extend %doc-icon-2; }
._icon-pug:before { background-position: -1rem -2rem; @extend %doc-icon-2; }
._icon-electron:before { background-position: -2rem -2rem; @extend %doc-icon-2; }
._icon-falcon:before { background-position: -3rem -2rem; @extend %doc-icon-2; }
._icon-godot:before { background-position: -4rem -2rem; @extend %doc-icon-2; }
._icon-nim:before { background-position: -5rem -2rem; @extend %doc-icon-2; @extend %darkIconFix !optional; }
._icon-vulkan:before { background-position: -6rem -2rem; @extend %doc-icon-2; @extend %darkIconFix !optional; }
._icon-d:before { background-position: -7rem -2rem; @extend %doc-icon-2; }
._icon-bluebird:before { background-position: -8rem -2rem; @extend %doc-icon-2; }
._icon-eslint:before { background-position: -9rem -2rem; @extend %doc-icon-2; }
._icon-homebrew:before { background-position: 0 -3rem; @extend %doc-icon-2; }
._icon-jekyll:before { background-position: -1rem -3rem; @extend %doc-icon-2; }
._icon-babel:before { background-position: -2rem -3rem; @extend %doc-icon-2; }
._icon-leaflet:before { background-position: -3rem -3rem; @extend %doc-icon-2; }
._icon-terraform:before { background-position: -4rem -3rem; @extend %doc-icon-2; }
._icon-pygame:before { background-position: -5rem -3rem; @extend %doc-icon-2; }
._icon-bash:before { background-position: -6rem -3rem; @extend %doc-icon-2; }
._icon-dart:before { background-position: -7rem -3rem; @extend %doc-icon-2; }
._icon-qt:before { background-position: -8rem -3rem; @extend %doc-icon-2; }

@ -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('')
%>

@ -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)

@ -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

@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1018 B

After

Width:  |  Height:  |  Size: 2.2 KiB

Loading…
Cancel
Save