Paragraph Formatter (VX Edition) Version: 2.0 Author: modern algebra Date: September 10, 2009 Version History
<Version 2.0> 09.10.2009 - Core rewritten. Formatter_2 removed from the main script and added as a separate algorithm which can be added if desired. It will be rewritten soon also. The draw method of the default Artist class can now accept another argument to determine whether the paragraph should be drawn justified or not <Version 1.1> 01.20.2008 - Better Error Catching; Better comments; Better facade <Version 1.0> 11.02.2007 - Original Script Planned Future Versions
Rewrite parts of Formatter_2 to make it cleaner and better documented Create a new Formatter class that works like Formatter_2 does but for non-fixed width fonts as well. (I.e. make the spacing more consistent where it can Create a new Formatted_Text, Formatter, and Artist set that allows for more creative paragraphs, allowing things like Bolded text, Italicized text, colour changes, icons and more. Description
This is a scripting tool. It's purpose is to allow the user to input an unbroken string and have returned a paragraph which has the ends of each line in line with each other. It serves roughly the same function as does the justifying option in Microsoft Word. See the screenshots if you are still confused.
Features
A nice, easy way to display long strings in a paragraph format Easy to add and modify at runtime, allowing for the switching between formatting classes for each situation You can write your own formatter or artist classes and use the paragraphing tool to suit your situation. Highly customizable. Screenshots Instructions As a scripter's tool, it can be quite heavy for non-scripters to use. That is why I wrote a facade for common use of the tool. Naturally, you will still need some scripting knowledge, but the facade allows for this code:
bitmap.draw_paragraph (x, y, max_width, max_height, string)
where bitmap is the bitmap you are drawing to. This can be self.contents in a window, or any instance of the bitmap class.
It can be used like this, if you want to draw the paragraph in a different way:
formatter = <formatter class you want to use> artist = <artist class you want to use> specifications = <max width, or bitmap> pg = Paragrapher.new (formatter, artist) text_bitmap = pg.paragraph (string, specifications) bitmap.blt (x, y, text_bitmap, Rect.new (0,0,text_bitmap.width, text_bitmap.height))
Basically, you choose your formatter and artist class at runtime. This means that if you want to use Paragraph::Formatter_2, because you are using a font with set width for all characters, then you would choose that here. Currently, there is only one Artist class, Paragraph::Artist, but of course you can make your own if it does not suit you. You can either specify a bitmap or a fixnum. The fixnum would just be the max width, and the paragrapher would create a bitmap which was at font_size 22, default font name, and it would space each line 32 pixels. With a bitmap, you specify max_width, max_height, font and font size, and anything else that has an effect. Naturally, bitmap in the code is the bitmap you are drawing the paragraph on. If you have any questions, just ask.
Also, the text_size method of Bitmap does not, in fact, work properly. In a little while I will post a way to get around this problem as it can get in the way of drawing nice paragraphs.
Put the scripts above main and below the default scripts.
For more pertinent instructions, see the header of the script
Script
#============================================================================== # Paragraph Formatter (VX) # Version: 2.0 # Author: modern algebra (rmrk.net) # Date: September 10, 2009 #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # Description: # The idea behind this script is to easily separate a long string into a # paragraph that fits in to the dimensions you specify. More than that, you # can also justify the paragraph #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # 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 form. You can easily change which # formatter or artist classes you want to use with the codes: # # bitmap.paragraph_formatter = Paragrapher::<formatter_name> # bitmap.paragraph_artist = Paragrapher::<artist_name #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # 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 #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # Summary of Changes: # new attr_writer - paragraph_formatter, paragraph_artist # new methods - paragraph_formatter, paragraph_artist, draw_paragraph #============================================================================== class Bitmap #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # * Public Instance Variables #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ attr_writer :paragraph_formatter # The formatting class for Paragraphing attr_writer :paragraph_artist # The artist class for Paragraphing #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # * Get Formatter #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def paragraph_formatter @paragraph_formatter = $game_system.default_formatter if @paragraph_formatter.nil? return @paragraph_formatter.new end #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # * Get Artist #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def paragraph_artist @paragraph_artist = $game_system.default_artist if @paragraph_artist.nil? return @paragraph_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.dup 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 #============================================================================== # *** Paragrapher #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # Module containing the objects for the Paragrapher #============================================================================== module Paragrapher #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # * New #`````````````````````````````````````````````````````````````````````````` # 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 #========================================================================== # ** Formatted_Text #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # Bundles together the result of a Formatter class #========================================================================== Formatted_Text = Struct.new (:lines, :blank_width, :bitmap) #========================================================================== # ** Paragrapher #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # This struct has accessible attributes and can easily paragraph objects. #========================================================================== class Paragrapher < Struct.new (:formatter, :artist) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # * Paragraph # string : the string to be broken into lines # specifications : the other arguments required for the Formatter #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def paragraph(string, *specifications) f = formatter.format (string, *specifications) return artist.draw (f) end end #============================================================================ # ** Formatter #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # This class converts a string into a formatted text object #============================================================================ class Formatter #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # * Format # string : the string to be formatted # specifications : the desired width of the paragraph, or a bitmap #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def format (string, specifications) @string = string # Checks whether specifications is a bitmap or a number. It then sets # max_width and f.bitmap accordingly if specifications.is_a? (Bitmap) bitmap = specifications @max_width = specifications.width elsif specifications.is_a? (Numeric) @max_width = specifications bitmap = Bitmap.new (@max_width, 32) else # Error Catching: Incorrect Specifications f = format ('Specifications Error', Bitmap.new (200, 64)) p 'Specifications Error: Please Pass Numeric or Bitmap' return f end # Initializes Formatted_Text object @format_text = Formatted_Text.new ([], [], bitmap) @line_break = 0 @last_word = 0 for i in 0...@string.size format_character (i) end # Adds the last line to f.lines @format_text.lines.push ( @string[@line_break, @string.size - @line_break].scan (/./) ) # Since the last line is drawn normally, blank_width should be 0 @format_text.blank_width.push (0) height = @format_text.lines.size*Window_Base::WLH @format_text.bitmap = Bitmap.new (@max_width, height) if specifications.is_a? (Numeric) # Returns the Formatted_Text object formatted_text = @format_text.dup @format_text = nil return formatted_text end #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # * Format Character # i : index of position in the string #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def format_character (i) character = @string[i, 1] # If at the end of a word if character == "\n" || character == " " || i == @string.size - 1 i += 1 if i == @string.size - 1 # Account for possible overlap at end # If this word fits on the current line substring = @string[@line_break, i - @line_break] if @format_text.bitmap.text_size (substring).width > @max_width next_line (@last_word) end if character == "\n" next_line (i) @format_text.blank_width[-1] = 0 end @last_word = i end end #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # * Proceed to Next Line # last_word : the index of the beginning of the previous word #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def next_line (last_word) line = @string[@line_break, last_word - @line_break] # Adds current line to f.lines @format_text.lines.push ( line.scan (/./) ) # Calculates the blank space left to cover in the line line_blank = @max_width - @format_text.bitmap.text_size(line).width @format_text.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 = last_word + 1 end end #============================================================================ # ** Artist #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # Interprets a Formatted Text object and returns a bitmap of the paragraph #============================================================================ class Artist #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # * Draw # f : Formatted Text Object # justify_text : boolean value on whether to justify text #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def draw (f, justify_text = true) # Calculates the necessary distance between lines line_distance = f.bitmap.height.to_f / f.lines.size.to_f line_distance = [f.bitmap.font.size + 4, 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 string = f.lines[i][j] tw = f.bitmap.text_size (string).width # Draws the string located at each index f.bitmap.draw_text (position, line_distance*i, tw, line_distance, string) # Keeps track of the position we are in in pixels position += tw position += blank_space if justify_text end end return f.bitmap end 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 @default_artist = Paragrapher::Artist end end
Additional Paragrapher Classes
Formatter_2 (based off Zeriab's algorithm, written for fonts with unvarying character width) Special Codes Formatter For all additional Paragrapher classes, just add them in the Script Editor below the Paragraph Formatter in their own slots, but still above Main.
Spoiler for Formatter_2 :
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
#============================================================================== # ** Paragrapher::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 #============================================================================== module Paragrapher 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 end
Spoiler for Special Codes Formatter :
This formatter & artist combo is the same as the regular formatter and artist, except that it is written to account for certain special codes in its composition and ignore them in calculating positioning of the text. The special codes it recognizes are listed in the attached text document.
Get it from the
attached text document or go
here Credit
Zeriab, for the idea, the motivation, and the instruction, as well as the algorithm for Formatter_2 modern algebra, for the sloppy code Support
Support will be provided anywhere that I (modern algebra) posts the script. At forums where it is posted by anyone else, I make no guarantees. I am willing to write formatting or artist methods to deal with any instances for which current algorithms are inapplicable or inefficient, as well as dealing with any bugs in the current algorithms.
Author's Notes
This script was inspired by Zeriab, and pretty much everything that is good about this script is due to Zeriab. Zeriab deserves more credit for this script then I do, rightly, but since the world isn't just...
Anyway, he deserves all my thanks for being an excellent teacher.
This script by
modern algebra is licensed under a
Creative Commons Attribution-Non-Commercial-Share Alike 2.5 Canada License .