#include "blobrect2.hpp" #include "blobgroup2.hpp" #include #include namespace { constexpr float k_retireMs = 120.0f; } // namespace BlobRect2::BlobRect2(QQuickItem* parent) : BlobShape2(parent) {} BlobRect2::~BlobRect2() { if (m_group) m_group->removeShape(this); } void BlobRect2::finishAtIdentity() { m_dm00 = 1.0f; m_dm01 = 0.0f; m_dm11 = 1.0f; m_deformMatrix = QMatrix4x4(); m_retiring = false; m_physicsActive = false; emit rawDeformMatrixChanged(); updateCenteredDeformMatrix(); } 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; m_retire01 = m_dm01; m_retire11 = m_dm11; m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f; m_retireElapsed.start(); m_retiring = true; } void BlobRect2::updatePhysics() { // Shapes are data only, so the group drives physics here. if (m_retiring) { const float t = std::clamp( static_cast(m_retireElapsed.elapsed()) / k_retireMs, 0.0f, 1.0f); const float s = t * t * (3.0f - 2.0f * t); if (t >= 1.0f) { finishAtIdentity(); return; } m_dm00 = m_retire00 + (1.0f - m_retire00) * s; m_dm01 = m_retire01 - m_retire01 * s; m_dm11 = m_retire11 + (1.0f - m_retire11) * s; m_deformMatrix = QMatrix4x4( m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); emit rawDeformMatrixChanged(); updateCenteredDeformMatrix(); 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(); const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0)); if (!m_hasPrevPos) { m_prevScenePos = scenePos; m_elapsed.start(); m_hasPrevPos = true; return; } const float dt = static_cast(m_elapsed.restart()) / 1000.0f; if (dt > 0.1f || dt < 0.001f) { m_prevScenePos = scenePos; if (m_physicsActive) checkAtRest(0.0f); return; } const float velX = static_cast(scenePos.x() - m_prevScenePos.x()) / dt; const float velY = static_cast(scenePos.y() - m_prevScenePos.y()) / dt; m_prevScenePos = scenePos; const float speed = std::sqrt(velX * velX + velY * velY); if (m_retiring) { if (speed > 5.0f) m_retiring = false; else return; } if (!m_physicsActive) { if (speed < 5.0f) return; m_physicsActive = true; } const float kStretchFactor = static_cast(m_deformScale); constexpr float kMaxStretch = 0.35f; float target00 = 1.0f; float target01 = 0.0f; float target11 = 1.0f; if (speed > 5.0f) { const float targetStretch = 1.0f + std::min(speed * kStretchFactor, kMaxStretch); const float targetCompress = 1.0f / targetStretch; const float cosA = velX / speed; const float sinA = velY / speed; const float cos2 = cosA * cosA; const float sin2 = sinA * sinA; const float cs = cosA * sinA; target00 = targetStretch * cos2 + targetCompress * sin2; target01 = (targetStretch - targetCompress) * cs; target11 = targetStretch * sin2 + targetCompress * cos2; } const float kStiffness = static_cast(m_stiffness); const float kDamping = static_cast(m_damping); const float invDamp = 1.0f / (1.0f + kDamping * dt); m_dmVel00 = (m_dmVel00 - kStiffness * (m_dm00 - target00) * dt) * invDamp; m_dm00 += m_dmVel00 * dt; m_dmVel01 = (m_dmVel01 - kStiffness * (m_dm01 - target01) * dt) * invDamp; m_dm01 += m_dmVel01 * dt; m_dmVel11 = (m_dmVel11 - kStiffness * (m_dm11 - target11) * dt) * invDamp; m_dm11 += m_dmVel11 * dt; m_deformMatrix = QMatrix4x4( m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); 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); } void BlobRect2::setTopLeftRadius(qreal r) { if (!qFuzzyCompare(m_topLeftRadius, r)) { m_topLeftRadius = r; emit topLeftRadiusChanged(); if (m_group) m_group->markDirty(); } } void BlobRect2::setTopRightRadius(qreal r) { if (!qFuzzyCompare(m_topRightRadius, r)) { m_topRightRadius = r; emit topRightRadiusChanged(); if (m_group) m_group->markDirty(); } } void BlobRect2::setBottomLeftRadius(qreal r) { if (!qFuzzyCompare(m_bottomLeftRadius, r)) { m_bottomLeftRadius = r; emit bottomLeftRadiusChanged(); if (m_group) m_group->markDirty(); } } void BlobRect2::setBottomRightRadius(qreal r) { if (!qFuzzyCompare(m_bottomRightRadius, r)) { m_bottomRightRadius = r; emit bottomRightRadiusChanged(); if (m_group) m_group->markDirty(); } } void BlobRect2::cornerRadii(float out[4]) const { const auto maxR = static_cast(std::min(width(), height())) * 0.5f; const auto base = std::min(static_cast(m_radius), maxR); out[0] = std::min( m_topRightRadius >= 0 ? static_cast(m_topRightRadius) : base, maxR); out[1] = std::min( m_bottomRightRadius >= 0 ? static_cast(m_bottomRightRadius) : base, maxR); out[2] = std::min( m_bottomLeftRadius >= 0 ? static_cast(m_bottomLeftRadius) : base, maxR); out[3] = std::min( m_topLeftRadius >= 0 ? static_cast(m_topLeftRadius) : base, maxR); } bool BlobRect2::isExcluded(const BlobShape2* other) const { for (const auto& ptr : m_exclude) { if (ptr == other) return true; } return false; } bool BlobRect2::isCornerExcluded(const BlobShape2* other) const { for (const auto& ptr : m_excludeCorners) { if (ptr == other) return true; } return false; } QQmlListProperty BlobRect2::exclude() { return QQmlListProperty( this, nullptr, &excludeAppend, &excludeCount, &excludeAt, &excludeClear, &excludeReplace, &excludeRemoveLast); } QQmlListProperty BlobRect2::excludeCorners() { return QQmlListProperty( this, nullptr, &excludeCornersAppend, &excludeCornersCount, &excludeCornersAt, &excludeCornersClear, &excludeCornersReplace, &excludeCornersRemoveLast); } void BlobRect2::excludeAppend( QQmlListProperty* prop, BlobRect2* rect) { auto* self = static_cast(prop->object); self->m_exclude.append(rect); if (self->m_group) self->m_group->markDirty(); emit self->excludeChanged(); } qsizetype BlobRect2::excludeCount(QQmlListProperty* prop) { auto* self = static_cast(prop->object); return self->m_exclude.size(); } BlobRect2* BlobRect2::excludeAt( QQmlListProperty* prop, qsizetype index) { auto* self = static_cast(prop->object); return self->m_exclude.at(index); } void BlobRect2::excludeClear(QQmlListProperty* prop) { auto* self = static_cast(prop->object); if (self->m_exclude.isEmpty()) return; self->m_exclude.clear(); if (self->m_group) self->m_group->markDirty(); emit self->excludeChanged(); } void BlobRect2::excludeReplace( QQmlListProperty* prop, qsizetype index, BlobRect2* rect) { auto* self = static_cast(prop->object); self->m_exclude[index] = rect; if (self->m_group) self->m_group->markDirty(); emit self->excludeChanged(); } void BlobRect2::excludeRemoveLast(QQmlListProperty* prop) { auto* self = static_cast(prop->object); if (self->m_exclude.isEmpty()) return; self->m_exclude.removeLast(); if (self->m_group) self->m_group->markDirty(); emit self->excludeChanged(); } void BlobRect2::excludeCornersAppend( QQmlListProperty* prop, BlobRect2* rect) { auto* self = static_cast(prop->object); self->m_excludeCorners.append(rect); if (self->m_group) self->m_group->markDirty(); emit self->excludeCornersChanged(); } qsizetype BlobRect2::excludeCornersCount(QQmlListProperty* prop) { auto* self = static_cast(prop->object); return self->m_excludeCorners.size(); } BlobRect2* BlobRect2::excludeCornersAt( QQmlListProperty* prop, qsizetype index) { auto* self = static_cast(prop->object); return self->m_excludeCorners.at(index); } void BlobRect2::excludeCornersClear(QQmlListProperty* prop) { auto* self = static_cast(prop->object); if (self->m_excludeCorners.isEmpty()) return; self->m_excludeCorners.clear(); if (self->m_group) self->m_group->markDirty(); emit self->excludeCornersChanged(); } void BlobRect2::excludeCornersReplace( QQmlListProperty* prop, qsizetype index, BlobRect2* rect) { auto* self = static_cast(prop->object); self->m_excludeCorners[index] = rect; if (self->m_group) self->m_group->markDirty(); emit self->excludeCornersChanged(); } void BlobRect2::excludeCornersRemoveLast(QQmlListProperty* prop) { auto* self = static_cast(prop->object); if (self->m_excludeCorners.isEmpty()) return; self->m_excludeCorners.removeLast(); 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(); }