fix: blobs now allow to be drawn in device pixels to stay sharp
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 25s
Python / static (pull_request) Successful in 34s
Rust / build (pull_request) Successful in 51s
Python / verify (pull_request) Failing after 1m25s
Rust / fmt (pull_request) Successful in 39s
Rust / clippy (pull_request) Successful in 59s

This commit is contained in:
2026-10-30 01:52:33 +01:00
parent 78a9577209
commit c5608dc9b9
30 changed files with 794 additions and 435 deletions
+109 -50
View File
@@ -6,14 +6,59 @@
namespace {
constexpr float k_retireMs = 120.0f;
constexpr qint64 k_minEvalMs = 10;
constexpr float k_staleSampleSec = 0.1f;
constexpr float k_staleMovePx = 2.0f;
constexpr float k_moveSpeed = 5.0f;
constexpr float k_settleDelta = 0.004f;
constexpr float k_settleVel = 0.05f;
constexpr float k_identityEps = 0.0005f;
constexpr float k_stillPx = 0.5f;
constexpr int k_settleStillSamples = 3;
} // namespace
BlobRect2::BlobRect2(QQuickItem* parent) : BlobShape2(parent) {}
BlobRect2::BlobRect2(QQuickItem* parent) : BlobShape2(parent) {
m_stillSamples = k_settleStillSamples;
}
BlobRect2::~BlobRect2() {
if (m_group) m_group->removeShape(this);
}
bool BlobRect2::needsFrame() const {
return m_physicsActive || m_retiring ||
m_stillSamples < k_settleStillSamples ||
std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) +
std::abs(m_dm11 - 1.0f) >
k_identityEps;
}
bool BlobRect2::isMoving() const {
return m_atRestOverride || m_physicsActive || m_retiring ||
m_stillSamples < k_settleStillSamples;
}
void BlobRect2::updateMoving() {
const bool moving = isMoving();
if (moving == m_movingEmitted) return;
m_movingEmitted = moving;
emit movingChanged();
}
void BlobRect2::resetDeformMatrix() {
m_dm00 = 1.0f;
m_dm01 = 0.0f;
m_dm11 = 1.0f;
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
m_retiring = false;
m_physicsActive = false;
BlobShape2::resetDeformMatrix();
}
void BlobRect2::requestPhysics() {
if (m_group) m_group->requestPhysics();
}
void BlobRect2::finishAtIdentity() {
m_dm00 = 1.0f;
m_dm01 = 0.0f;
@@ -23,11 +68,12 @@ void BlobRect2::finishAtIdentity() {
m_physicsActive = false;
emit rawDeformMatrixChanged();
updateCenteredDeformMatrix();
if (m_group) m_group->markDirty();
updateMoving();
}
void BlobRect2::beginRetire() {
// The settle check would otherwise re-enter this every frame and restart the
// curve timer, so the curve never reaches t = 1.
if (m_retiring) return;
m_retire00 = m_dm00;
@@ -36,10 +82,26 @@ void BlobRect2::beginRetire() {
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
m_retireElapsed.start();
m_retiring = true;
requestPhysics();
updateMoving();
}
void BlobRect2::trySettle(float speed) {
if (m_retiring) return;
if (!m_physicsActive) return;
const float totalDelta =
std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f);
const float totalVel =
std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11);
if (speed < k_moveSpeed && totalDelta < k_settleDelta &&
totalVel < k_settleVel && m_stillSamples >= k_settleStillSamples) {
beginRetire();
}
}
void BlobRect2::updatePhysics() {
// Shapes are data only, so the group drives physics here.
if (m_retiring) {
const float t = std::clamp(
static_cast<float>(m_retireElapsed.elapsed()) / k_retireMs,
@@ -63,12 +125,16 @@ void BlobRect2::updatePhysics() {
if (!m_physicsActive) return;
}
// No "not active" guard here: an inactive shape still has to be evaluated so it
// can see the panel start moving and become active. The frame loop is only kept
// alive while active, and it is requested before anything can return early —
// requesting it at the end lets a shape stop being evaluated with the matrix
// still off identity, which is what left the panel contents blurry.
if (m_physicsActive && m_group) m_group->markDirty();
if (!m_physicsActive && !m_retiring) {
const float stuck = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) +
std::abs(m_dm11 - 1.0f);
if (stuck > k_identityEps) {
beginRetire();
return;
}
}
if (m_physicsActive) requestPhysics();
const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0));
@@ -76,34 +142,54 @@ void BlobRect2::updatePhysics() {
m_prevScenePos = scenePos;
m_elapsed.start();
m_hasPrevPos = true;
m_stillSamples = k_settleStillSamples;
updateMoving();
return;
}
const float dt = static_cast<float>(m_elapsed.restart()) / 1000.0f;
if (dt > 0.1f || dt < 0.001f) {
m_prevScenePos = scenePos;
if (m_physicsActive) checkAtRest(0.0f);
const qint64 evalMs = m_elapsed.elapsed();
if (evalMs < k_minEvalMs) {
return;
}
m_elapsed.restart();
const float velX =
static_cast<float>(scenePos.x() - m_prevScenePos.x()) / dt;
const float velY =
static_cast<float>(scenePos.y() - m_prevScenePos.y()) / dt;
const float dx = static_cast<float>(scenePos.x() - m_prevScenePos.x());
const float dy = static_cast<float>(scenePos.y() - m_prevScenePos.y());
m_prevScenePos = scenePos;
if (std::abs(dx) + std::abs(dy) > k_stillPx)
m_stillSamples = 0;
else
m_stillSamples += 1;
const float dt = static_cast<float>(evalMs) / 1000.0f;
if (dt > k_staleSampleSec) {
if (std::abs(dx) + std::abs(dy) > k_staleMovePx) {
m_physicsActive = true;
requestPhysics();
updateMoving();
} else {
trySettle(0.0f);
}
return;
}
const float velX = dx / dt;
const float velY = dy / dt;
const float speed = std::sqrt(velX * velX + velY * velY);
if (m_retiring) {
if (speed > 5.0f)
if (speed > k_moveSpeed)
m_retiring = false;
else
return;
}
if (!m_physicsActive) {
if (speed < 5.0f) return;
if (speed < k_moveSpeed) return;
m_physicsActive = true;
requestPhysics();
}
const float kStretchFactor = static_cast<float>(m_deformScale);
@@ -113,7 +199,7 @@ void BlobRect2::updatePhysics() {
float target01 = 0.0f;
float target11 = 1.0f;
if (speed > 5.0f) {
if (speed > k_moveSpeed) {
const float targetStretch =
1.0f + std::min(speed * kStretchFactor, kMaxStretch);
const float targetCompress = 1.0f / targetStretch;
@@ -147,22 +233,8 @@ void BlobRect2::updatePhysics() {
emit rawDeformMatrixChanged();
updateCenteredDeformMatrix();
// Start the retire as soon as the matrix is close to identity, as in v1. Waiting
// for every term to fall below kEpsilon keeps the matrix just off identity for
// longer, and the panel content is transformed by it, which reads as blur on
// text.
const float totalDelta =
std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f);
const float totalVel =
std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11);
// The residual is already inside the rest epsilon, so the eased curve would only
// be aborted or restarted every frame by the panel creeping. Snap instead.
if (totalDelta < 0.004f && totalVel < 0.05f) {
finishAtIdentity();
return;
}
checkAtRest(speed);
trySettle(speed);
updateMoving();
}
void BlobRect2::setTopLeftRadius(qreal r) {
@@ -338,16 +410,3 @@ void BlobRect2::excludeCornersRemoveLast(QQmlListProperty<BlobRect2>* prop) {
if (self->m_group) self->m_group->markDirty();
emit self->excludeCornersChanged();
}
void BlobRect2::checkAtRest(float speed) {
if (m_retiring) return;
constexpr float kEpsilon = 0.002f;
const bool atRest =
std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon &&
std::abs(m_dm11 - 1.0f) < kEpsilon && std::abs(m_dmVel00) < kEpsilon &&
std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon &&
speed < 5.0f;
if (atRest) beginRetire();
}