mirror of https://github.com/aseprite/aseprite.git
				
				
				
			Add sensor tweaks to specify min/max thresholds of the sensor input
In this way we can translate the sensor input to a better output range for our specific device (mouse, stylus, etc.).
This commit is contained in:
		
							parent
							
								
									8677c809fe
								
							
						
					
					
						commit
						1d15bacdcd
					
				|  | @ -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 = <<<END | ||||
| Change the brush size | ||||
| depending on the sensor value | ||||
| END | ||||
| angle = Angle: | ||||
| angle = Angle | ||||
| angle_tooltip = <<<END | ||||
| Change the brush angle | ||||
| depending on the sensor value | ||||
| END | ||||
| gradient = Gradient: | ||||
| gradient = Gradient | ||||
| gradient_tooltip = <<<END | ||||
| Gradient between foreground | ||||
| and background colors | ||||
| END | ||||
| max_point_value = Max Point Value: | ||||
| sensors_tweaks = Sensor Tweaks | ||||
| 
 | ||||
| [export_file] | ||||
| title = Export File | ||||
|  |  | |||
|  | @ -22,9 +22,9 @@ | |||
|     </buttonset> | ||||
|     </hbox> | ||||
| 
 | ||||
|     <separator id="separator" text="@.max_point_value" horizontal="true" /> | ||||
| 
 | ||||
|     <grid id="options" columns="2" childspacing="0" expansive="true"> | ||||
|       <separator id="separator" text="@.max_point_value" horizontal="true" cell_hspan="2" /> | ||||
| 
 | ||||
|       <label id="max_size_label" text="@.size" style="mini_label" /> | ||||
|       <slider id="max_size" value="64" min="1" max="64" cell_align="horizontal" /> | ||||
| 
 | ||||
|  | @ -33,6 +33,13 @@ | |||
| 
 | ||||
|       <label id="gradient_label" text="@.gradient" style="mini_label"  /> | ||||
|       <hbox id="gradient_placeholder" /> | ||||
| 
 | ||||
|       <separator id="separator2" text="@.sensors_tweaks" horizontal="true" cell_hspan="2" /> | ||||
| 
 | ||||
|       <label id="pressure_label" text="@.pressure" style="mini_label" /> | ||||
|       <hbox id="pressure_placeholder" cell_align="horizontal" /> | ||||
|       <label id="velocity_label" text="@.velocity" style="mini_label" /> | ||||
|       <hbox id="velocity_placeholder" cell_align="horizontal" /> | ||||
|     </grid> | ||||
| 
 | ||||
|   </vbox> | ||||
|  |  | |||
|  | @ -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 || | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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 <algorithm> | ||||
| #include <cmath> | ||||
| 
 | ||||
| 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<SkinTheme*>(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<SkinTheme*>(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<MouseMessage*>(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<MouseMessage*>(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<MouseMessage*>(msg); | ||||
|       m_lastPos = mouseMsg->position(); | ||||
|       m_velocity = gfx::Point(0, 0); | ||||
|       m_lastPointerT = base::current_tick(); | ||||
|       break; | ||||
|     } | ||||
| 
 | ||||
|     case kMouseMoveMessage: { | ||||
|       auto mouseMsg = static_cast<MouseMessage*>(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); | ||||
| } | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -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 || | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue