Making better animation
This commit is contained in:
54
CLAUDE.md
54
CLAUDE.md
@@ -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.
Binary file not shown.
393
src/main.cpp
393
src/main.cpp
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user