644 lines
21 KiB
Rust
644 lines
21 KiB
Rust
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)
|
|
}
|