-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMusic Drawing Program.py
More file actions
345 lines (277 loc) · 13.5 KB
/
Music Drawing Program.py
File metadata and controls
345 lines (277 loc) · 13.5 KB
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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
"""
Music Notation GUI - Naomi Beck
Python capstone project for Intro to Computer Science, Fall 2023
This interactive Python program lets users compose simple sheet music
by placing, connecting, and deleting symbols on a canvas.
Requires the 'Draw' graphics module.
"""
import Draw
import math
#GLOBALS
CANVAS_WIDTH = 500
CANVAS_HEIGHT = 500
STICK_HEIGHT = 20
OVAL_WIDTH = 10
OVAL_HEIGHT = 11
CIRCLE_SIZE = 15
MAX_DELETE_DISTANCE = 10
NUM_LINE_SETS = 3
TOP_LINE_Y = 50
MUSIC_LINE_X = 50
MUSIC_LINE_LENGTH = 400
LINE_SPACING = 12
PALLET_X = 25
PALLET_Y = 350
PALLET_CELL_WIDTH = 30
PALLET = ["filledDown", "outlinedDown", "filledUp", "outlinedUp", "circle",
"filledDownDot", "outlinedDownDot", "filledUpDot", "outlinedUpDot",
"line", "sharp", "flat"]
CONNECT_CELL_NUMBER = 12
# input an x, a y, and a description and DrawNotes() will draw it at x,y
def DrawNotes(x,y, desc):
Draw.setFontItalic(True)
# font size is oval width so that notes are symmetric to other charachters
Draw.setFontSize(OVAL_WIDTH)
desc = desc.lower()
# if its a special case then draw that speacil case centered at the x,y
if desc == "sharp":
Draw.string("#", x-OVAL_WIDTH/2, y-OVAL_WIDTH/1.5)
elif desc == "flat":
Draw.string("b", x-OVAL_WIDTH/2, y-OVAL_WIDTH/1.5)
elif desc == "circle":
Draw.oval(x-CIRCLE_SIZE/2, y-CIRCLE_SIZE/2, CIRCLE_SIZE, CIRCLE_SIZE)
elif desc == "line":
# draw a line with the center at x,y
# and the length a little bit more than muic note width
Draw.line(x-OVAL_WIDTH/2-2, y, x+OVAL_WIDTH/2+2, y)
else: # if its not one of the other shapes
# draw a note with the centerpoint at x,y, following the description
if "filled" in desc:
Draw.filledOval(x-OVAL_WIDTH/2, y-OVAL_HEIGHT/2,
OVAL_WIDTH, OVAL_HEIGHT)
else: # "outlined"
Draw.oval(x-OVAL_WIDTH/2, y-OVAL_HEIGHT/2, OVAL_WIDTH, OVAL_HEIGHT)
if "up" in desc:
Draw.line(x-OVAL_WIDTH/2, y, x-OVAL_WIDTH/2, y+ STICK_HEIGHT)
#x, y+OVAL_HEIGHT/2, x, y-STICK_HEIGHT
else: # "down"
Draw.line(x+OVAL_WIDTH/2, y, x+OVAL_WIDTH/2, y-STICK_HEIGHT)
if "dot" in desc:
Draw.filledOval(x+OVAL_WIDTH, y-OVAL_WIDTH/4,
OVAL_WIDTH/10, OVAL_WIDTH/10)
Draw.setFontItalic(False)
# DrawScreen() draws all the elements of the canvas
def DrawScreen(notes):
Draw.clear()
# draw the notes stored in the notes list
for note in notes:
# if it is a connected note: Draw the connector
if len(note) == 5:
Draw.line(note[0], note[1], note[2], note[3])
# otherwise invoke DrawNotes the usual way
else:
if len(note) == 3 :
DrawNotes(note[0], note[1], note[2])
# draw the horizontal lines
for i in range(NUM_LINE_SETS):
for j in range(1,6):
Draw.line(MUSIC_LINE_X,
(TOP_LINE_Y + LINE_SPACING*(j-1))+i*6*LINE_SPACING,
MUSIC_LINE_X + MUSIC_LINE_LENGTH,
(TOP_LINE_Y + LINE_SPACING*(j-1))+i*6*LINE_SPACING)
# draw the verticle lines breaking up into meters
for k in range(4):
Draw.line(MUSIC_LINE_X + (k+1)*(MUSIC_LINE_LENGTH/4),
TOP_LINE_Y + i*LINE_SPACING*6,
MUSIC_LINE_X+ (k+1)*(MUSIC_LINE_LENGTH/4),
TOP_LINE_Y + LINE_SPACING*4 + i*LINE_SPACING*6)
# draw another verticle line at the end of each set
Draw.line(MUSIC_LINE_X + MUSIC_LINE_LENGTH - 4,
TOP_LINE_Y + LINE_SPACING*6*i,
MUSIC_LINE_X + MUSIC_LINE_LENGTH - 4,
TOP_LINE_Y + LINE_SPACING*4 + LINE_SPACING*6*i )
# draw the symbol at the start of the first line
Draw.picture("musicSymbol.gif", MUSIC_LINE_X, TOP_LINE_Y-2)
# draw the pallet outlines including the lines for the connectors and delete
for row in range(len(PALLET)+3):
# draw a double cell for the connector's cell
if row == CONNECT_CELL_NUMBER:
Draw.rect(PALLET_X + PALLET_CELL_WIDTH*row, PALLET_Y,
PALLET_CELL_WIDTH*2, PALLET_CELL_WIDTH)
# and leave out the cell after the connector's cell
elif row == CONNECT_CELL_NUMBER+1: None
# othersise Draw a regular cell
else:
Draw.rect(PALLET_X + PALLET_CELL_WIDTH*row,
PALLET_Y,
PALLET_CELL_WIDTH, PALLET_CELL_WIDTH)
# draw the pallet contents
for i in range(len(PALLET)):
if "up" in PALLET[i].lower():
# draw the note in the center of the cell
# a little bit higher so the stick wont touch the cell outline
DrawNotes(PALLET_X+PALLET_CELL_WIDTH/2 +PALLET_CELL_WIDTH*i,
PALLET_Y+PALLET_CELL_WIDTH/2-7, PALLET[i])
# a little bit higher so the stick wont touch the cell outline
elif "down" in PALLET[i].lower():
DrawNotes(PALLET_X+PALLET_CELL_WIDTH/2 +PALLET_CELL_WIDTH*i,
PALLET_Y+PALLET_CELL_WIDTH/2+6, PALLET[i])
# otherwise draw in the center and thats all
else:
DrawNotes(PALLET_X+PALLET_CELL_WIDTH/2 +PALLET_CELL_WIDTH*i,
PALLET_Y+PALLET_CELL_WIDTH/2, PALLET[i])
# Draw connect botton
Draw.string("Connect", PALLET_X+PALLET_CELL_WIDTH*12+5, \
PALLET_Y+PALLET_CELL_WIDTH/4)
# draw delete button
Draw.picture("TrashCan.gif",PALLET_X + PALLET_CELL_WIDTH*14.2,
PALLET_Y+PALLET_CELL_WIDTH/10)
# draw message box
DrawMessageBox()
Draw.show()
# draws the red message box to "reset" it
def DrawMessageBox():
Draw.setColor(Draw.RED)
Draw.filledRect(0, CANVAS_HEIGHT-100, CANVAS_WIDTH, 100)
Draw.setColor(Draw.BLACK)
# input a y and sanpTo() will return the closest y value of the line or halfline
def snapTo(y):
deltaY = y - TOP_LINE_Y
denom = .5 * LINE_SPACING
return int(deltaY / denom + .5) * denom + TOP_LINE_Y
# input an x,y and notes and deleteNotes() removes the note selected from notes
def deleteNote(x,y,notes):
# get the note number
noteNumber = getNoteNumber(x,y,notes)
# if the note exists then delete that note/ remove it from notes
if noteNumber >= 0:
del notes[noteNumber]
# returns True if the note imputed is one that is allowed to connect to another
def okToConnect(noteNumber, notes):
# if the note is a sharp, flat, or, circle then return False
note_type = notes[noteNumber][2].lower()
if note_type == "sharp" or note_type == "flat" or note_type == "circle":
return False
return True
# input a note and getTipLocation() returns the x,y of the tip
def getTipLocation(note, notes):
if "down" in notes[note][2].lower():
return notes[note][0] +OVAL_WIDTH/2, notes[note][1] -STICK_HEIGHT
else: # line not needed but it flows better to me
return notes[note][0] -OVAL_WIDTH/2, notes[note][1] +STICK_HEIGHT
# returns True if both notes inputed have the same orientation
def sameOrientation(firstNoteNumber, secondNoteNumber, notes):
if "up" in (notes[firstNoteNumber][2]).lower() and \
"down" in (notes[secondNoteNumber][2]).lower() or \
"down" in (notes[firstNoteNumber][2]).lower() and \
"up" in (notes[secondNoteNumber][2]).lower():
return False
return True
# returns the closest note number within MAX_DELETE_DISTANCE of the x,y inputed
def getNoteNumber(x,y, notes):
closestNoteNumber = None
closestDistance = 99999999
for i in range(len(notes)):
# get distance of each note to the x,y inputed
dx = notes[i][0] - x
dy = notes[i][1] - y
distance = math.sqrt(dx**2 + dy**2)
# get the closest note
if distance < closestDistance:
closestNoteNumber = i
closestDistance = distance
# if the closest note is within MAX_DELETE_DISTANCE then return the note num
if closestDistance <= MAX_DELETE_DISTANCE:
return closestNoteNumber
# otherwise return -1 representing that there is no eligable note by the x,y
return -1
# returns a "connector" object - lookes like [x,y,x,y,"connector"]
def getConnector(notes):
# set first and second to None
first = None
second = None
while True: # loop forever
# draw messages to the user explaining what they need to do next
Draw.setFontSize(15)
if first == None:
Draw.string("Waiting for first note", PALLET_X, CANVAS_HEIGHT-50)
elif second == None:
# redraw the message box
DrawMessageBox()
Draw.string("Waiting for second note", CANVAS_WIDTH/2, CANVAS_HEIGHT-50)
# if the user clicked: get the x,y of the click
if Draw.mousePressed():
x = Draw.mouseX()
y = Draw.mouseY()
# get the note number of the click
newNote = getNoteNumber(x,y,notes)
# if newNote is a valid note and is ok to connect
if newNote >= 0 and okToConnect(newNote, notes):
# if first note is stil not selected then make this first note
if first == None:
first = newNote
# otherwise if newNote is not the same as first
# and has differnt orientation then the first: make it second
elif newNote != first and sameOrientation(first, newNote, notes):
second = newNote
# give the user a message that they succesfully connected
DrawMessageBox()
Draw.setFontSize(15)
Draw.string("Conected!", PALLET_X, CANVAS_HEIGHT-50)
Draw.show(500)
# return the note looking like: [x,y,x,y,"connector"]
# getTipLocation returns a tupple so index inv
return [(getTipLocation(first, notes))[0],
(getTipLocation(first, notes))[1],
(getTipLocation(second, notes))[0],
(getTipLocation(second, notes))[1],"connector"]
def main():
# draw the canvas and initialise notes
Draw.setCanvasSize(CANVAS_WIDTH, CANVAS_HEIGHT)
notes = []
DrawScreen(notes)
Draw.string("Click a note on the pallet to begin. \n" +
"Once a note is selected, click on the music sheet to draw. \n" +
"To delete, click the delete botton and then any notes to delete. \n" +
"To connect two notes, cick the connect botton and then the two notes.",
PALLET_X, CANVAS_HEIGHT- 85)
shape = None
while True: #loop forever
# if user clicked then get the x and y of the click
if Draw.mousePressed():
x = Draw.mouseX()
y = Draw.mouseY()
# if the x, y is within the pallet: set the shape to the cell clicked
if x > PALLET_X and x < PALLET_X + PALLET_CELL_WIDTH *12 and \
y > PALLET_Y and y < PALLET_Y + PALLET_CELL_WIDTH:
cell = (x-PALLET_X)//PALLET_CELL_WIDTH
shape = PALLET[cell]
# otherwise if x,y is within the delete button, set shape to delete
elif x > PALLET_X + PALLET_CELL_WIDTH * (CONNECT_CELL_NUMBER+2) and \
x < PALLET_X + PALLET_CELL_WIDTH * (CONNECT_CELL_NUMBER+3) and \
y > PALLET_Y and y < PALLET_Y + PALLET_CELL_WIDTH:
shape = "delete"
# otherwise if x,y is within the connenct button
elif x > PALLET_X + PALLET_CELL_WIDTH * CONNECT_CELL_NUMBER and \
x < PALLET_X + PALLET_CELL_WIDTH * CONNECT_CELL_NUMBER*2 and \
y > PALLET_Y and y < PALLET_Y + PALLET_CELL_WIDTH:
# add the returned note from getConnector() to note
notes += [getConnector(notes)]
# otherwise if x,y is within the drawing area and shape is
# not None and shape is not “delete”
elif y > TOP_LINE_Y - LINE_SPACING*2 and \
y < TOP_LINE_Y + LINE_SPACING* NUM_LINE_SETS*6 and \
x > MUSIC_LINE_X and x < MUSIC_LINE_X + MUSIC_LINE_LENGTH and \
shape != None and shape != "delete":
# append the new note, using snapTo() for the y, to notes
snapY = snapTo(y)
notes += [[x,snapY,shape]]
# otherwise if x,y is within the drawing area and shape is “delete”
elif y > TOP_LINE_Y - LINE_SPACING*2 and \
y < TOP_LINE_Y + LINE_SPACING* NUM_LINE_SETS*6 and \
x > MUSIC_LINE_X and x < MUSIC_LINE_X + MUSIC_LINE_LENGTH and \
shape == "delete":
# delete the note using the deleteNote() function
deleteNote(x,y,notes)
DrawScreen(notes)
main()