Spaces:
Running
Running
Antigravity
feat: predictive lookahead camera, duel-axis pack framing, edge radar badges, and high-stakes comment bait UI
4c906aa Download squarecore/render/camera.py from Sarvesh213/auto2.0: direct link, hf CLI and curl.
- Browser
- Download file 7.15 kB
-
https://huggingface.co/spaces/Sarvesh213/auto2.0/resolve/main/squarecore/render/camera.py
- Command line
-
hf download hf://spaces/Sarvesh213/auto2.0/squarecore/render/camera.py
-
curl -L -o camera.py https://huggingface.co/spaces/Sarvesh213/auto2.0/resolve/main/squarecore/render/camera.py
7.15 kB
| """Camera tracking and coordinate transforms for PyCairo with graceful leader switching.""" | |
| from __future__ import annotations | |
| import math | |
| from raceforge.schemas.timeline import FrameRacerTransform | |
| class FollowLeaderCamera: | |
| """Critically-damped broadcast camera with leader hysteresis and battle cluster centroid. | |
| Eliminates camera jitter, snaps, and shaking when two or more squares are in close | |
| proximity or rapidly alternating ranks. | |
| """ | |
| def __init__( | |
| self, | |
| viewport_w: float = 1080.0, | |
| viewport_h: float = 1920.0, | |
| stiffness: float = 0.08, | |
| dead_zone_px: float = 40.0, | |
| ) -> None: | |
| self.vw = viewport_w | |
| self.vh = viewport_h | |
| self.stiffness = stiffness | |
| self.dead_zone = dead_zone_px | |
| self.cam_x = viewport_w / 2.0 | |
| self.cam_y = viewport_h / 2.0 | |
| self.vel_x = 0.0 | |
| self.vel_y = 0.0 | |
| self.zoom = 1.55 | |
| self.vel_zoom = 0.0 | |
| # Leader switching hysteresis (prevents alternating rank chattering) | |
| self.locked_leader_id: str | None = None | |
| self.candidate_leader_id: str | None = None | |
| self.candidate_frames: int = 0 | |
| self.switch_threshold_frames: int = 15 # ~0.25s sustained lead to switch primary | |
| # Critically damped spring parameters | |
| self.omega = 4.0 # Natural frequency (rad/s) | |
| self.dt = 1.0 / 60.0 # 60 FPS standard timestep | |
| self.max_vel = 280.0 # Max pan velocity in px/s (~4.67 px/frame) | |
| def update( | |
| self, | |
| frame_idx: int, | |
| total_frames: int, | |
| racers: list[FrameRacerTransform], | |
| ) -> tuple[float, float, float]: | |
| """Returns (cam_x, cam_y, zoom) for current frame with guaranteed C^2 smoothness.""" | |
| if not racers: | |
| return (self.cam_x, self.cam_y, self.zoom) | |
| # 1. Determine Stable Leader using Hysteresis | |
| raw_leader = min(racers, key=lambda r: r.rank) | |
| if self.locked_leader_id is None: | |
| self.locked_leader_id = raw_leader.id.value | |
| self.candidate_leader_id = raw_leader.id.value | |
| self.candidate_frames = 0 | |
| elif raw_leader.id.value != self.locked_leader_id: | |
| if raw_leader.id.value == self.candidate_leader_id: | |
| self.candidate_frames += 1 | |
| # Switch if new leader has finished or held sustained lead | |
| if raw_leader.has_finished or self.candidate_frames >= self.switch_threshold_frames: | |
| self.locked_leader_id = raw_leader.id.value | |
| self.candidate_frames = 0 | |
| else: | |
| self.candidate_leader_id = raw_leader.id.value | |
| self.candidate_frames = 1 | |
| else: | |
| self.candidate_frames = 0 | |
| self.candidate_leader_id = self.locked_leader_id | |
| # 2. Predictive Look-Ahead & Duel Axis Centroid | |
| # Anticipates velocity heading so camera looks down the corridor before racers hit corners | |
| active_racers = [r for r in racers if not getattr(r, "is_eliminated", False)] or racers | |
| r_sorted = sorted(active_racers, key=lambda r: r.rank) | |
| leader = r_sorted[0] | |
| second = r_sorted[1] if len(r_sorted) > 1 else leader | |
| # Predictive lookahead: anticipate trajectory by ~0.16s | |
| lookahead_t = 0.16 | |
| pred_positions: list[tuple[float, float]] = [] | |
| weights: list[float] = [] | |
| for r in active_racers: | |
| px = r.x + r.vx * lookahead_t | |
| py = r.y + r.vy * lookahead_t | |
| w = 2.4 / (r.rank ** 0.5) | |
| weights.append(w) | |
| pred_positions.append((px, py)) | |
| tot_w = sum(weights) | |
| pack_x = sum(w * p[0] for w, p in zip(weights, pred_positions)) / max(1e-6, tot_w) | |
| pack_y = sum(w * p[1] for w, p in zip(weights, pred_positions)) / max(1e-6, tot_w) | |
| # Duel axis blend (when Rank 1 and 2 are in a close duel, bias framing toward the battle line) | |
| duel_x = (leader.x + second.x) / 2.0 + (leader.vx + second.vx) * 0.5 * lookahead_t | |
| duel_y = (leader.y + second.y) / 2.0 + (leader.vy + second.vy) * 0.5 * lookahead_t | |
| duel_dist = math.hypot(leader.x - second.x, leader.y - second.y) | |
| duel_weight = max(0.0, min(0.60, 1.0 - (duel_dist / 380.0))) | |
| target_x = duel_weight * duel_x + (1.0 - duel_weight) * pack_x | |
| target_y = duel_weight * duel_y + (1.0 - duel_weight) * pack_y | |
| # 3. Smart Adaptive Pack Zoom (Golden Sweet-Spot: 1.38x to 1.72x) | |
| xs = [r.x for r in active_racers] | |
| ys = [r.y for r in active_racers] | |
| spread_x = max(xs) - min(xs) if xs else 100.0 | |
| spread_y = max(ys) - min(ys) if ys else 100.0 | |
| finish_near = frame_idx > (total_frames - 120) | |
| has_winner = any(r.has_finished for r in racers) | |
| if has_winner: | |
| target_zoom = 1.80 | |
| elif finish_near or leader.y > 1520.0: | |
| target_zoom = 1.68 | |
| elif duel_dist < 130.0: | |
| # Intimate neck-and-neck dogfight | |
| target_zoom = 1.68 | |
| else: | |
| # Fit all active contenders comfortably with safe margin padding | |
| fit_zoom_x = (self.vw - 180.0) / max(220.0, spread_x + 120.0) | |
| fit_zoom_y = (self.vh - 380.0) / max(320.0, spread_y + 180.0) | |
| ideal_pack_zoom = min(fit_zoom_x, fit_zoom_y) | |
| target_zoom = max(1.38, min(1.68, ideal_pack_zoom)) | |
| # Dynamic spring response: faster tracking when racers hit sonic speed boosts | |
| max_active_spd = max((math.hypot(r.vx, r.vy) for r in active_racers), default=300.0) | |
| eff_omega = self.omega * (1.30 if max_active_spd > 460.0 else 1.0) | |
| f_zoom = 2.0 * eff_omega * (target_zoom - self.zoom) - 2.0 * eff_omega * self.vel_zoom | |
| self.vel_zoom += f_zoom * self.dt | |
| self.zoom += self.vel_zoom * self.dt | |
| self.zoom = max(1.35, min(1.85, self.zoom)) | |
| # 4. Soft Clamping (Keep camera centered on playable arena with 44% upper-bias framing) | |
| half_vw = (self.vw / self.zoom) / 2.0 | |
| half_vh = (self.vh / self.zoom) / 2.0 | |
| # Bias framing slightly down so racers appear at ~44% screen height, showing path ahead | |
| biased_target_y = target_y + (self.vh * 0.05) / self.zoom | |
| clamped_target_x = max(half_vw, min(self.vw - half_vw, target_x)) | |
| clamped_target_y = max(half_vh, min(self.vh - half_vh, biased_target_y)) | |
| # 5. Second-Order Critically-Damped Spring Integration with Adaptive Omega | |
| ax = (eff_omega ** 2) * (clamped_target_x - self.cam_x) - 2.0 * eff_omega * self.vel_x | |
| ay = (eff_omega ** 2) * (clamped_target_y - self.cam_y) - 2.0 * eff_omega * self.vel_y | |
| self.vel_x += ax * self.dt | |
| self.vel_y += ay * self.dt | |
| # Velocity limiting prevents abrupt camera sweeps | |
| curr_spd = math.hypot(self.vel_x, self.vel_y) | |
| if curr_spd > self.max_vel: | |
| scale = self.max_vel / curr_spd | |
| self.vel_x *= scale | |
| self.vel_y *= scale | |
| self.cam_x += self.vel_x * self.dt | |
| self.cam_y += self.vel_y * self.dt | |
| return (self.cam_x, self.cam_y, self.zoom) | |