diff --git a/data/strings/en.ini b/data/strings/en.ini index 02f91c16a..080609a71 100644 --- a/data/strings/en.ini +++ b/data/strings/en.ini @@ -526,22 +526,23 @@ pressure = Pressure pressure_tooltip = Control parameters through the pen pressure sensor velocity = Velocity velocity_tooltip = Control parameters through the mouse velocity -size = Size: +size = Size size_tooltip = << - - + + diff --git a/src/app/tools/dynamics.h b/src/app/tools/dynamics.h index a155da13a..45efc3d5e 100644 --- a/src/app/tools/dynamics.h +++ b/src/app/tools/dynamics.h @@ -8,7 +8,6 @@ #define APP_TOOLS_DYNAMICS_H_INCLUDED #pragma once -#include "render/dithering_algorithm.h" #include "render/dithering_matrix.h" namespace app { @@ -26,8 +25,9 @@ namespace tools { DynamicSensor gradient = DynamicSensor::Static; int maxSize = 0; int maxAngle = 0; - render::DitheringAlgorithm ditheringAlgorithm = render::DitheringAlgorithm::None; render::DitheringMatrix ditheringMatrix; + float minPressureThreshold = 0.0f, maxPressureThreshold = 1.0f; + float minVelocityThreshold = 0.0f, maxVelocityThreshold = 1.0f; bool isDynamic() const { return (size != DynamicSensor::Static || diff --git a/src/app/tools/tool_loop_manager.cpp b/src/app/tools/tool_loop_manager.cpp index 66a2e3e02..321a4ce1b 100644 --- a/src/app/tools/tool_loop_manager.cpp +++ b/src/app/tools/tool_loop_manager.cpp @@ -420,12 +420,44 @@ void ToolLoopManager::adjustPointWithDynamics(const Pointer& pointer, // Pressure bool hasP = (pointer.type() == Pointer::Type::Pen || pointer.type() == Pointer::Type::Eraser); - float p = (hasP ? pointer.pressure(): 1.0f); + float p = 1.0f; + if (hasP) { + p = pointer.pressure(); + if (p < m_dynamics.minPressureThreshold) { + p = 0.0f; + } + else if (p > m_dynamics.maxPressureThreshold || + // To avoid div by zero + m_dynamics.minPressureThreshold == m_dynamics.maxPressureThreshold) { + p = 1.0f; + } + else { + p = + (p - m_dynamics.minPressureThreshold) / + (m_dynamics.maxPressureThreshold - m_dynamics.minPressureThreshold); + } + } ASSERT(p >= 0.0f && p <= 1.0f); + p = base::clamp(p, 0.0f, 1.0f); // Velocity float v = float(std::sqrt(m_velocity.x*m_velocity.x + - m_velocity.y*m_velocity.y)) / 16.0f; // TODO 16 should be configurable + m_velocity.y*m_velocity.y)) / 32.0f; // TODO 32 should be configurable + v = base::clamp(v, 0.0f, 1.0f); + if (v < m_dynamics.minVelocityThreshold) { + v = 0.0f; + } + else if (v > m_dynamics.maxVelocityThreshold || + // To avoid div by zero + m_dynamics.minVelocityThreshold == m_dynamics.maxVelocityThreshold) { + v = 1.0f; + } + else { + v = + (v - m_dynamics.minVelocityThreshold) / + (m_dynamics.maxVelocityThreshold - m_dynamics.minVelocityThreshold); + } + ASSERT(v >= 0.0f && v <= 1.0f); v = base::clamp(v, 0.0f, 1.0f); switch (m_dynamics.size) { diff --git a/src/app/ui/dynamics_popup.cpp b/src/app/ui/dynamics_popup.cpp index 03d87730f..6d519b3a6 100644 --- a/src/app/ui/dynamics_popup.cpp +++ b/src/app/ui/dynamics_popup.cpp @@ -13,10 +13,18 @@ #include "app/ui/dithering_selector.h" #include "app/ui/skin/skin_theme.h" #include "base/clamp.h" +#include "os/font.h" +#include "os/surface.h" #include "ui/message.h" +#include "ui/paint_event.h" +#include "ui/scale.h" +#include "ui/size_hint_event.h" +#include "ui/widget.h" #include "dynamics.xml.h" + #include +#include namespace app { @@ -42,6 +50,163 @@ enum { } // anonymous namespace +// Special slider to set min/max values of a sensor +class DynamicsPopup::MinMaxSlider : public Widget { +public: + MinMaxSlider() { + setExpansive(true); + } + + float minThreshold() const { return m_minThreshold; } + float maxThreshold() const { return m_maxThreshold; } + void setSensorValue(float v) { + m_sensorValue = v; + invalidate(); + } + +private: + void onInitTheme(InitThemeEvent& ev) override { + SkinTheme* theme = static_cast(this->theme()); + setBorder( + gfx::Border( + theme->parts.miniSliderEmpty()->bitmapW()->width(), + theme->parts.miniSliderEmpty()->bitmapN()->height(), + theme->parts.miniSliderEmpty()->bitmapE()->width(), + theme->parts.miniSliderEmpty()->bitmapS()->height())); + + Widget::onInitTheme(ev); + } + + void onSizeHint(SizeHintEvent& ev) override { + int w = 0; + int h = 2*textHeight(); + + w += border().width(); + h += border().height(); + + ev.setSizeHint(w, h); + } + + void onPaint(PaintEvent& ev) override { + Graphics* g = ev.graphics(); + SkinTheme* theme = static_cast(this->theme()); + gfx::Rect rc = clientBounds(); + gfx::Color bgcolor = bgColor(); + g->fillRect(bgcolor, rc); + + rc.shrink(border()); + const int minX = this->minX(); + const int maxX = this->maxX(); + rc = clientBounds(); + + // Draw customized background + const skin::SkinPartPtr& nw = theme->parts.miniSliderEmpty(); + os::Surface* thumb = + (hasFocus() ? theme->parts.miniSliderThumbFocused()->bitmap(0): + theme->parts.miniSliderThumb()->bitmap(0)); + + // Draw background + g->fillRect(bgcolor, rc); + + // Draw thumb + int thumb_y = rc.y; + rc.shrink(gfx::Border(0, thumb->height(), 0, 0)); + + // Draw borders + if (rc.h > 4*guiscale()) { + rc.shrink(gfx::Border(3, 0, 3, 1) * guiscale()); + theme->drawRect(g, rc, nw.get()); + } + + const int sensorW = float(rc.w)*m_sensorValue; + + // Draw background + if (m_minThreshold > 0.0f) { + theme->drawRect( + g, gfx::Rect(rc.x, rc.y, minX-rc.x, rc.h), + theme->parts.miniSliderFull().get()); + } + if (m_maxThreshold < 1.0f) { + theme->drawRect( + g, gfx::Rect(maxX, rc.y, rc.x2()-maxX, rc.h), + theme->parts.miniSliderFull().get()); + } + + g->fillRect(theme->colors.sliderEmptyText(), + gfx::Rect(rc.x, rc.y+rc.h/2-rc.h/8, sensorW, rc.h/4)); + + g->drawRgbaSurface(thumb, minX-thumb->width()/2, thumb_y); + g->drawRgbaSurface(thumb, maxX-thumb->width()/2, thumb_y); + } + + bool onProcessMessage(Message* msg) override { + switch (msg->type()) { + + case kMouseDownMessage: { + auto mouseMsg = static_cast(msg); + const int u = mouseMsg->position().x - origin().x; + const int minX = this->minX(); + const int maxX = this->maxX(); + if (ABS(u-minX) < + ABS(u-maxX)) + capture = Capture::Min; + else + capture = Capture::Max; + captureMouse(); + break; + } + + case kMouseUpMessage: + if (hasCapture()) + releaseMouse(); + break; + + case kMouseMoveMessage: { + if (!hasCapture()) + break; + + auto mouseMsg = static_cast(msg); + const gfx::Rect rc = bounds(); + float u = (mouseMsg->position().x - rc.x) / float(rc.w); + u = base::clamp(u, 0.0f, 1.0f); + switch (capture) { + case Capture::Min: + m_minThreshold = u; + if (m_maxThreshold < u) + m_maxThreshold = u; + invalidate(); + break; + case Capture::Max: + m_maxThreshold = u; + if (m_minThreshold > u) + m_minThreshold = u; + invalidate(); + break; + } + break; + } + } + return Widget::onProcessMessage(msg); + } + + int minX() const { + gfx::Rect rc = clientBounds(); + return rc.x + float(rc.w)*m_minThreshold; + } + + int maxX() const { + gfx::Rect rc = clientBounds(); + return rc.x + float(rc.w)*m_maxThreshold; + } + + enum Capture { Min, Max }; + + float m_minThreshold = 0.1f; + float m_sensorValue = 0.0f; + float m_maxThreshold = 0.9f; + Capture capture; +}; + DynamicsPopup::DynamicsPopup(Delegate* delegate) : PopupWindow("", PopupWindow::ClickBehavior::CloseOnClickOutsideHotRegion, @@ -56,6 +221,8 @@ DynamicsPopup::DynamicsPopup(Delegate* delegate) }); m_dynamics->gradientPlaceholder()->addChild(m_ditheringSel); + m_dynamics->pressurePlaceholder()->addChild(m_pressureTweaks = new MinMaxSlider); + m_dynamics->velocityPlaceholder()->addChild(m_velocityTweaks = new MinMaxSlider); addChild(m_dynamics); onValuesChange(nullptr); @@ -78,8 +245,13 @@ tools::DynamicsOptions DynamicsPopup::getDynamics() const tools::DynamicSensor::Static); opts.maxSize = m_dynamics->maxSize()->getValue(); opts.maxAngle = m_dynamics->maxAngle()->getValue(); - opts.ditheringAlgorithm = m_ditheringSel->ditheringAlgorithm(); opts.ditheringMatrix = m_ditheringSel->ditheringMatrix(); + + opts.minPressureThreshold = m_pressureTweaks->minThreshold(); + opts.maxPressureThreshold = m_pressureTweaks->maxThreshold(); + opts.minVelocityThreshold = m_velocityTweaks->minThreshold(); + opts.maxVelocityThreshold = m_velocityTweaks->maxThreshold(); + return opts; } @@ -129,6 +301,12 @@ void DynamicsPopup::onValuesChange(ButtonSet::Item* item) } } + const bool hasPressure = (isCheck(SIZE_WITH_PRESSURE) || + isCheck(ANGLE_WITH_PRESSURE) || + isCheck(GRADIENT_WITH_PRESSURE)); + const bool hasVelocity = (isCheck(SIZE_WITH_VELOCITY) || + isCheck(ANGLE_WITH_VELOCITY) || + isCheck(GRADIENT_WITH_VELOCITY)); const bool needsSize = (isCheck(SIZE_WITH_PRESSURE) || isCheck(SIZE_WITH_VELOCITY)); const bool needsAngle = (isCheck(ANGLE_WITH_PRESSURE) || @@ -156,6 +334,11 @@ void DynamicsPopup::onValuesChange(ButtonSet::Item* item) m_dynamics->separator()->setVisible(any); m_dynamics->options()->setVisible(any); + m_dynamics->separator2()->setVisible(any); + m_dynamics->pressureLabel()->setVisible(hasPressure); + m_dynamics->pressurePlaceholder()->setVisible(hasPressure); + m_dynamics->velocityLabel()->setVisible(hasVelocity); + m_dynamics->velocityPlaceholder()->setVisible(hasVelocity); auto oldBounds = bounds(); layout(); @@ -171,13 +354,58 @@ void DynamicsPopup::onValuesChange(ButtonSet::Item* item) bool DynamicsPopup::onProcessMessage(Message* msg) { switch (msg->type()) { + case kOpenMessage: m_hotRegion = gfx::Region(bounds()); setHotRegion(m_hotRegion); + manager()->addMessageFilter(kMouseMoveMessage, this); + disableFlags(IGNORE_MOUSE); break; + case kCloseMessage: m_hotRegion.clear(); + manager()->removeMessageFilter(kMouseMoveMessage, this); break; + + case kMouseEnterMessage: { + auto mouseMsg = static_cast(msg); + m_lastPos = mouseMsg->position(); + m_velocity = gfx::Point(0, 0); + m_lastPointerT = base::current_tick(); + break; + } + + case kMouseMoveMessage: { + auto mouseMsg = static_cast(msg); + + if (mouseMsg->pointerType() == PointerType::Pen || + mouseMsg->pointerType() == PointerType::Eraser) { + if (m_dynamics->pressurePlaceholder()->isVisible()) { + m_pressureTweaks->setSensorValue(mouseMsg->pressure()); + } + } + + if (m_dynamics->velocityPlaceholder()->isVisible()) { + // TODO merge this with ToolLoopManager::getSpriteStrokePt() and + // ToolLoopManager::adjustPointWithDynamics() + const base::tick_t t = base::current_tick(); + const base::tick_t dt = t - m_lastPointerT; + m_lastPointerT = t; + + float a = base::clamp(float(dt) / 50.0f, 0.0f, 1.0f); + + gfx::Point newVelocity(mouseMsg->position() - m_lastPos); + m_velocity.x = (1.0f-a)*m_velocity.x + a*newVelocity.x; + m_velocity.y = (1.0f-a)*m_velocity.y + a*newVelocity.y; + m_lastPos = mouseMsg->position(); + + float v = float(std::sqrt(m_velocity.x*m_velocity.x + + m_velocity.y*m_velocity.y)) / 32.0f; // TODO 32 should be configurable + v = base::clamp(v, 0.0f, 1.0f); + m_velocityTweaks->setSensorValue(v); + } + break; + } } return PopupWindow::onProcessMessage(msg); } diff --git a/src/app/ui/dynamics_popup.h b/src/app/ui/dynamics_popup.h index 09a95e914..c7373e37b 100644 --- a/src/app/ui/dynamics_popup.h +++ b/src/app/ui/dynamics_popup.h @@ -10,6 +10,7 @@ #include "app/tools/dynamics.h" #include "app/ui/button_set.h" +#include "base/time.h" #include "doc/brush.h" #include "gfx/region.h" #include "ui/popup_window.h" @@ -33,6 +34,8 @@ namespace app { tools::DynamicsOptions getDynamics() const; private: + class MinMaxSlider; + void setCheck(int i, bool state); bool isCheck(int i) const; void onValuesChange(ButtonSet::Item* item); @@ -42,6 +45,10 @@ namespace app { gen::Dynamics* m_dynamics; DitheringSelector* m_ditheringSel; gfx::Region m_hotRegion; + MinMaxSlider* m_pressureTweaks; + MinMaxSlider* m_velocityTweaks; + gfx::Point m_lastPos, m_velocity; + base::tick_t m_lastPointerT; }; } // namespace app diff --git a/src/app/ui/editor/tool_loop_impl.cpp b/src/app/ui/editor/tool_loop_impl.cpp index 8a9c2ac6f..01e44d48c 100644 --- a/src/app/ui/editor/tool_loop_impl.cpp +++ b/src/app/ui/editor/tool_loop_impl.cpp @@ -146,8 +146,11 @@ public: , m_secondaryColor(button == tools::ToolLoop::Left ? m_bgColor: m_fgColor) { #ifdef ENABLE_UI // TODO add dynamics support when UI is not enabled - if (m_controller->isFreehand()) + if (m_controller->isFreehand() && + !m_ink->isEraser() && + !m_pointShape->isFloodFill()) { m_dynamics = App::instance()->contextBar()->getDynamics(); + } #endif if (m_tracePolicy == tools::TracePolicy::Accumulate ||