crystalwindow 4.6__py3-none-any.whl → 4.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
crystalwindow/assets.py CHANGED
@@ -1,6 +1,5 @@
1
- # assets.py
2
1
  import os
3
- import random
2
+ import random, json, pickle
4
3
  from tkinter import PhotoImage
5
4
 
6
5
  try:
@@ -15,90 +14,129 @@ ASSETS = {} # cache for all loaded images
15
14
  # --------------------------------------------------------
16
15
  # MAIN IMAGE LOADER
17
16
  # --------------------------------------------------------
18
- def load_image(path, flip_h=False, flip_v=False):
17
+ def load_image(path=None, flip_h=False, flip_v=False, size=None, color=None):
19
18
  """
20
- Loads an image and returns a Tk PhotoImage.
21
- Supports flipping using Pillow.
19
+ Unified image loader w/ caching, flips, and fallback.
20
+ Handles:
21
+ - path images
22
+ - solid color gens
23
+ - missing textures
24
+ - Pillow and pure-tk fallback
22
25
  """
23
26
 
24
- key = f"{path}|h={flip_h}|v={flip_v}"
27
+ # ----------------------------------------------------
28
+ # NO PATH → GENERATE STATIC IMAGE
29
+ # ----------------------------------------------------
30
+ if path is None:
31
+ if Image is None:
32
+ # basic tk only
33
+ w, h = size if size else (64, 64)
34
+ img = PhotoImage(width=w, height=h)
35
+ return img
36
+
37
+ w, h = size if size else (64, 64)
38
+
39
+ # solid or missing texture
40
+ pil_img = (
41
+ Image.new("RGB", (w, h), color) if color
42
+ else generate_missing_texture((w, h))
43
+ )
44
+
45
+ # flips BEFORE resize
46
+ if flip_h:
47
+ pil_img = pil_img.transpose(Image.FLIP_LEFT_RIGHT)
48
+ if flip_v:
49
+ pil_img = pil_img.transpose(Image.FLIP_TOP_BOTTOM)
50
+
51
+ if size:
52
+ pil_img = pil_img.resize(size, Image.Resampling.LANCZOS)
53
+
54
+ return ImageTk.PhotoImage(pil_img)
55
+
56
+ # ----------------------------------------------------
57
+ # USE FULL KEY AS CACHE
58
+ # ----------------------------------------------------
59
+ key = f"{path}|h={flip_h}|v={flip_v}|size={size}|color={color}"
25
60
  if key in ASSETS:
26
61
  return ASSETS[key]
27
62
 
63
+ # ----------------------------------------------------
64
+ # INVALID PATH → MISSING TEXTURE
65
+ # ----------------------------------------------------
28
66
  if not os.path.exists(path):
29
- print(f"⚠️ Missing file: {path}")
30
- img = generate_fallback(path)
31
- ASSETS[key] = img
32
- return img
67
+ print(f" Missing image: {path}")
68
+ fb = load_image(None, size=size, color=color)
69
+ ASSETS[key] = fb
70
+ return fb
33
71
 
34
- # If Pillow is missing → load normal image
35
- if Image is None or ImageTk is None:
72
+ # ----------------------------------------------------
73
+ # PIL NOT INSTALLED PURE TK LOADING
74
+ # ----------------------------------------------------
75
+ if Image is None:
36
76
  try:
37
77
  img = PhotoImage(file=path)
78
+ # cannot flip or resize without PIL, soz
38
79
  ASSETS[key] = img
39
80
  return img
40
81
  except:
41
- fb = generate_fallback(path)
82
+ fb = load_image(None, size=size, color=color)
42
83
  ASSETS[key] = fb
43
84
  return fb
44
85
 
45
- # Load with PIL to allow flipping
86
+ # ----------------------------------------------------
87
+ # LOAD USING PIL (BEST MODE)
88
+ # ----------------------------------------------------
46
89
  try:
47
- pil_img = Image.open(path)
90
+ pil_img = Image.open(path).convert("RGBA")
48
91
 
92
+ # flips
49
93
  if flip_h:
