Add pressure visual modes and breakout game
This commit is contained in:
643
src/breakout.rs
Normal file
643
src/breakout.rs
Normal file
@@ -0,0 +1,643 @@
|
||||
use eframe::egui;
|
||||
|
||||
use crate::style::{self, ACCENT_BLUE, ACCENT_GREEN, ACCENT_ORANGE, ACCENT_RED, ONE_DARK_PRO};
|
||||
|
||||
const BRICK_ROWS: usize = 7;
|
||||
const BRICK_COLS: usize = 12;
|
||||
const PADDLE_W: f32 = 0.24;
|
||||
const PADDLE_H: f32 = 0.030;
|
||||
const PADDLE_Y: f32 = 0.88;
|
||||
const PADDLE_SPEED: f32 = 0.92;
|
||||
const BALL_RADIUS: f32 = 0.010;
|
||||
const BALL_SPEED: f32 = 0.52;
|
||||
const MAX_BALL_SPEED: f32 = 0.86;
|
||||
const PRESSURE_RANGE_MIN: f32 = 0.0;
|
||||
const PRESSURE_RANGE_MAX: f32 = 7000.0;
|
||||
const PAUSE_COOLDOWN: f64 = 0.64;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum BreakoutPhase {
|
||||
Idle,
|
||||
Running,
|
||||
Paused,
|
||||
Over,
|
||||
}
|
||||
|
||||
struct Brick {
|
||||
rect: egui::Rect,
|
||||
alive: bool,
|
||||
flash: f32,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
pub struct BreakoutControl {
|
||||
pub axis: f32,
|
||||
pub top_force: f32,
|
||||
pub left_force: f32,
|
||||
pub right_force: f32,
|
||||
}
|
||||
|
||||
pub struct BreakoutGame {
|
||||
phase: BreakoutPhase,
|
||||
bricks: Vec<Brick>,
|
||||
paddle_x: f32,
|
||||
ball_pos: egui::Pos2,
|
||||
ball_vel: egui::Vec2,
|
||||
score: u32,
|
||||
combo: u32,
|
||||
lives: u32,
|
||||
level: u32,
|
||||
last_time: Option<f64>,
|
||||
previous_pause_gesture: bool,
|
||||
pause_locked_until: f64,
|
||||
}
|
||||
|
||||
impl Default for BreakoutGame {
|
||||
fn default() -> Self {
|
||||
let mut game = Self {
|
||||
phase: BreakoutPhase::Idle,
|
||||
bricks: Vec::new(),
|
||||
paddle_x: 0.5,
|
||||
ball_pos: egui::pos2(0.5, PADDLE_Y - PADDLE_H - BALL_RADIUS),
|
||||
ball_vel: egui::vec2(0.0, 0.0),
|
||||
score: 0,
|
||||
combo: 0,
|
||||
lives: 3,
|
||||
level: 1,
|
||||
last_time: None,
|
||||
previous_pause_gesture: false,
|
||||
pause_locked_until: 0.0,
|
||||
};
|
||||
game.rebuild_bricks();
|
||||
game
|
||||
}
|
||||
}
|
||||
|
||||
impl BreakoutGame {
|
||||
pub fn draw_viewport(
|
||||
&mut self,
|
||||
ctx: &egui::Context,
|
||||
rect: egui::Rect,
|
||||
visible: &mut bool,
|
||||
control: BreakoutControl,
|
||||
) {
|
||||
if !*visible {
|
||||
self.last_time = None;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.request_repaint();
|
||||
let now = ctx.input(|input| input.time);
|
||||
let dt = self
|
||||
.last_time
|
||||
.map(|last| (now - last) as f32)
|
||||
.unwrap_or(1.0 / 60.0)
|
||||
.clamp(0.0, 1.0 / 30.0);
|
||||
self.last_time = Some(now);
|
||||
|
||||
let mut close_requested = false;
|
||||
egui::Area::new(egui::Id::new("breakout_game_viewport"))
|
||||
.fixed_pos(rect.min)
|
||||
.order(egui::Order::Middle)
|
||||
.show(ctx, |ui| {
|
||||
ui.set_min_size(rect.size());
|
||||
ui.set_max_size(rect.size());
|
||||
style::group_frame().show(ui, |ui| {
|
||||
ui.set_min_size(rect.size() - egui::vec2(20.0, 16.0));
|
||||
if self.draw_contents(ui, control, dt, now, true) {
|
||||
close_requested = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if close_requested {
|
||||
*visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_contents(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
control: BreakoutControl,
|
||||
dt: f32,
|
||||
now: f64,
|
||||
embedded: bool,
|
||||
) -> bool {
|
||||
let mut close_requested = false;
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(
|
||||
ACCENT_BLUE,
|
||||
egui::RichText::new("NEON BREAKOUT").monospace().size(13.0),
|
||||
);
|
||||
ui.label(style::subtle_text("压力矩阵控制 / 左右分区驱动"));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if embedded && ui.add(style::tag_button("退出分屏")).clicked() {
|
||||
close_requested = true;
|
||||
}
|
||||
if ui.add(style::tag_button("重开")).clicked() {
|
||||
self.reset();
|
||||
self.start();
|
||||
}
|
||||
if ui.add(style::tag_button("暂停")).clicked() {
|
||||
self.toggle_pause();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(5.0);
|
||||
ui.horizontal(|ui| {
|
||||
status_chip(ui, "状态", self.status_text(), status_color(self.phase));
|
||||
status_chip(ui, "分数", self.score.to_string(), ACCENT_GREEN);
|
||||
status_chip(ui, "连击", self.combo.to_string(), ACCENT_ORANGE);
|
||||
status_chip(ui, "生命", self.lives.to_string(), ACCENT_RED);
|
||||
status_chip(ui, "关卡", self.level.to_string(), ACCENT_BLUE);
|
||||
});
|
||||
|
||||
ui.add_space(7.0);
|
||||
let available = ui.available_size();
|
||||
let game_size = egui::vec2(available.x.max(500.0), (available.y - 6.0).max(330.0));
|
||||
let (rect, response) = ui.allocate_exact_size(game_size, egui::Sense::click());
|
||||
if response.clicked() && self.phase != BreakoutPhase::Running {
|
||||
self.start();
|
||||
}
|
||||
|
||||
let keyboard_axis = ui.input(|input| {
|
||||
(input.key_down(egui::Key::ArrowRight) as i32
|
||||
- input.key_down(egui::Key::ArrowLeft) as i32) as f32
|
||||
});
|
||||
if ui.input(|input| input.key_pressed(egui::Key::Space)) {
|
||||
self.start();
|
||||
}
|
||||
if ui.input(|input| input.key_pressed(egui::Key::P)) {
|
||||
self.toggle_pause();
|
||||
}
|
||||
|
||||
let axis = if keyboard_axis.abs() > 0.0 {
|
||||
keyboard_axis
|
||||
} else {
|
||||
control.axis
|
||||
};
|
||||
self.handle_pressure_gesture(control.top_force, now);
|
||||
self.update(dt, axis);
|
||||
self.paint(ui, rect, control);
|
||||
close_requested
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.phase = BreakoutPhase::Idle;
|
||||
self.paddle_x = 0.5;
|
||||
self.ball_pos = egui::pos2(0.5, PADDLE_Y - PADDLE_H - BALL_RADIUS);
|
||||
self.ball_vel = egui::Vec2::ZERO;
|
||||
self.score = 0;
|
||||
self.combo = 0;
|
||||
self.lives = 3;
|
||||
self.level = 1;
|
||||
self.rebuild_bricks();
|
||||
}
|
||||
|
||||
fn start(&mut self) {
|
||||
if self.phase == BreakoutPhase::Over {
|
||||
self.reset();
|
||||
}
|
||||
if self.phase != BreakoutPhase::Running {
|
||||
self.phase = BreakoutPhase::Running;
|
||||
if self.ball_vel == egui::Vec2::ZERO {
|
||||
self.launch_ball();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_pause(&mut self) {
|
||||
self.phase = match self.phase {
|
||||
BreakoutPhase::Running => BreakoutPhase::Paused,
|
||||
BreakoutPhase::Paused => BreakoutPhase::Running,
|
||||
other => other,
|
||||
};
|
||||
}
|
||||
|
||||
fn handle_pressure_gesture(&mut self, top_force: f32, now: f64) {
|
||||
let threshold = pressure_pause_threshold();
|
||||
let active = top_force >= threshold;
|
||||
if active && !self.previous_pause_gesture && now >= self.pause_locked_until {
|
||||
if self.phase == BreakoutPhase::Idle || self.phase == BreakoutPhase::Over {
|
||||
self.start();
|
||||
} else {
|
||||
self.toggle_pause();
|
||||
}
|
||||
self.pause_locked_until = now + PAUSE_COOLDOWN;
|
||||
}
|
||||
self.previous_pause_gesture = active;
|
||||
}
|
||||
|
||||
fn update(&mut self, dt: f32, axis: f32) {
|
||||
for brick in &mut self.bricks {
|
||||
brick.flash = (brick.flash - dt * 2.4).max(0.0);
|
||||
}
|
||||
|
||||
if self.phase != BreakoutPhase::Running {
|
||||
self.ball_pos = egui::pos2(self.paddle_x, PADDLE_Y - PADDLE_H - BALL_RADIUS);
|
||||
return;
|
||||
}
|
||||
|
||||
self.paddle_x = (self.paddle_x + axis.clamp(-1.0, 1.0) * PADDLE_SPEED * dt)
|
||||
.clamp(PADDLE_W * 0.5, 1.0 - PADDLE_W * 0.5);
|
||||
|
||||
self.ball_pos += self.ball_vel * dt;
|
||||
self.resolve_wall_collision();
|
||||
self.resolve_paddle_collision();
|
||||
self.resolve_brick_collision();
|
||||
}
|
||||
|
||||
fn launch_ball(&mut self) {
|
||||
let direction = if self.level % 2 == 0 { -0.24 } else { 0.24 };
|
||||
self.ball_pos = egui::pos2(self.paddle_x, PADDLE_Y - PADDLE_H - BALL_RADIUS);
|
||||
self.ball_vel = egui::vec2(direction, -1.0).normalized() * BALL_SPEED;
|
||||
}
|
||||
|
||||
fn resolve_wall_collision(&mut self) {
|
||||
if self.ball_pos.x - BALL_RADIUS <= 0.0 {
|
||||
self.ball_pos.x = BALL_RADIUS;
|
||||
self.ball_vel.x = self.ball_vel.x.abs();
|
||||
} else if self.ball_pos.x + BALL_RADIUS >= 1.0 {
|
||||
self.ball_pos.x = 1.0 - BALL_RADIUS;
|
||||
self.ball_vel.x = -self.ball_vel.x.abs();
|
||||
}
|
||||
|
||||
if self.ball_pos.y - BALL_RADIUS <= 0.0 {
|
||||
self.ball_pos.y = BALL_RADIUS;
|
||||
self.ball_vel.y = self.ball_vel.y.abs();
|
||||
}
|
||||
|
||||
if self.ball_pos.y - BALL_RADIUS > 1.0 {
|
||||
self.lives = self.lives.saturating_sub(1);
|
||||
self.combo = 0;
|
||||
if self.lives == 0 {
|
||||
self.phase = BreakoutPhase::Over;
|
||||
self.ball_vel = egui::Vec2::ZERO;
|
||||
} else {
|
||||
self.ball_pos = egui::pos2(self.paddle_x, PADDLE_Y - PADDLE_H - BALL_RADIUS);
|
||||
self.launch_ball();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_paddle_collision(&mut self) {
|
||||
if self.ball_vel.y <= 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let paddle = paddle_rect(self.paddle_x);
|
||||
if !circle_hits_rect(self.ball_pos, BALL_RADIUS, paddle) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.ball_pos.y = paddle.top() - BALL_RADIUS;
|
||||
let offset = ((self.ball_pos.x - self.paddle_x) / (PADDLE_W * 0.5)).clamp(-1.0, 1.0);
|
||||
let speed = self.ball_vel.length().clamp(BALL_SPEED, MAX_BALL_SPEED);
|
||||
self.ball_vel = egui::vec2(offset * 0.82, -1.0).normalized() * speed;
|
||||
self.combo = 0;
|
||||
}
|
||||
|
||||
fn resolve_brick_collision(&mut self) {
|
||||
for brick in &mut self.bricks {
|
||||
if !brick.alive || !circle_hits_rect(self.ball_pos, BALL_RADIUS, brick.rect) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let overlap_x = (self.ball_pos.x + BALL_RADIUS - brick.rect.left())
|
||||
.min(brick.rect.right() - (self.ball_pos.x - BALL_RADIUS));
|
||||
let overlap_y = (self.ball_pos.y + BALL_RADIUS - brick.rect.top())
|
||||
.min(brick.rect.bottom() - (self.ball_pos.y - BALL_RADIUS));
|
||||
if overlap_x < overlap_y {
|
||||
self.ball_vel.x *= -1.0;
|
||||
} else {
|
||||
self.ball_vel.y *= -1.0;
|
||||
}
|
||||
|
||||
brick.alive = false;
|
||||
brick.flash = 1.0;
|
||||
self.combo += 1;
|
||||
self.score += 40 + self.combo * 5;
|
||||
let speed = (self.ball_vel.length() * 1.014).min(MAX_BALL_SPEED);
|
||||
self.ball_vel = self.ball_vel.normalized() * speed;
|
||||
break;
|
||||
}
|
||||
|
||||
if self.bricks.iter().all(|brick| !brick.alive) {
|
||||
self.level += 1;
|
||||
self.rebuild_bricks();
|
||||
self.launch_ball();
|
||||
}
|
||||
}
|
||||
|
||||
fn rebuild_bricks(&mut self) {
|
||||
self.bricks.clear();
|
||||
let gap_x = 0.012;
|
||||
let gap_y = 0.017;
|
||||
let left = 0.075;
|
||||
let top = 0.10;
|
||||
let total_w = 0.85;
|
||||
let brick_w = (total_w - gap_x * (BRICK_COLS - 1) as f32) / BRICK_COLS as f32;
|
||||
let brick_h = 0.040;
|
||||
|
||||
for row in 0..BRICK_ROWS {
|
||||
for col in 0..BRICK_COLS {
|
||||
let x = left + col as f32 * (brick_w + gap_x);
|
||||
let y = top + row as f32 * (brick_h + gap_y);
|
||||
self.bricks.push(Brick {
|
||||
rect: egui::Rect::from_min_size(egui::pos2(x, y), egui::vec2(brick_w, brick_h)),
|
||||
alive: true,
|
||||
flash: 0.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn status_text(&self) -> &'static str {
|
||||
match self.phase {
|
||||
BreakoutPhase::Idle => "待机",
|
||||
BreakoutPhase::Running => "运行",
|
||||
BreakoutPhase::Paused => "暂停",
|
||||
BreakoutPhase::Over => "结束",
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&self, ui: &egui::Ui, rect: egui::Rect, control: BreakoutControl) {
|
||||
let painter = ui.painter_at(rect);
|
||||
painter.rect_filled(
|
||||
rect,
|
||||
egui::CornerRadius::same(6),
|
||||
egui::Color32::from_rgb(8, 14, 19),
|
||||
);
|
||||
painter.rect_stroke(
|
||||
rect,
|
||||
egui::CornerRadius::same(6),
|
||||
egui::Stroke::new(1.0, color_alpha(ONE_DARK_PRO.accent, 110)),
|
||||
egui::StrokeKind::Outside,
|
||||
);
|
||||
|
||||
paint_arena_grid(&painter, rect);
|
||||
for (index, brick) in self.bricks.iter().enumerate() {
|
||||
paint_brick(&painter, rect, brick, index);
|
||||
}
|
||||
|
||||
paint_paddle(&painter, rect, self.paddle_x);
|
||||
paint_ball(&painter, rect, self.ball_pos);
|
||||
paint_control_meter(&painter, rect, control);
|
||||
|
||||
if self.phase == BreakoutPhase::Idle
|
||||
|| self.phase == BreakoutPhase::Paused
|
||||
|| self.phase == BreakoutPhase::Over
|
||||
{
|
||||
paint_center_overlay(&painter, rect, self.phase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn control_from_matrix(raw: &[u32], rows: u32, cols: u32) -> BreakoutControl {
|
||||
if raw.is_empty() {
|
||||
return BreakoutControl::default();
|
||||
}
|
||||
|
||||
let rows = rows.max(1) as usize;
|
||||
let cols = cols.max(1) as usize;
|
||||
let sample_rows = rows.min(2);
|
||||
let sample_cols = cols.min(2);
|
||||
let avg = |row_start: usize, row_end: usize, col_start: usize, col_end: usize| -> f32 {
|
||||
let mut sum = 0.0;
|
||||
let mut count = 0.0;
|
||||
for row in row_start..row_end {
|
||||
for col in col_start..col_end {
|
||||
let index = row * cols + col;
|
||||
if let Some(value) = raw.get(index) {
|
||||
sum += *value as f32;
|
||||
count += 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if count > 0.0 { sum / count } else { 0.0 }
|
||||
};
|
||||
|
||||
let tl = avg(0, sample_rows, 0, sample_cols);
|
||||
let tr = avg(0, sample_rows, cols.saturating_sub(sample_cols), cols);
|
||||
let bl = avg(rows.saturating_sub(sample_rows), rows, 0, sample_cols);
|
||||
let br = avg(
|
||||
rows.saturating_sub(sample_rows),
|
||||
rows,
|
||||
cols.saturating_sub(sample_cols),
|
||||
cols,
|
||||
);
|
||||
|
||||
let left_force = tl + bl;
|
||||
let right_force = tr + br;
|
||||
let top_force = tl + tr;
|
||||
let span = 1200.0_f32.max((PRESSURE_RANGE_MAX - PRESSURE_RANGE_MIN) * 0.22);
|
||||
let raw_axis = ((right_force - left_force) / span).clamp(-1.0, 1.0);
|
||||
let axis = if raw_axis.abs() < 0.045 {
|
||||
0.0
|
||||
} else {
|
||||
raw_axis
|
||||
};
|
||||
|
||||
BreakoutControl {
|
||||
axis,
|
||||
top_force,
|
||||
left_force,
|
||||
right_force,
|
||||
}
|
||||
}
|
||||
|
||||
fn pressure_pause_threshold() -> f32 {
|
||||
420.0_f32.max(((PRESSURE_RANGE_MAX - PRESSURE_RANGE_MIN).max(1000.0) * 0.07).round())
|
||||
}
|
||||
|
||||
fn paddle_rect(paddle_x: f32) -> egui::Rect {
|
||||
egui::Rect::from_center_size(
|
||||
egui::pos2(paddle_x, PADDLE_Y),
|
||||
egui::vec2(PADDLE_W, PADDLE_H),
|
||||
)
|
||||
}
|
||||
|
||||
fn circle_hits_rect(center: egui::Pos2, radius: f32, rect: egui::Rect) -> bool {
|
||||
let closest_x = center.x.clamp(rect.left(), rect.right());
|
||||
let closest_y = center.y.clamp(rect.top(), rect.bottom());
|
||||
let dx = center.x - closest_x;
|
||||
let dy = center.y - closest_y;
|
||||
dx * dx + dy * dy <= radius * radius
|
||||
}
|
||||
|
||||
fn status_chip(ui: &mut egui::Ui, label: &'static str, value: impl ToString, color: egui::Color32) {
|
||||
egui::Frame::new()
|
||||
.fill(color_alpha(ONE_DARK_PRO.panel_deep, 190))
|
||||
.stroke(egui::Stroke::new(1.0, color_alpha(color, 92)))
|
||||
.corner_radius(egui::CornerRadius::same(4))
|
||||
.inner_margin(egui::Margin::symmetric(8, 4))
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(ONE_DARK_PRO.text_subtle, label);
|
||||
ui.colored_label(color, value.to_string());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn status_color(phase: BreakoutPhase) -> egui::Color32 {
|
||||
match phase {
|
||||
BreakoutPhase::Idle => ONE_DARK_PRO.text_dim,
|
||||
BreakoutPhase::Running => ACCENT_GREEN,
|
||||
BreakoutPhase::Paused => ACCENT_ORANGE,
|
||||
BreakoutPhase::Over => ACCENT_RED,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_screen(rect: egui::Rect, point: egui::Pos2) -> egui::Pos2 {
|
||||
egui::pos2(
|
||||
rect.left() + point.x * rect.width(),
|
||||
rect.top() + point.y * rect.height(),
|
||||
)
|
||||
}
|
||||
|
||||
fn to_screen_rect(rect: egui::Rect, local: egui::Rect) -> egui::Rect {
|
||||
egui::Rect::from_min_max(
|
||||
rect.left_top() + local.min.to_vec2() * rect.size(),
|
||||
rect.left_top() + local.max.to_vec2() * rect.size(),
|
||||
)
|
||||
}
|
||||
|
||||
fn paint_arena_grid(painter: &egui::Painter, rect: egui::Rect) {
|
||||
// 游戏背景保持纯净,避免网格和外框干扰砖块本身的色块节奏。
|
||||
painter.rect_filled(
|
||||
rect,
|
||||
egui::CornerRadius::same(6),
|
||||
egui::Color32::from_rgb(2, 8, 10),
|
||||
);
|
||||
|
||||
painter.rect_stroke(
|
||||
rect,
|
||||
egui::CornerRadius::same(6),
|
||||
egui::Stroke::new(1.0, color_alpha(ONE_DARK_PRO.accent, 70)),
|
||||
egui::StrokeKind::Inside,
|
||||
);
|
||||
}
|
||||
|
||||
fn paint_brick(painter: &egui::Painter, arena: egui::Rect, brick: &Brick, index: usize) {
|
||||
let rect = to_screen_rect(arena, brick.rect);
|
||||
if brick.alive {
|
||||
let row = index / BRICK_COLS;
|
||||
let col = index % BRICK_COLS;
|
||||
// 参考示例图:纯色矩形砖块,黑色间隔由砖块之间的空隙自然露出。
|
||||
painter.rect_filled(rect, egui::CornerRadius::ZERO, brick_color(row, col));
|
||||
} else if brick.flash > 0.0 {
|
||||
let alpha = (brick.flash * 150.0) as u8;
|
||||
painter.rect_stroke(
|
||||
rect.expand(4.0 * brick.flash),
|
||||
egui::CornerRadius::same(4),
|
||||
egui::Stroke::new(1.4, color_alpha(ONE_DARK_PRO.accent_hot, alpha)),
|
||||
egui::StrokeKind::Outside,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_paddle(painter: &egui::Painter, arena: egui::Rect, paddle_x: f32) {
|
||||
let rect = to_screen_rect(arena, paddle_rect(paddle_x));
|
||||
// 托盘也改成纯色块,和砖块保持同一种视觉语言。
|
||||
painter.rect_filled(rect, egui::CornerRadius::ZERO, ACCENT_BLUE);
|
||||
}
|
||||
|
||||
fn paint_ball(painter: &egui::Painter, arena: egui::Rect, ball_pos: egui::Pos2) {
|
||||
let center = to_screen(arena, ball_pos);
|
||||
let radius = BALL_RADIUS * arena.width().min(arena.height());
|
||||
// 球保留为一个普通实心圆,避免额外高光和光晕。
|
||||
painter.circle_filled(center, radius, egui::Color32::from_rgb(241, 207, 70));
|
||||
}
|
||||
|
||||
fn paint_control_meter(painter: &egui::Painter, arena: egui::Rect, control: BreakoutControl) {
|
||||
let meter_width = arena.width().mul_add(0.30, 0.0).clamp(180.0, 240.0);
|
||||
let meter = egui::Rect::from_min_size(
|
||||
arena.left_bottom() + egui::vec2(18.0, -34.0),
|
||||
egui::vec2(meter_width, 18.0),
|
||||
);
|
||||
painter.rect_filled(
|
||||
meter,
|
||||
egui::CornerRadius::same(4),
|
||||
color_alpha(ONE_DARK_PRO.panel_deep, 220),
|
||||
);
|
||||
painter.rect_stroke(
|
||||
meter,
|
||||
egui::CornerRadius::same(4),
|
||||
egui::Stroke::new(1.0, color_alpha(ONE_DARK_PRO.border, 120)),
|
||||
egui::StrokeKind::Outside,
|
||||
);
|
||||
let center_x = meter.center().x;
|
||||
painter.line_segment(
|
||||
[
|
||||
egui::pos2(center_x, meter.top()),
|
||||
egui::pos2(center_x, meter.bottom()),
|
||||
],
|
||||
egui::Stroke::new(1.0, ONE_DARK_PRO.border_soft),
|
||||
);
|
||||
let marker_x = center_x + control.axis.clamp(-1.0, 1.0) * meter.width() * 0.45;
|
||||
painter.circle_filled(
|
||||
egui::pos2(marker_x, meter.center().y),
|
||||
5.0,
|
||||
color_alpha(ACCENT_GREEN, 210),
|
||||
);
|
||||
painter.circle_filled(
|
||||
egui::pos2(marker_x, meter.center().y),
|
||||
2.3,
|
||||
color_alpha(egui::Color32::WHITE, 150),
|
||||
);
|
||||
painter.text(
|
||||
meter.right_center() + egui::vec2(10.0, 0.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
format!(
|
||||
"L {:>4.0} R {:>4.0} T {:>4.0}",
|
||||
control.left_force, control.right_force, control.top_force
|
||||
),
|
||||
egui::FontId::monospace(10.0),
|
||||
ONE_DARK_PRO.text_dim,
|
||||
);
|
||||
}
|
||||
|
||||
fn paint_center_overlay(painter: &egui::Painter, rect: egui::Rect, phase: BreakoutPhase) {
|
||||
painter.rect_filled(
|
||||
rect,
|
||||
egui::CornerRadius::same(6),
|
||||
color_alpha(egui::Color32::BLACK, 82),
|
||||
);
|
||||
let (title, detail) = match phase {
|
||||
BreakoutPhase::Idle => ("按压顶部或点击开始", "左右分区压力控制挡板"),
|
||||
BreakoutPhase::Paused => ("已暂停", "再次按压顶部、P 或点击暂停继续"),
|
||||
BreakoutPhase::Over => ("游戏结束", "点击或空格重开"),
|
||||
BreakoutPhase::Running => ("", ""),
|
||||
};
|
||||
painter.text(
|
||||
rect.center() - egui::vec2(0.0, 10.0),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
title,
|
||||
egui::FontId::proportional(24.0),
|
||||
ONE_DARK_PRO.text,
|
||||
);
|
||||
painter.text(
|
||||
rect.center() + egui::vec2(0.0, 20.0),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
detail,
|
||||
egui::FontId::proportional(13.0),
|
||||
ONE_DARK_PRO.text_dim,
|
||||
);
|
||||
}
|
||||
|
||||
fn brick_color(row: usize, col: usize) -> egui::Color32 {
|
||||
const PALETTE: [egui::Color32; 5] = [
|
||||
egui::Color32::from_rgb(46, 178, 104),
|
||||
egui::Color32::from_rgb(73, 188, 220),
|
||||
egui::Color32::from_rgb(234, 199, 73),
|
||||
egui::Color32::from_rgb(232, 125, 64),
|
||||
egui::Color32::from_rgb(237, 68, 62),
|
||||
];
|
||||
|
||||
PALETTE[(col + row * 2) % PALETTE.len()]
|
||||
}
|
||||
|
||||
fn color_alpha(color: egui::Color32, alpha: u8) -> egui::Color32 {
|
||||
egui::Color32::from_rgba_premultiplied(color.r(), color.g(), color.b(), alpha)
|
||||
}
|
||||
Reference in New Issue
Block a user