From 145971f6ab13cf8ed7ab8c1a24d977880c55ae41 Mon Sep 17 00:00:00 2001 From: max Date: Tue, 8 Sep 2026 23:20:57 +0200 Subject: [PATCH] fix(assets): correct walk frame scaling -- remove 1.25 overcorrection and wrong GAIT_SHIFT The walk polygon was rendered 25% too large due to an erroneous * 1.25 factor on scale_x / scale_y. On top of this, GAIT_SHIFT_X=-6.383 and GAIT_SHIFT_Y=3.76 (internal cat.js group transforms) were incorrectly ported as canvas offsets, pushing the polygon partially above the top edge of the overlay window (visible clipping). Root cause: the SVG viewBox is 0 0 150 120. Walk polygon coordinates span 4-145 in X and 8-112 in Y -- already fitting the viewBox with no additional scaling or translation needed. Fix: use scale_x = (cat_width / 150) * SS and scale_y = (cat_height / 120) * SS with zero offset. Walk frame now renders at the same visual size as paw/ scruff/tail/stretch frames (all on a cat_width x cat_height canvas). Result: cat body is no longer clipped at the top and matches size of all other animation states. --- catser/assets_manager.py | 51 ++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/catser/assets_manager.py b/catser/assets_manager.py index 46fbc70..9501a2c 100644 --- a/catser/assets_manager.py +++ b/catser/assets_manager.py @@ -204,40 +204,37 @@ class AssetsManager: target_w = self.cat_width target_h = self.cat_height - # 4x Supersampling for clean anti-aliased polygon rasterization + # 4× supersampling for clean anti-aliased polygon edges SS = 4 canvas_w = target_w * SS canvas_h = target_h * SS img = Image.new("RGBA", (canvas_w, canvas_h), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) - # Scale from SVG viewBox (150x120) to supersampled canvas - # Note: In cat.js viewBox is 0 0 150 120 and CAT_BOX is 121.2x92.4 with shift - # Shift in cat.js: translate(GAIT_SHIFT_X * unit, GAIT_SHIFT_Y * unit) - unit = (target_w / CAT_BOX_WIDTH) * SS - shift_x = -6.383 * unit - shift_y = 3.76 * unit - scale_x = (target_w / 150.0) * SS * 1.25 # Aspect ratio fitting - scale_y = (target_h / 120.0) * SS * 1.25 + # The Workcat SVG walk frames use viewBox="0 0 150 120". + # Polygon coordinates span ~4–145 in X and ~8–112 in Y, fitting the viewBox. + # We simply map viewBox coords → supersampled canvas pixels. + # No GAIT_SHIFT or overcorrection needed: those are internal cat.js transforms + # that cancel out at the viewBox level. + scale_x = (target_w / 150.0) * SS + scale_y = (target_h / 120.0) * SS if self.walk_path_strings: pts = _parse_svg_polygon_points(self.walk_path_strings[frame_index]) - ss_points = [(p[0] * scale_x + shift_x, p[1] * scale_y + shift_y) for p in pts] + ss_points = [(p[0] * scale_x, p[1] * scale_y) for p in pts] fur_rgba = (*coat.fur_color, 255) draw.polygon(ss_points, fill=fur_rgba) - # Draw Face Features - # Face coordinates in 150x120 viewBox: - # Eye L: cx=120.4, cy=46.2, r=2.2 - # Eye R: cx=140.3, cy=46.2, r=2.2 - # Mouth: Q curves around x=127-135, y=50-52 + # Face features — all coordinates in the same 150×120 viewBox space. + # Eyes: left at (120.4, 46.2), right at (140.3, 46.2), radius 2.2 + # Mouth: W-shape centred around (131.6, 51.0) ink_rgba = (*coat.ink_color, 255) - eye_l_cx = 120.4 * scale_x + shift_x - eye_l_cy = 46.2 * scale_y + shift_y - eye_r_cx = 140.3 * scale_x + shift_x - eye_r_cy = 46.2 * scale_y + shift_y - eye_r = 2.2 * scale_x + eye_l_cx = 120.4 * scale_x + eye_l_cy = 46.2 * scale_y + eye_r_cx = 140.3 * scale_x + eye_r_cy = 46.2 * scale_y + eye_r = 2.2 * scale_x if face_type == "open": draw.ellipse( @@ -249,26 +246,23 @@ class AssetsManager: fill=ink_rgba, ) elif face_type == "blink": - # Downward blink lines w_eye = eye_r * 1.5 draw.line([(eye_l_cx - w_eye, eye_l_cy), (eye_l_cx + w_eye, eye_l_cy)], fill=ink_rgba, width=int(2 * SS)) draw.line([(eye_r_cx - w_eye, eye_r_cy), (eye_r_cx + w_eye, eye_r_cy)], fill=ink_rgba, width=int(2 * SS)) elif face_type == "happy": - # Upward arc happy eyes (^^) w_eye = eye_r * 1.6 h_eye = eye_r * 1.3 draw.arc([eye_l_cx - w_eye, eye_l_cy - h_eye, eye_l_cx + w_eye, eye_l_cy + h_eye], start=180, end=360, fill=ink_rgba, width=int(2 * SS)) draw.arc([eye_r_cx - w_eye, eye_r_cy - h_eye, eye_r_cx + w_eye, eye_r_cy + h_eye], start=180, end=360, fill=ink_rgba, width=int(2 * SS)) - # Mouth (w shape) - mouth_cx = 131.6 * scale_x + shift_x - mouth_cy = 51.0 * scale_y + shift_y + mouth_cx = 131.6 * scale_x + mouth_cy = 51.0 * scale_y mw = 4.0 * scale_x mh = 2.5 * scale_y - draw.arc([mouth_cx - mw, mouth_cy - mh, mouth_cx, mouth_cy + mh], start=0, end=180, fill=ink_rgba, width=int(1.8 * SS)) - draw.arc([mouth_cx, mouth_cy - mh, mouth_cx + mw, mouth_cy + mh], start=0, end=180, fill=ink_rgba, width=int(1.8 * SS)) + draw.arc([mouth_cx - mw, mouth_cy - mh, mouth_cx, mouth_cy + mh], start=0, end=180, fill=ink_rgba, width=int(1.8 * SS)) + draw.arc([mouth_cx, mouth_cy - mh, mouth_cx + mw, mouth_cy + mh], start=0, end=180, fill=ink_rgba, width=int(1.8 * SS)) - # Downsample back to target dimensions with high-quality Lanczos resampling + # Downsample to target resolution with high-quality Lanczos resampling final_img = img.resize((target_w, target_h), Image.Resampling.LANCZOS) if facing_left: @@ -277,6 +271,7 @@ class AssetsManager: self._cache[cache_key] = final_img return final_img + def get_paw_frame(self, frame_index: int, coat: Coat, facing_left: bool = False) -> Image.Image: """ Retrieves and tints a frame from the paw strike animation (f001-f017).