Перейти к содержимому

Уроки · 58

Стрелки

draw_line рисует просто отрезок, а на экране от него часто хочется не просто линии, а именно стрелки — с наконечником на конце, чтобы сразу было видно, куда она указывает. Готовой функции для этого в macroquad нет, но все кусочки для неё мы уже проходили раньше. Свою функцию draw_arrow сделаем так, чтобы наконечник можно было настраивать — не просто рисовать стрелку, а рисовать её с нужными нам размерами, — поэтому помимо точек «откуда» и «куда» она примет ещё два числа, head_length и head_width, про которые дальше и пойдёт речь.

Возьмём точку, откуда стрелка летит, и точку, куда она летит. Разница между ними — это направление, в точности как в уроке «Направление»: вычитаем координаты, делим на длину. Если длина — ноль, направлять некуда, и тут снова пригождается урок «Деление на ноль» — в этом случае просто ничего не рисуем. Проверяем это через == — знак сравнения на равенство, а не присваивание: = в уроке «Переменные» означало «запомни это значение», а == просто спрашивает, равны ли два числа, и отвечает true или false, как </> в уроке «Радиус обнаружения».

let dx = to.x - from.x;
let dy = to.y - from.y;
let length = (dx * dx + dy * dy).sqrt();
if length == 0.0 {
return;
}
let direction_x = dx / length;
let direction_y = dy / length;

Дальше — сам наконечник. Он должен быть треугольником, который смотрит туда же, куда летит стрелка. Для этого отступим от самого кончика немного назад, против направления — получится точка back, где-то посередине между концом стрелки и её настоящим кончиком. Насколько именно назад — решает не сама функция, а тот, кто её вызывает: head_length — это параметр draw_arrow, обычное число, которое передают снаружи, как и color. Чем оно больше, тем длиннее получится наконечник. Сам шаг назад — обычное сложение точки и вектора, как в уроке «Точка плюс вектор», только вектор здесь — направление, развёрнутое в обратную сторону и растянутое на head_length:

let back_x = to.x - direction_x * head_length;
let back_y = to.y - direction_y * head_length;

От back нужно разойтись в стороны — перпендикулярно направлению стрелки, по одному шагу влево и вправо. Насколько широко разойтись, задаёт второй параметр, head_width: чем он больше, тем шире получится наконечник у основания. А сам перпендикуляр считается ровно тем трюком, что в уроке «Поворот на 90°»: меняем x и y местами и одному из них даём минус.

let perp_x = -direction_y;
let perp_y = direction_x;
let wing1_x = back_x + perp_x * head_width;
let wing1_y = back_y + perp_y * head_width;
let wing2_x = back_x - perp_x * head_width;
let wing2_y = back_y - perp_y * head_width;

Три точки — сам кончик стрелки to и два «крыла», wing1 и wing2, — и есть треугольник наконечника. А тело стрелки рисуем не до самого кончика, а только до back, иначе линия вылезет из-под треугольника и будет торчать наружу.

Стрелка смотрит вертикально вверх — так направления видно сразу: «назад» — это вниз, а крылья наконечника — влево и вправо. Кнопка ниже проигрывает само построение: шаг назад до back, потом крыло влево, потом вправо, а потом оба крыла соединяются с кончиком — и получается треугольник.

Стрелка летит из центра экрана прямо в курсор мыши (клик по демо переносит на него фокус). Поменяй head_length и head_width ниже:

src/main.rs
use macroquad::prelude::*;
// Точка — как в уроке «Точка и прямоугольник».
struct Point {
x: f32,
y: f32,
}
// Рисует стрелку из from в to — линию с треугольным наконечником.
fn draw_arrow(from: Point, to: Point, head_length: f32, head_width: f32, color: Color) {
// Направление и длина — как в «Направлении».
let dx = to.x - from.x;
let dy = to.y - from.y;
let length = (dx * dx + dy * dy).sqrt();
// Нулевой вектор не на что направить — как в «Делении на ноль».
if length == 0.0 {
return;
}
let direction_x = dx / length;
let direction_y = dy / length;
// Шаг назад от кончика вдоль направления.
let back_x = to.x - direction_x * head_length;
let back_y = to.y - direction_y * head_length;
// Перпендикуляр к направлению — поворот на 90°, как в «Повороте на 90°».
let perp_x = -direction_y;
let perp_y = direction_x;
// Два «крыла» наконечника — по разные стороны от back. Каждая
// координата — своя переменная, чтобы было видно, как она
// считается.
let wing1_x = back_x + perp_x * head_width;
let wing1_y = back_y + perp_y * head_width;
let wing2_x = back_x - perp_x * head_width;
let wing2_y = back_y - perp_y * head_width;
// Тело стрелки — до back, а не до самого кончика, чтобы не вылезать
// из-под наконечника.
draw_line(from.x, from.y, back_x, back_y, 4.0, color);
// draw_triangle() в macroquad хочет точки в виде Vec2, а не просто
// x и y по отдельности — vec2() здесь просто упаковывает уже
// готовые координаты.
draw_triangle(
vec2(to.x, to.y),
vec2(wing1_x, wing1_y),
vec2(wing2_x, wing2_y),
color,
);
}
#[macroquad::main("Стрелки")]
async fn main() {
loop {
clear_background(BLACK);
// Стрелка летит из центра экрана в курсор мыши.
let center = Point { x: screen_width() / 2.0, y: screen_height() / 2.0 };
let (mouse_x, mouse_y) = mouse_position();
let cursor = Point { x: mouse_x, y: mouse_y };
draw_arrow(center, cursor, 24.0, 12.0, YELLOW);
next_frame().await;
}
}
  • direction_x/direction_y — направление стрелки, длина ровно 1, тот же приём, что в уроке «Направление».
  • back — точка чуть позади кончика, на расстоянии head_length против направления. Отсюда расходятся оба крыла наконечника.
  • perp_x/perp_y — перпендикуляр к направлению, посчитанный поворотом на 90° из урока «Поворот на 90°».
  • wing1 и wing2 — та же точка back, сдвинутая на head_width в две противоположные перпендикулярные стороны.
  • Сам кончик to и оба крыла — это и есть три вершины треугольника наконечника.
  • head_length и head_width управляют размером наконечника: первый — насколько он длинный вдоль стрелки, второй — насколько широко расходятся крылья.