Outer Glow, brush Smoothing and the rest of Compositor 1.2.0
This commit is contained in:
1 parent
2cd0d1b86d
commit
374ba8ec73
32 files changed
+624
-47
No files matched your search
@@ -31,6 +31,8 @@ struct BrushSettings {
|
||||
double blue = 0;
|
||||
// Caps the whole stroke: overlapping dabs never exceed it.
|
||||
double opacity = 1;
|
||||
// 0–100: the brush trails the pointer on this string.
|
||||
double smoothing = 0;
|
||||
// Erase clears the layer's pixels instead of painting on them.
|
||||
bool erasing = false;
|
||||
// Spot healing takes nearby pixels instead of the colour.
|
||||
|
||||
@@ -111,6 +111,8 @@ void EditorSession::beginBrush(QPointF point)
|
||||
m_brushStroke->clone = clone;
|
||||
m_brushStroke->isBlur = m_tool == NavigationTool::blur;
|
||||
m_brushStroke->append(point);
|
||||
m_brushAnchor = point;
|
||||
m_brushPointer = point;
|
||||
m_lastBrushPoint = LastBrushPoint{point, layer.id, m_isMaskSelected};
|
||||
++m_brushRevision;
|
||||
resumeFileRequests();
|
||||
@@ -135,9 +137,13 @@ void EditorSession::continueBrush(QPointF point)
|
||||
}
|
||||
if (!m_brushStroke)
|
||||
return;
|
||||
m_brushPointer = point;
|
||||
const std::optional<QPointF> painted = smoothed(point);
|
||||
if (!painted)
|
||||
return;
|
||||
try {
|
||||
m_brushStroke->append(point);
|
||||
m_lastBrushPoint->point = point;
|
||||
m_brushStroke->append(*painted);
|
||||
m_lastBrushPoint->point = *painted;
|
||||
++m_brushRevision;
|
||||
notify();
|
||||
} catch (const ProjectError &error) {
|
||||
@@ -149,6 +155,20 @@ void EditorSession::continueBrush(QPointF point)
|
||||
}
|
||||
}
|
||||
|
||||
// Photoshop's string: the brush moves once pulled taut.
|
||||
std::optional<QPointF> EditorSession::smoothed(QPointF point)
|
||||
{
|
||||
if (m_tool != NavigationTool::brush || !(m_brushSettings.smoothing > 0) || !m_brushAnchor)
|
||||
return point;
|
||||
const double radius = m_brushSettings.smoothing / std::max(0.01, viewport.zoom());
|
||||
const QPointF delta = point - *m_brushAnchor;
|
||||
const double distance = std::hypot(delta.x(), delta.y());
|
||||
if (!(distance > radius))
|
||||
return std::nullopt;
|
||||
m_brushAnchor = *m_brushAnchor + delta * ((distance - radius) / distance);
|
||||
return m_brushAnchor;
|
||||
}
|
||||
|
||||
// Where a Shift-click's line starts, on the same target.
|
||||
std::optional<QPointF> EditorSession::shiftLineStart() const
|
||||
{
|
||||
@@ -161,6 +181,8 @@ void EditorSession::cancelBrush()
|
||||
{
|
||||
m_warpStroke.reset();
|
||||
m_brushStroke.reset();
|
||||
m_brushAnchor.reset();
|
||||
m_brushPointer.reset();
|
||||
++m_brushRevision;
|
||||
resumeFileRequests();
|
||||
notify();
|
||||
@@ -180,6 +202,9 @@ bool EditorSession::finishBrushImmediately()
|
||||
if (m_isProjectBusy)
|
||||
return false;
|
||||
try {
|
||||
// Smoothing leaves the brush short; the stroke ends there.
|
||||
if (m_brushPointer && m_brushAnchor && *m_brushPointer != *m_brushAnchor && m_tool == NavigationTool::brush && m_brushSettings.smoothing > 0)
|
||||
m_brushStroke->append(*m_brushPointer);
|
||||
m_brushStroke->flush();
|
||||
if (m_brushStroke->settings.healing)
|
||||
m_brushStroke->heal();
|
||||
|
||||
@@ -22,3 +22,5 @@ public:
|
||||
|
||||
private:
|
||||
void finishRasterCommit();
|
||||
// Where Smoothing lets the brush go; none while slack.
|
||||
std::optional<QPointF> smoothed(QPointF point);
|
||||
@@ -427,6 +427,9 @@ private:
|
||||
// Clone Stamp and the Smear keep their own soft tips.
|
||||
std::map<int, ParkedTip> m_parkedBrushTips{{1, {40, 0, 1}}, {2, {40, 0, 1}}};
|
||||
std::optional<LastBrushPoint> m_lastBrushPoint;
|
||||
// Smoothing's brush, and the pointer it trails.
|
||||
std::optional<QPointF> m_brushAnchor;
|
||||
std::optional<QPointF> m_brushPointer;
|
||||
int m_brushRevision = 0;
|
||||
std::optional<PendingOpacityDigit> m_pendingOpacityDigit;
|
||||
std::optional<CommittingRaster> m_committingRaster;
|
||||
|
||||
@@ -104,6 +104,8 @@ double LayerEffectsRenderer::margin(const LayerEffects &all)
|
||||
margin = std::max(margin, effects.stroke->size);
|
||||
if (effects.shadow)
|
||||
margin = std::max(margin, effects.shadow->distance + effects.shadow->blur * 3);
|
||||
if (effects.outerGlow)
|
||||
margin = std::max(margin, effects.outerGlow->size * 3);
|
||||
return std::ceil(margin) + 2;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ void take(LayerEffects &into, const LayerEffects &from, LayerEffectKind kind)
|
||||
case LayerEffectKind::shadow: into.shadow = from.shadow; return;
|
||||
case LayerEffectKind::colorOverlay: into.colorOverlay = from.colorOverlay; return;
|
||||
case LayerEffectKind::innerShadow: into.innerShadow = from.innerShadow; return;
|
||||
case LayerEffectKind::outerGlow: into.outerGlow = from.outerGlow; return;
|
||||
}
|
||||
throw std::logic_error("unknown effect kind");
|
||||
}
|
||||
@@ -70,6 +71,10 @@ void EditorSession::addEffect(LayerEffectKind kind)
|
||||
if (!effects.innerShadow)
|
||||
effects.innerShadow = InnerShadowEffect();
|
||||
break;
|
||||
case LayerEffectKind::outerGlow:
|
||||
if (!effects.outerGlow)
|
||||
effects.outerGlow = OuterGlowEffect();
|
||||
break;
|
||||
}
|
||||
setEffects(effects, id, QStringLiteral("Add ") + rawValue(kind));
|
||||
selectEffect(kind, id, true);
|
||||
|
||||
@@ -56,6 +56,11 @@ bool InnerShadowEffect::isValid() const
|
||||
return shadowIsValid(angle, distance, blur, opacity) && colourIsValid(red, green, blue);
|
||||
}
|
||||
|
||||
bool OuterGlowEffect::isValid() const
|
||||
{
|
||||
return within(size, 0, 500) && within(opacity, 0, 1) && colourIsValid(red, green, blue);
|
||||
}
|
||||
|
||||
QString rawValue(LayerEffectKind kind)
|
||||
{
|
||||
switch (kind) {
|
||||
@@ -63,6 +68,7 @@ QString rawValue(LayerEffectKind kind)
|
||||
case LayerEffectKind::shadow: return QStringLiteral("Drop Shadow");
|
||||
case LayerEffectKind::colorOverlay: return QStringLiteral("Color Overlay");
|
||||
case LayerEffectKind::innerShadow: return QStringLiteral("Inner Shadow");
|
||||
case LayerEffectKind::outerGlow: return QStringLiteral("Outer Glow");
|
||||
}
|
||||
throw std::logic_error("unknown effect kind");
|
||||
}
|
||||
@@ -70,7 +76,7 @@ QString rawValue(LayerEffectKind kind)
|
||||
bool LayerEffects::isValid() const
|
||||
{
|
||||
return (!stroke || stroke->isValid()) && (!shadow || shadow->isValid()) && (!colorOverlay || colorOverlay->isValid())
|
||||
&& (!innerShadow || innerShadow->isValid());
|
||||
&& (!innerShadow || innerShadow->isValid()) && (!outerGlow || outerGlow->isValid());
|
||||
}
|
||||
|
||||
std::vector<LayerEffectKind> LayerEffects::kinds() const
|
||||
@@ -90,6 +96,7 @@ bool LayerEffects::contains(LayerEffectKind kind) const
|
||||
case LayerEffectKind::shadow: return shadow.has_value();
|
||||
case LayerEffectKind::colorOverlay: return colorOverlay.has_value();
|
||||
case LayerEffectKind::innerShadow: return innerShadow.has_value();
|
||||
case LayerEffectKind::outerGlow: return outerGlow.has_value();
|
||||
}
|
||||
throw std::logic_error("unknown effect kind");
|
||||
}
|
||||
@@ -101,6 +108,7 @@ bool LayerEffects::isEnabled(LayerEffectKind kind) const
|
||||
case LayerEffectKind::shadow: return shadow && shadow->isEnabled();
|
||||
case LayerEffectKind::colorOverlay: return colorOverlay && colorOverlay->isEnabled();
|
||||
case LayerEffectKind::innerShadow: return innerShadow && innerShadow->isEnabled();
|
||||
case LayerEffectKind::outerGlow: return outerGlow && outerGlow->isEnabled();
|
||||
}
|
||||
throw std::logic_error("unknown effect kind");
|
||||
}
|
||||
@@ -112,6 +120,7 @@ std::optional<PaletteColor> LayerEffects::color(LayerEffectKind kind) const
|
||||
case LayerEffectKind::shadow: return shadow ? std::optional(shadow->color()) : std::nullopt;
|
||||
case LayerEffectKind::colorOverlay: return colorOverlay ? std::optional(colorOverlay->color()) : std::nullopt;
|
||||
case LayerEffectKind::innerShadow: return innerShadow ? std::optional(innerShadow->color()) : std::nullopt;
|
||||
case LayerEffectKind::outerGlow: return outerGlow ? std::optional(outerGlow->color()) : std::nullopt;
|
||||
}
|
||||
throw std::logic_error("unknown effect kind");
|
||||
}
|
||||
@@ -146,6 +155,7 @@ void LayerEffects::setColor(const PaletteColor &color, LayerEffectKind kind)
|
||||
case LayerEffectKind::shadow: paint(shadow, color); return;
|
||||
case LayerEffectKind::colorOverlay: paint(colorOverlay, color); return;
|
||||
case LayerEffectKind::innerShadow: paint(innerShadow, color); return;
|
||||
case LayerEffectKind::outerGlow: paint(outerGlow, color); return;
|
||||
}
|
||||
throw std::logic_error("unknown effect kind");
|
||||
}
|
||||
@@ -157,6 +167,7 @@ void LayerEffects::remove(LayerEffectKind kind)
|
||||
case LayerEffectKind::shadow: shadow.reset(); return;
|
||||
case LayerEffectKind::colorOverlay: colorOverlay.reset(); return;
|
||||
case LayerEffectKind::innerShadow: innerShadow.reset(); return;
|
||||
case LayerEffectKind::outerGlow: outerGlow.reset(); return;
|
||||
}
|
||||
throw std::logic_error("unknown effect kind");
|
||||
}
|
||||
@@ -168,11 +179,12 @@ void LayerEffects::setEnabled(bool enabled, LayerEffectKind kind)
|
||||
case LayerEffectKind::shadow: enable(shadow, enabled); return;
|
||||
case LayerEffectKind::colorOverlay: enable(colorOverlay, enabled); return;
|
||||
case LayerEffectKind::innerShadow: enable(innerShadow, enabled); return;
|
||||
case LayerEffectKind::outerGlow: enable(outerGlow, enabled); return;
|
||||
}
|
||||
throw std::logic_error("unknown effect kind");
|
||||
}
|
||||
|
||||
LayerEffects LayerEffects::visible() const
|
||||
{
|
||||
return LayerEffects{shown(stroke), shown(shadow), shown(colorOverlay), shown(innerShadow)};
|
||||
return LayerEffects{shown(stroke), shown(shadow), shown(colorOverlay), shown(innerShadow), shown(outerGlow)};
|
||||
}
|
||||
@@ -74,10 +74,24 @@ struct InnerShadowEffect {
|
||||
friend bool operator==(const InnerShadowEffect &, const InnerShadowEffect &) = default;
|
||||
};
|
||||
|
||||
// A soft glow round the outside of the layer.
|
||||
struct OuterGlowEffect {
|
||||
std::optional<bool> enabled = std::nullopt;
|
||||
double size = 20;
|
||||
double red = 1;
|
||||
double green = 1;
|
||||
double blue = 1;
|
||||
double opacity = 0.75;
|
||||
bool isEnabled() const { return enabled.value_or(true); }
|
||||
PaletteColor color() const { return {red, green, blue}; }
|
||||
bool isValid() const;
|
||||
friend bool operator==(const OuterGlowEffect &, const OuterGlowEffect &) = default;
|
||||
};
|
||||
|
||||
// Swift's raw values are the panel's and the menu's words.
|
||||
enum class LayerEffectKind { stroke, shadow, colorOverlay, innerShadow };
|
||||
enum class LayerEffectKind { stroke, shadow, colorOverlay, innerShadow, outerGlow };
|
||||
inline constexpr std::array allLayerEffectKinds{LayerEffectKind::stroke, LayerEffectKind::shadow, LayerEffectKind::colorOverlay,
|
||||
LayerEffectKind::innerShadow};
|
||||
LayerEffectKind::innerShadow, LayerEffectKind::outerGlow};
|
||||
QString rawValue(LayerEffectKind kind);
|
||||
|
||||
// What a layer draws round itself; its pixels stay untouched.
|
||||
@@ -86,7 +100,8 @@ struct LayerEffects {
|
||||
std::optional<ShadowEffect> shadow = std::nullopt;
|
||||
std::optional<ColorOverlayEffect> colorOverlay = std::nullopt;
|
||||
std::optional<InnerShadowEffect> innerShadow = std::nullopt;
|
||||
bool isEmpty() const { return !stroke && !shadow && !colorOverlay && !innerShadow; }
|
||||
std::optional<OuterGlowEffect> outerGlow = std::nullopt;
|
||||
bool isEmpty() const { return !stroke && !shadow && !colorOverlay && !innerShadow && !outerGlow; }
|
||||
bool isValid() const;
|
||||
std::vector<LayerEffectKind> kinds() const;
|
||||
bool contains(LayerEffectKind kind) const;
|
||||
|
||||
Reference in new issue
Block a user