|
| 1 | +import sys |
| 2 | +import tkinter as tk |
| 3 | +import tkinter.ttk as ttk |
| 4 | +import time |
| 5 | + |
| 6 | +BOUNCE_FACTOR = .88 |
| 7 | +GRAVITY = 0.1 |
| 8 | +FRICTION = 1.0125 |
| 9 | +THROW_FACTOR = 200 |
| 10 | +POP_FACTOR = 2 |
| 11 | + |
| 12 | +class Window: |
| 13 | + def __init__(self): |
| 14 | + self.root = tk.Tk() |
| 15 | + |
| 16 | + self.root.overrideredirect(True) |
| 17 | + self.root.attributes('-topmost', True) |
| 18 | + |
| 19 | + self.root.attributes('-alpha', 0.8) |
| 20 | + |
| 21 | + self.screen_width = self.root.winfo_screenwidth() |
| 22 | + self.screen_height = self.root.winfo_screenheight() |
| 23 | + |
| 24 | + self.hasBeenUnderMousePreviousFrame = False |
| 25 | + |
| 26 | + self.root.geometry('{}x{}'.format(1, 1)) |
| 27 | + self.root.resizable(False, False) |
| 28 | + |
| 29 | + self.root.title("Wonky Window") |
| 30 | + |
| 31 | + if sys.platform == "darwin": # macos |
| 32 | + self.root.createcommand( |
| 33 | + 'tk::mac::ShowPreferences', |
| 34 | + self.on_settings |
| 35 | + ) |
| 36 | + self.root['menu'] = tk.Menu(self.root) |
| 37 | + else: |
| 38 | + menubar = tk.Menu(self.root) |
| 39 | + settings_menu = tk.Menu(menubar, tearoff=0) |
| 40 | + settings_menu.add_command(label="Settings", command=self.on_settings, |
| 41 | + accelerator="Ctrl+,") |
| 42 | + menubar.add_cascade(label="Settings", menu=settings_menu) |
| 43 | + self.root['menu'] = menubar |
| 44 | + |
| 45 | + self.windowX = self.root.winfo_pointerx() - 50 |
| 46 | + self.windowY = self.root.winfo_pointery() - 50 |
| 47 | + self.mouseDown = False |
| 48 | + |
| 49 | + self.root.bind('<ButtonPress-1>', lambda event: self.mouse_down(True)) |
| 50 | + self.root.bind('<ButtonRelease-1>', lambda event: self.mouse_down(False)) |
| 51 | + self.root.bind('<Motion>', lambda event: self.motion()) |
| 52 | + |
| 53 | + self.root.bind('<Control-comma>', lambda event: self.on_settings()) |
| 54 | + self.root.bind('<Command-comma>', lambda event: self.on_settings()) |
| 55 | + |
| 56 | + self.velX = 0 |
| 57 | + self.velY = 0 |
| 58 | + |
| 59 | + self.offsetX = 0 |
| 60 | + self.offsetY = 0 |
| 61 | + |
| 62 | + self.previousFramesX = [0, 0, 0, 0, 0, 0] |
| 63 | + self.previousFramesY = [0, 0, 0, 0, 0, 0] |
| 64 | + self.previousTimestamps = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] |
| 65 | + |
| 66 | + self.state = "growing" |
| 67 | + self.animate_grow() |
| 68 | + self.root.mainloop() |
| 69 | + |
| 70 | + def mouse_down(self, down): |
| 71 | + if hasattr(self, 'gravity') or hasattr(self, 'bounciness') or hasattr(self, 'friction'): |
| 72 | + widget_under_mouse = self.root.winfo_containing(self.root.winfo_pointerx(), self.root.winfo_pointery()) |
| 73 | + if widget_under_mouse == self.gravity or widget_under_mouse == self.bounciness or widget_under_mouse == self.friction or widget_under_mouse == self.throw: |
| 74 | + self.hasBeenUnderMousePreviousFrame = True |
| 75 | + |
| 76 | + if self.mouseDown == False and down == True: |
| 77 | + self.velX = 0 |
| 78 | + self.velY = 0 |
| 79 | + self.previousFramesX = [0, 0, 0, 0, 0, 0] |
| 80 | + self.previousFramesY = [0, 0, 0, 0, 0, 0] |
| 81 | + self.mouseDown = down |
| 82 | + self.offsetX = self.root.winfo_rootx() - self.root.winfo_pointerx() |
| 83 | + self.offsetY = self.root.winfo_rooty() - self.root.winfo_pointery() |
| 84 | + |
| 85 | + if not down: |
| 86 | + distancesX = [] |
| 87 | + distancesY = [] |
| 88 | + |
| 89 | + i = 0 |
| 90 | + for frame in self.previousFramesX: |
| 91 | + if i == 0: |
| 92 | + i += 1 |
| 93 | + continue |
| 94 | + |
| 95 | + try: |
| 96 | + distancesX.append((self.previousFramesX[i] - self.previousFramesX[i-1]) / (self.previousTimestamps[i] - self.previousTimestamps[i-1])) |
| 97 | + except Exception: |
| 98 | + distancesX.append(0) |
| 99 | + i += 1 |
| 100 | + i = 0 |
| 101 | + for frame in self.previousFramesY: |
| 102 | + if i == 0: |
| 103 | + i += 1 |
| 104 | + continue |
| 105 | + try: |
| 106 | + distancesY.append((self.previousFramesY[i] - self.previousFramesY[i-1]) / (self.previousTimestamps[i] - self.previousTimestamps[i-1])) |
| 107 | + except Exception: |
| 108 | + distancesY.append(0) |
| 109 | + i += 1 |
| 110 | + |
| 111 | + self.velX = sum(distancesX) / len(distancesX) / THROW_FACTOR |
| 112 | + self.velY = sum(distancesY) / len(distancesY) / THROW_FACTOR |
| 113 | + |
| 114 | + def motion(self): |
| 115 | + if self.mouseDown == False: |
| 116 | + self.hasBeenUnderMousePreviousFrame = False |
| 117 | + if (self.mouseDown and not self.hasBeenUnderMousePreviousFrame): |
| 118 | + self.previousFramesX.append(self.root.winfo_pointerx()) |
| 119 | + self.previousFramesY.append(self.root.winfo_pointery()) |
| 120 | + self.previousTimestamps.append(time.time()) |
| 121 | + self.previousFramesX.pop(0) |
| 122 | + self.previousFramesY.pop(0) |
| 123 | + self.previousTimestamps.pop(0) |
| 124 | + self.windowX = self.root.winfo_pointerx() + self.offsetX |
| 125 | + self.windowY = self.root.winfo_pointery() + self.offsetY |
| 126 | + self.check_window_collision() |
| 127 | + self.root.geometry('+{}+{}'.format(self.root.winfo_pointerx() + self.offsetX, self.root.winfo_pointery() + self.offsetY)) |
| 128 | + |
| 129 | + def on_gravity_change(self, value): |
| 130 | + global GRAVITY |
| 131 | + GRAVITY = float(value) |
| 132 | + |
| 133 | + def on_bounciness_change(self, value): |
| 134 | + global BOUNCE_FACTOR |
| 135 | + BOUNCE_FACTOR = float(value) |
| 136 | + |
| 137 | + def on_friction_change(self, value): |
| 138 | + global FRICTION |
| 139 | + FRICTION = float(value) |
| 140 | + |
| 141 | + def on_throw_factor_change(self, value): |
| 142 | + global THROW_FACTOR |
| 143 | + THROW_FACTOR = 750 - float(value) |
| 144 | + |
| 145 | + def on_pop_factor_change(self, value): |
| 146 | + global POP_FACTOR |
| 147 | + POP_FACTOR = float(value) |
| 148 | + |
| 149 | + def show_sliders(self): |
| 150 | + self.hasBeenUnderMousePreviousFrame = False |
| 151 | + |
| 152 | + self.gravity = ttk.Scale(self.root, from_=0, to=2, orient='horizontal', length=300, command=self.on_gravity_change) |
| 153 | + self.gravity.set(GRAVITY) |
| 154 | + self.gravity.pack(expand=True, pady=(25, 0)) |
| 155 | + self.gravity_label = tk.Label(self.root, text="Gravity") |
| 156 | + self.gravity_label.pack(pady=(0, 50)) |
| 157 | + |
| 158 | + self.bounciness = ttk.Scale(self.root, from_=0, to=1, orient='horizontal', length=300, command=self.on_bounciness_change) |
| 159 | + self.bounciness.set(BOUNCE_FACTOR) |
| 160 | + self.bounciness.pack(expand=True) |
| 161 | + self.bounciness_label = tk.Label(self.root, text="Bounciness") |
| 162 | + self.bounciness_label.pack(pady=(0, 50)) |
| 163 | + |
| 164 | + self.friction = ttk.Scale(self.root, from_=1, to=1.2, orient='horizontal', length=300, command=self.on_friction_change) |
| 165 | + self.friction.set(FRICTION) |
| 166 | + self.friction.pack(expand=True) |
| 167 | + self.friction_label = tk.Label(self.root, text="Friction") |
| 168 | + self.friction_label.pack(pady=(0, 50)) |
| 169 | + |
| 170 | + self.throw = ttk.Scale(self.root, from_=25, to=700, orient='horizontal', length=300, command=self.on_throw_factor_change) |
| 171 | + self.throw.set(750 - THROW_FACTOR) |
| 172 | + self.throw.pack(expand=True) |
| 173 | + self.throw_label = tk.Label(self.root, text="Throw power") |
| 174 | + self.throw_label.pack(pady=(0, 25)) |
| 175 | + |
| 176 | + self.slide_in_effect() |
| 177 | + |
| 178 | + def slide_in_effect(self, scale=0.0): |
| 179 | + if scale >= 300: |
| 180 | + self.gravity.configure(length=300) |
| 181 | + self.friction.configure(length=300) |
| 182 | + self.bounciness.configure(length=300) |
| 183 | + self.throw.configure(length=300) |
| 184 | + return |
| 185 | + self.gravity.configure(length=scale) |
| 186 | + self.bounciness.configure(length=scale) |
| 187 | + self.friction.configure(length=scale) |
| 188 | + self.throw.configure(length=scale) |
| 189 | + self.root.after(1, self.slide_in_effect, scale+2) |
| 190 | + |
| 191 | + def hide_sliders(self, scale=300): |
| 192 | + if scale <= 0: |
| 193 | + self.gravity.destroy() |
| 194 | + self.gravity_label.destroy() |
| 195 | + self.bounciness.destroy() |
| 196 | + self.bounciness_label.destroy() |
| 197 | + self.friction.destroy() |
| 198 | + self.friction_label.destroy() |
| 199 | + self.throw.destroy() |
| 200 | + self.throw_label.destroy() |
| 201 | + return |
| 202 | + self.gravity.configure(length=scale) |
| 203 | + self.bounciness.configure(length=scale) |
| 204 | + self.friction.configure(length=scale) |
| 205 | + self.throw.configure(length=scale) |
| 206 | + |
| 207 | + self.root.after(1, self.hide_sliders, scale-2) |
| 208 | + |
| 209 | + def animate_settings_open(self, center_x, center_y, i=0): |
| 210 | + if i < 400: |
| 211 | + self.root.geometry('+{}+{}'.format(int(center_x - (int(100+i)) / 2), int(center_y - (int(100+i*.75) / 2)))) |
| 212 | + self.root.geometry(f'{int(100+i)}x{int(100+i*.75)}') |
| 213 | + self.root.after(10, self.animate_settings_open, center_x, center_y, i+10) |
| 214 | + else: |
| 215 | + self.root.geometry(f'500x400') |
| 216 | + self.root.update() |
| 217 | + self.state = "settings" |
| 218 | + |
| 219 | + def animate_settings_close(self, center_x, center_y, i=0): |
| 220 | + if i < 400: |
| 221 | + self.root.geometry('+{}+{}'.format(int(center_x - (int(500-i)) / 2), int(center_y - (int(400-i*.75) / 2)))) |
| 222 | + self.root.geometry(f'{int(500-i)}x{int(400-i*.75)}') |
| 223 | + self.root.after(10, self.animate_settings_close, center_x, center_y, i+10) |
| 224 | + else: |
| 225 | + self.root.geometry(f'100x100') |
| 226 | + self.root.update() |
| 227 | + self.state = "loop" |
| 228 | + |
| 229 | + def on_settings(self, i=1): |
| 230 | + self.mouse_down(False) |
| 231 | + self.velX = 0 |
| 232 | + self.velY = 0 |
| 233 | + if self.state == "loop": |
| 234 | + self.state = "opening settings" |
| 235 | + self.show_sliders() |
| 236 | + center_x = self.windowX + 50 |
| 237 | + center_y = self.windowY + 50 |
| 238 | + self.animate_settings_open(center_x=center_x, center_y=center_y) |
| 239 | + |
| 240 | + elif self.state == "settings": |
| 241 | + self.state = "closing settings" |
| 242 | + self.hide_sliders() |
| 243 | + self.windowX = (self.root.winfo_rootx() + self.root.winfo_width() // 2) - 50 |
| 244 | + self.windowY = (self.root.winfo_rooty() + self.root.winfo_height() // 2) - 50 |
| 245 | + center_x = self.windowX + 50 |
| 246 | + center_y = self.windowY + 50 |
| 247 | + self.animate_settings_close(center_x=center_x, center_y=center_y) |
| 248 | + |
| 249 | + def check_window_collision(self): |
| 250 | + if self.state == "loop": |
| 251 | + if not self.mouseDown: |
| 252 | + width = self.screen_width - self.root.winfo_width() |
| 253 | + height = self.screen_height - self.root.winfo_height() |
| 254 | + |
| 255 | + if self.windowX < 0: |
| 256 | + distance = round(self.windowX - 0) / POP_FACTOR |
| 257 | + self.velX = (-self.velX * BOUNCE_FACTOR) + distance |
| 258 | + self.windowX = 0 |
| 259 | + elif self.windowX > width: |
| 260 | + distance = round(self.windowX - width) / POP_FACTOR |
| 261 | + self.velX = (-self.velX * BOUNCE_FACTOR) + distance |
| 262 | + self.windowX = width |
| 263 | + |
| 264 | + if self.windowY > height: |
| 265 | + distance = round(self.windowY - height) / POP_FACTOR |
| 266 | + self.velY = (-self.velY * BOUNCE_FACTOR) + distance |
| 267 | + self.windowY = height |
| 268 | + elif self.windowY < 38: |
| 269 | + distance = round(self.windowY - 38) / POP_FACTOR |
| 270 | + self.velY = (-self.velY * BOUNCE_FACTOR) + distance |
| 271 | + self.windowY = 38 |
| 272 | + |
| 273 | + def loop(self): |
| 274 | + if self.state == "loop": |
| 275 | + if not self.mouseDown: |
| 276 | + self.windowX += self.velX |
| 277 | + self.windowY += self.velY |
| 278 | + self.check_window_collision() |
| 279 | + self.root.geometry('+{}+{}'.format(int(self.windowX), int(self.windowY))) |
| 280 | + self.velY += GRAVITY |
| 281 | + self.velX /= FRICTION |
| 282 | + self.root.after(8, self.loop) |
| 283 | + |
| 284 | + def animate_grow(self, i=1): |
| 285 | + if i <= 50: |
| 286 | + center_x = self.root.winfo_pointerx() |
| 287 | + center_y = self.root.winfo_pointery() |
| 288 | + self.root.geometry('+{}+{}'.format(int(center_x - i), int(center_y - i))) |
| 289 | + self.root.geometry(f'{i*2}x{i*2}') |
| 290 | + |
| 291 | + self.root.after(10, self.animate_grow, i+1) |
| 292 | + else: |
| 293 | + self.windowX = self.root.winfo_x() |
| 294 | + self.windowY = self.root.winfo_y() |
| 295 | + self.state = "loop" |
| 296 | + self.loop() |
| 297 | + |
| 298 | +window = Window() |
0 commit comments