#========================================================================
# Paragraph Formatter (VX)
# Version: 1.1
# Author: modern algebra (rmrk.net)
# Date: March 4, 2008
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Apology:
# The idea behind this script is to easily separate a long string into a paragraph that fits in to
# the dimensions you specify.
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Instructions:
# For ease of use of people who are not neccesarily interested in writing their own algorithm,
# I have included a facade which you can use simply by this code:
#
# bitmap.draw_paragraph (x, y, width, height, string)
#
# where x & y are the x & y coordinates on the specified bitmap, and width and height are the
# maximum dimensions of the paragraph and string is the text you want to display in
# paragraph formatter
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# How it works:
# The paragrapher expects two objects when initialized, a Formatter and an Artist. The idea
# behind the formatter is that it is expected to take the initial specifications and convert it to a
# Formatted Text object. Then, the Artist class is expected to interpret the Formatted Text
# object and draw the paragraph. For details on how each specific algorithm works, visit the
# comments above and inside them. It is not necessary to use the default Formatter, Artist, or
# Formatted Text objects.
#========================================================================
#======================================================================
# ** Bitmap
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Facade for easy application of the Paragraph Formatter
#======================================================================
class Bitmap
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# * Get Formatter
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def paragraph_formatter
@p_formatter = $game_system.default_formatter.new if @p_formatter.nil?
return @p_formatter
end
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# * Set Formatter
# formatter : The uninitialized formatter class you would like to use
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def paragraph_formatter= (formatter)
@p_formatter = formatter.new
end
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# * Get Artist
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def paragraph_artist
@p_artist = $game_system.default_artist.new if @p_artist.nil?
return @p_artist
end
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# * Set Artist
# artist : The uninitialized artist class you would like to use
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def paragraph_artist= (artist)
@p_artist = artist.new
end
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# * The Facade, which uses default Formatter and Artist to draw the formatted text directly
# to a bitmap, such as self.contents
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def draw_paragraph (x, y, max_width, max_height, string)
bitmap = Bitmap.new (max_width, max_height)
bitmap.font = self.font
pg = Paragrapher.new(paragraph_formatter, paragraph_artist)
bitmap = pg.paragraph (string, bitmap)
blt (x, y, bitmap, bitmap.rect)
# Dispose of the proxy bitmap
bitmap.dispose
end
end
#========================================================================
# ** Game_System
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Summary of changes:
# new instance variables - default_formatter, default_artist
# aliased methods - initialize
#========================================================================
class Game_System
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# * Public Instance Variables
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
attr_accessor :default_formatter
attr_accessor :default_artist
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# * Object Initialization
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
alias ma_paragraph_formatter_init initialize
def initialize
# Run original method
ma_paragraph_formatter_init
# Initialize original default format and artist classes
@default_formatter = Paragrapher::Formatter_2
@default_artist = Paragrapher::Artist
end
end
#======================================================================
# ** Paragrapher
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Module containing the objects for the Paragrapher
#======================================================================
module Paragrapher
#===================================================================
# Allows the 'Paragrapher.new' command outside of the module to be used
# rather than having to use 'Paragrapher::Paragrapher.new'
#===================================================================
class << self
def new(*args, &block)
return Paragrapher.new(*args, &block)
end
end
#===================================================================
# * The Paragrapher class
#===================================================================
class Paragrapher
def initialize(formatter, artist)
@formatter = formatter
@artist = artist
end
def paragraph(string, *specifications)
f = @formatter.format(string, *specifications)
return @artist.draw(f)
end
end
#===================================================================
# * The Formatter class
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# This class converts a string into a formatted text object, which is then
# passed on to the Artist class
#===================================================================
class Formatter
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# * Format
# string : the string to be formatted
# specifications : the desired width of the paragraph, or the bitmap
#-------------------------------------------------------------------------------------------------------------------
# This works on a very simple algorithm. Basically, it just formats the
# text by going through each word in the string. If a word
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def format(string, specifications)
# Initializes Formatted_Text object
f = Formatted_Text.new
# Checks whether specifications is a bitmap or a number. It then sets
# max_width and f.bitmap accordingly
if specifications.class == Bitmap
f.bitmap = specifications
max_width = specifications.width
elsif specifications.class == Fixnum || specifications.class == Float
max_width = specifications
f.bitmap = Bitmap.new (max_width, 32)
else
# Error Catching: Bad Specifications
bitmap = Bitmap.new (200, 64)
f = format ('Specifications Error', bitmap)
p 'Specifications Error: Please Pass Fixnum, Float or Bitmap'
return f
end
# Breaks the given string into an array of all it's characters
temp_word_array = string.scan (/./)
position = 0
line_break = 0
# Initializes f.lines
f.lines = []
f.blank_width = []
for i in 0...temp_word_array.size
character = temp_word_array[i]
# Error catching
if character == "\n"
p 'This formatter does not recognize line breaks'
character = " "
end
# If at a new word
if character == " " || i == temp_word_array.size - 1
# Take into account the last character of the string
if i == temp_word_array.size - 1
i += 1
end
# If this word fits on the current line
if f.bitmap.text_size (string[line_break, i-line_break]).width <= max_width
position = i
else
line = temp_word_array[line_break, position-line_break]
# Adds the first lines to f.lines
f.lines.push (line)
# Calculates the blank space left to cover in the line
line_blank = max_width - f.bitmap.text_size(string[line_break,position-line_break]).width
# Calculates the necessary distance between letters to make up for
# line_blank and adds that value to the f.blank_width array
f.blank_width.push (line_blank.to_f / (line.size.to_f-1.0))
# Keeps track of the position in the array of each line
line_break = position + 1
position = i
end
end
end
# Adds the last line to f.lines
f.lines.push (temp_word_array[line_break, temp_word_array.size - line_break])
# Since the last line is drawn normally, blank_width should be 0
f.blank_width.push (0)
if specifications.class == Fixnum
# Sets up the bitmap if it was unspecified.
f.bitmap = Bitmap.new(max_width, f.lines.size*32)
end
# Returns the Formatted_Text object
return f
end
end
#===================================================================
# * Formatter 2 (Using Zeriab's Algorithm)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# This algorithm was written by Zeriab for fonts which have characters of the same width.
# This is like Courier New, UMEGothic and fonts of that sort. This algorithm attaches a
# cost to each line based on the amount of white space at the end of that line. It will
# display the way of writing the text with the lowest total cost. In prcatice, this will mean
# that it will, as much as possible, reduce the spacing between letters in a line and make
# the spacing more consistent for each line of the paragraph
#===================================================================
class Formatter_2
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# * Format
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def format (string, specifications)
f = Formatted_Text.new
f.lines, f.blank_width, word_lengths, words = [], [], [], []
tracker = 0
for i in 0...string.size
if string[i,1] == " " || i == string.size - 1
if i == string.size - 1
i += 1
end
word_lengths.push (i - tracker)
words.push (string[tracker, i - tracker])
tracker = i + 1
end
end
if specifications.class == Bitmap
max_width = specifications.width
f.bitmap = specifications
elsif specifications.class == Fixnum || specifications.class == Float
max_width = specifications
f.bitmap = Bitmap.new (1,1)
else
# Error Catching: Bad specification
bitmap = Bitmap.new (200, 64)
f = format ('Specifications Error', bitmap)
p 'Specifications Error: Please Pass Fixnum, Float or Bitmap'
return f
end
tw = f.bitmap.text_size('a').width
max_width = [max_width / tw, 180].min
# Error Catching: Word too long
if word_lengths.max > max_width
f = format ('Too long' , specifications)
p 'One or more words is too long for specified width'
return f
end
position = line_break (word_lengths, max_width)
lines = give_lines (position, position.size - 1, words)
max_width *= tw
for i in 0...lines.size
line = lines[i]
f.lines.push (line.scan (/./))
if i == lines.size - 1
f.blank_width.push (0)
else
text_width = line.size * tw
extra_space = max_width - text_width
f.blank_width.push (extra_space.to_f / (line.size.to_f - 1.0))
end
end
if f.bitmap != specifications
f.bitmap = Bitmap.new (max_width, f.lines.size*32)
end
return f
end
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# * Line Break (written by Zeriab)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def line_break(word_lengths, max_length)
return false if max_length > 180
word_lengths.unshift(nil)
extra_spaces = Table.new(word_lengths.size,word_lengths.size)
line_prices = Table.new(word_lengths.size,word_lengths.size)
word_price = []
position = []
inf = max_length*max_length + 1
for i in 1...word_lengths.size
extra_spaces[i,i] = max_length - word_lengths[i]
for j in (i+1)..[word_lengths.size-1, max_length/2+i+1].min
extra_spaces[i,j] = extra_spaces[i,j-1] - word_lengths[j]-1
end
end
for i in 1...word_lengths.size
for j in i..[word_lengths.size-1, max_length/2+i+1].min
if extra_spaces[i,j] < 0
line_prices[i,j] = inf
elsif j == word_lengths.size-1 and extra_spaces[i,j] >= 0
line_prices[i,j] = 0
else
line_prices[i,j] = extra_spaces[i,j]*extra_spaces[i,j]
end
end
end
word_price[0] = 0
for j in 1...word_lengths.size
word_price[j] = inf
for ik in 1..j
i = j - ik + 1
break if line_prices[i,j] == inf
if word_price[i-1] + line_prices[i,j] < word_price[j]
word_price[j] = word_price[i-1] + line_prices[i,j]
position[j] = i
end
end
end
return position
end
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# * Give_Lines (written by Zeriab)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def give_lines(position,last_index,words)
first_index = position[last_index]
word_array = []
if first_index != 1
word_array = give_lines(position, first_index - 1,words)
end
str = ""
for x in first_index..last_index
str += ' ' if x != first_index
str += words[x-1]
end
word_array << str
return word_array
end
end
#===================================================================
# * The Artist class
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Interprets a Formatted Text object and draws the paragraph encoded
#===================================================================
class Artist
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# * Draw
# f : Formatted Text Object
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def draw (f)
# Calculates the necessary distance between lines
line_distance = f.bitmap.height.to_f / f.lines.size.to_f
line_distance = [f.bitmap.font.size + 10, line_distance].min
# For all lines in the lines array
for i in 0...f.lines.size
blank_space = f.blank_width[i]
position = 0
# For all indices of the line array
for j in 0...f.lines[i].size
word = f.lines[i][j]
ws = f.bitmap.text_size (word)
position += blank_space if j != 0
# Adds blank_space and position, and draws the string located at each index
f.bitmap.draw_text (position, line_distance*i,ws.width+1,ws.height+1,word)
# Keeps track of the position we are in in pixels
position += ws.width
end
end
return f.bitmap
end
end
#===================================================================
# * The Formatted_Text class containing the results of the formatter
#===================================================================
class Formatted_Text
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# * Public Instance Variables
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
attr_accessor :lines # An array of strings, each a line of the paragraph
attr_accessor :blank_width # The amount of white space between each letter
attr_accessor :bitmap # The bitmap drawn to
end
end