50
94
  pil_img = pil_img.transpose(Image.FLIP_LEFT_RIGHT)
51
95
  if flip_v:
52
96
  pil_img = pil_img.transpose(Image.FLIP_TOP_BOTTOM)
53
97
 
54
- tk_img = ImageTk.PhotoImage(pil_img)
55
- ASSETS[key] = tk_img
56
- return tk_img
98
+ # resize AFTER flip
99
+ if size:
100
+ pil_img = pil_img.resize(size, Image.Resampling.LANCZOS)
101
+
102
+ tk = ImageTk.PhotoImage(pil_img)
103
+ ASSETS[key] = tk
104
+ return tk
57
105
 
58
106
  except Exception as e:
59
- print(f"⚠️ Error loading {path}: {e}")
60
- fb = generate_fallback(path)
107
+ print(f" Error loading {path}: {e}")
108
+ fb = load_image(None, size=size, color=color)
61
109
  ASSETS[key] = fb
62
110
  return fb
63
111
 
64
112
 
65
- # --------------------------------------------------------
66
- # FALLBACK NODE
67
- # --------------------------------------------------------
68
- def generate_fallback(path):
69
- """Fallback colored block used when an image is missing."""
70
- rand_color = (
71
- random.randint(50, 255),
72
- random.randint(50, 255),
73
- random.randint(50, 255),
74
- )
75
-
76
- if Image:
77
- try:
78
- pil_img = Image.open(path)
79
- w, h = pil_img.size
80
- except:
81
- w, h = 64, 64
82
- else:
83
- w, h = 64, 64
113
+ # ========================================================
114
+ # MISSING TEXTURE GENERATOR
115
+ # ========================================================
116
+ def generate_missing_texture(size):
117
+ w, h = size
118
+ img = Image.new("RGB", (w, h))
119
+ px = img.load()
84
120
 
85
- return {
86
- "fallback": True,
87
- "size": (w, h),
88
- "color": rand_color
89
- }
121
+ pink = (255, 0, 255)
122
+ black = (0, 0, 0)
90
123
 
