Skip to content

Commit

Permalink
Rubocop made (almost) happy
Browse files Browse the repository at this point in the history
  • Loading branch information
zverok committed Feb 28, 2015
1 parent 507ca12 commit be8d28f
Show file tree
Hide file tree
Showing 17 changed files with 320 additions and 197 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
tmp
vendor
.bundle
36 changes: 21 additions & 15 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,32 +1,38 @@
#inherit_from: ./rubocop/rubocop-todo.yml
inherit_from: ./rubocop/rubocop-todo.yml

AllCops:
Include:
- 'lib/*'
- 'lib/**/*'
Exclude:
- 'samples/*'
- 'samples/**/*'
- 'vendor/**/*'

SpaceAroundOperators:
# I'm using it for control flow
Style/AndOr:
Enabled: false

SpaceAfterComma:
Style/Blocks:
Enabled: false

SpaceBeforeBlockBraces:
EnforcedStyle: no_space
Style/Lambda:
Enabled: false

# As it's about arrays, see? Arrays should be in square braces.
Style/PercentLiteralDelimiters:
PreferredDelimiters:
! '%w': ! '[]'

SpaceInsideBlockBraces:
Style/SpaceBeforeBlockBraces:
EnforcedStyle: no_space

SpaceInsideBlockBraces:
Enabled: false

TrailingWhitespace:
Enabled: false
# Just better like it this way
Style/SpaceInsideBlockBraces:
EnforcedStyle: no_space
SpaceBeforeBlockParameters: false

SingleLineMethods:
Style/StructInheritance:
Enabled: false

AndOr:
# Some math just looks better this way
Style/SpaceAroundOperators:
Enabled: false
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
source 'https://rubygems.org'

gemspec
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,14 @@ cloud = MagicCloud.new(words, width, height, palette: palette, rotate: rotate)
* `:none` - all words are horizontal (looks boooring)
* `:free` - any word rotation angle, looks cool, but not very readable
and slower to layout
* `[array, of, angles]` - each of possible angle should be number 0..360
* any lambda, accepting `(tag, index)` and returning 0..360
* any object, responding to `rotate(tag, index)` and returning 0..360
* Scale - how word sizes would be scaled to fit into (FONT_MIN..FONT_MAX) range:
* `:no` - no scaling, all word sizes are treated as is;
* `:linear` - linear scaling (default);
* `:log` - logarithmic scaling;
* `:sqrt` - square root scaling.

Services
--------
Expand Down
2 changes: 2 additions & 0 deletions lib/magic_cloud.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# encoding: utf-8

# Wordle-like word cloud main module
module MagicCloud
end

Expand Down
18 changes: 11 additions & 7 deletions lib/magic_cloud/bit_matrix.rb
Original file line number Diff line number Diff line change
@@ -1,35 +1,39 @@
# encoding: utf-8
module MagicCloud
# Dead simple 2-dimensional "bit matrix", storing 1s and 0s.
# Not memory effectife at all, but the fastest pure-Ruby solution
# I've tried.
class BitMatrix
def initialize(width, height)
@bits = (0..height).map{[0] * width}
end

attr_reader :bits

def put(x, y, px=1)
def put(x, y, px = 1)
x < width or fail("#{x} outside matrix: #{width}")
y < height or fail("#{y} outside matrix: #{height}")

bits[y][x] = 1 unless px == 0 # It's faster with unless
end

# returns true/false
# FIXME: maybe #put should also accept true/false
def at(x, y)
!bits[y][x].zero?
end

def height
bits.count
end

def width
bits.first ? bits.first.count : 0
end

def dump
(0...height).map{|y|
(0...width).map{|x| at(x,y) ? ' ' : 'x'}.join
(0...width).map{|x| at(x, y) ? ' ' : 'x'}.join
}.join("\n")
end
end
Expand Down
40 changes: 23 additions & 17 deletions lib/magic_cloud/canvas.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,26 @@
require 'RMagick'

module MagicCloud
# Thin wrapper around RMagick, incapsulating ALL the real drawing.
# As it's only class that "knows" about underlying graphics library,
# it should be possible to replace it with another canvas with same
# interface, not using RMagick.
class Canvas
def initialize(w, h, back = 'transparent')
@width, @height = w,h
@width, @height = w, h
@internal = Magick::Image.new(w, h){|i| i.background_color = back}
end

attr_reader :internal, :width, :height

RADIANS = Math::PI / 180

def draw_text(text, options = {})
draw = Magick::Draw.new

draw.font_family = 'Impact'
draw.font_weight = Magick::NormalWeight

draw.translate(options[:x] || 0, options[:y] || 0)
draw.pointsize = options[:font_size]
draw.fill_color(options[:color])
Expand All @@ -27,31 +31,33 @@ def draw_text(text, options = {})

metrics = draw.get_type_metrics('"' + text + 'm"')
w, h = rotated_metrics(
metrics.width,
metrics.height,
metrics.width,
metrics.height,
options[:rotate] || 0)

