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.
This commit is contained in:
+23
-28
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user