diff --git a/lib/game_icons/icon.rb b/lib/game_icons/icon.rb index a0b0688..775a446 100644 --- a/lib/game_icons/icon.rb +++ b/lib/game_icons/icon.rb @@ -14,14 +14,184 @@ def string # Modify the background and foreground colors and their opacities def recolor(bg: '#000', fg: '#fff', bg_opacity: "1.0", fg_opacity: "1.0") + modify(bg: bg, fg: fg, bg_opacity: bg_opacity.to_f * 100, fg_opacity: fg_opacity.to_f * 100) + self + end + + ## Modify the icon according to your wishes. Lots of options available. + # bg: background color + # bg_opacity: background color opacity, a number between 0 and 100 + # bg_gradient: false, 'o' ('radial'), '-' ('horizontal'), '|' ('vertical'), + # '\' ('diagonal_down'), or '/' ('diagonal_up') + # bg2: second background color (when bg_gradient is not false) + # bg2_opacity: opacity of second background color (when bg_gradient is not false), + # a number between 0 and 100 + # shape: 'circle', 'triangle', 'square', 'square-alt', 'rounded-square', 'star5', 'star5-alt', + # 'star6', 'star6-alt', 'star7', 'start7-alt', 'hexa', 'hexa-alt', 'octa' or 'octa-alt' + # cr: corner radius for the shape 'rounded-square', a number between 0 and 256, + # where 0 makes a square and 256 makes a circle + # frame: true or false + # f: frame color + # f_opacity: frame opacity, a number between 0 and 100 + # f_width: frame width, where 1 equals 1/512th of the width of the icon + # fg: foreground color + # fg_opacity: foreground color opacity, a number between 0 and 100 + # fg_gradient: false, 'o' ('radial'), '-' ('horizontal'), '|' ('vertical'), + # '\' ('diagonal_down'), or '/' ('diagonal_up') + # fg2: second foreground color (when fg_gradient is not false) + # fg2_opacity: opacity of second foreground color (when fg_gradient is not false), + # a number between 0 and 100 + # shadow: shadow/glow effect, true or false + # sh: shadow color + # sh_opacity: shadow color opacity, a number between 0 and 100 + # sh_blur: the size of the shadow blur, where 1 equals 1/512th of the width of the icon + # sh_x: horizontal offset for the shadow, where 1 equals 1/512th of the width of the icon + # sh_y: vertical offset for the shadow, where 1 equals 1/512th of the width of the icon + # sh_side: 'in' or 'out' + # stroke: true or false + # s: stroke color + # s_opacity: stroke opacity, a number between 0 and 100 + # s_width: stroke width, where 1 equals 1/512th of the width of the icon + # x: horizontal foreground offset, where 1 equals 1/512th of the width of the icon + # y: vertical foreground offset, where 1 equals 1/512th of the width of the icon + # clip: keep the foreground inside the bounds of the background, true or false + # flip_h: flip horizontally, true or false + # flip_v: flip vertically, true or false + # rotate: rotation of the foreground in degrees, use positive number for clockwise rotation + # or negative number for anti-clockwise rotation + # scale: scale of foreground, a number between 0 and 100, + # use this to shrink the icon without shrinking the background + # preset: use an in-built color preset + # possible values: false, 'original', 'negatif', 'transparent', 'fire', 'ice', + # 'forest', 'silver', 'gold', 'terminal', 'candy' or 'gordon' + # if not false, any given arguments for colors, gradients, frame and stroke will be ignored + def modify(bg: '#000', bg_opacity: 100, bg_gradient: false, bg2: '#fff', bg2_opacity: 100, shape: 'square', cr: 32, frame: false, f: '#fff', f_opacity: 100, f_width: 8, fg: '#fff', fg_opacity: 100, fg_gradient: false, fg2: '#000', fg2_opacity: 100, shadow: false, sh: '#fff', sh_opacity: 100, sh_blur: 15, sh_x: 0, sh_y: 0, sh_side: 'out', stroke: false, s: '#ccc', s_opacity: 100, s_width: 8, x: 0, y: 0, clip: false, flip_h: false, flip_v: false, rotate: 0, scale: 100, preset: false) OptionalDeps.require_nokogiri - bg.prepend('#') unless bg.start_with? '#' - fg.prepend('#') unless fg.start_with? '#' - doc = Nokogiri::XML(self.string) - doc.css('path')[0]['fill'] = bg # dark backdrop - doc.css('path')[1]['fill'] = fg # light drawing - doc.css('path')[0]['fill-opacity'] = bg_opacity.to_s # dark backdrop - doc.css('path')[1]['fill-opacity'] = fg_opacity.to_s # light drawing + doc = Nokogiri::XML(File.open(@file) { |f| f.read }) + + # set color arguments according to the given preset + if preset + bg, fg, shadow, sh, sh_blur, sh_x, sh_y, sh_side, stroke, s, s_width = + PRESET_BG[preset], PRESET_FG[preset], PRESET_SHADOW[preset], PRESET_SH[preset], PRESET_SH_BLUR[preset], PRESET_SH_X[preset], PRESET_SH_Y[preset], PRESET_SH_SIDE[preset], PRESET_STROKE[preset], PRESET_S[preset], PRESET_S_WIDTH[preset] + bg_opacity = preset == 'transparent' ? 0 : 100 + bg_gradient = false + frame = false + fg_opacity = 100 + fg_gradient = false + sh_opacity = 100 + s_opacity = 100 + end + + # we will need to add some definitions later + if bg_gradient || fg_gradient || shadow || clip + doc.root.first_element_child.before('') + defs = doc.at_css('defs') + end + + # convert opacities and scale to floats in range [0..1] + bg_opacity, bg2_opacity, f_opacity, fg_opacity, fg2_opacity, sh_opacity, s_opacity, scale = + bg_opacity/100.0, bg2_opacity/100.0, f_opacity/100.0, fg_opacity/100.0, fg2_opacity/100.0, sh_opacity/100.0, s_opacity/100.0, scale/100.0 + + # convert all the colors into #rrggbb or rgba format + bg, bg2, f, fg, fg2, s, sh = + to_rgb(bg), to_rgb(bg2), to_rgb(f), to_rgb(fg), to_rgb(fg2), to_rgb(s), to_rgba(sh, sh_opacity) + + # icon path + foreground = doc.css('path')[1] + + # background shape + case shape + when 'square' + background = doc.at_css('path') + when 'circle' + doc.at_css('path').remove + foreground.before(SHAPE[shape]) + background = doc.at_css('circle') + when 'rounded-square' + doc.at_css('path').remove + foreground.before(SHAPE[shape]) + background = doc.at_css('rect') + background['rx'] = cr + background['ry'] = cr + else + doc.at_css('path').remove + foreground.before(SHAPE[shape]) + background = doc.at_css('polygon') + end + + # background colors + if bg_gradient + gradient = defs.add_child(GRADIENT[bg_gradient])[0] + gradient['id'] = 'gradient-bg' + gradient.add_child("") + background['fill'] = 'url(#gradient-bg)' + else + background['fill'] = bg + background['fill-opacity'] = bg_opacity.to_s unless bg_opacity >= 1 + end + + # background frame (TODO: background should be shrunk in order to fit the frame inside the icon) + if frame + background['stroke'] = f + background['stroke-opacity'] = f_opacity unless f_opacity >= 1 + background['stroke-width'] = f_width + end + + # foreground colors + if fg_gradient + gradient = defs.add_child(GRADIENT[fg_gradient])[0] + gradient['id'] = 'gradient-fg' + gradient.add_child("") + foreground['fill'] = 'url(#gradient-fg)' + else + foreground['fill'] = fg + foreground['fill-opacity'] = fg_opacity.to_s unless fg_opacity >= 1 + end + + # foreground stroke + if stroke + foreground['stroke'] = s + foreground['stroke-opacity'] = s_opacity unless s_opacity >= 1 + foreground['stroke-width'] = s_width + end + + # foreground shadow/glow effect + if shadow + filter = defs.add_child("")[0] + if sh_side == 'in' + filter.at_css('feComposite')['operator'] = 'out' + filter.add_child('') + else + filter.at_css('feComposite')['operator'] = 'atop' + filter.add_child('') + end + foreground['filter'] = 'url(#shadow)' + end + + # clip foreground to background + if clip + clip_path = defs.add_child('' + background.to_xml + '')[0].first_element_child + foreground['clip-path'] = 'url(#icon-bg)' + end + + # transform: translate, scale, rotate and flip + if x != 0 || y != 0 || scale != 1.0 || flip_h || flip_v || rotate != 0 + x += 256 * (1 - scale) + y += 256 * (1 - scale) + x_f = flip_h ? x + 512 : x + y_f = flip_v ? y + 512 : y + x_c = flip_h ? x + 512 : -x + y_c = flip_v ? y + 512 : -y + scale_x = flip_h ? -scale : scale + scale_y = flip_v ? -scale : scale + scale_x_c = flip_h ? -2 - scale : 2 - scale + scale_y_c = flip_v ? -2 - scale : 2 - scale + foreground['transform'] = "translate(#{x_f}, #{y_f}) scale(#{scale_x}, #{scale_y}) rotate(#{rotate}, 256, 256)" + if clip # TODO: clip does not work correctly for all possible transformation combinations (but it already works better than in the studio on game-icons.net) + clip_path['transform'] = "translate(#{x_c}, #{y_c}) scale(#{scale_x_c}, #{scale_y_c}) rotate(#{-rotate}, 256, 256)" + end + end + @svgstr = doc.to_xml self end @@ -31,11 +201,212 @@ def recolor(bg: '#000', fg: '#fff', bg_opacity: "1.0", fg_opacity: "1.0") def correct_pathdata 10.times do # this is a bit of a hack b/c my regex isn't perfect @svgstr = self.string - .gsub(/(\d)\-/,'\1 -') # separate negatives + .gsub(/(\d)\-/,'\1 -') # separate negatives .gsub(/(\.)(\d+)(\.)/,'\1\2 \3') # separate multi-decimals end self end + private + + # Convert the color to #rrggbb format + def to_rgb(color) + begin + Cairo::Color.parse(color).to_s[0..-3] + rescue ArgumentError + Cairo::Color.parse(color.prepend('#')).to_s[0..-3] + end + end + + # Convert the color and opacity to rgba(red, green, blue, alpha) format + def to_rgba(color, opacity) + color = to_rgb(color) + 'rgba(' + color[1..2].to_i(16).to_s + ', ' + color[3..4].to_i(16).to_s + ', ' + color[5..6].to_i(16).to_s + ', ' + opacity.to_s + ')' + end + + # Constants + PRESET_BG = { + 'original' => '#000', + 'negatif' => '#fff', + 'transparent' => '#fff', + 'fire' => '#F44242', + 'ice' => '#156DE2', + 'forest' => '#7C432F', + 'silver' => '#6B6B6B', + 'gold' => '#F8E71C', + 'terminal' => '#334033', + 'candy' => '#F8E0C9', + 'gordon' => '#3E320A' + } + + PRESET_FG = { + 'original' => '#fff', + 'negatif' => '#000', + 'transparent' => '#000', + 'fire' => '#FDEB05', + 'ice' => '#5AE9FF', + 'forest' => '#2AD422', + 'silver' => '#E8E8E8', + 'gold' => '#E3AA00', + 'terminal' => '#0f0', + 'candy' => '#F31070', + 'gordon' => '#F5C823' + } + + PRESET_SHADOW = { + 'original' => false, + 'negatif' => false, + 'transparent' => false, + 'fire' => true, + 'ice' => true, + 'forest' => false, + 'silver' => true, + 'gold' => true, + 'terminal' => true, + 'candy' => true, + 'gordon' => true + } + + PRESET_SH = { + 'original' => '#fff', + 'negatif' => '#000', + 'transparent' => '#000', + 'fire' => '#FDEB05', + 'ice' => '#fff', + 'forest' => '#fff', + 'silver' => '#fff', + 'gold' => '#E3AA00', + 'terminal' => '#0f0', + 'candy' => '#D0021B', + 'gordon' => '#F3D771' + } + + PRESET_SH_BLUR = { + 'original' => 15, + 'negatif' => 15, + 'transparent' => 15, + 'fire' => 20, + 'ice' => 15, + 'forest' => 15, + 'silver' => 0, + 'gold' => 15, + 'terminal' => 15, + 'candy' => 0, + 'gordon' => 10 + } + + PRESET_SH_X = { + 'original' => 0, + 'negatif' => 0, + 'transparent' => 0, + 'fire' => 0, + 'ice' => 15, + 'forest' => 0, + 'silver' => 0, + 'gold' => 0, + 'terminal' => 0, + 'candy' => 0, + 'gordon' => 0 + } + + PRESET_SH_Y = { + 'original' => 0, + 'negatif' => 0, + 'transparent' => 0, + 'fire' => 0, + 'ice' => 0, + 'forest' => 0, + 'silver' => 10, + 'gold' => 0, + 'terminal' => 0, + 'candy' => 10, + 'gordon' => 0 + } + + PRESET_SH_SIDE = { + 'original' => 'out', + 'negatif' => 'out', + 'transparent' => 'out', + 'fire' => 'out', + 'ice' => 'in', + 'forest' => 'out', + 'silver' => 'out', + 'gold' => 'out', + 'terminal' => 'out', + 'candy' => 'out', + 'gordon' => 'out' + } + + PRESET_STROKE = { + 'original' => false, + 'negatif' => false, + 'transparent' => false, + 'fire' => false, + 'ice' => false, + 'forest' => false, + 'silver' => false, + 'gold' => false, + 'terminal' => false, + 'candy' => false, + 'gordon' => true + } + + PRESET_S = { + 'original' => '#ccc', + 'negatif' => '#ccc', + 'transparent' => '#ccc', + 'fire' => '#ccc', + 'ice' => '#ccc', + 'forest' => '#ccc', + 'silver' => '#ccc', + 'gold' => '#ccc', + 'terminal' => '#ccc', + 'candy' => '#ccc', + 'gordon' => '#F3D771' + } + + PRESET_S_WIDTH = { + 'original' => 8, + 'negatif' => 8, + 'transparent' => 8, + 'fire' => 8, + 'ice' => 8, + 'forest' => 8, + 'silver' => 8, + 'gold' => 8, + 'terminal' => 8, + 'candy' => 8, + 'gordon' => 2 + } + + SHAPE = { + 'circle' => '', + 'rounded-square' => '', + 'triangle' => '', + 'star5' => '', + 'star5-alt' => '', + 'star6' => '', + 'star6-alt' => '', + 'star7' => '', + 'star7-alt' => '', + 'hexa' => '', + 'hexa-alt' => '', + 'octa' => '', + 'octa-alt' => '' + } + + GRADIENT = { + 'o' => '', + 'radial' => '', + '-' => '', + 'horizontal' => '', + '|' => '', + 'vertical' => '', + '\\' => '', + 'diagonal_down' => '', + '/' => '', + 'diagonal_up' => '' + } + end end