Browse Source

drop shadows! they're a thing now.

Fixes #306, #264
dev
Andy Meneely 4 years ago
parent
commit
b25f0d535b
  1. 4
      CHANGELOG.md
  2. 4
      docs/conf.py
  3. 52
      docs/dsl/save_png.rst
  4. 39
      lib/squib/args/drop_shadow.rb
  5. 8
      lib/squib/dsl/save_png.rb
  6. 14
      lib/squib/graphics/cairo_context_wrapper.rb
  7. 65
      lib/squib/graphics/save_images.rb
  8. 20
      samples/shadows/_blend_ops.rb
  9. 77
      samples/shadows/_shadow.rb
  10. BIN
      samples/shadows/doodle.png
  11. BIN
      samples/shadows/gradient_blur_00_expected.png
  12. BIN
      samples/shadows/no_blur_00_expected.png
  13. BIN
      samples/shadows/transparent_bg_shadow_00_expected.png
  14. BIN
      samples/shadows/with_shadow_00_expected.png
  15. BIN
      samples/shadows/with_shadow_test_00_expected.png

4
CHANGELOG.md

@ -1,9 +1,9 @@
# Squib CHANGELOG
Squib follows [semantic versioning](http://semver.org).
## v0.16.1 / 2021-07-22
## v0.17.0 / 2021-07-23
Features:
* Drop shadows! The `save_png` method now supports a bunch of `shadow_` arguments that will add a drop shadow just before rendering. This is intended for using in rulebooks or marketing. Try it out by adding `shadow_radius: 8` to your save_png (#306, #264)
* Added debug methods for checking font access. `Squib.system_fonts` and `Squib.print_system_fonts` (#334)
Bugs:

4
docs/conf.py

@ -55,9 +55,9 @@ author = u'Andy Meneely'
# built documents.
#
# The short X.Y version.
version = u'v0.16'
version = u'v0.17'
# The full version, including alpha/beta/rc tags.
release = u'v0.16.1'
release = u'v0.17.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

52
docs/dsl/save_png.rst

@ -41,7 +41,59 @@ trim_radius
the rounded rectangle radius around the card to trim before saving.
shadow_radius
default: ``nil``
adds a drop shadow behind the card just before rendering, when non-nil. Does nothing when set to nil.
`Drop Shadows`. A larger radius extends the blur's reach. A radius of ``0`` will enable the feature but not do any blurring (which can still look good!).
The final image will have a new width of: ``w + shadow_offset_x + (3 * shadow_radius) - (2 * shadow_trim) - (2 * trim)``, and similar for height.
The image will be painted at ``shadow_radius, shadow_radius`` in the new image.
This drop shadow happens before rendering and does not alter the original card graphic.
At this stage, the feature will just draw a rectangle and blur from there. So if your card has transparency (other than from ``trim_radius``), from, say, a circular chit, then this won't work. We're working on a better implementation.
See [blur algorithm](https://github.com/rcairo/rcairo/blob/master/lib/cairo/context/blur.rb) for details on blur implementation. Recommended range: 3-10 pixels. Supports :doc:`/units`.
shadow_offset_x
default: ``3``
the horizontal distance that the drop shadow will be shifted beneath the final image.
Ignored when ``shadow_radius`` is ``nil``. See ``shadow_radius`` above for drop shadow details.
Supports :doc:`/units`.
shadow_offset_y
default: ``3``
Ignored when `shadow_radius` is ``nil``. See ``shadow_radius`` above for drop shadow details.
Supports :doc:`/units`.
shadow_trim
default: ``0``
the space around the edge of the output image trimmed when a drop shadow is drawn.
A negative number here would enlarge the margin on the right and bottom only.
Ignored when `shadow_radius` is ``nil``. See ``shadow_radius`` above for drop shadow details.
Supports :doc:`/units`.
shadow_color
default: ``:black``
the color or gradient of the drop shadow. See :doc:`/colors`.
`Note about gradients:` while gradients are technically supported here, Squib will do the blurring for you. But, using a gradient here does give you enormous control over the softness of the shadow. See example below of doing a custom gradient for customizing your look.
.. include:: /args/range.rst
Examples
--------
This sample `lives here <https://github.com/andymeneely/squib/tree/master/samples/shadows>`_.
.. literalinclude:: ../../samples/shadows/_shadow.rb
:language: ruby
:linenos:
.. raw:: html
<img src="../saves/with_shadow_00_expected.png" class="figure">
<img src="../saves/with_shadow_test_00_expected.png" class="figure">

39
lib/squib/args/drop_shadow.rb

@ -0,0 +1,39 @@
require_relative 'color_validator'
module Squib::Args
module_function def extract_drop_shadow(opts, deck)
DropShadow.new(deck.custom_colors).extract! opts, deck
end
class DropShadow
include ArgLoader
include ColorValidator
def initialize(custom_colors)
@custom_colors = custom_colors
end
def self.parameters
{
shadow_color: :black,
shadow_offset_x: 3,
shadow_offset_y: 3,
shadow_radius: nil,
shadow_trim: 0,
}
end
def self.expanding_parameters
self.parameters.keys # all of them
end
def self.params_with_units
[:shadow_offset_x, :shadow_offset_y, :shadow_radius, :shadow_trim]
end
def validate_shadow_color(arg, _i)
colorify(arg, @custom_colors)
end
end
end

8
lib/squib/dsl/save_png.rb

@ -1,5 +1,7 @@
require_relative '../errors_warnings/warn_unexpected_params'
require_relative '../args/card_range'
require_relative '../args/save_batch'
require_relative '../args/drop_shadow'
module Squib
class Deck
@ -24,6 +26,7 @@ module Squib
range
dir prefix suffix count_format
rotate trim trim_radius
shadow_offset_x shadow_offset_y shadow_radius shadow_color shadow_trim
)
end
@ -31,9 +34,10 @@ module Squib
warn_if_unexpected opts
range = Args.extract_range opts, deck
batch = Args.extract_save_batch opts, deck
shadow = Args.extract_drop_shadow opts, deck
@bar.start("Saving PNGs to #{batch.summary}", deck.size) do |bar|
range.map do |i|
deck.cards[i].save_png(batch[i])
deck.cards[i].save_png(batch[i], shadow[i])
bar.increment
end
end

14
lib/squib/graphics/cairo_context_wrapper.rb

@ -20,12 +20,14 @@ module Squib
def_delegators :cairo_cxt, :save, :set_source_color, :paint, :restore,
:translate, :rotate, :move_to, :update_pango_layout, :width, :height,
:show_pango_layout, :rectangle, :rounded_rectangle, :set_line_width, :stroke, :fill,
:set_source, :scale, :render_rsvg_handle, :circle, :triangle, :line_to,
:operator=, :show_page, :clip, :transform, :mask, :create_pango_layout,
:antialias=, :curve_to, :matrix, :matrix=, :identity_matrix, :pango_layout_path,
:stroke_preserve, :target, :new_path, :new_sub_path, :reset_clip, :fill_preserve, :close_path,
:set_line_join, :set_line_cap, :set_dash, :arc, :arc_negative
:show_pango_layout, :rectangle, :rounded_rectangle, :set_line_width,
:stroke, :fill, :set_source, :scale, :render_rsvg_handle, :circle,
:triangle, :line_to, :operator=, :show_page, :clip, :transform, :mask,
:create_pango_layout, :antialias=, :curve_to, :matrix, :matrix=,
:identity_matrix, :pango_layout_path, :stroke_preserve, :target,
:new_path, :new_sub_path, :reset_clip, :fill_preserve, :close_path,
:set_line_join, :set_line_cap, :set_dash, :arc, :arc_negative,
:pseudo_blur
# :nodoc:
# @api private

65
lib/squib/graphics/save_images.rb

@ -1,12 +1,14 @@
require_relative 'cairo_context_wrapper'
module Squib
class Card
# :nodoc:
# @api private
def save_png(batch)
surface = if preprocess_save?(batch)
def save_png(batch, shadow)
surface = if preprocess_save?(batch, shadow)
w, h = compute_dimensions(batch.rotate, batch.trim)
preprocessed_save(w, h, batch)
preprocessed_save(w, h, batch, shadow)
else
@cairo_surface
end
@ -15,8 +17,8 @@ module Squib
# :nodoc:
# @api private
def preprocess_save?(batch)
batch.rotate != false || batch.trim > 0
def preprocess_save?(batch, shadow)
batch.rotate != false || batch.trim > 0 || !(shadow.shadow_radius.nil?)
end
def compute_dimensions(rotate, trim)
@ -27,25 +29,62 @@ module Squib
end
end
def preprocessed_save(width, height, batch)
new_cc = Cairo::Context.new(Cairo::ImageSurface.new(width, height))
def preprocessed_save(w, h, batch, shadow)
new_cc = Cairo::Context.new(Cairo::ImageSurface.new(w, h))
trim_radius = batch.trim_radius
if batch.rotate != false
new_cc.translate(width * 0.5, height * 0.5)
new_cc.rotate(batch.angle)
new_cc.translate(height * -0.5, width * -0.5)
new_cc.rounded_rectangle(0, 0, height, width, trim_radius, trim_radius)
new_cc.translate w * 0.5, h * 0.5
new_cc.rotate batch.angle
new_cc.translate h * -0.5, w * -0.5
new_cc.rounded_rectangle(0, 0, h, w, trim_radius, trim_radius)
else
new_cc.rounded_rectangle(0, 0, width, height, trim_radius, trim_radius)
new_cc.rounded_rectangle(0, 0, w, h, trim_radius, trim_radius)
end
new_cc.clip
new_cc.set_source(@cairo_surface, -batch.trim, -batch.trim)
new_cc.paint
new_cc.reset_clip
new_cc = drop_shadow(new_cc, shadow, batch) unless shadow.shadow_radius.nil?
return new_cc.target
end
# pseudo-blur behave weirdly with a radius of 0 - wrapping
def blur(cc, r, &block)
if r == 0
yield(block)
else
cc.pseudo_blur(r, &block)
end
end
def drop_shadow(cc, s, batch)
off_x = s.shadow_offset_x
off_y = s.shadow_offset_y
s_trim = s.shadow_trim
s_rad = s.shadow_radius
new_w = cc.target.width + off_x + 3 * s_rad - (2 * s_trim)
new_h = cc.target.height + off_y + 3 * s_rad - (2 * s_trim)
new_cc = Squib::Graphics::CairoContextWrapper.new(
Cairo::Context.new(Cairo::ImageSurface.new(new_w, new_h)))
blur(new_cc, s_rad) do
# fill in with shadow color
new_cc.set_source_squibcolor s.shadow_color
new_cc.rectangle 0, 0, new_cc.target.width, new_cc.target.height
new_cc.fill
# then, paint but blend with :dest_in to get a shadow-shaped drawing
new_cc.set_source cc.target, s_rad + off_x, s_rad + off_y
new_cc.operator = :dest_in # see https://www.cairographics.org/operators/
new_cc.paint
end
new_cc.set_source cc.target, s_rad, s_rad
new_cc.operator = :over
new_cc.paint
return new_cc
end
def write_png(surface, i, b)
surface.write_to_png("#{b.dir}/#{b.prefix}#{b.count_format % i}#{b.suffix}.png")
filename = "#{b.dir}/#{b.prefix}#{b.count_format % i}#{b.suffix}.png"
surface.write_to_png filename
end
end

20
samples/shadows/_blend_ops.rb

@ -0,0 +1,20 @@
require 'squib'
orig = Cairo::ImageSurface.new(100,100)
orig_cc = Cairo::Context.new(orig)
orig_cc.circle(50,50, 30)
orig_cc.set_source_color(:red)
orig_cc.fill
orig.write_to_png '_output/blend_orig.png'
new_img = Cairo::ImageSurface.new(120, 120)
new_cc = Cairo::Context.new(new_img)
new_cc.set_source_color(:black)
new_cc.rectangle(0,0,120,120)
new_cc.fill
new_cc.set_source orig
new_cc.operator = :dest_in
new_cc.paint
new_img.write_to_png '_output/blend_new.png'

77
samples/shadows/_shadow.rb

@ -0,0 +1,77 @@
require_relative '../../lib/squib'
# require 'squib'
# The save_png method supports drop shadows on the final save
# This is useful for when you want to generate images for your rulebook
Squib::Deck.new(width: 100, height: 150) do
background color: '#abc'
svg file: '../spanner.svg',
x: 'middle - 25', y: 'middle - 25',
width: 50, height: 50
# Shadows off by default, i.e. shadow_radius is nil
# So our final dimensions are 100 - 2*15 and 150-2*15
save_png prefix: 'no_shadow_', trim: 15, trim_radius: 15
# Here's a nice-looking drop shadow
# Defaults are designed to be generally good, so I recommend just
# trying out a shadow_radius of 3 to 10 and see how it looks first
save_png prefix: 'with_shadow_', trim: 15, trim_radius: 15,
shadow_radius: 8,
shadow_offset_x: 3, shadow_offset_y: 3, # about r / 2.5 looks good
shadow_trim: 2.5, # about r/ 3 looks good
shadow_color: '#101010aa' #tip: start the shadow color kinda transparent
# Don't want a blur? Use a radius of 0
save_png prefix: 'no_blur_', trim: 15, trim_radius: 15,
shadow_radius: 0
# Ok this next stop is crazytown, but it does give you ultimate control
# Remember that all Squib colors can also be gradients.
# They can be clunky but they do work here.
# - x,y's are centered in the card itself
# - stops go from fully empty to fully black
# - we need to still turn on radius to get the effect
# - but, this makes the upper-left corner not have a glowing effect and
# have a harder edge, which (to my eye at least) feels more realistic
# since the card would obscure the upper-left shadow at that angle
# - this also allows you have a larger, softer blur without making it look
# like it's glowing
#
# Oh just because it's easier to write we can use a ruby heredoc
save_png prefix: 'gradient_blur_', trim: 15, trim_radius: 15,
shadow_radius: 10,
shadow_color: <<~EOS
(25,25)
(175,175)
#0000@0.0
#000f@1.0
EOS
# This one looks weird I know but it's for regression testing
save_png prefix: 'with_shadow_test_',
trim: 15, trim_radius: 15, rotate: :clockwise,
shadow_offset_x: 5, shadow_offset_y: 25, shadow_radius: 10,
shadow_trim: 10,
shadow_color: '#123'
end
Squib::Deck.new(width:50, height: 50) do
# What if we had a transparent card but just some shapes?
# Like chits or something
# background defaults to fully transparent here
# star x: 50, y: 50, inner_radius: 15, outer_radius: 35,
# fill_color: :red, stroke_color: :red
png file: 'doodle.png'
# save_png prefix: 'no_bg_shadow_', shadow_radius: 8, shadow_offset_x: 3, shadow_offset_y: 3
save_png prefix: 'transparent_bg_shadow_',
shadow_radius: 2,
shadow_offset_x: 2, shadow_offset_y: 2,
shadow_color: :black
end

BIN
samples/shadows/doodle.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
samples/shadows/gradient_blur_00_expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
samples/shadows/no_blur_00_expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
samples/shadows/transparent_bg_shadow_00_expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
samples/shadows/with_shadow_00_expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
samples/shadows/with_shadow_test_00_expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Loading…
Cancel
Save