draw.translate(w/2, h/2)
draw.rotate(options[:rotate] || 0)
draw.translate(0, h/8) # RMagick text_align is really weird, trust me!
draw.text(0, 0, text)

draw.draw(@internal)

Rect.new(0, 0, w, h)
end

def pixels(w = nil, h = nil)
@internal.export_pixels(0, 0, w || width, h || height, 'RGBA')
end


# rubocop:disable TrivialAccessors
def render
@internal
end

# rubocop:enable TrivialAccessors

private
def rotated_metrics(width, height, degrees)

def rotated_metrics(w, h, degrees)
radians = degrees * Math::PI / 180

# FIXME: not too clear, just straightforward from d3.cloud
Expand All @@ -64,8 +70,8 @@ def rotated_metrics(width, height, degrees)

w = [(wcr + hsr).abs, (wcr - hsr).abs].max.to_i
h = [(wsr + hcr).abs, (wsr - hcr).abs].max.to_i
[w,h]

[w, h]
end
end
end
64 changes: 35 additions & 29 deletions lib/magic_cloud/cloud.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
require_relative './rect'

module MagicCloud
# Main word-cloud class. Takes words with sizes, returns image
class Cloud
def initialize(words, options = {})
@words = words.sort_by(&:last).reverse
Expand All @@ -15,42 +16,42 @@ def initialize(words, options = {})
@rotator = make_rotator(options[:rotate] || :square)
@palette = make_palette(options[:palette] || :default)
end

PALETTES = {
color20: %w[
#1f77b4 #aec7e8 #ff7f0e #ffbb78 #2ca02c
#98df8a #d62728 #ff9896 #9467bd #c5b0d5
#8c564b #c49c94 #e377c2 #f7b6d2 #7f7f7f
#1f77b4 #aec7e8 #ff7f0e #ffbb78 #2ca02c
#98df8a #d62728 #ff9896 #9467bd #c5b0d5
#8c564b #c49c94 #e377c2 #f7b6d2 #7f7f7f
#c7c7c7 #bcbd22 #dbdb8d #17becf #9edae5
]
}

def draw(width, height)
# FIXME: do it in init, for specs would be happy
shapes = @words.each_with_index.map{|(word, size), i|
shapes = @words.each_with_index.map{|(word, size), i|
Tag.new(
word,
font_size: scaler.call(word, size, i),
word,
font_size: scaler.call(word, size, i),
color: palette.call(word, i),
rotate: rotator.call(word, i)
)
}

Debug.reinit!

Spriter.make_sprites!(shapes)
layouter = Layouter.new(width, height)
visible = layouter.layout!(shapes)

canvas = Canvas.new(width, height, 'white')
visible.each{|sh| sh.draw(canvas)}
canvas.render
end

private

attr_reader :palette, :rotator, :scaler

def make_palette(source)
case source
when :default
Expand All @@ -61,22 +62,26 @@ def make_palette(source)
fail ArgumentError, "Unknown palette: #{source.inspect}"
end
end

def make_const_palette(sym)
palette = PALETTES[sym] or
palette = PALETTES[sym] or
fail(ArgumentError, "Unknown palette: #{sym.inspect}")
->(word, index){palette[index % palette.size]}

->(_, index){palette[index % palette.size]}
end

def make_rotator(source)
case source
when :none
->(*){0}
when :square
->(*){ (rand * 2).to_i * 90 }
->(*){
(rand * 2).to_i * 90
}
when :free
->(*){ (((rand * 6) - 3) * 30).round }
->(*){
(((rand * 6) - 3) * 30).round
}
when Proc
source
when ->(s){s.respond_to?(:rotate)}
Expand All @@ -89,12 +94,13 @@ def make_rotator(source)
# FIXME: should be options too
FONT_MIN = 10
FONT_MAX = 100

def make_scaler(words, algo)
norm =
norm =
case algo
when :no
return ->(word, size, index){size} # no normalization, treat tag weights as font size
# no normalization, treat tag weights as font size
return ->(_word, size, _index){size}
when :linear
->(x){x}
when :log
Expand All @@ -104,14 +110,14 @@ def make_scaler(words, algo)
else
fail ArgumentError, "Unknown scaling algo: #{algo.inspect}"
end

smin = norm.call(words.map(&:last).min)
smax = norm.call(words.map(&:last).max)
koeff = (FONT_MAX-FONT_MIN).to_f/(smax-smin)
->(word, size, index){
koeff = (FONT_MAX - FONT_MIN).to_f / (smax - smin)

->(_word, size, _index){
ssize = norm.call(size)
((ssize - smin).to_f*koeff + FONT_MIN).to_i
((ssize - smin).to_f * koeff + FONT_MIN).to_i
}
end
end
Expand Down
Loading

0 comments on commit be8d28f

Please sign in to comment.