app/services/image/tip_service.rb
class Image::TipService < Base::ImageService
option :fragments
option :tips
WIDTH = 500
MAIN_HEIGHT = 100
CHEER_FONT_SIZE = 16
CHEER_ROW_HEIGHT = 20
CHEER_PAD = 35
private
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def composite_image
cheer_pad = any_cheers? ? CHEER_PAD : BODY_PAD
height = MAIN_HEIGHT + cheer_pad + (CHEER_ROW_HEIGHT * cheer_fragments.size) + BODY_PAD
comp = solid_color_background(WIDTH, height)
comp = add_header_bg(comp)
comp = resize_and_round(comp, "#{WIDTH}x#{height}", 5)
comp = add_recipient_avatars(comp)
comp = add_sender_avatar(comp)
comp = add_quantity(comp)
comp = add_from_or_channel_text(comp)
comp = add_body_text(comp)
comp = add_cheers(comp)
add_animated_gif(comp)
end
def add_cheer_text(comp)
draw = Magick::Draw.new
draw.font = FONT_FILE
draw.pointsize = CHEER_FONT_SIZE
draw.gravity = Magick::NorthWestGravity
colors = body_colors.reverse
y = MAIN_HEIGHT + CHEER_PAD
cheer_fragments.each do |text|
x = BODY_PAD
text.split(IMG_DELIM).compact_blank.each_with_index do |chunk, idx|
draw.annotate(comp, 0, 0, x, y, chunk) do |m|
m.fill = colors[idx % 2]
end
x += draw.get_type_metrics(chunk).width.to_i
end
y += CHEER_ROW_HEIGHT
end
comp
end
def add_from_or_channel_text(comp)
# Channel
if config[:show_channel]
channel_name = "#{CHAN_PREFIX}#{first_tip.from_channel_name}"
draw = Magick::Draw.new
draw.font = FONT_FILE
draw.pointsize = 16
draw.gravity = Magick::NorthEastGravity
badge_width = draw.get_type_metrics(channel_name).width + 6
x = 46
y = 21
badge_height = 20
bg_color = BG_COLOR[theme]
badge = Magick::Image.new(badge_width, badge_height) { |m| m.background_color = bg_color }
badge = resize_and_round(badge, "#{badge_width}x#{badge_height}", 5)
comp = comp.composite(badge, Magick::NorthEastGravity, x, y, Magick::OverCompositeOp)
color = BODY_COLORS[theme].second
draw.annotate(comp, 0, 0, x + 2, y - 1, channel_name) do |m|
m.fill = color
end
else
# "from"
draw = Magick::Draw.new
draw.font = FONT_FILE
draw.pointsize = 16
draw.gravity = Magick::NorthEastGravity
color = BODY_COLORS[theme].first
draw.annotate(comp, 0, 0, 45, 24, 'from') do |m|
m.fill = color
end
end
comp
end
def add_body_text(comp)
body_width = WIDTH - 100
body_height = 57
text_width = body_width + 1
text_height = body_height + 1
# puts "max: #{body_width} x #{body_height}"
fontsize = MAX_FONT_SIZE
draw = Magick::Draw.new
draw.font = FONT_FILE
draw.gravity = Magick::NorthWestGravity
while (text_width > body_width || text_height > body_height) && fontsize > MIN_FONT_SIZE
fontsize -= 1
draw.pointsize = fontsize
metrics = draw.get_multiline_type_metrics(raw_body_text)
text_width = metrics.width
text_height = metrics.height
# puts "at fontsize #{fontsize}, w: #{text_width}, h: #{text_height}"
end
# puts "Final: #{fontsize}"
colors = body_colors
body_height = 76
header_height = 44
y = header_height + ((body_height - text_height) / 2.0).floor
main_fragments.each_with_index do |fragment, frag_idx|
next if fragment.blank?
x = BODY_PAD
fragment.split(IMG_DELIM).each_with_index do |chunk, idx|
next if chunk.blank?
draw = Magick::Draw.new
if frag_idx == 2
chunk = truncate(chunk, length: 70)
draw.font = FONT_FILE_ITALIC
else
draw.font = FONT_FILE
end
draw.pointsize = fontsize
draw.gravity = Magick::NorthWestGravity
draw.annotate(comp, 0, 0, x, y, chunk) do |m|
m.fill = colors[idx % 2]
end
x += draw.get_type_metrics(chunk).width.to_i
end
y += fontsize + (fontsize * 0.3).floor
end
# Cover extra long text with same color background
comp.composite \
solid_color_background(300, 300),
Magick::NorthWestGravity,
body_width + 10,
46,
Magick::OverCompositeOp
end
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def raw_body_text
@raw_body_text ||=
main_fragments.compact
.map { |frag| frag.gsub(IMG_DELIM, '').gsub(/\s+/, ' ').gsub(' ,', ',') }
.join("\n")
end
def add_cheers(comp)
return comp unless any_cheers?
comp = add_cheer_hr(comp)
add_cheer_text(comp)
end
def add_cheer_hr(comp)
draw = Magick::Draw.new
draw.stroke('gray')
draw.stroke_width(1)
draw.line(BODY_PAD, MAIN_HEIGHT + 25, WIDTH - BODY_PAD, MAIN_HEIGHT + 25)
draw.draw(comp)
comp
end
def any_cheers?
config[:enable_cheers] && cheer_fragments.any?
end
def cheer_fragments
@cheer_fragments ||= fragments.slice(:leveling, :streak).values.compact_blank
end
def main_fragments
@main_fragments ||= fragments.slice(:lead, :main, :note).values
end
def timestamp
@timestamp ||= Time.use_zone(config[:time_zone]) { first_tip.created_at }
end
def profile_avatar(profile)
avatar_image(small_profile_avatar_url(profile))
end
def add_animated_gif(comp) # rubocop:disable Metrics/AbcSize
gif_sequence = random_gif(48)
force_loop = gif_sequence.first.filename.split('/')[-2].in?(GIF_NO_REST)
comp.delay = GIF_SPEED
Magick::ImageList.new.tap do |ilist|
gif_sequence.each_with_index do |frame, idx|
# frame = frame.resize(48, 48, Magick::PointFilter) # Scale up but retain pixelart
new_frame = comp.composite(frame, WIDTH - 65, 58, Magick::OverCompositeOp)
new_frame.delay = GIF_REST if idx + 1 == gif_sequence.size && !force_loop
ilist << new_frame
end
end
end
def add_sender_avatar(comp)
sender_avatar = profile_avatar(first_tip.from_profile)
comp.composite \
sender_avatar,
Magick::NorthEastGravity,
HEADER_PAD,
HEADER_PAD,
Magick::OverCompositeOp
end
def add_quantity(comp)
first_tip.quantity.in?((1..5).to_a) ? add_graphical_quantity(comp) : add_text_quantity(comp)
end
def add_graphical_quantity(comp)
prefix = first_tip.jab? ? :minus : :plus # TODO: Add -5 to -1
path = "#{BASE_PATH}/quantities/#{prefix}-#{first_tip.quantity.to_i.abs}.png"
img = Magick::ImageList.new(path).first
x = avatar_stack_right + 44
comp.composite(img, Magick::NorthWestGravity, x, 6, Magick::OverCompositeOp)
end
def add_text_quantity(comp) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
draw = Magick::Draw.new
draw.font = FONT_FILE
draw.pointsize = 34
draw.gravity = Magick::NorthWestGravity
x = avatar_stack_right + 44
y = 3
prefix, color = first_tip.jab? ? ['-', '#d43808'] : ['+', '#f0cf28']
text = "#{prefix}#{points_format(first_tip.quantity.abs)}"
draw.annotate(comp, 0, 0, x, y, text) { |m| m.fill = '#3b1b20' }
draw.annotate(comp, 0, 0, x - 2, y - 2, text) { |m| m.fill = color }
comp
end
def num_avatars
@num_avatars ||= [recipient_profiles.size, MAX_PROFILES].min
end
def avatar_stack_right
@avatar_stack_right ||= HEADER_PAD + (PROFILE_EDGE * (num_avatars - 1))
end
def add_recipient_avatars(comp)
x = avatar_stack_right
# Add stack of up to MAX_PROFILES
recipient_profiles.shuffle.take(num_avatars).each do |profile|
recipient_avatar = profile_avatar(profile)
comp = comp.composite(recipient_avatar, x, HEADER_PAD, Magick::OverCompositeOp)
x -= PROFILE_EDGE
end
comp
end
def first_tip
@first_tip ||= tips.first
end
def recipient_profiles
tips.map(&:to_profile).uniq
end
end