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.
|
display be in local coordinates.
|
||||||
Local coordinates have center (0,0) at location of radar
|
Local coordinates have center (0,0) at location of radar
|
||||||
base at the community boating center. Maximum coordinate size
|
base at the community boating center. Maximum coordinate size
|
||||||
is 15 miles from the center.
|
is 6 miles from the center.
|
||||||
|
|
||||||
Signal strength:
|
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"
|
1. Add small text window above that a scope that says "Operator Manually Changing Range Graticule"
|
||||||
This appears only when the graticule is changing
|
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
|
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;
|
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) ──────────────────────────────
|
// ─── Range configs (shared by Features 3, 4, 5) ──────────────────────────────
|
||||||
|
|
||||||
struct RangeConfig {
|
struct RangeConfig {
|
||||||
@@ -465,21 +474,25 @@ struct RangeConfig {
|
|||||||
float rings[8]; // explicit ring positions in miles (up to 8)
|
float rings[8]; // explicit ring positions in miles (up to 8)
|
||||||
};
|
};
|
||||||
|
|
||||||
// PPI scope range ring configs (selectable by operator): 2, 4, 6 miles
|
// PPI scope range ring configs (selectable by operator): 1, 2, 3, 4, 6 miles
|
||||||
static const RangeConfig RANGE_CONFIGS[3] = {
|
static const RangeConfig RANGE_CONFIGS[5] = {
|
||||||
|
{ 1.0f, 2, { 0.5f, 1.0f } },
|
||||||
{ 2.0f, 2, { 1.0f, 2.0f } },
|
{ 2.0f, 2, { 1.0f, 2.0f } },
|
||||||
|
{ 3.0f, 3, { 1.0f, 2.0f, 3.0f } },
|
||||||
{ 4.0f, 2, { 2.0f, 4.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
|
// A scope graticule configs — shared range with PPI: 1, 2, 3, 4, 6 miles
|
||||||
static const RangeConfig ASCOPE_RANGE_CONFIGS[3] = {
|
static const RangeConfig ASCOPE_RANGE_CONFIGS[5] = {
|
||||||
|
{ 1.0f, 2, { 0.5f, 1.0f } },
|
||||||
{ 2.0f, 2, { 1.0f, 2.0f } },
|
{ 2.0f, 2, { 1.0f, 2.0f } },
|
||||||
|
{ 3.0f, 3, { 1.0f, 2.0f, 3.0f } },
|
||||||
{ 4.0f, 2, { 2.0f, 4.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 ────────────────────────────────
|
// ─── Feature 3: A scope replaceable graticule ────────────────────────────────
|
||||||
|
|
||||||
@@ -554,8 +567,7 @@ static AScopeGraticule buildAScopeGraticule(
|
|||||||
const float labelY = L.asTop + asH * 0.90f;
|
const float labelY = L.asTop + asH * 0.90f;
|
||||||
for (int ri = 0; ri < rc.numRings; ++ri) {
|
for (int ri = 0; ri < rc.numRings; ++ri) {
|
||||||
float x = gx0 + (rc.rings[ri] / rc.maxMiles) * sigW;
|
float x = gx0 + (rc.rings[ri] / rc.maxMiles) * sigW;
|
||||||
int labelMi = (int)std::round(rc.rings[ri]);
|
appendTextQuads(textV, fa, fmtRangeMi(rc.rings[ri]), x, labelY, W, H);
|
||||||
appendTextQuads(textV, fa, std::to_string(labelMi), x, labelY, W, H);
|
|
||||||
}
|
}
|
||||||
ag.textVerts = (int)textV.size() / 4;
|
ag.textVerts = (int)textV.size() / 4;
|
||||||
makeTextVAO(ag.textVAO, ag.textVBO, textV);
|
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 frac = rcDraw.rings[ri] / rcDraw.maxMiles;
|
||||||
float px = L.ppiCX;
|
float px = L.ppiCX;
|
||||||
float py = L.ppiCY + frac * L.ppiR + LABEL_OFFSET_PX;
|
float py = L.ppiCY + frac * L.ppiR + LABEL_OFFSET_PX;
|
||||||
int lmi = (int)std::round(rcDraw.rings[ri]);
|
appendTextQuads(tv, fa, fmtRangeMi(rcDraw.rings[ri]), px, py, W, H);
|
||||||
appendTextQuads(tv, fa, std::to_string(lmi), px, py, W, H);
|
|
||||||
}
|
}
|
||||||
if (!tv.empty()) {
|
if (!tv.empty()) {
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, rl.textVBO);
|
glBindBuffer(GL_ARRAY_BUFFER, rl.textVBO);
|
||||||
@@ -774,6 +785,7 @@ struct FakeTarget {
|
|||||||
float lengthFt; // vessel length
|
float lengthFt; // vessel length
|
||||||
float beamFt; // vessel beam (width)
|
float beamFt; // vessel beam (width)
|
||||||
float headingDeg; // vessel heading (° true)
|
float headingDeg; // vessel heading (° true)
|
||||||
|
float speedKnots; // speed over ground (knots)
|
||||||
mutable float lastActT; // glfwGetTime() when sweep last lit this target
|
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,
|
static void sizeFromApparent(float apparent, float ppiR,
|
||||||
float& coreRad, float& bloomRad, float& bright)
|
float& coreRad, float& bloomRad, float& bright)
|
||||||
{
|
{
|
||||||
|
// Sizes and brightness increased for smartphone video visibility (New Update 3)
|
||||||
if (apparent >= 100.0f) {
|
if (apparent >= 100.0f) {
|
||||||
coreRad = ppiR * 0.028f;
|
coreRad = ppiR * 0.042f;
|
||||||
bloomRad = 0.0f;
|
bloomRad = ppiR * 0.018f;
|
||||||
bright = 1.00f;
|
bright = 1.00f;
|
||||||
} else if (apparent >= 50.0f) {
|
} else if (apparent >= 50.0f) {
|
||||||
coreRad = ppiR * 0.022f;
|
coreRad = ppiR * 0.032f;
|
||||||
bloomRad = 0.0f;
|
bloomRad = 0.0f;
|
||||||
bright = 1.00f;
|
bright = 1.00f;
|
||||||
} else if (apparent >= 10.0f) {
|
} else if (apparent >= 10.0f) {
|
||||||
coreRad = ppiR * 0.015f;
|
coreRad = ppiR * 0.022f;
|
||||||
bloomRad = 0.0f;
|
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 {
|
} else {
|
||||||
coreRad = ppiR * 0.010f;
|
// Very small target (paddle boarder, kayak, etc.) — dim but visible on PPI
|
||||||
|
coreRad = ppiR * 0.014f;
|
||||||
bloomRad = 0.0f;
|
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 maxRangeMi, float ascopeBearingDeg,
|
||||||
float W, float H)
|
float W, float H)
|
||||||
{
|
{
|
||||||
static const float THRESH = 3.5f;
|
// Gaussian beam envelope — signal grows as bearing approaches target, shrinks as it passes
|
||||||
static const float PAD = 4.0f;
|
static const float BEAM_SIGMA = 7.0f; // degrees half-width
|
||||||
static const float HALF_W = 3.0f; // spike half-width in pixels
|
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 asH = L.asBot - L.asTop;
|
||||||
const float gx0 = L.asLeft + PAD;
|
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) {
|
for (int i = 0; i < nTgts; ++i) {
|
||||||
const FakeTarget& t = tgts[i];
|
const FakeTarget& t = tgts[i];
|
||||||
|
|
||||||
// Only show targets whose bearing falls within the A scope beam
|
// Gaussian beam response: pulse grows as bearing approaches target bearing,
|
||||||
if (angDiff(ascopeBearingDeg, t.bearingDeg) >= THRESH) continue;
|
// 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;
|
float rangeFrac = t.rangeMiles / maxRangeMi;
|
||||||
if (rangeFrac > 1.0f) continue;
|
if (rangeFrac > 1.0f) continue;
|
||||||
@@ -871,7 +893,8 @@ static void renderAScopeTrace(AScopeTrace& at, const Layout& L,
|
|||||||
float coreRad, bloomRad, baseBright;
|
float coreRad, bloomRad, baseBright;
|
||||||
sizeFromApparent(apparentFt(t), L.ppiR, 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 sx = gx0 + rangeFrac * sigW;
|
||||||
float sy0 = gy1;
|
float sy0 = gy1;
|
||||||
@@ -884,7 +907,7 @@ static void renderAScopeTrace(AScopeTrace& at, const Layout& L,
|
|||||||
};
|
};
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, at.vbo);
|
glBindBuffer(GL_ARRAY_BUFFER, at.vbo);
|
||||||
glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_DYNAMIC_DRAW);
|
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);
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1538,6 +1561,57 @@ static void renderShorelineLayer(const ShorelineLayer& sl, const Layout& L,
|
|||||||
glDisable(GL_PROGRAM_POINT_SIZE);
|
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 ─────────────────────────────────────────────────────────────
|
// ─── Key callback ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
static void onKey(GLFWwindow* win, int key, int /*scan*/, int action, int /*mods*/)
|
static void onKey(GLFWwindow* win, int key, int /*scan*/, int action, int /*mods*/)
|
||||||
@@ -1609,16 +1683,17 @@ int main()
|
|||||||
// Feature 4 — PPI range rings
|
// Feature 4 — PPI range rings
|
||||||
RingLayer rl = buildRingLayer(fa);
|
RingLayer rl = buildRingLayer(fa);
|
||||||
|
|
||||||
// Feature 5 — Four fake targets per CLAUDE.md spec:
|
// Feature 5 — New Update 3 targets (closer, for museum demo video)
|
||||||
// 1. 5 mi N, 100 ft / 20 ft, heading S at 1 kt (head-on) → apparent ~20 ft
|
// bear range length beam heading speed lastActT
|
||||||
// 2. 5 mi S, 20 ft / 5 ft, heading S at 20 kt (stern-on) → apparent ~5 ft
|
// T1: 1 mi N, large yacht (80×20 ft), heading S at 4 kt
|
||||||
// 3. 6 mi E, 30 ft / 10 ft, heading N at 30 kt (full side) → apparent ~30 ft
|
// T2: 1 mi S, 20ft lobster boat (20×8), heading N at 6 kt
|
||||||
// 4. 6 mi W, 100 ft / 25 ft, heading S at 5 kt (full side) → apparent ~100 ft
|
// 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] = {
|
FakeTarget targets[4] = {
|
||||||
{ 0.0f, 5.0f, 100.0f, 20.0f, 180.0f, -999.0f }, // N – head-on, ~20 ft apparent
|
{ 0.0f, 1.000f, 80.0f, 20.0f, 180.0f, 4.0f, -999.0f }, // N large yacht
|
||||||
{ 180.0f, 5.0f, 20.0f, 5.0f, 180.0f, -999.0f }, // S – stern-on, ~5 ft apparent
|
{ 180.0f, 1.000f, 20.0f, 8.0f, 0.0f, 6.0f, -999.0f }, // S lobster boat
|
||||||
{ 90.0f, 6.0f, 30.0f, 10.0f, 0.0f, -999.0f }, // E – full side, ~30 ft apparent
|
{ 315.0f, 0.333f, 3.0f, 2.0f, 135.0f, 2.0f, -999.0f }, // NW paddle boarder
|
||||||
{ 270.0f, 6.0f, 100.0f, 25.0f, 180.0f, -999.0f }, // W – full side, ~100 ft apparent
|
{ 225.0f, 0.750f, 20.0f, 8.0f, 45.0f, 8.0f, -999.0f }, // SW workboat
|
||||||
};
|
};
|
||||||
TargetLayer tl = buildTargetLayer();
|
TargetLayer tl = buildTargetLayer();
|
||||||
|
|
||||||
@@ -1643,20 +1718,49 @@ int main()
|
|||||||
// NDC height of A scope box — used for slide animation
|
// NDC height of A scope box — used for slide animation
|
||||||
const float scopeNDCH = (layout.asBot - layout.asTop) * 2.0f / H;
|
const float scopeNDCH = (layout.asBot - layout.asTop) * 2.0f / H;
|
||||||
|
|
||||||
// Feature 3 animation state — 4 phases per range cycle
|
// Feature 3 graticule animation — triggered by operator cycle, not auto-cycling
|
||||||
// PPI and A scope cycle independently but share the same phase timer
|
|
||||||
enum class GratPhase { HOLD, SLIDE_OUT, WAIT, SLIDE_IN };
|
enum class GratPhase { HOLD, SLIDE_OUT, WAIT, SLIDE_IN };
|
||||||
int curRange = 0;
|
int curRange = 4; // start at 6 mi max (index 4)
|
||||||
int nextRange = 1;
|
int nextRange = 4;
|
||||||
int curAScopeRange = 0;
|
int curAScopeRange = 4;
|
||||||
int nextAScopeRange = 1;
|
int nextAScopeRange = 4;
|
||||||
GratPhase gratPhase = GratPhase::HOLD;
|
bool rangeChanging = false;
|
||||||
float phaseTimer = 0.0f;
|
GratPhase gratPhase = GratPhase::HOLD;
|
||||||
|
float phaseTimer = 0.0f;
|
||||||
|
|
||||||
// A scope bearing (° true, CW from N).
|
// Animated cursor (PPI) and A scope bearing — driven by operator cycle
|
||||||
// New suggestion #4: temporarily locked to targets[0] bearing (north, 0°).
|
float cursorBear = 0.0f; // degrees CW from N
|
||||||
// Will be driven by Control 7 once hardware is available.
|
float cursorRange = 5.0f; // miles from radar
|
||||||
float ascopeBearingDeg = targets[0].bearingDeg; // 0.0° — north
|
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 sweepAngle = 0.0f;
|
||||||
float prevTime = static_cast<float>(glfwGetTime());
|
float prevTime = static_cast<float>(glfwGetTime());
|
||||||
@@ -1672,16 +1776,12 @@ int main()
|
|||||||
// ── Advance sweep (Features 4 & 5) ───────────────────────────────────
|
// ── Advance sweep (Features 4 & 5) ───────────────────────────────────
|
||||||
sweepAngle = std::fmod(sweepAngle + SWEEP_DEG_PS * dt, 360.0f);
|
sweepAngle = std::fmod(sweepAngle + SWEEP_DEG_PS * dt, 360.0f);
|
||||||
|
|
||||||
// ── Advance Feature 3 animation ───────────────────────────────────────
|
// ── Advance Feature 3 graticule animation (triggered by operator cycle) ─
|
||||||
phaseTimer += dt;
|
if (gratPhase != GratPhase::HOLD)
|
||||||
|
phaseTimer += dt;
|
||||||
switch (gratPhase) {
|
switch (gratPhase) {
|
||||||
case GratPhase::HOLD:
|
case GratPhase::HOLD:
|
||||||
if (phaseTimer >= HOLD_SEC) {
|
// Holds indefinitely — operator cycle calls startRangeChange to begin
|
||||||
nextRange = (curRange + 1) % RANGE_COUNT;
|
|
||||||
nextAScopeRange = (curAScopeRange + 1) % ASCOPE_RANGE_COUNT;
|
|
||||||
gratPhase = GratPhase::SLIDE_OUT;
|
|
||||||
phaseTimer = 0.0f;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case GratPhase::SLIDE_OUT:
|
case GratPhase::SLIDE_OUT:
|
||||||
if (phaseTimer >= SLIDE_OUT_SEC) {
|
if (phaseTimer >= SLIDE_OUT_SEC) {
|
||||||
@@ -1699,12 +1799,173 @@ int main()
|
|||||||
break;
|
break;
|
||||||
case GratPhase::SLIDE_IN:
|
case GratPhase::SLIDE_IN:
|
||||||
if (phaseTimer >= SLIDE_IN_SEC) {
|
if (phaseTimer >= SLIDE_IN_SEC) {
|
||||||
gratPhase = GratPhase::HOLD;
|
gratPhase = GratPhase::HOLD;
|
||||||
phaseTimer = 0.0f;
|
phaseTimer = 0.0f;
|
||||||
|
rangeChanging = false;
|
||||||
}
|
}
|
||||||
break;
|
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);
|
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
@@ -1771,7 +2032,7 @@ int main()
|
|||||||
|
|
||||||
// ── A scope signal trace ──────────────────────────────────────────────
|
// ── A scope signal trace ──────────────────────────────────────────────
|
||||||
renderAScopeTrace(at, layout, targets, 4,
|
renderAScopeTrace(at, layout, targets, 4,
|
||||||
ASCOPE_RANGE_CONFIGS[curAScopeRange].maxMiles, ascopeBearingDeg,
|
ASCOPE_RANGE_CONFIGS[curAScopeRange].maxMiles, ascopeBear,
|
||||||
W, H);
|
W, H);
|
||||||
|
|
||||||
// ── Feature 4: PPI range rings ────────────────────────────────────────
|
// ── Feature 4: PPI range rings ────────────────────────────────────────
|
||||||
@@ -1785,21 +2046,19 @@ int main()
|
|||||||
// ── Feature 9: Shoreline + terrain ───────────────────────────────────
|
// ── Feature 9: Shoreline + terrain ───────────────────────────────────
|
||||||
renderShorelineLayer(sl, layout, curRange, sweepAngle, W, H);
|
renderShorelineLayer(sl, layout, curRange, sweepAngle, W, H);
|
||||||
|
|
||||||
// ── Feature 6: PPI cursor locked to targets[2] (east, 6 mi) ──────────
|
// ── Feature 6: PPI cursor — animated by operator cycle ────────────────
|
||||||
// New suggestion #5: cursor follows one target; not free-roaming.
|
|
||||||
// Only draw when the target is within the current range setting.
|
|
||||||
{
|
{
|
||||||
const float cursorRangeFrac = targets[2].rangeMiles
|
float maxMi = RANGE_CONFIGS[curRange].maxMiles;
|
||||||
/ RANGE_CONFIGS[curRange].maxMiles;
|
float cursorRangeFrac = cursorRange / maxMi;
|
||||||
const float cursorBearingDeg = targets[2].bearingDeg;
|
// Clamp to scope boundary — cursor cannot appear outside the circle
|
||||||
if (cursorRangeFrac <= 1.0f)
|
cursorRangeFrac = std::min(cursorRangeFrac, 1.0f);
|
||||||
renderCursor(cl, layout, cursorRangeFrac, cursorBearingDeg, W, H);
|
renderCursor(cl, layout, cursorRangeFrac, cursorBear, W, H);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Bearing display window (new suggestion #3) ────────────────────────
|
// ── Bearing display window (new suggestion #3) ────────────────────────
|
||||||
renderBearingDisplay(bd, layout,
|
renderBearingDisplay(bd, layout,
|
||||||
sb.prog, bg.textProg,
|
sb.prog, bg.textProg,
|
||||||
fa, ascopeBearingDeg,
|
fa, ascopeBear,
|
||||||
ASCOPE_RANGE_CONFIGS[curAScopeRange].maxMiles, W, H);
|
ASCOPE_RANGE_CONFIGS[curAScopeRange].maxMiles, W, H);
|
||||||
|
|
||||||
glfwSwapBuffers(win);
|
glfwSwapBuffers(win);
|
||||||
|
|||||||
Reference in New Issue
Block a user