124
+ for y in range(h):
125
+ for x in range(w):
126
+ if (x // 8 + y // 8) % 2 == 0:
127
+ px[x, y] = pink
128
+ else:
129
+ px[x, y] = black
91
130
 
92
- # --------------------------------------------------------
93
- # PYGAME-STYLE FLIP HELPERS
94
- # --------------------------------------------------------
131
+ return img
132
+
133
+
134
+ # ========================================================
135
+ # PYGAME-STYLE FLIP HELPERS (FOR ALREADY-TK IMAGES)
136
+ # ========================================================
95
137
  def flip_image(img, flip_h=False, flip_v=False):
96
- """
97
- Returns a NEW flipped PhotoImage.
98
- Like pygame.transform.flip().
99
- """
100
138
  if Image is None or ImageTk is None:
101
- print("⚠️ Pillow not installed; cannot flip images.")
139
+ print(" Pillow not installed; cannot flip images.")
102
140
  return img
103
141
 
104
142
  pil_img = ImageTk.getimage(img)
@@ -117,9 +155,8 @@ def flip_horizontal(img):
117
155
 
118
156
  def flip_vertical(img):
119
157
  return flip_image(img, flip_v=True)
120
-
121
158
  # --------------------------------------------------------
122
- # LOOPING ANIMAITONS/IMAGES
159
+ # LOOPING ANIMATIONS / IMAGES
123
160
  # --------------------------------------------------------
124
161
  class LoopAnim:
125
162
  def __init__(self, frames, speed=0.2):
@@ -144,8 +181,6 @@ def loop_image(*imgs, speed=0.2):
144
181
  Usage:
145
182
  anim = loop_image(img1, img2, img3)
146
183
  anim = loop_image(load_image("p1.png"), load_image("p2.png"))
147
-
148
- Returns a LoopAnim object.
149
184
  """
150
185
  frames = [x for x in imgs if x is not None]
151
186
 
@@ -180,12 +215,168 @@ def load_folder_images(folder, nested=True):
180
215
 
181
216
  return result
182
217
 
218
+ #=========================================================
219
+ # LOAD FILES
220
+ #=========================================================
221
+ def load_file(path):
222
+ """
223
+ Smart auto-loader for multiple formats.
224
+ Reads:
225
+ - .txt, .md, .cfg → text
226
+ - .json → parsed json
227
+ - .py → raw text
228
+ - .bin, .dat → raw bytes
229
+ - .pickle, .pkl → python objects
230
+ - .png/.gif → forwarded to load_image()
231
+ - .pdf → reads metadata only (no full extract)
232
+ """
233
+
234
+ if not os.path.exists(path):
235
+ print(f"⚠️ Missing file: {path}")
236
+ return None
237
+
238
+ ext = path.lower().split(".")[-1]
239
+
240
+ # ---------------- TEXT FILES ----------------
241
+ if ext in ("txt", "md", "cfg", "ini", "py"):
242
+ try:
243
+ with open(path, "r", encoding="utf-8") as f:
244
+ return {
245
+ "type": "text",
246
+ "content": f.read()
247
+ }
248
+ except Exception as e:
249
+ print(f"⚠️ Error reading text file: {e}")
250
+ return None
251
+
252
+ # ---------------- JSON ----------------
253
+ if ext == "json":
254
+ try:
255
+ with open(path, "r", encoding="utf-8") as f:
256
+ return {
257
+ "type": "json",
258
+ "content": json.load(f)
259
+ }
260
+ except Exception as e:
261
+ print(f"⚠️ JSON error: {e}")
262
+ return None
263
+
264
+ # ---------------- PICKLE ----------------
265
+ if ext in ("pickle", "pkl"):
266
+ try:
267
+ with open(path, "rb") as f:
268
+ return {
269
+ "type": "pickle",
270
+ "content": pickle.load(f)
271
+ }
272
+ except Exception as e:
273
+ print(f"⚠️ Pickle error: {e}")
274
+ return None
275
+
276
+ # ---------------- BINARY ----------------
277
+ if ext in ("bin", "dat"):
278
+ try:
279
+ with open(path, "rb") as f:
280
+ return {
281
+ "type": "binary",
282
+ "content": f.read()
283
+ }
284
+ except Exception as e:
285
+ print(f"⚠️ Binary read error: {e}")
286
+ return None
287
+
288
+ # ---------------- PDF ----------------
289
+ if ext == "pdf":
290
+ try:
291
+ with open(path, "rb") as f:
292
+ head = f.read(1024) # first 1KB for metadata
293
+ return {
294
+ "type": "pdf",
295
+ "meta_preview": head,
296
+ "note": "only metadata preview, not full pdf extract"
297
+ }
298
+ except Exception as e:
299
+ print(f"⚠️ PDF read error: {e}")
300
+ return None
301
+
302
+ # ---------------- IMAGE ----------------
303
+ if ext in ("png", "gif", "jpg", "jpeg"):
304
+ try:
305
+ return {
306
+ "type": "image",
307
+ "content": load_image(path)
308
+ }
309
+ except Exception as e:
310
+ print(f"⚠️ Image load error: {e}")
311
+ return None
312
+
313
+ # ---------------- UNKNOWN TYPE ----------------
314
+ try:
315
+ with open(path, "rb") as f:
316
+ raw = f.read()
317
+ return {
318
+ "type": "unknown",
319
+ "content": raw
320
+ }
321
+ except:
322
+ print("⚠️ Unknown file load failed")
323
+ return None
183
324
 
184
325
  # --------------------------------------------------------
185
- # MUSIC PLACEHOLDER
326
+ # MUSIC PLACEHOLDER (REPLACED BY NEW SOUND ENGINE)
186
327
  # --------------------------------------------------------
328
+ from matplotlib.pylab import size
329
+ import sounddevice as sd
330
+ import soundfile as sf
331
+ import threading
332
+
333
+
334
+ class Sound:
335
+ def __init__(self, path, vol=1.0):
336
+ self.path = path
337
+ self.data, self.sr = sf.read(path, dtype="float32")
338
+ self.volume = vol
339
+ self._looping = False
340
+ self._thread = None
341
+
342
+ def _play_audio(self, loop=False):
343
+ self._looping = loop
344
+ while True:
345
+ sd.play(self.data * self.volume, self.sr)
346
+ sd.wait()
347
+ if not self._looping:
348
+ break
349
+
350
+ def play(self):
351
+ threading.Thread(target=self._play_audio, daemon=True).start()
352
+
353
+ def play_once(self):
354
+ self.stop()
355
+ threading.Thread(target=self._play_audio, daemon=True).start()
356
+
357
+ def loop(self):
358
+ self.stop()
359
+ self._thread = threading.Thread(
360
+ target=self._play_audio, args=(True,), daemon=True
361
+ )
362
+ self._thread.start()
363
+
364
+ def stop(self):
365
+ self._looping = False
366
+ sd.stop()
367
+
368
+ def set_volume(self, v):
369
+ self.volume = max(0, min(1, float(v)))
370
+
371
+
372
+ def load_sfx(path, vol=1.0):
373
+ return Sound(path, vol)
374
+
375
+
376
+ # backwards compat but not real music engine
187
377
  def load_music(path):
188
- print(f"[assets] Music load not supported: {path}")
378
+ print(f"[assets] use load_sfx() instead: {path}")
379
+
189
380
 
190
381
  def play_music(loop=-1):
191
- print("[assets] Music playback not supported.")
382
+ print("[assets] play_music() disabled, use SoundObject.loop()")
crystalwindow/clock.py CHANGED
@@ -2,74 +2,37 @@ import time
2
2
  from datetime import datetime
3
3
  from collections import deque
4
4
 
5
-
6
5
  # ============================================================
7
- # Stopwatch Helper
6
+ # Timer/Wait for a specific duration
8
7
  # ============================================================
9
- class Stopwatch:
8
+ class CountdownTimer:
10
9
  def __init__(self):
11
- self.start_time = None
12
- self.elapsed_time = 0.0
10
+ self.start_time = 0.0
11
+ self.duration = 0.0
13
12
  self.running = False
14
13
 
15
- def start(self):
16
- if not self.running:
17
- self.start_time = time.perf_counter()
18
- self.running = True
19
-
20
- def stop(self):
21
- if self.running:
22
- self.elapsed_time += time.perf_counter() - self.start_time
23
- self.running = False
14
+ def start(self, duration: float):
15
+ self.duration = duration
16
+ self.start_time = time.perf_counter()
17
+ self.running = True
24
18
 
25
19
  def reset(self):
26
- self.start_time = None
27
- self.elapsed_time = 0.0
20
+ self.start_time = 0.0
21
+ self.duration = 0.0
28
22
  self.running = False
29
23
 
30
- def elapsed(self):
31
- if self.running:
32
- return self.elapsed_time + (time.perf_counter() - self.start_time)
33
- return self.elapsed_time
34
-
35
-
36
- # ============================================================
37
- # Countdown Timer Helper
38
- # ============================================================
39
- class CountdownTimer:
40
- def __init__(self):
41
- self.target_time = None
42
-
43
- def start(self, seconds: float):
44
- self.target_time = time.perf_counter() + seconds
45
-
46
- def remaining(self):
47
- if not self.target_time:
48
- return 0
49
- return max(0, self.target_time - time.perf_counter())
50
-
51
- def done(self):
52
- return self.remaining() == 0
53
-
54
-
55
- # ============================================================
56
- # Event Scheduler Helper
57
- # ============================================================
58
- class Scheduler:
59
- def __init__(self):
60
- self.events = [] # list of (interval, last_run, function)
61
-
62
- def schedule(self, interval: float, func):
63
- """Run function every `interval` seconds."""
64
- self.events.append([interval, time.perf_counter(), func])
24
+ def is_finished(self):
25
+ if not self.running:
26
+ return True
27
+ elapsed = time.perf_counter() - self.start_time
28
+ return elapsed >= self.duration
65
29
 
66
- def run_pending(self):
67
- now = time.perf_counter()
68
- for event in self.events:
69
- interval, last_run, func = event
70
- if now - last_run >= interval:
71
- func()
72
- event[1] = now # update last_run
30
+ def get_remaining(self):
31
+ if not self.running:
32
+ return 0.0
33
+ elapsed = time.perf_counter() - self.start_time
34
+ remaining = self.duration - elapsed
35
+ return max(0.0, remaining)
73
36
 
74
37
 
75
38
  # ============================================================
@@ -87,11 +50,6 @@ class Clock:
87
50
 
88
51
  self.paused = False
89
52
 
90
- # Utilities
91
- self.stopwatch = Stopwatch()
92
- self.timer = CountdownTimer()
93
- self.scheduler = Scheduler()
94
-
95
53
  # ----------------------------------------------------------
96
54
  # TICK + FPS
97
55
  # ----------------------------------------------------------
@@ -28,20 +28,14 @@ class Color:
28
28
  if not name.startswith("__")}
29
29
 
30
30
  def __init__(self, r=None, g=None, b=None, a=255, hex_value=None):
31
- # -------------------------------------------------
32
- # 1) raw tuple variable: Color(Blue)
33
- # -------------------------------------------------
34
31
  if isinstance(r, (tuple, list)):
35
- if len(r) == 3: # rgb
32
+ if len(r) == 3:
36
33
  self.r, self.g, self.b = r
37
34
  self.a = a
38
- elif len(r) == 4: # rgba
35
+ elif len(r) == 4:
39
36
  self.r, self.g, self.b, self.a = r
40
37
  return
41
38
 
42
- # -------------------------------------------------
43
- # 2) name string: Color("blue")
44
- # -------------------------------------------------
45
39
  if isinstance(r, str):
46
40
  name = r.lower()
47
41
  if name not in self.COLOR_NAMES:
@@ -54,22 +48,15 @@ class Color:
54
48
  self.a = a
55
49
  return
56
50
 
57
- # -------------------------------------------------
58
- # 3) HEX input
59
- # -------------------------------------------------
60
51
  if hex_value is not None:
61
52
  self.r, self.g, self.b, self.a = self.hex_to_rgba(hex_value)
62
53
  return
63
54
 
64
- # -------------------------------------------------
65
- # 4) manual RGB
66
- # -------------------------------------------------
67
55
  self.r = r or 0
68
56
  self.g = g or 0
69
57
  self.b = b or 0
70
58
  self.a = a
71
59
 
72
- # convert hex -> rgba
73
60
  @staticmethod
74
61
  def hex_to_rgba(hex_value: str):
75
62
  hex_value = hex_value.replace("#", "")
@@ -84,23 +71,19 @@ class Color:
84
71
  b = int(hex_value[4:6], 16)
85
72
  a = int(hex_value[6:8], 16)
86
73
  else:
87
- raise ValueError("bad hex bruh")
74
+ raise ValueError("bad hex")
88
75
  return r, g, b, a
89
76
 
90
- # return pygame-friendly color tuple
91
77
  def to_tuple(self):
92
78
  return (self.r, self.g, self.b, self.a)
93
79
 
94
- # rgba -> hex
95
80
  def to_hex(self):
96
81
  return "#{:02X}{:02X}{:02X}{:02X}".format(self.r, self.g, self.b, self.a)
97
82
 
98
- # quick alpha edit
99
83
  def set_alpha(self, a):
100
84
  self.a = max(0, min(255, a))
101
85
  return self
102
86
 
103
- # color math
104
87
  def brighten(self, amt=30):
105
88
  self.r = min(255, self.r + amt)
106
89
  self.g = min(255, self.g + amt)
@@ -115,7 +98,6 @@ class Color:
115
98
 
116
99
  @staticmethod
117
100
  def lerp(c1, c2, t: float):
118
- """blend between two colors"""
119
101
  r = c1.r + (c2.r - c1.r) * t
120
102
  g = c1.g + (c2.g - c1.g) * t
121
103
  b = c1.b + (c2.b - c1.b) * t