summaryrefslogtreecommitdiffhomepage
path: root/samples/05_mouse/03_mouse_move_paint_app/app/main.rb
blob: 9303949b1676db896be1fe105c6fff69083a505d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
=begin

 APIs listing that haven't been encountered in previous sample apps:

 - Floor: Method that returns an integer number smaller than or equal to the original with no decimal.

   For example, if we have a variable, a = 13.7, and we called floor on it, it would look like this...
   puts a.floor()
   which would print out 13.
   (There is also a ceil method, which returns an integer number greater than or equal to the original
   with no decimal. If we had called ceil on the variable a, the result would have been 14.)

 Reminders:

 - Hashes: Collection of unique keys and their corresponding values. The value can be found
   using their keys.

   For example, if we have a "numbers" hash that stores numbers in English as the
   key and numbers in Spanish as the value, we'd have a hash that looks like this...
   numbers = { "one" => "uno", "two" => "dos", "three" => "tres" }
   and on it goes.

   Now if we wanted to find the corresponding value of the "one" key, we could say
   puts numbers["one"]
   which would print "uno" to the console.

 - args.state.new_entity: Used when we want to create a new object, like a sprite or button.
   In this sample app, new_entity is used to create a new button that clears the grid.
   (Remember, you can use state to define ANY property and it will be retained across frames.)

 - args.inputs.mouse.click.point.(x|y): The x and y location of the mouse.

 - args.inputs.mouse.click.point.created_at: The frame the mouse click occurred in.

 - args.outputs.labels: An array. The values in the array generate a label.
   The parameters are [X, Y, TEXT, SIZE, ALIGN, RED, GREEN, BLUE, ALPHA, FONT STYLE]
   For more information about labels, go to mygame/documentation/02-labels.md.

 - ARRAY#inside_rect?: Returns true or false depending on if the point is inside the rect.

=end

# This sample app shows an empty grid that the user can paint on.
# To paint, the user must keep their mouse presssed and drag it around the grid.
# The "clear" button allows users to clear the grid so they can start over.

