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:
David Capello 2020-04-23 18:03:34 -03:00
parent 8677c809fe
commit 1d15bacdcd
7 changed files with 289 additions and 11 deletions

View File

@ -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

View 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>

View File

@ -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 ||

View File

@ -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) {

View File

@ -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);
}

View File

@ -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

View File

@ -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 ||