Making better animation

This commit is contained in:
2026-04-08 07:18:13 -07:00
parent 5a7350dbae
commit 18f7171d5c
4 changed files with 379 additions and 68 deletions

View File

@@ -132,7 +132,7 @@ Please note that all target information furnished to the
display be in local coordinates.
Local coordinates have center (0,0) at location of radar
base at the community boating center. Maximum coordinate size
is 15 miles from the center.
is 6 miles from the center.
Signal strength:
@@ -305,6 +305,58 @@ New update 2
1. Add small text window above that a scope that says "Operator Manually Changing Range Graticule"
This appears only when the graticule is changing
New update 3 (This involves the four fake targets.
We need to change the timing of the operator as there is
no real controls nor real operator and I want to show a
video of the radar operations on a video I will take on my
phone.
Fist, make the target brighter so they can be seen on a smartphone video
2nd, make sure the height of pulse on a scope should reflect the sterength of
target
3. As the operator turns the bearing crank for the a scope, the target must gradually
gets bigger as the bearing gets close to that of the target they are moving to and
then gets smaller as the operator moves the crank from that target and moves on.
Target 1. One mile north of radar. A strong signal (large yacht)
Target 2. One mile south of radar. A bit weaker, (20 foot lobster boat)
Target 3. 1/3 mile northwest of radar. (person on paddle board)
Target 4. 3/4 mile southwest of radar. (20 foot metal workboat)
Maintain current target speeds and headings.
Let cycle repeat until the application exits via keyboard escape key
Adjust speeds and heading for boats that have been assigned closer
for target 3, make the target on A scope very small, but still visible and
on ppi scope, barely visible.
Cycle of operator
1. set range to maximum on both a scope and ppi scope so they can
watch entire bay
2. Wait 5 seconds
3. change range of both scopes to 2 miles so they can look closer to targets
4. Wait 1 second
5. In operating range and bearing, make the movement of the cranks by a human;
not perfect automatic cranking. Combination of small random jitter/wobble;
occasional slight overshooting and correction; variable speeed
5a. Move the cursor to the Target 1. Also rotate
a scope bearing to that of Target 1
6. wait 5 seconds
7. Move cursor to target 2 on ppi and bearing on a scope to bearing
of target 2
8. wait 5 seconds
8a. change range to 1 mile. and wait 5 seconds.
9. Move cursor to target 3 on ppi and bearing on a scope to bearing
of target 3
10. Wait 5 seconds
11. Move cursor to target 4 on ppi and bearing on a scope to bearing
of target 4
12. Wait 5 seconds move range to maximum for 5 seconds and then go to step 1
NOTE on my plan for coding

Binary file not shown.

View File

@@ -457,6 +457,15 @@ static BearingGraticule buildBearingGraticule(const Layout& L, const FontAtlas&
return bg;
}
// Format a range label: whole numbers as integers ("2"), fractions as 1-decimal ("0.5")
static std::string fmtRangeMi(float mi)
{
if (mi == std::floor(mi)) return std::to_string(static_cast<int>(mi));
char buf[16];
std::snprintf(buf, sizeof(buf), "%.1f", mi);
return std::string(buf);
}
// ─── Range configs (shared by Features 3, 4, 5) ──────────────────────────────
struct RangeConfig {
@@ -465,21 +474,25 @@ struct RangeConfig {
float rings[8]; // explicit ring positions in miles (up to 8)
};
// PPI scope range ring configs (selectable by operator): 2, 4, 6 miles
static const RangeConfig RANGE_CONFIGS[3] = {
// PPI scope range ring configs (selectable by operator): 1, 2, 3, 4, 6 miles
static const RangeConfig RANGE_CONFIGS[5] = {
{ 1.0f, 2, { 0.5f, 1.0f } },
{ 2.0f, 2, { 1.0f, 2.0f } },
{ 3.0f, 3, { 1.0f, 2.0f, 3.0f } },
{ 4.0f, 2, { 2.0f, 4.0f } },
{ 6.0f, 4, { 1.0f, 2.0f, 4.0f, 6.0f } },
{ 6.0f, 3, { 2.0f, 4.0f, 6.0f } },
};
static constexpr int RANGE_COUNT = 3;
static constexpr int RANGE_COUNT = 5;
// A scope graticule configs — independent from PPI: 2, 4, 6 miles
static const RangeConfig ASCOPE_RANGE_CONFIGS[3] = {
// A scope graticule configs — shared range with PPI: 1, 2, 3, 4, 6 miles
static const RangeConfig ASCOPE_RANGE_CONFIGS[5] = {
{ 1.0f, 2, { 0.5f, 1.0f } },
{ 2.0f, 2, { 1.0f, 2.0f } },
{ 3.0f, 3, { 1.0f, 2.0f, 3.0f } },
{ 4.0f, 2, { 2.0f, 4.0f } },
{ 6.0f, 4, { 1.0f, 2.0f, 4.0f, 6.0f } },
{ 6.0f, 3, { 2.0f, 4.0f, 6.0f } },
};
static constexpr int ASCOPE_RANGE_COUNT = 3;
static constexpr int ASCOPE_RANGE_COUNT = 5;
// ─── Feature 3: A scope replaceable graticule ────────────────────────────────
@@ -554,8 +567,7 @@ static AScopeGraticule buildAScopeGraticule(
const float labelY = L.asTop + asH * 0.90f;
for (int ri = 0; ri < rc.numRings; ++ri) {
float x = gx0 + (rc.rings[ri] / rc.maxMiles) * sigW;
int labelMi = (int)std::round(rc.rings[ri]);
appendTextQuads(textV, fa, std::to_string(labelMi), x, labelY, W, H);
appendTextQuads(textV, fa, fmtRangeMi(rc.rings[ri]), x, labelY, W, H);
}
ag.textVerts = (int)textV.size() / 4;
makeTextVAO(ag.textVAO, ag.textVBO, textV);
@@ -736,8 +748,7 @@ static void renderRingLayer(RingLayer& rl, const Layout& L,
float frac = rcDraw.rings[ri] / rcDraw.maxMiles;
float px = L.ppiCX;
float py = L.ppiCY + frac * L.ppiR + LABEL_OFFSET_PX;
int lmi = (int)std::round(rcDraw.rings[ri]);
appendTextQuads(tv, fa, std::to_string(lmi), px, py, W, H);
appendTextQuads(tv, fa, fmtRangeMi(rcDraw.rings[ri]), px, py, W, H);
}
if (!tv.empty()) {
glBindBuffer(GL_ARRAY_BUFFER, rl.textVBO);
@@ -774,6 +785,7 @@ struct FakeTarget {
float lengthFt; // vessel length
float beamFt; // vessel beam (width)
float headingDeg; // vessel heading (° true)
float speedKnots; // speed over ground (knots)
mutable float lastActT; // glfwGetTime() when sweep last lit this target
};
@@ -790,22 +802,28 @@ static float apparentFt(const FakeTarget& t)
static void sizeFromApparent(float apparent, float ppiR,
float& coreRad, float& bloomRad, float& bright)
{
// Sizes and brightness increased for smartphone video visibility (New Update 3)
if (apparent >= 100.0f) {
coreRad = ppiR * 0.028f;
bloomRad = 0.0f;
coreRad = ppiR * 0.042f;
bloomRad = ppiR * 0.018f;
bright = 1.00f;
} else if (apparent >= 50.0f) {
coreRad = ppiR * 0.022f;
coreRad = ppiR * 0.032f;
bloomRad = 0.0f;
bright = 1.00f;
} else if (apparent >= 10.0f) {
coreRad = ppiR * 0.015f;
coreRad = ppiR * 0.022f;
bloomRad = 0.0f;
bright = 0.90f;
bright = 0.95f;
} else if (apparent >= 5.0f) {
coreRad = ppiR * 0.016f;
bloomRad = 0.0f;
bright = 0.85f;
} else {
coreRad = ppiR * 0.010f;
// Very small target (paddle boarder, kayak, etc.) — dim but visible on PPI
coreRad = ppiR * 0.014f;
bloomRad = 0.0f;
bright = 0.75f;
bright = 0.55f;
}
}
@@ -841,9 +859,10 @@ static void renderAScopeTrace(AScopeTrace& at, const Layout& L,
float maxRangeMi, float ascopeBearingDeg,
float W, float H)
{
static const float THRESH = 3.5f;
static const float PAD = 4.0f;
static const float HALF_W = 3.0f; // spike half-width in pixels
// Gaussian beam envelope — signal grows as bearing approaches target, shrinks as it passes
static const float BEAM_SIGMA = 7.0f; // degrees half-width
static const float PAD = 4.0f;
static const float HALF_W = 3.0f; // spike half-width in pixels
const float asH = L.asBot - L.asTop;
const float gx0 = L.asLeft + PAD;
@@ -862,8 +881,11 @@ static void renderAScopeTrace(AScopeTrace& at, const Layout& L,
for (int i = 0; i < nTgts; ++i) {
const FakeTarget& t = tgts[i];
// Only show targets whose bearing falls within the A scope beam
if (angDiff(ascopeBearingDeg, t.bearingDeg) >= THRESH) continue;
// Gaussian beam response: pulse grows as bearing approaches target bearing,
// shrinks as the operator cranks away — simulates antenna beam pattern
float bearDiff = angDiff(ascopeBearingDeg, t.bearingDeg);
float beamGain = std::exp(-bearDiff * bearDiff / (2.0f * BEAM_SIGMA * BEAM_SIGMA));
if (beamGain < 0.02f) continue;
float rangeFrac = t.rangeMiles / maxRangeMi;
if (rangeFrac > 1.0f) continue;
@@ -871,7 +893,8 @@ static void renderAScopeTrace(AScopeTrace& at, const Layout& L,
float coreRad, bloomRad, baseBright;
sizeFromApparent(apparentFt(t), L.ppiR, coreRad, bloomRad, baseBright);
float height = sigH * baseBright; // upward (sigH is negative)
float gain = baseBright * beamGain;
float height = sigH * gain; // upward (sigH is negative)
float sx = gx0 + rangeFrac * sigW;
float sy0 = gy1;
@@ -884,7 +907,7 @@ static void renderAScopeTrace(AScopeTrace& at, const Layout& L,
};
glBindBuffer(GL_ARRAY_BUFFER, at.vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_DYNAMIC_DRAW);
glUniform3f(locCol, P1_R * baseBright, P1_G * baseBright, P1_B * baseBright);
glUniform3f(locCol, P1_R * gain, P1_G * gain, P1_B * gain);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
@@ -1538,6 +1561,57 @@ static void renderShorelineLayer(const ShorelineLayer& sl, const Layout& L,
glDisable(GL_PROGRAM_POINT_SIZE);
}
// ─── New Update 3: Human-like crank motion helpers ───────────────────────────
//
// Simulate an operator turning encoders: variable speed (ease-in/out),
// small random jitter while moving, and a slight overshoot+correction
// near the destination.
// Angular interpolation (bearing) — takes shortest path around the circle
static float crankAngle(float from, float to, float progress, float now)
{
float t = std::max(0.0f, std::min(1.0f, progress));
float smooth = t * t * (3.0f - 2.0f * t); // smoothstep ease-in/out
float over = 0.0f;
if (t > 0.70f) { // overshoot in final 30%
float u = (t - 0.70f) / 0.30f;
over = 0.06f * std::sin(u * PI * 1.5f) * std::exp(-3.0f * u);
}
float diff = to - from;
while (diff > 180.0f) diff -= 360.0f;
while (diff < -180.0f) diff += 360.0f;
// Jitter decreases to zero as we arrive
float jitter = 2.5f * (1.0f - smooth)
* std::sin(now * 7.3f) * std::cos(now * 4.1f);
return from + diff * (smooth + over) + jitter;
}
// Linear interpolation (range distance)
static float crankLinear(float from, float to, float progress, float now)
{
float t = std::max(0.0f, std::min(1.0f, progress));
float smooth = t * t * (3.0f - 2.0f * t);
float over = 0.0f;
if (t > 0.70f) {
float u = (t - 0.70f) / 0.30f;
over = 0.06f * std::sin(u * PI * 1.5f) * std::exp(-3.0f * u);
}
float span = to - from;
float jitter = std::fabs(span) * 0.03f * (1.0f - smooth)
* std::sin(now * 5.3f);
return from + span * (smooth + over) + jitter;
}
// Duration (seconds) for a crank move — scales with angular and range distance
static float moveDurationFor(float bearSrc, float bearDst,
float rngSrc, float rngDst)
{
float db = bearSrc - bearDst;
while (db > 180.0f) db -= 360.0f;
while (db < -180.0f) db += 360.0f;
return std::max(2.5f, std::fabs(db) / 22.0f + std::fabs(rngSrc - rngDst) * 0.4f);
}
// ─── Key callback ─────────────────────────────────────────────────────────────
static void onKey(GLFWwindow* win, int key, int /*scan*/, int action, int /*mods*/)
@@ -1609,16 +1683,17 @@ int main()
// Feature 4 — PPI range rings
RingLayer rl = buildRingLayer(fa);
// Feature 5 — Four fake targets per CLAUDE.md spec:
// 1. 5 mi N, 100 ft / 20 ft, heading S at 1 kt (head-on) → apparent ~20 ft
// 2. 5 mi S, 20 ft / 5 ft, heading S at 20 kt (stern-on) → apparent ~5 ft
// 3. 6 mi E, 30 ft / 10 ft, heading N at 30 kt (full side) → apparent ~30 ft
// 4. 6 mi W, 100 ft / 25 ft, heading S at 5 kt (full side) → apparent ~100 ft
// Feature 5 — New Update 3 targets (closer, for museum demo video)
// bear range length beam heading speed lastActT
// T1: 1 mi N, large yacht (80×20 ft), heading S at 4 kt
// T2: 1 mi S, 20ft lobster boat (20×8), heading N at 6 kt
// T3: 1/3 mi NW, paddle boarder (3×2 ft), heading SE at 2 kt
// T4: 3/4 mi SW, 20ft metal workboat (20×8), heading NE at 8 kt
FakeTarget targets[4] = {
{ 0.0f, 5.0f, 100.0f, 20.0f, 180.0f, -999.0f }, // N head-on, ~20 ft apparent
{ 180.0f, 5.0f, 20.0f, 5.0f, 180.0f, -999.0f }, // S stern-on, ~5 ft apparent
{ 90.0f, 6.0f, 30.0f, 10.0f, 0.0f, -999.0f }, // E full side, ~30 ft apparent
{ 270.0f, 6.0f, 100.0f, 25.0f, 180.0f, -999.0f }, // W full side, ~100 ft apparent
{ 0.0f, 1.000f, 80.0f, 20.0f, 180.0f, 4.0f, -999.0f }, // N large yacht
{ 180.0f, 1.000f, 20.0f, 8.0f, 0.0f, 6.0f, -999.0f }, // S lobster boat
{ 315.0f, 0.333f, 3.0f, 2.0f, 135.0f, 2.0f, -999.0f }, // NW paddle boarder
{ 225.0f, 0.750f, 20.0f, 8.0f, 45.0f, 8.0f, -999.0f }, // SW workboat
};
TargetLayer tl = buildTargetLayer();
@@ -1643,20 +1718,49 @@ int main()
// NDC height of A scope box — used for slide animation
const float scopeNDCH = (layout.asBot - layout.asTop) * 2.0f / H;
// Feature 3 animation state — 4 phases per range cycle
// PPI and A scope cycle independently but share the same phase timer
// Feature 3 graticule animation — triggered by operator cycle, not auto-cycling
enum class GratPhase { HOLD, SLIDE_OUT, WAIT, SLIDE_IN };
int curRange = 0;
int nextRange = 1;
int curAScopeRange = 0;
int nextAScopeRange = 1;
GratPhase gratPhase = GratPhase::HOLD;
float phaseTimer = 0.0f;
int curRange = 4; // start at 6 mi max (index 4)
int nextRange = 4;
int curAScopeRange = 4;
int nextAScopeRange = 4;
bool rangeChanging = false;
GratPhase gratPhase = GratPhase::HOLD;
float phaseTimer = 0.0f;
// A scope bearing (° true, CW from N).
// New suggestion #4: temporarily locked to targets[0] bearing (north, 0°).
// Will be driven by Control 7 once hardware is available.
float ascopeBearingDeg = targets[0].bearingDeg; // 0.0° — north
// Animated cursor (PPI) and A scope bearing — driven by operator cycle
float cursorBear = 0.0f; // degrees CW from N
float cursorRange = 5.0f; // miles from radar
float ascopeBear = 0.0f; // degrees CW from N
// Source/destination for human-like crank animation
float srcCursorBear = cursorBear;
float srcCursorRange = cursorRange;
float srcAscopeBear = ascopeBear;
float dstCursorBear = cursorBear;
float dstCursorRange = cursorRange;
float dstAscopeBear = ascopeBear;
float moveProgress = 0.0f;
float moveDuration = 3.0f;
// New Update 3: operator demo cycle state machine
enum class OpState {
MAX_RANGE_HOLD, // step 1+2: hold 5s at 6 mi (max)
ZOOMING_TO_MIN, // step 3: wait for 6→2 mi graticule animation
ZOOM_WAIT, // step 4: 1s pause after zoom in
MOVING_TO_T1, // step 5a: crank to Target 1
HOLD_T1, // step 6: 5s at Target 1
MOVING_TO_T2, // step 7
HOLD_T2, // step 8: 5s at Target 2
RANGE_TO_1MI, // step 8a: zoom to 1 mi before Target 3
MOVING_TO_T3, // step 9
HOLD_T3, // step 10: 5s at Target 3
MOVING_TO_T4, // step 11
HOLD_T4, // step 12: 5s, then zoom out
ZOOMING_TO_MAX, // wait for 1→6 mi graticule animation, then loop
};
OpState opState = OpState::MAX_RANGE_HOLD;
float opTimer = 0.0f;
float sweepAngle = 0.0f;
float prevTime = static_cast<float>(glfwGetTime());
@@ -1672,16 +1776,12 @@ int main()
// ── Advance sweep (Features 4 & 5) ───────────────────────────────────
sweepAngle = std::fmod(sweepAngle + SWEEP_DEG_PS * dt, 360.0f);
// ── Advance Feature 3 animation ──────────────────────────────────────
phaseTimer += dt;
// ── Advance Feature 3 graticule animation (triggered by operator cycle)
if (gratPhase != GratPhase::HOLD)
phaseTimer += dt;
switch (gratPhase) {
case GratPhase::HOLD:
if (phaseTimer >= HOLD_SEC) {
nextRange = (curRange + 1) % RANGE_COUNT;
nextAScopeRange = (curAScopeRange + 1) % ASCOPE_RANGE_COUNT;
gratPhase = GratPhase::SLIDE_OUT;
phaseTimer = 0.0f;
}
// Holds indefinitely — operator cycle calls startRangeChange to begin
break;
case GratPhase::SLIDE_OUT:
if (phaseTimer >= SLIDE_OUT_SEC) {
@@ -1699,12 +1799,173 @@ int main()
break;
case GratPhase::SLIDE_IN:
if (phaseTimer >= SLIDE_IN_SEC) {
gratPhase = GratPhase::HOLD;
phaseTimer = 0.0f;
gratPhase = GratPhase::HOLD;
phaseTimer = 0.0f;
rangeChanging = false;
}
break;
}
// ── Operator demo cycle (New Update 3) ───────────────────────────────
opTimer += dt;
// Trigger a graticule range change (sets animation in motion)
auto startRangeChange = [&](int newIdx) {
if (curRange == newIdx) { rangeChanging = false; return; }
nextRange = newIdx;
nextAScopeRange = newIdx;
gratPhase = GratPhase::SLIDE_OUT;
phaseTimer = 0.0f;
rangeChanging = true;
};
// Set up src/dst for a human-like move to target tIdx
auto startMoveTo = [&](int tIdx) {
srcCursorBear = cursorBear;
srcCursorRange = cursorRange;
srcAscopeBear = ascopeBear;
dstCursorBear = targets[tIdx].bearingDeg;
dstCursorRange = targets[tIdx].rangeMiles;
dstAscopeBear = targets[tIdx].bearingDeg;
moveDuration = moveDurationFor(srcCursorBear, dstCursorBear,
srcCursorRange, dstCursorRange);
moveProgress = 0.0f;
};
switch (opState) {
case OpState::MAX_RANGE_HOLD:
if (opTimer >= 5.0f) {
startRangeChange(1); // zoom to 2 mi (index 1)
opState = OpState::ZOOMING_TO_MIN;
opTimer = 0.0f;
}
break;
case OpState::ZOOMING_TO_MIN:
if (!rangeChanging) {
opState = OpState::ZOOM_WAIT;
opTimer = 0.0f;
}
break;
case OpState::ZOOM_WAIT:
if (opTimer >= 1.0f) {
startMoveTo(0);
opState = OpState::MOVING_TO_T1;
opTimer = 0.0f;
}
break;
case OpState::MOVING_TO_T1:
moveProgress = std::min(1.0f, opTimer / moveDuration);
cursorBear = crankAngle (srcCursorBear, dstCursorBear, moveProgress, now);
cursorRange = crankLinear(srcCursorRange, dstCursorRange, moveProgress, now);
ascopeBear = crankAngle (srcAscopeBear, dstAscopeBear, moveProgress, now);
if (moveProgress >= 1.0f) {
cursorBear = dstCursorBear; cursorRange = dstCursorRange;
ascopeBear = dstAscopeBear;
opState = OpState::HOLD_T1; opTimer = 0.0f;
}
break;
case OpState::HOLD_T1:
if (opTimer >= 5.0f) {
startMoveTo(1);
opState = OpState::MOVING_TO_T2; opTimer = 0.0f;
}
break;
case OpState::MOVING_TO_T2:
moveProgress = std::min(1.0f, opTimer / moveDuration);
cursorBear = crankAngle (srcCursorBear, dstCursorBear, moveProgress, now);
cursorRange = crankLinear(srcCursorRange, dstCursorRange, moveProgress, now);
ascopeBear = crankAngle (srcAscopeBear, dstAscopeBear, moveProgress, now);
if (moveProgress >= 1.0f) {
cursorBear = dstCursorBear; cursorRange = dstCursorRange;
ascopeBear = dstAscopeBear;
opState = OpState::HOLD_T2; opTimer = 0.0f;
}
break;
case OpState::HOLD_T2:
if (opTimer >= 5.0f) {
startRangeChange(0); // step 8a: zoom to 1 mi (index 0)
opState = OpState::RANGE_TO_1MI; opTimer = 0.0f;
}
break;
case OpState::RANGE_TO_1MI:
if (!rangeChanging) {
startMoveTo(2);
opState = OpState::MOVING_TO_T3; opTimer = 0.0f;
}
break;
case OpState::MOVING_TO_T3:
moveProgress = std::min(1.0f, opTimer / moveDuration);
cursorBear = crankAngle (srcCursorBear, dstCursorBear, moveProgress, now);
cursorRange = crankLinear(srcCursorRange, dstCursorRange, moveProgress, now);
ascopeBear = crankAngle (srcAscopeBear, dstAscopeBear, moveProgress, now);
if (moveProgress >= 1.0f) {
cursorBear = dstCursorBear; cursorRange = dstCursorRange;
ascopeBear = dstAscopeBear;
opState = OpState::HOLD_T3; opTimer = 0.0f;
}
break;
case OpState::HOLD_T3:
if (opTimer >= 5.0f) {
startMoveTo(3);
opState = OpState::MOVING_TO_T4; opTimer = 0.0f;
}
break;
case OpState::MOVING_TO_T4:
moveProgress = std::min(1.0f, opTimer / moveDuration);
cursorBear = crankAngle (srcCursorBear, dstCursorBear, moveProgress, now);
cursorRange = crankLinear(srcCursorRange, dstCursorRange, moveProgress, now);
ascopeBear = crankAngle (srcAscopeBear, dstAscopeBear, moveProgress, now);
if (moveProgress >= 1.0f) {
cursorBear = dstCursorBear; cursorRange = dstCursorRange;
ascopeBear = dstAscopeBear;
opState = OpState::HOLD_T4; opTimer = 0.0f;
}
break;
case OpState::HOLD_T4:
if (opTimer >= 5.0f) {
startRangeChange(4); // zoom back to 6 mi max (index 4)
opState = OpState::ZOOMING_TO_MAX;
opTimer = 0.0f;
}
break;
case OpState::ZOOMING_TO_MAX:
if (!rangeChanging) {
opState = OpState::MAX_RANGE_HOLD;
opTimer = 0.0f;
}
break;
}
// ── Update target positions (slow movement simulation) ────────────────
for (int i = 0; i < 4; ++i) {
FakeTarget& t = targets[i];
float mps = t.speedKnots / 3600.0f; // knots → miles per second
float headRad = t.headingDeg * (PI / 180.0f);
float dx = mps * dt * std::sin(headRad);
float dy = mps * dt * std::cos(headRad);
float bearRad = t.bearingDeg * (PI / 180.0f);
float ex = t.rangeMiles * std::sin(bearRad) + dx;
float ey = t.rangeMiles * std::cos(bearRad) + dy;
t.rangeMiles = std::sqrt(ex * ex + ey * ey);
if (t.rangeMiles > 0.001f) {
t.bearingDeg = std::atan2(ex, ey) * (180.0f / PI);
if (t.bearingDeg < 0.0f) t.bearingDeg += 360.0f;
}
}
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
@@ -1771,7 +2032,7 @@ int main()
// ── A scope signal trace ──────────────────────────────────────────────
renderAScopeTrace(at, layout, targets, 4,
ASCOPE_RANGE_CONFIGS[curAScopeRange].maxMiles, ascopeBearingDeg,
ASCOPE_RANGE_CONFIGS[curAScopeRange].maxMiles, ascopeBear,
W, H);
// ── Feature 4: PPI range rings ────────────────────────────────────────
@@ -1785,21 +2046,19 @@ int main()
// ── Feature 9: Shoreline + terrain ───────────────────────────────────
renderShorelineLayer(sl, layout, curRange, sweepAngle, W, H);
// ── Feature 6: PPI cursor locked to targets[2] (east, 6 mi) ──────────
// New suggestion #5: cursor follows one target; not free-roaming.
// Only draw when the target is within the current range setting.
// ── Feature 6: PPI cursor — animated by operator cycle ────────────────
{
const float cursorRangeFrac = targets[2].rangeMiles
/ RANGE_CONFIGS[curRange].maxMiles;
const float cursorBearingDeg = targets[2].bearingDeg;
if (cursorRangeFrac <= 1.0f)
renderCursor(cl, layout, cursorRangeFrac, cursorBearingDeg, W, H);
float maxMi = RANGE_CONFIGS[curRange].maxMiles;
float cursorRangeFrac = cursorRange / maxMi;
// Clamp to scope boundary — cursor cannot appear outside the circle
cursorRangeFrac = std::min(cursorRangeFrac, 1.0f);
renderCursor(cl, layout, cursorRangeFrac, cursorBear, W, H);
}
// ── Bearing display window (new suggestion #3) ────────────────────────
renderBearingDisplay(bd, layout,
sb.prog, bg.textProg,
fa, ascopeBearingDeg,
fa, ascopeBear,
ASCOPE_RANGE_CONFIGS[curAScopeRange].maxMiles, W, H);
glfwSwapBuffers(win);