class PaintApp
  attr_accessor :inputs, :state, :outputs, :grid, :args

  # Runs methods necessary for the game to function properly.
  def tick
    print_title
    add_grid
    check_click
    draw_buttons
  end

  # Prints the title onto the screen by using a label.
  # Also separates the title from the grid with a line as a horizontal separator.
  def print_title
    args.outputs.labels << [ 640, 700, 'Paint!', 0, 1 ]
    outputs.lines << horizontal_separator(660, 0, 1280)
  end

  # Sets the starting position, ending position, and color for the horizontal separator.
  # The starting and ending positions have the same y values.
  def horizontal_separator y, x, x2
    [x, y, x2, y, 150, 150, 150]
  end

  # Sets the starting position, ending position, and color for the vertical separator.
  # The starting and ending positions have the same x values.
  def vertical_separator x, y, y2
    [x, y, x, y2, 150, 150, 150]
  end

  # Outputs a border and a grid containing empty squares onto the screen.
  def add_grid

    # Sets the x, y, height, and width of the grid.
    # There are 31 horizontal lines and 31 vertical lines in the grid.
    # Feel free to count them yourself before continuing!
    x, y, h, w = 640 - 500/2, 640 - 500, 500, 500 # calculations done so the grid appears in screen's center
    lines_h = 31
    lines_v = 31

    # Sets values for the grid's border, grid lines, and filled squares.
    # The filled_squares variable is initially set to an empty array.
    state.grid_border ||= [ x, y, h, w ] # definition of grid's outer border
    state.grid_lines ||= draw_grid(x, y, h, w, lines_h, lines_v) # calls draw_grid method
    state.filled_squares ||= [] # there are no filled squares until the user fills them in

    # Outputs the grid lines, border, and filled squares onto the screen.
    outputs.lines.concat state.grid_lines
    outputs.borders << state.grid_border
    outputs.solids << state.filled_squares
  end

  # Draws the grid by adding in vertical and horizontal separators.
  def draw_grid x, y, h, w, lines_h, lines_v

    # The grid starts off empty.
    grid = []

    # Calculates the placement and adds horizontal lines or separators into the grid.
    curr_y = y # start at the bottom of the box
    dist_y = h / (lines_h + 1) # finds distance to place horizontal lines evenly throughout 500 height of grid
    lines_h.times do
      curr_y += dist_y # increment curr_y by the distance between the horizontal lines
      grid << horizontal_separator(curr_y, x, x + w - 1) # add a separator into the grid
    end

    # Calculates the placement and adds vertical lines or separators into the grid.
    curr_x = x # now start at the left of the box
    dist_x = w / (lines_v + 1) # finds distance to place vertical lines evenly throughout 500 width of grid
    lines_v.times do
      curr_x += dist_x # increment curr_x by the distance between the vertical lines
      grid << vertical_separator(curr_x, y + 1, y  + h) # add separator
    end

    # paint_grid uses a hash to assign values to keys.
    state.paint_grid ||= {"x" => x, "y" => y, "h" => h, "w" => w, "lines_h" => lines_h,
                          "lines_v" => lines_v, "dist_x" => dist_x,
                          "dist_y" => dist_y }

    return grid
  end

  # Checks if the user is keeping the mouse pressed down and sets the mouse_hold variable accordingly using boolean values.
  # If the mouse is up, the user cannot drag the mouse.
  def check_click
    if inputs.mouse.down #is mouse up or down?
      state.mouse_held = true # mouse is being held down
    elsif inputs.mouse.up # if mouse is up
    state.mouse_held = false # mouse is not being held down or dragged
      state.mouse_dragging = false
    end

    if state.mouse_held &&    # mouse needs to be down
      !inputs.mouse.click &&     # must not be first click
      ((inputs.mouse.previous_click.point.x - inputs.mouse.position.x).abs > 15) # Need to move 15 pixels before "drag"
      state.mouse_dragging = true
    end

    # If the user clicks their mouse inside the grid, the search_lines method is called with a click input type.
    if ((inputs.mouse.click) && (inputs.mouse.click.point.inside_rect? state.grid_border))
      search_lines(inputs.mouse.click.point, :click)

    # If the user drags their mouse inside the grid, the search_lines method is called with a drag input type.
    elsif ((state.mouse_dragging) && (inputs.mouse.position.inside_rect? state.grid_border))
      search_lines(inputs.mouse.position, :drag)
    end
  end

  # Sets the definition of a grid box and handles user input to fill in or clear grid boxes.
  def search_lines (point, input_type)
    point.x -= state.paint_grid["x"] # subtracts the value assigned to the "x" key in the paint_grid hash
    point.y -= state.paint_grid["y"] # subtracts the value assigned to the "y" key in the paint_grid hash

    # Remove code following the .floor and see what happens when you try to fill in grid squares
    point.x = (point.x / state.paint_grid["dist_x"]).floor * state.paint_grid["dist_x"]
    point.y = (point.y / state.paint_grid["dist_y"]).floor * state.paint_grid["dist_y"]

    point.x += state.paint_grid["x"]
    point.y += state.paint_grid["y"]

    # Sets definition of a grid box, meaning its x, y, width, and height.
    # Floor is called on the point.x and point.y variables.
    # Ceil method is called on values of the distance hash keys, setting the width and height of a box.
    grid_box = [ point.x.floor, point.y.floor, state.paint_grid["dist_x"].ceil, state.paint_grid["dist_y"].ceil ]

    if input_type == :click # if user clicks their mouse
      if state.filled_squares.include? grid_box # if grid box is already filled in
        state.filled_squares.delete grid_box # box is cleared and removed from filled_squares
      else
        state.filled_squares << grid_box # otherwise, box is filled in and added to filled_squares
      end
    elsif input_type == :drag # if user drags mouse
      unless state.filled_squares.include? grid_box # unless the grid box dragged over is already filled in
        state.filled_squares << grid_box # the box is filled in and added to filled_squares
      end
    end
  end

  # Creates and outputs a "Clear" button on the screen using a label and a border.
  # If the button is clicked, the filled squares are cleared, making the filled_squares collection empty.
  def draw_buttons
    x, y, w, h = 390, 50, 240, 50
    state.clear_button        ||= state.new_entity(:button_with_fade)

    # The x and y positions are set to display the label in the center of the button.
    # Try changing the first two parameters to simply x, y and see what happens to the text placement!
    state.clear_button.label  ||= [x + w.half, y + h.half + 10, "Clear", 0, 1] # placed in center of border
    state.clear_button.border ||= [x, y, w, h]

    # If the mouse is clicked inside the borders of the clear button,
    # the filled_squares collection is emptied and the squares are cleared.
    if inputs.mouse.click && inputs.mouse.click.point.inside_rect?(state.clear_button.border)
      state.clear_button.clicked_at = inputs.mouse.click.created_at # time (frame) the click occurred
      state.filled_squares.clear
      inputs.mouse.previous_click = nil
    end

    outputs.labels << state.clear_button.label
    outputs.borders << state.clear_button.border

    # When the clear button is clicked, the color of the button changes
    # and the transparency changes, as well. If you change the time from
    # 0.25.seconds to 1.25.seconds or more, the change will last longer.
    if state.clear_button.clicked_at
      outputs.solids << [x, y, w, h, 0, 180, 80, 255 * state.clear_button.clicked_at.ease(0.25.seconds, :flip)]
    end
  end
end

$paint_app = PaintApp.new

def tick args
  $paint_app.inputs = args.inputs
  $paint_app.state = args.state
  $paint_app.grid = args.grid
  $paint_app.args = args
  $paint_app.outputs = args.outputs
  $paint_app.tick
  tick_instructions args, "How to create a simple paint app. CLICK and HOLD to draw."
end

def tick_instructions args, text, y = 715
  return if args.state.key_event_occurred
  if args.inputs.mouse.click ||
     args.inputs.keyboard.directional_vector ||
     args.inputs.keyboard.key_down.enter ||
     args.inputs.keyboard.key_down.escape
    args.state.key_event_occurred = true
  end

  args.outputs.debug << [0, y - 50, 1280, 60].solid
  args.outputs.debug << [640, y, text, 1, 1, 255, 255, 255].label
  args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label
end