Units face eight directions and animate their actions
- five sprite views per kind and gear tier (front, front three-quarter, profile, back three-quarter, back), mirrored for the other three directions; chosen from the unit's facing and the camera - units face where they walk, dig, repair or fight - animations: imps stomp while claiming (squash on landing), swing while digging, hammer while repairing; lunges in combat, punches in training, nodding while eating, swaying while reading or praying, breathing when idle, leaning back with loads, lying in bed, chained creatures pulling at their chains, flyers flapping Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1016ee5d6c
commit
d06ce2edb5
3 files changed
+234
-21
No files matched your search
+33
@@ -210,6 +210,8 @@ pub struct Ent {
|
|||||||
pub anim: f32,
|
pub anim: f32,
|
||||||
pub flash: f32,
|
pub flash: f32,
|
||||||
pub face: f32,
|
pub face: f32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub dir: f32, // facing direction on the map (radians, 0 = +x), for the eight sprite views
|
||||||
pub moving: bool,
|
pub moving: bool,
|
||||||
pub working: bool,
|
pub working: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -282,6 +284,7 @@ impl Ent {
|
|||||||
anim: 0.0,
|
anim: 0.0,
|
||||||
flash: 0.0,
|
flash: 0.0,
|
||||||
face: 1.0,
|
face: 1.0,
|
||||||
|
dir: std::f32::consts::FRAC_PI_2,
|
||||||
moving: false,
|
moving: false,
|
||||||
working: false,
|
working: false,
|
||||||
slow: 0.0,
|
slow: 0.0,
|
||||||
@@ -1016,7 +1019,9 @@ impl Game {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mut e = std::mem::replace(&mut self.ents[i], Ent::dummy());
|
let mut e = std::mem::replace(&mut self.ents[i], Ent::dummy());
|
||||||
|
let (x0, y0) = (e.x, e.y);
|
||||||
self.update_ent(&mut e, dt);
|
self.update_ent(&mut e, dt);
|
||||||
|
self.turn(&mut e, x0, y0);
|
||||||
self.ents[i] = e;
|
self.ents[i] = e;
|
||||||
}
|
}
|
||||||
self.ents.retain(|e| e.alive);
|
self.ents.retain(|e| e.alive);
|
||||||
@@ -1621,6 +1626,34 @@ impl Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the direction a unit faces: where it walks, otherwise what it works on or fights
|
||||||
|
fn turn(&self, e: &mut Ent, x0: f32, y0: f32) {
|
||||||
|
let (dx, dy) = (e.x - x0, e.y - y0);
|
||||||
|
if dx * dx + dy * dy > 1e-8 {
|
||||||
|
e.dir = dy.atan2(dx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !e.working {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let tile = |t: usize| {
|
||||||
|
let (x, y) = xy(t);
|
||||||
|
Some((x as f32 + 0.5, y as f32 + 0.5))
|
||||||
|
};
|
||||||
|
let target = match e.job {
|
||||||
|
Job::Dig(t) | Job::Reinforce(t) | Job::Help(t) | Job::TearBridge(t) => tile(t),
|
||||||
|
Job::Attack(id) | Job::Torment(id) | Job::Carry(id) => self.find(id).map(|j| (self.ents[j].x, self.ents[j].y)),
|
||||||
|
Job::Repair(id) | Job::AttackTower(id) => self.towers.iter().find(|t| t.id == id).map(|t| t.pos()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some((tx, ty)) = target {
|
||||||
|
let (dx, dy) = (tx - e.x, ty - e.y);
|
||||||
|
if dx * dx + dy * dy > 0.01 {
|
||||||
|
e.dir = dy.atan2(dx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// moves a unit along its path; true = still moving
|
// moves a unit along its path; true = still moving
|
||||||
pub(crate) fn step_path(&mut self, e: &mut Ent, dt: f32) -> bool {
|
pub(crate) fn step_path(&mut self, e: &mut Ent, dt: f32) -> bool {
|
||||||
if e.path.is_empty() {
|
if e.path.is_empty() {
|
||||||
|
|||||||
+119
-17
@@ -1095,7 +1095,7 @@ pub const SPRITE_KINDS: [EK; 17] = [
|
|||||||
];
|
];
|
||||||
// width of the sprite texture in pixels
|
// width of the sprite texture in pixels
|
||||||
pub const SPRITE_TEX_W: f32 = (17 * 12) as f32;
|
pub const SPRITE_TEX_W: f32 = (17 * 12) as f32;
|
||||||
pub const SPRITE_TEX_H: f32 = (2 * GEAR_TIERS * SPR_CELL) as f32;
|
pub const SPRITE_TEX_H: f32 = (2 * VIEWS * GEAR_TIERS * SPR_CELL) as f32;
|
||||||
|
|
||||||
pub fn sprite_index(k: EK) -> u32 {
|
pub fn sprite_index(k: EK) -> u32 {
|
||||||
SPRITE_KINDS.iter().position(|&s| s == k).unwrap_or(0) as u32
|
SPRITE_KINDS.iter().position(|&s| s == k).unwrap_or(0) as u32
|
||||||
@@ -1112,12 +1112,106 @@ fn is_hero_kind(k: EK) -> bool {
|
|||||||
matches!(k, EK::Knight | EK::Archer | EK::Priest | EK::Dwarf | EK::Paladin)
|
matches!(k, EK::Knight | EK::Archer | EK::Priest | EK::Dwarf | EK::Paladin)
|
||||||
}
|
}
|
||||||
|
|
||||||
// the 12x12 cell of a kind with its gear painted around and onto the silhouette
|
// Views of a sprite for eight directions (the others are mirrored): 0 front, 1 front
|
||||||
fn sprite_with_gear(k: EK, tier: u32) -> [[Option<Color>; 12]; 12] {
|
// three-quarter, 2 side, 3 back three-quarter, 4 back. The drawn sprites face the viewer;
|
||||||
let mut g = [[None; 12]; 12];
|
// the turned views are derived from them and face to the right.
|
||||||
|
pub const VIEWS: u32 = 5;
|
||||||
|
|
||||||
|
type Rows = [[u8; SPR]; SPR];
|
||||||
|
|
||||||
|
fn is_body(ch: u8) -> bool {
|
||||||
|
!matches!(ch, b'.' | b'k' | b'e' | b'f')
|
||||||
|
}
|
||||||
|
|
||||||
|
// the commonest body colour of a sprite (for painting over eyes and faces)
|
||||||
|
fn body_char(r: &Rows) -> u8 {
|
||||||
|
let mut n = [0usize; 128];
|
||||||
|
for row in r.iter() {
|
||||||
|
for &ch in row.iter() {
|
||||||
|
if is_body(ch) && ch < 128 {
|
||||||
|
n[ch as usize] += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(0..128u8).max_by_key(|&c| n[c as usize]).filter(|&c| n[c as usize] > 0).unwrap_or(b'k')
|
||||||
|
}
|
||||||
|
|
||||||
|
// moves the eyes one pixel to the right (where there is face or body to move onto)
|
||||||
|
fn eyes_right(r: &mut Rows) {
|
||||||
|
for row in r.iter_mut() {
|
||||||
|
for x in (0..SPR - 1).rev() {
|
||||||
|
if row[x] == b'e' && row[x + 1] != b'k' && row[x + 1] != b'.' && row[x + 1] != b'e' {
|
||||||
|
row[x] = row[x + 1];
|
||||||
|
row[x + 1] = b'e';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// removes the middle column of the silhouette: a narrower body, as seen from the side
|
||||||
|
fn narrow(r: &mut Rows) {
|
||||||
|
let cols: Vec<usize> = (0..SPR).filter(|&x| r.iter().any(|row| row[x] != b'.')).collect();
|
||||||
|
let (Some(&l), Some(&rr)) = (cols.first(), cols.last()) else { return };
|
||||||
|
let m = (l + rr) / 2;
|
||||||
|
for row in r.iter_mut() {
|
||||||
|
for x in (1..=m).rev() {
|
||||||
|
row[x] = row[x - 1];
|
||||||
|
}
|
||||||
|
row[0] = b'.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sprite_view(k: EK, view: u32) -> Rows {
|
||||||
|
let mut r: Rows = [[b'.'; SPR]; SPR];
|
||||||
for (y, row) in sprite_rows(k).iter().enumerate() {
|
for (y, row) in sprite_rows(k).iter().enumerate() {
|
||||||
for (x, ch) in row.chars().enumerate().take(SPR) {
|
for (x, ch) in row.bytes().enumerate().take(SPR) {
|
||||||
g[y + 1][x + 1] = pal(ch);
|
r[y][x] = ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let body = body_char(&r);
|
||||||
|
match view {
|
||||||
|
1 => eyes_right(&mut r),
|
||||||
|
2 => {
|
||||||
|
narrow(&mut r);
|
||||||
|
// in profile only the eye on the facing side is seen
|
||||||
|
for row in r.iter_mut() {
|
||||||
|
let eyes: Vec<usize> = (0..SPR).filter(|&x| row[x] == b'e').collect();
|
||||||
|
if eyes.len() >= 2 {
|
||||||
|
let x = eyes[0];
|
||||||
|
row[x] = if x > 0 && is_body(row[x - 1]) { row[x - 1] } else if row[x + 1] == b'f' { b'f' } else { body };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eyes_right(&mut r);
|
||||||
|
}
|
||||||
|
3 | 4 => {
|
||||||
|
// from behind: mirrored (the weapon is in the other hand), no eyes, no face
|
||||||
|
for row in r.iter_mut() {
|
||||||
|
row.reverse();
|
||||||
|
}
|
||||||
|
for y in 0..SPR {
|
||||||
|
for x in 0..SPR {
|
||||||
|
let ch = r[y][x];
|
||||||
|
if ch == b'e' || ch == b'f' {
|
||||||
|
let above = if y > 0 { r[y - 1][x] } else { b'.' };
|
||||||
|
r[y][x] = if is_body(above) { above } else { body };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if view == 3 {
|
||||||
|
narrow(&mut r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
// the 12x12 cell of a kind with its gear painted around and onto the silhouette
|
||||||
|
fn sprite_with_gear(k: EK, tier: u32, view: u32) -> [[Option<Color>; 12]; 12] {
|
||||||
|
let mut g = [[None; 12]; 12];
|
||||||
|
for (y, row) in sprite_view(k, view).iter().enumerate() {
|
||||||
|
for (x, &ch) in row.iter().enumerate() {
|
||||||
|
g[y + 1][x + 1] = pal(ch as char);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if tier == 0 {
|
if tier == 0 {
|
||||||
@@ -1196,19 +1290,22 @@ fn sprite_with_gear(k: EK, tier: u32) -> [[Option<Color>; 12]; 12] {
|
|||||||
g
|
g
|
||||||
}
|
}
|
||||||
|
|
||||||
// rows 2*tier: sprites with the gear of the tier, rows 2*tier+1: white silhouettes (hit flash)
|
// row (view * GEAR_TIERS + tier) * 2: sprites with the gear of the tier, the row after it:
|
||||||
|
// white silhouettes (hit flash)
|
||||||
fn gen_sprites() -> Image {
|
fn gen_sprites() -> Image {
|
||||||
let n = SPRITE_KINDS.len() as u32;
|
let n = SPRITE_KINDS.len() as u32;
|
||||||
let mut img = Image::gen_image_color((n * SPR_CELL) as u16, (2 * GEAR_TIERS * SPR_CELL) as u16, Color::new(0.0, 0.0, 0.0, 0.0));
|
let mut img = Image::gen_image_color((n * SPR_CELL) as u16, (2 * VIEWS * GEAR_TIERS * SPR_CELL) as u16, Color::new(0.0, 0.0, 0.0, 0.0));
|
||||||
for (i, &k) in SPRITE_KINDS.iter().enumerate() {
|
for (i, &k) in SPRITE_KINDS.iter().enumerate() {
|
||||||
for tier in 0..GEAR_TIERS {
|
for view in 0..VIEWS {
|
||||||
let g = sprite_with_gear(k, tier);
|
for tier in 0..GEAR_TIERS {
|
||||||
for (y, row) in g.iter().enumerate() {
|
let g = sprite_with_gear(k, tier, view);
|
||||||
for (x, px) in row.iter().enumerate() {
|
for (y, row) in g.iter().enumerate() {
|
||||||
if let Some(col) = px {
|
for (x, px) in row.iter().enumerate() {
|
||||||
let (px, py) = (i as u32 * SPR_CELL + x as u32, tier * 2 * SPR_CELL + y as u32);
|
if let Some(col) = px {
|
||||||
img.set_pixel(px, py, *col);
|
let (px, py) = (i as u32 * SPR_CELL + x as u32, (view * GEAR_TIERS + tier) * 2 * SPR_CELL + y as u32);
|
||||||
img.set_pixel(px, py + SPR_CELL, WHITE);
|
img.set_pixel(px, py, *col);
|
||||||
|
img.set_pixel(px, py + SPR_CELL, WHITE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1351,8 +1448,13 @@ impl Gfx {
|
|||||||
|
|
||||||
// the whole 12x12 cell with the gear of a level (billboards)
|
// the whole 12x12 cell with the gear of a level (billboards)
|
||||||
pub fn sprite_cell(k: EK, white: bool, level: u32) -> Rect {
|
pub fn sprite_cell(k: EK, white: bool, level: u32) -> Rect {
|
||||||
|
Gfx::sprite_cell_view(k, white, level, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the cell of one of the five views (see VIEWS)
|
||||||
|
pub fn sprite_cell_view(k: EK, white: bool, level: u32, view: u32) -> Rect {
|
||||||
let i = sprite_index(k);
|
let i = sprite_index(k);
|
||||||
let row = gear_tier(level) * 2 + white as u32;
|
let row = (view.min(VIEWS - 1) * GEAR_TIERS + gear_tier(level)) * 2 + white as u32;
|
||||||
Rect::new((i * SPR_CELL) as f32, (row * SPR_CELL) as f32, SPR_CELL as f32, SPR_CELL as f32)
|
Rect::new((i * SPR_CELL) as f32, (row * SPR_CELL) as f32, SPR_CELL as f32, SPR_CELL as f32)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+82
-4
@@ -1085,6 +1085,9 @@ impl R3d {
|
|||||||
0.5
|
0.5
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// the camera's right and viewing direction on the ground (for the eight views)
|
||||||
|
let rg = vec2(r.x, r.z).normalize_or_zero();
|
||||||
|
let tg = vec2(toward.x, toward.z).normalize_or_zero();
|
||||||
let mut mb = MB::new();
|
let mut mb = MB::new();
|
||||||
let sw = SPRITE_TEX_W;
|
let sw = SPRITE_TEX_W;
|
||||||
let sh = SPRITE_TEX_H;
|
let sh = SPRITE_TEX_H;
|
||||||
@@ -1095,12 +1098,81 @@ impl R3d {
|
|||||||
let w = sprite_size(e.kind);
|
let w = sprite_size(e.kind);
|
||||||
let h = w;
|
let h = w;
|
||||||
let sleeping = matches!(e.job, Job::Sleep(_)) && !e.moving;
|
let sleeping = matches!(e.job, Job::Sleep(_)) && !e.moving;
|
||||||
|
// which of the eight directions the unit shows (five views, three of them mirrored)
|
||||||
|
let d = vec2(e.dir.cos(), e.dir.sin());
|
||||||
|
let ang = d.dot(rg).atan2(d.dot(tg));
|
||||||
|
let view = ((ang / std::f32::consts::FRAC_PI_4).round().abs() as u32).min(4);
|
||||||
|
let mirror = ang < 0.0;
|
||||||
|
let fwd = if mirror { -1.0 } else { 1.0 }; // screen direction the unit faces
|
||||||
|
let ph = t * 10.0 + e.id as f32 * 1.7;
|
||||||
|
let (mut sx, mut sy, mut lunge) = (1.0f32, 1.0f32, 0.0f32);
|
||||||
let mut bob = if e.moving { (e.anim * 12.0).sin().abs() * 0.06 } else { 0.0 };
|
let mut bob = if e.moving { (e.anim * 12.0).sin().abs() * 0.06 } else { 0.0 };
|
||||||
|
let mut rot = 0.0;
|
||||||
|
if e.moving {
|
||||||
|
// a little sway while walking; loads are carried leaning back
|
||||||
|
rot = (e.anim * 12.0).sin() * 0.05;
|
||||||
|
if e.carry > 0 || matches!(e.job, Job::Carry(_)) {
|
||||||
|
rot += 0.12 * fwd;
|
||||||
|
bob *= 0.6;
|
||||||
|
}
|
||||||
|
} else if e.working && !sleeping {
|
||||||
|
match e.job {
|
||||||
|
// claiming: stomping the ground, squashed on landing
|
||||||
|
Job::Claim(_) | Job::Help(_) => {
|
||||||
|
let k = (ph * 1.1).sin();
|
||||||
|
bob = k.max(0.0) * 0.13;
|
||||||
|
if k < 0.0 {
|
||||||
|
sy = 1.0 + k * 0.14;
|
||||||
|
sx = 1.0 - k * 0.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// digging: a swing at the wall
|
||||||
|
Job::Dig(_) | Job::TearBridge(_) => {
|
||||||
|
let k = (ph * 1.3).sin();
|
||||||
|
rot = -(k * 0.35).max(-0.12) * fwd;
|
||||||
|
lunge = k.max(0.0) * 0.07;
|
||||||
|
}
|
||||||
|
// hammering
|
||||||
|
Job::Reinforce(_) | Job::Repair(_) => {
|
||||||
|
let k = (ph * 2.2).sin();
|
||||||
|
rot = -k.max(0.0) * 0.25 * fwd;
|
||||||
|
bob = (ph * 2.2).cos().max(0.0) * 0.03;
|
||||||
|
}
|
||||||
|
// fighting: lunging at the enemy
|
||||||
|
Job::Attack(_) | Job::AttackTower(_) | Job::AttackCore | Job::Torment(_) => {
|
||||||
|
let k = (ph * 0.9).sin().max(0.0).powi(2);
|
||||||
|
lunge = k * 0.14;
|
||||||
|
rot = -k * 0.2 * fwd;
|
||||||
|
}
|
||||||
|
// training: quick punches
|
||||||
|
Job::Train(_) => {
|
||||||
|
let k = (ph * 1.6).sin();
|
||||||
|
lunge = k.max(0.0) * 0.08;
|
||||||
|
rot = -k * 0.12 * fwd;
|
||||||
|
bob = (ph * 3.2).sin().abs() * 0.03;
|
||||||
|
}
|
||||||
|
// eating: nodding over the food
|
||||||
|
Job::Eat(_) => {
|
||||||
|
sy = 1.0 - (ph * 0.8).sin().abs() * 0.1;
|
||||||
|
rot = -(ph * 0.8).sin().abs() * 0.12 * fwd;
|
||||||
|
}
|
||||||
|
// reading and praying: a slow sway
|
||||||
|
Job::Study(_) | Job::Pray(_) | Job::HealCore => rot = (t * 1.8 + e.id as f32).sin() * 0.07,
|
||||||
|
_ => rot = (ph * 1.4).sin() * 0.15,
|
||||||
|
}
|
||||||
|
} else if !sleeping {
|
||||||
|
// breathing
|
||||||
|
sy = 1.0 + (t * 2.4 + e.id as f32).sin() * 0.025;
|
||||||
|
}
|
||||||
// flying creatures hover and flap (on their perch while sleeping)
|
// flying creatures hover and flap (on their perch while sleeping)
|
||||||
if flies(e.kind) && !sleeping {
|
if flies(e.kind) && !sleeping {
|
||||||
bob = 0.45 + (t * 7.0 + e.anim * 3.0).sin() * 0.06;
|
bob = 0.45 + (t * 7.0 + e.anim * 3.0).sin() * 0.06;
|
||||||
|
sx *= 1.0 + (t * 14.0 + e.id as f32).sin() * 0.06;
|
||||||
|
} else if sleeping {
|
||||||
|
// asleep: lying in bed
|
||||||
|
rot = std::f32::consts::FRAC_PI_2 * 0.9 * fwd;
|
||||||
|
bob = 0.04;
|
||||||
}
|
}
|
||||||
let mut rot = if e.working && !sleeping { (t * 14.0 + e.anim).sin() * 0.15 } else { 0.0 };
|
|
||||||
// prisoners: lying unconscious, carried above an imp, or shaking in the torture chamber
|
// prisoners: lying unconscious, carried above an imp, or shaking in the torture chamber
|
||||||
match e.captive {
|
match e.captive {
|
||||||
1 => rot = std::f32::consts::FRAC_PI_2,
|
1 => rot = std::f32::consts::FRAC_PI_2,
|
||||||
@@ -1111,11 +1183,16 @@ impl R3d {
|
|||||||
3 => rot = (t * 25.0 + e.anim).sin() * 0.08,
|
3 => rot = (t * 25.0 + e.anim).sin() * 0.08,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
if e.chained {
|
||||||
|
// pulling at the chains
|
||||||
|
rot = (t * 3.0 + e.id as f32).sin() * 0.1;
|
||||||
|
lunge = 0.0;
|
||||||
|
}
|
||||||
let white = e.flash > 0.07;
|
let white = e.flash > 0.07;
|
||||||
let src = Gfx::sprite_cell(e.kind, white, e.level);
|
let src = Gfx::sprite_cell_view(e.kind, white, e.level, if e.captive != 0 { 0 } else { view });
|
||||||
let (mut u0, v0) = (src.x / sw, src.y / sh);
|
let (mut u0, v0) = (src.x / sw, src.y / sh);
|
||||||
let (mut u1, v1) = ((src.x + src.w) / sw, (src.y + src.h) / sh);
|
let (mut u1, v1) = ((src.x + src.w) / sw, (src.y + src.h) / sh);
|
||||||
if e.face < 0.0 {
|
if mirror {
|
||||||
std::mem::swap(&mut u0, &mut u1);
|
std::mem::swap(&mut u0, &mut u1);
|
||||||
}
|
}
|
||||||
let a = amb(e.x, e.y) * if sleeping || e.captive == 1 { 0.6 } else { 1.0 };
|
let a = amb(e.x, e.y) * if sleeping || e.captive == 1 { 0.6 } else { 1.0 };
|
||||||
@@ -1128,9 +1205,10 @@ impl R3d {
|
|||||||
};
|
};
|
||||||
// pulled towards the camera: unchanged on screen (orthographic), but walls behind
|
// pulled towards the camera: unchanged on screen (orthographic), but walls behind
|
||||||
// the unit no longer cut into the sprite
|
// the unit no longer cut into the sprite
|
||||||
let base = vec3(e.x, bob, e.y) + toward * 0.45;
|
let base = vec3(e.x, bob, e.y) + toward * 0.45 + r * (lunge * fwd);
|
||||||
let (sr, cr) = rot.sin_cos();
|
let (sr, cr) = rot.sin_cos();
|
||||||
let corner = |dx: f32, dy: f32| {
|
let corner = |dx: f32, dy: f32| {
|
||||||
|
let (dx, dy) = (dx * sx, dy * sy);
|
||||||
let x = dx * cr - dy * sr;
|
let x = dx * cr - dy * sr;
|
||||||
let y = dx * sr + dy * cr;
|
let y = dx * sr + dy * cr;
|
||||||
base + r * x + Vec3::Y * y
|
base + r * x + Vec3::Y * y
|
||||||
|
|||||||
Reference in new issue
Block a user