mirror of
https://github.com/aseprite/aseprite.git
synced 2025-01-29 21:33:12 +00:00
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…
x
Reference in New Issue
Block a user