diff --git a/src/ui/GameWorldView.cpp b/src/ui/GameWorldView.cpp index 61f373e..35ed472 100644 --- a/src/ui/GameWorldView.cpp +++ b/src/ui/GameWorldView.cpp @@ -15,11 +15,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -1487,6 +1489,9 @@ void GameWorldView::drawDebugOverlay(QPainter& painter) void GameWorldView::drawBeams(QPainter& painter) { + const QPainter::RenderHints savedHints = painter.renderHints(); + painter.setRenderHint(QPainter::Antialiasing, true); + for (const ActiveBeam& beam : m_activeBeams) { const std::optional shooterPos = entityPosition(beam.event.shooter); @@ -1500,10 +1505,55 @@ void GameWorldView::drawBeams(QPainter& painter) case BeamKind::Repair: color = m_visuals->beams.repairColor; break; case BeamKind::Salvage: color = m_visuals->beams.salvageColor; break; } - painter.setPen(QPen(color, m_visuals->beams.widthPx)); - painter.drawLine(worldToWidget(*shooterPos), - worldToWidget(*targetPos + beam.targetOffset)); + + const QPointF s = worldToWidget(*shooterPos); + const QPointF t = worldToWidget(*targetPos + beam.targetOffset); + + // Unit direction/perpendicular of the beam in widget space. A degenerate + // zero-length beam (shooter and target coincide) has no direction to + // taper along, so skip it. + const QVector2D delta(static_cast(t.x() - s.x()), + static_cast(t.y() - s.y())); + const float lengthPx = delta.length(); + if (lengthPx < 0.001f) { continue; } + const QVector2D dir = delta / lengthPx; + const QVector2D perp(-dir.y(), dir.x()); + + // Directional taper: draw the beam as a quad that is wide at the shooter + // and narrows to a faint tip at the target, so it reads as an arrow + // pointing away from whoever fired it. Without this, a beam strung + // between two nearby ships is symmetric and gives no cue which end is the + // source (the readability problem this addresses). + const float widthPx = std::max(1.0f, static_cast(m_visuals->beams.widthPx)); + const float baseHalf = widthPx * 1.f; + const float tipHalf = widthPx * 0.35f; + + QPolygonF quad; + quad << QPointF(s.x() + static_cast(perp.x() * baseHalf), + s.y() + static_cast(perp.y() * baseHalf)) + << QPointF(s.x() - static_cast(perp.x() * baseHalf), + s.y() - static_cast(perp.y() * baseHalf)) + << QPointF(t.x() - static_cast(perp.x() * tipHalf), + t.y() - static_cast(perp.y() * tipHalf)) + << QPointF(t.x() + static_cast(perp.x() * tipHalf), + t.y() + static_cast(perp.y() * tipHalf)); + + QColor bright = color; + bright.setAlpha(255); + QColor faint = color; + faint.setAlpha(90); + + QLinearGradient bodyGrad(s, t); + bodyGrad.setColorAt(0.0, bright); + bodyGrad.setColorAt(1.0, faint); + + painter.setPen(Qt::NoPen); + painter.setBrush(bodyGrad); + painter.drawPolygon(quad); } + + painter.setBrush(Qt::NoBrush); + painter.setRenderHints(savedHints); } void GameWorldView::drawOverlays(QPainter& painter)