Blur and noise adjustment layers, Image Trim, Inner Glow (12.8c)
This commit is contained in:
1 parent
7ae263de80
commit
31e2dd3864
60 files changed
+1673
-105
No files matched your search
@@ -81,6 +81,9 @@ void EditorSession::openAdjustmentEditor(QUuid id, const LayerAdjustment &origin
|
||||
case AdjustmentKind::exposure:
|
||||
case AdjustmentKind::gradientMap:
|
||||
case AdjustmentKind::grain:
|
||||
case AdjustmentKind::addNoise:
|
||||
case AdjustmentKind::gaussianBlur:
|
||||
case AdjustmentKind::motionBlur:
|
||||
case AdjustmentKind::blackWhite:
|
||||
case AdjustmentKind::colorBalance: {
|
||||
FilterSettings settings;
|
||||
@@ -90,6 +93,12 @@ void EditorSession::openAdjustmentEditor(QUuid id, const LayerAdjustment &origin
|
||||
settings.grain = original.grain();
|
||||
settings.blackWhite = original.blackWhite();
|
||||
settings.colorBalance = original.colorBalance();
|
||||
settings.radius = original.gaussianRadius();
|
||||
settings.angle = original.resolvedMotionAngle();
|
||||
settings.distance = original.resolvedMotionDistance();
|
||||
settings.amount = original.resolvedNoiseAmount();
|
||||
settings.gaussian = original.resolvedNoiseGaussian();
|
||||
settings.monochromatic = original.resolvedNoiseMonochromatic();
|
||||
m_filterEdit.emplace(filterKind(original.kind).value(), layer, std::nullopt, settings, std::nullopt);
|
||||
break;
|
||||
}
|
||||
@@ -121,6 +130,16 @@ LayerAdjustment EditorSession::editedAdjustment() const
|
||||
case AdjustmentKind::grain: value.setGrain(m_filterEdit.value().settings.grain); break;
|
||||
case AdjustmentKind::blackWhite: value.setBlackWhite(m_filterEdit.value().settings.blackWhite); break;
|
||||
case AdjustmentKind::colorBalance: value.setColorBalance(m_filterEdit.value().settings.colorBalance); break;
|
||||
case AdjustmentKind::gaussianBlur: value.setGaussianRadius(m_filterEdit.value().settings.radius); break;
|
||||
case AdjustmentKind::motionBlur:
|
||||
value.setResolvedMotionAngle(m_filterEdit.value().settings.angle);
|
||||
value.setResolvedMotionDistance(m_filterEdit.value().settings.distance);
|
||||
break;
|
||||
case AdjustmentKind::addNoise:
|
||||
value.setResolvedNoiseAmount(m_filterEdit.value().settings.amount);
|
||||
value.setResolvedNoiseGaussian(m_filterEdit.value().settings.gaussian);
|
||||
value.setResolvedNoiseMonochromatic(m_filterEdit.value().settings.monochromatic);
|
||||
break;
|
||||
case AdjustmentKind::invert: throw std::logic_error("Invert has no editor to read");
|
||||
}
|
||||
return value;
|
||||
|
||||
@@ -134,8 +134,8 @@ QImage PixelFilter::run(const FilterJob &job)
|
||||
break;
|
||||
case FilterKind::addNoise:
|
||||
image = drawn(job.image);
|
||||
noise_add(image.bits(), size_t(width), size_t(height), size_t(image.bytesPerLine()), float(settings.amount), settings.gaussian ? 1 : 0,
|
||||
settings.monochromatic ? 1 : 0, job.seed);
|
||||
noise_add_at(image.bits(), size_t(width), size_t(height), size_t(image.bytesPerLine()), float(settings.amount), settings.gaussian ? 1 : 0,
|
||||
settings.monochromatic ? 1 : 0, job.seed, int64_t(std::floor(job.noiseOrigin.x())), int64_t(std::floor(job.noiseOrigin.y())));
|
||||
break;
|
||||
case FilterKind::lensCorrection: {
|
||||
// Relative to the image's size, so previews bend alike.
|
||||
|
||||
@@ -87,6 +87,8 @@ struct FilterJob {
|
||||
// The point colour shown alone, or −1 for the grade.
|
||||
int visualizesPointColor = -1;
|
||||
bool showsSharpenMask = false;
|
||||
// A live adjustment's noise field, fixed in its region's pixels.
|
||||
QPointF noiseOrigin{};
|
||||
};
|
||||
|
||||
// Swift's PixelFilter.
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
#include "Document/ImageTrim.h"
|
||||
#include "Document/CanvasSize.h"
|
||||
#include "IO/CanvasResizer.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
#include <cstdlib>
|
||||
|
||||
extern "C" {
|
||||
#include "BrushPixels.h"
|
||||
}
|
||||
|
||||
QString rawValue(TrimBasedOn basedOn)
|
||||
{
|
||||
switch (basedOn) {
|
||||
case TrimBasedOn::transparentPixels: return QStringLiteral("Transparent Pixels");
|
||||
case TrimBasedOn::topLeftPixelColor: return QStringLiteral("Top Left Pixel Color");
|
||||
case TrimBasedOn::bottomRightPixelColor: return QStringLiteral("Bottom Right Pixel Color");
|
||||
}
|
||||
throw std::logic_error("unknown trim basis");
|
||||
}
|
||||
|
||||
bool TrimOptions::trimsAny() const
|
||||
{
|
||||
return top || bottom || left || right;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Unchecked edges stay at the image's; content is never empty.
|
||||
QRect kept(int left, int top, int right, int bottom, int width, int height, const TrimOptions &options)
|
||||
{
|
||||
const int minX = options.left ? left : 0, minY = options.top ? top : 0;
|
||||
const int maxX = options.right ? right : width, maxY = options.bottom ? bottom : height;
|
||||
return QRect(minX, minY, maxX - minX, maxY - minY);
|
||||
}
|
||||
|
||||
// Swift's calculateColorTrimRect: rows scanned in from both ends.
|
||||
std::optional<QRect> colorTrimRect(const QImage &pixels, QPoint sample, const TrimOptions &options)
|
||||
{
|
||||
const int width = pixels.width(), height = pixels.height(), tolerance = options.tolerance;
|
||||
const uchar *target = pixels.constScanLine(sample.y()) + sample.x() * 4;
|
||||
const auto matches = [&](const uchar *row, int x) {
|
||||
const uchar *pixel = row + x * 4;
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
if (std::abs(pixel[channel] - target[channel]) > tolerance)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
int left = width, right = 0, top = height, bottom = 0;
|
||||
for (int y = 0; y < height; ++y) {
|
||||
const uchar *row = pixels.constScanLine(y);
|
||||
int first = 0;
|
||||
while (first < width && matches(row, first))
|
||||
++first;
|
||||
if (first == width)
|
||||
continue;
|
||||
// The unmatched pixel at `first` stops this scan.
|
||||
int last = width;
|
||||
while (matches(row, last - 1))
|
||||
--last;
|
||||
left = std::min(left, first);
|
||||
right = std::max(right, last);
|
||||
top = std::min(top, y);
|
||||
bottom = y + 1;
|
||||
}
|
||||
// Every pixel matched the sample.
|
||||
if (right == 0)
|
||||
return std::nullopt;
|
||||
return kept(left, top, right, bottom, width, height, options);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<QRect> ImageTrim::calculateTrimRect(const QImage &image, const TrimOptions &options)
|
||||
{
|
||||
if (!options.trimsAny() || image.isNull())
|
||||
return std::nullopt;
|
||||
// Swift's try? returns nil; running out here is explained instead.
|
||||
const QImage pixels = image.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
|
||||
if (pixels.isNull())
|
||||
throw ExportError(ExportError::Kind::render);
|
||||
const int width = pixels.width(), height = pixels.height();
|
||||
switch (options.basedOn) {
|
||||
case TrimBasedOn::transparentPixels: {
|
||||
size_t edges[4] = {0, 0, 0, 0};
|
||||
brush_alpha_bounds(pixels.constBits(), size_t(width), size_t(height), size_t(pixels.bytesPerLine()), edges);
|
||||
// A right edge of zero: every pixel is clear.
|
||||
if (edges[2] == 0)
|
||||
return std::nullopt;
|
||||
return kept(int(edges[0]), int(edges[1]), int(edges[2]), int(edges[3]), width, height, options);
|
||||
}
|
||||
case TrimBasedOn::topLeftPixelColor: return colorTrimRect(pixels, QPoint(0, 0), options);
|
||||
case TrimBasedOn::bottomRightPixelColor: return colorTrimRect(pixels, QPoint(width - 1, height - 1), options);
|
||||
}
|
||||
throw std::logic_error("unknown trim basis");
|
||||
}
|
||||
|
||||
std::optional<ProjectSnapshot> ImageTrim::trim(const ProjectSnapshot &snapshot, const TrimOptions &options)
|
||||
{
|
||||
const std::optional<QRect> rect = calculateTrimRect(ImageExporter::render(snapshot).image, options);
|
||||
if (!rect)
|
||||
return std::nullopt;
|
||||
if (*rect == QRect(0, 0, int(snapshot.manifest.width), int(snapshot.manifest.height)))
|
||||
return snapshot;
|
||||
return CanvasResizer::resize(snapshot, CanvasSizeOptions{.width = rect->width(), .height = rect->height(),
|
||||
.contentOffset = QPointF(-rect->left(), -rect->top())});
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#include "IO/ProjectStore.h"
|
||||
#include <QImage>
|
||||
#include <QRect>
|
||||
#include <array>
|
||||
#include <optional>
|
||||
|
||||
// What Trim compares each edge against.
|
||||
enum class TrimBasedOn { transparentPixels, topLeftPixelColor, bottomRightPixelColor };
|
||||
inline constexpr std::array allTrimBasedOn{TrimBasedOn::transparentPixels, TrimBasedOn::topLeftPixelColor, TrimBasedOn::bottomRightPixelColor};
|
||||
// Swift's rawValue: the sheet's words.
|
||||
QString rawValue(TrimBasedOn basedOn);
|
||||
|
||||
struct TrimOptions {
|
||||
TrimBasedOn basedOn = TrimBasedOn::transparentPixels;
|
||||
bool top = true;
|
||||
bool bottom = true;
|
||||
bool left = true;
|
||||
bool right = true;
|
||||
// Per channel, premultiplied: how far a pixel may stray.
|
||||
quint8 tolerance = 0;
|
||||
bool trimsAny() const;
|
||||
friend bool operator==(const TrimOptions &, const TrimOptions &) = default;
|
||||
};
|
||||
|
||||
namespace ImageTrim {
|
||||
// The rect to keep, or none when nothing would remain.
|
||||
std::optional<QRect> calculateTrimRect(const QImage &image, const TrimOptions &options);
|
||||
// The rendered canvas trimmed by the canvas resizer, or none.
|
||||
std::optional<ProjectSnapshot> trim(const ProjectSnapshot &snapshot, const TrimOptions &options);
|
||||
}
|
||||
@@ -13,6 +13,9 @@ QString rawValue(AdjustmentKind kind)
|
||||
case AdjustmentKind::exposure: return QStringLiteral("Exposure");
|
||||
case AdjustmentKind::gradientMap: return QStringLiteral("Gradient Map");
|
||||
case AdjustmentKind::grain: return QStringLiteral("Grain");
|
||||
case AdjustmentKind::addNoise: return QStringLiteral("Add Noise");
|
||||
case AdjustmentKind::gaussianBlur: return QStringLiteral("Gaussian Blur");
|
||||
case AdjustmentKind::motionBlur: return QStringLiteral("Motion Blur");
|
||||
case AdjustmentKind::invert: return QStringLiteral("Invert");
|
||||
case AdjustmentKind::blackWhite: return QStringLiteral("Black & White");
|
||||
case AdjustmentKind::colorBalance: return QStringLiteral("Color Balance");
|
||||
@@ -36,6 +39,9 @@ std::optional<FilterKind> filterKind(AdjustmentKind kind)
|
||||
case AdjustmentKind::exposure: return FilterKind::exposure;
|
||||
case AdjustmentKind::gradientMap: return FilterKind::gradientMap;
|
||||
case AdjustmentKind::grain: return FilterKind::grain;
|
||||
case AdjustmentKind::addNoise: return FilterKind::addNoise;
|
||||
case AdjustmentKind::gaussianBlur: return FilterKind::gaussianBlur;
|
||||
case AdjustmentKind::motionBlur: return FilterKind::motionBlur;
|
||||
case AdjustmentKind::blackWhite: return FilterKind::blackWhite;
|
||||
case AdjustmentKind::colorBalance: return FilterKind::colorBalance;
|
||||
// Panels of their own; Invert has nothing to set.
|
||||
@@ -106,6 +112,85 @@ void LayerAdjustment::setColorBalance(const ColorBalanceSettings &value)
|
||||
colorBalanceSettings = value;
|
||||
}
|
||||
|
||||
double LayerAdjustment::gaussianRadius() const
|
||||
{
|
||||
return blurRadius.value_or(10);
|
||||
}
|
||||
|
||||
void LayerAdjustment::setGaussianRadius(double value)
|
||||
{
|
||||
blurRadius = value;
|
||||
}
|
||||
|
||||
double LayerAdjustment::resolvedMotionAngle() const
|
||||
{
|
||||
return motionAngle.value_or(0);
|
||||
}
|
||||
|
||||
void LayerAdjustment::setResolvedMotionAngle(double value)
|
||||
{
|
||||
motionAngle = value;
|
||||
}
|
||||
|
||||
double LayerAdjustment::resolvedMotionDistance() const
|
||||
{
|
||||
return motionDistance.value_or(10);
|
||||
}
|
||||
|
||||
void LayerAdjustment::setResolvedMotionDistance(double value)
|
||||
{
|
||||
motionDistance = value;
|
||||
}
|
||||
|
||||
double LayerAdjustment::resolvedNoiseAmount() const
|
||||
{
|
||||
return noiseAmount.value_or(10);
|
||||
}
|
||||
|
||||
void LayerAdjustment::setResolvedNoiseAmount(double value)
|
||||
{
|
||||
noiseAmount = value;
|
||||
}
|
||||
|
||||
bool LayerAdjustment::resolvedNoiseGaussian() const
|
||||
{
|
||||
return noiseGaussian.value_or(false);
|
||||
}
|
||||
|
||||
void LayerAdjustment::setResolvedNoiseGaussian(bool value)
|
||||
{
|
||||
noiseGaussian = value;
|
||||
}
|
||||
|
||||
bool LayerAdjustment::resolvedNoiseMonochromatic() const
|
||||
{
|
||||
return noiseMonochromatic.value_or(false);
|
||||
}
|
||||
|
||||
void LayerAdjustment::setResolvedNoiseMonochromatic(bool value)
|
||||
{
|
||||
noiseMonochromatic = value;
|
||||
}
|
||||
|
||||
quint32 LayerAdjustment::resolvedNoiseSeed() const
|
||||
{
|
||||
return noiseSeed.value_or(0);
|
||||
}
|
||||
|
||||
void LayerAdjustment::setResolvedNoiseSeed(quint32 value)
|
||||
{
|
||||
noiseSeed = value;
|
||||
}
|
||||
|
||||
double LayerAdjustment::samplingMargin() const
|
||||
{
|
||||
switch (kind) {
|
||||
case AdjustmentKind::gaussianBlur: return gaussianRadius() * 3 + 2;
|
||||
case AdjustmentKind::motionBlur: return resolvedMotionDistance() / 2 + 2;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Bounds refuse what is no number: no isFinite terms.
|
||||
bool LayerAdjustment::isValid() const
|
||||
{
|
||||
@@ -130,10 +215,11 @@ bool LayerAdjustment::isValid() const
|
||||
return false;
|
||||
}
|
||||
return curves.isValid() && exposure().isValid() && gradientMap().isValid() && grain().isValid() && blackWhite().isValid()
|
||||
&& colorBalance().isValid();
|
||||
&& colorBalance().isValid() && gaussianRadius() >= 0.1 && gaussianRadius() <= 250 && std::abs(resolvedMotionAngle()) <= 90
|
||||
&& resolvedMotionDistance() >= 1 && resolvedMotionDistance() <= 2000 && resolvedNoiseAmount() >= 0.1 && resolvedNoiseAmount() <= 400;
|
||||
}
|
||||
|
||||
QImage LayerAdjustment::apply(const QImage &image, std::optional<QRectF> region) const
|
||||
QImage LayerAdjustment::apply(const QImage &image, std::optional<QRectF> region, double scale) const
|
||||
{
|
||||
switch (kind) {
|
||||
case AdjustmentKind::hsv:
|
||||
@@ -144,6 +230,21 @@ QImage LayerAdjustment::apply(const QImage &image, std::optional<QRectF> region)
|
||||
case AdjustmentKind::gradientMap: return gradientMap().apply(image);
|
||||
case AdjustmentKind::blackWhite: return blackWhite().apply(image);
|
||||
case AdjustmentKind::colorBalance: return colorBalance().apply(image);
|
||||
case AdjustmentKind::addNoise:
|
||||
case AdjustmentKind::gaussianBlur:
|
||||
case AdjustmentKind::motionBlur: {
|
||||
const FilterSettings settings{.radius = gaussianRadius(),
|
||||
.angle = resolvedMotionAngle(),
|
||||
.distance = resolvedMotionDistance(),
|
||||
.amount = resolvedNoiseAmount(),
|
||||
.gaussian = resolvedNoiseGaussian(),
|
||||
.monochromatic = resolvedNoiseMonochromatic()};
|
||||
FilterJob job{filterKind(kind).value(), image, settings, scale, std::nullopt, QTransform(), resolvedNoiseSeed()};
|
||||
// Swift's region origin, counted in the image's own pixels.
|
||||
if (region)
|
||||
job.noiseOrigin = region->topLeft() * (image.width() / region->width());
|
||||
return PixelFilter::run(job);
|
||||
}
|
||||
case AdjustmentKind::invert: return PixelInvert::run(PixelInvert::Job{image, false, QTransform(), std::nullopt});
|
||||
case AdjustmentKind::grain: {
|
||||
// Grain sits in the document, however the image is cut.
|
||||
@@ -169,6 +270,8 @@ void EditorSession::addAdjustment(AdjustmentKind kind)
|
||||
grain.seed = QRandomGenerator::global()->generate();
|
||||
adjustment.setGrain(grain);
|
||||
}
|
||||
if (kind == AdjustmentKind::addNoise)
|
||||
adjustment.setResolvedNoiseSeed(QRandomGenerator::global()->generate());
|
||||
layer.adjustment = adjustment;
|
||||
const std::optional<ImageLayer> active = activeLayer();
|
||||
layer.parentID = active && active->isGroup ? m_activeLayerID : active ? active->parentID : std::nullopt;
|
||||
|
||||
@@ -10,10 +10,11 @@
|
||||
#include <optional>
|
||||
|
||||
// An adjustment layer's kind, in the Layer menu's order.
|
||||
enum class AdjustmentKind { hsv, levels, curves, exposure, gradientMap, grain, invert, blackWhite, colorBalance };
|
||||
inline constexpr std::array allAdjustmentKinds{AdjustmentKind::hsv, AdjustmentKind::levels, AdjustmentKind::curves,
|
||||
AdjustmentKind::exposure, AdjustmentKind::gradientMap, AdjustmentKind::grain,
|
||||
AdjustmentKind::invert, AdjustmentKind::blackWhite, AdjustmentKind::colorBalance};
|
||||
enum class AdjustmentKind { hsv, levels, curves, exposure, gradientMap, grain, addNoise, gaussianBlur, motionBlur, invert, blackWhite, colorBalance };
|
||||
inline constexpr std::array allAdjustmentKinds{AdjustmentKind::hsv, AdjustmentKind::levels, AdjustmentKind::curves,
|
||||
AdjustmentKind::exposure, AdjustmentKind::gradientMap, AdjustmentKind::grain,
|
||||
AdjustmentKind::addNoise, AdjustmentKind::gaussianBlur, AdjustmentKind::motionBlur,
|
||||
AdjustmentKind::invert, AdjustmentKind::blackWhite, AdjustmentKind::colorBalance};
|
||||
// Swift's rawValue: the new layer's name and the manifest's word.
|
||||
QString rawValue(AdjustmentKind kind);
|
||||
// The kind a manifest names, or none.
|
||||
@@ -40,6 +41,14 @@ struct LayerAdjustment {
|
||||
std::optional<GrainSettings> grainSettings = std::nullopt;
|
||||
std::optional<BlackWhiteSettings> blackWhiteSettings = std::nullopt;
|
||||
std::optional<ColorBalanceSettings> colorBalanceSettings = std::nullopt;
|
||||
// Absent in projects from before blur and noise layers.
|
||||
std::optional<double> blurRadius = std::nullopt;
|
||||
std::optional<double> motionAngle = std::nullopt;
|
||||
std::optional<double> motionDistance = std::nullopt;
|
||||
std::optional<double> noiseAmount = std::nullopt;
|
||||
std::optional<bool> noiseGaussian = std::nullopt;
|
||||
std::optional<bool> noiseMonochromatic = std::nullopt;
|
||||
std::optional<quint32> noiseSeed = std::nullopt;
|
||||
HueSaturationSettings resolvedHSV() const;
|
||||
// Swift's computed properties over the optional settings.
|
||||
ExposureSettings exposure() const;
|
||||
@@ -52,8 +61,24 @@ struct LayerAdjustment {
|
||||
void setBlackWhite(const BlackWhiteSettings &value);
|
||||
ColorBalanceSettings colorBalance() const;
|
||||
void setColorBalance(const ColorBalanceSettings &value);
|
||||
double gaussianRadius() const;
|
||||
void setGaussianRadius(double value);
|
||||
double resolvedMotionAngle() const;
|
||||
void setResolvedMotionAngle(double value);
|
||||
double resolvedMotionDistance() const;
|
||||
void setResolvedMotionDistance(double value);
|
||||
double resolvedNoiseAmount() const;
|
||||
void setResolvedNoiseAmount(double value);
|
||||
bool resolvedNoiseGaussian() const;
|
||||
void setResolvedNoiseGaussian(bool value);
|
||||
bool resolvedNoiseMonochromatic() const;
|
||||
void setResolvedNoiseMonochromatic(bool value);
|
||||
quint32 resolvedNoiseSeed() const;
|
||||
void setResolvedNoiseSeed(quint32 value);
|
||||
// Document pixels a partial redraw reads past its rect.
|
||||
double samplingMargin() const;
|
||||
bool isValid() const;
|
||||
// `region`: the document `image` covers, whole pixels by default.
|
||||
QImage apply(const QImage &image, std::optional<QRectF> region = std::nullopt) const;
|
||||
// Region: the document shown; scale: image pixels a document pixel.
|
||||
QImage apply(const QImage &image, std::optional<QRectF> region = std::nullopt, double scale = 1) const;
|
||||
friend bool operator==(const LayerAdjustment &, const LayerAdjustment &) = default;
|
||||
};
|
||||
@@ -11,6 +11,7 @@ void take(LayerEffects &into, const LayerEffects &from, LayerEffectKind kind)
|
||||
case LayerEffectKind::colorOverlay: into.colorOverlay = from.colorOverlay; return;
|
||||
case LayerEffectKind::innerShadow: into.innerShadow = from.innerShadow; return;
|
||||
case LayerEffectKind::outerGlow: into.outerGlow = from.outerGlow; return;
|
||||
case LayerEffectKind::innerGlow: into.innerGlow = from.innerGlow; return;
|
||||
}
|
||||
throw std::logic_error("unknown effect kind");
|
||||
}
|
||||
@@ -75,6 +76,10 @@ void EditorSession::addEffect(LayerEffectKind kind)
|
||||
if (!effects.outerGlow)
|
||||
effects.outerGlow = OuterGlowEffect();
|
||||
break;
|
||||
case LayerEffectKind::innerGlow:
|
||||
if (!effects.innerGlow)
|
||||
effects.innerGlow = InnerGlowEffect();
|
||||
break;
|
||||
}
|
||||
setEffects(effects, id, QStringLiteral("Add ") + rawValue(kind));
|
||||
selectEffect(kind, id, true);
|
||||
|
||||
@@ -61,6 +61,11 @@ bool OuterGlowEffect::isValid() const
|
||||
return within(size, 0, 500) && within(opacity, 0, 1) && colourIsValid(red, green, blue);
|
||||
}
|
||||
|
||||
bool InnerGlowEffect::isValid() const
|
||||
{
|
||||
return within(size, 0, 500) && within(opacity, 0, 1) && colourIsValid(red, green, blue);
|
||||
}
|
||||
|
||||
QString rawValue(LayerEffectKind kind)
|
||||
{
|
||||
switch (kind) {
|
||||
@@ -69,6 +74,7 @@ QString rawValue(LayerEffectKind kind)
|
||||
case LayerEffectKind::colorOverlay: return QStringLiteral("Color Overlay");
|
||||
case LayerEffectKind::innerShadow: return QStringLiteral("Inner Shadow");
|
||||
case LayerEffectKind::outerGlow: return QStringLiteral("Outer Glow");
|
||||
case LayerEffectKind::innerGlow: return QStringLiteral("Inner Glow");
|
||||
}
|
||||
throw std::logic_error("unknown effect kind");
|
||||
}
|
||||
@@ -76,7 +82,7 @@ QString rawValue(LayerEffectKind kind)
|
||||
bool LayerEffects::isValid() const
|
||||
{
|
||||
return (!stroke || stroke->isValid()) && (!shadow || shadow->isValid()) && (!colorOverlay || colorOverlay->isValid())
|
||||
&& (!innerShadow || innerShadow->isValid()) && (!outerGlow || outerGlow->isValid());
|
||||
&& (!innerShadow || innerShadow->isValid()) && (!outerGlow || outerGlow->isValid()) && (!innerGlow || innerGlow->isValid());
|
||||
}
|
||||
|
||||
std::vector<LayerEffectKind> LayerEffects::kinds() const
|
||||
@@ -97,6 +103,7 @@ bool LayerEffects::contains(LayerEffectKind kind) const
|
||||
case LayerEffectKind::colorOverlay: return colorOverlay.has_value();
|
||||
case LayerEffectKind::innerShadow: return innerShadow.has_value();
|
||||
case LayerEffectKind::outerGlow: return outerGlow.has_value();
|
||||
case LayerEffectKind::innerGlow: return innerGlow.has_value();
|
||||
}
|
||||
throw std::logic_error("unknown effect kind");
|
||||
}
|
||||
@@ -109,6 +116,7 @@ bool LayerEffects::isEnabled(LayerEffectKind kind) const
|
||||
case LayerEffectKind::colorOverlay: return colorOverlay && colorOverlay->isEnabled();
|
||||
case LayerEffectKind::innerShadow: return innerShadow && innerShadow->isEnabled();
|
||||
case LayerEffectKind::outerGlow: return outerGlow && outerGlow->isEnabled();
|
||||
case LayerEffectKind::innerGlow: return innerGlow && innerGlow->isEnabled();
|
||||
}
|
||||
throw std::logic_error("unknown effect kind");
|
||||
}
|
||||
@@ -121,6 +129,7 @@ std::optional<PaletteColor> LayerEffects::color(LayerEffectKind kind) const
|
||||
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;
|
||||
case LayerEffectKind::innerGlow: return innerGlow ? std::optional(innerGlow->color()) : std::nullopt;
|
||||
}
|
||||
throw std::logic_error("unknown effect kind");
|
||||
}
|
||||
@@ -156,6 +165,7 @@ void LayerEffects::setColor(const PaletteColor &color, LayerEffectKind kind)
|
||||
case LayerEffectKind::colorOverlay: paint(colorOverlay, color); return;
|
||||
case LayerEffectKind::innerShadow: paint(innerShadow, color); return;
|
||||
case LayerEffectKind::outerGlow: paint(outerGlow, color); return;
|
||||
case LayerEffectKind::innerGlow: paint(innerGlow, color); return;
|
||||
}
|
||||
throw std::logic_error("unknown effect kind");
|
||||
}
|
||||
@@ -168,6 +178,7 @@ void LayerEffects::remove(LayerEffectKind kind)
|
||||
case LayerEffectKind::colorOverlay: colorOverlay.reset(); return;
|
||||
case LayerEffectKind::innerShadow: innerShadow.reset(); return;
|
||||
case LayerEffectKind::outerGlow: outerGlow.reset(); return;
|
||||
case LayerEffectKind::innerGlow: innerGlow.reset(); return;
|
||||
}
|
||||
throw std::logic_error("unknown effect kind");
|
||||
}
|
||||
@@ -180,11 +191,12 @@ void LayerEffects::setEnabled(bool enabled, LayerEffectKind kind)
|
||||
case LayerEffectKind::colorOverlay: enable(colorOverlay, enabled); return;
|
||||
case LayerEffectKind::innerShadow: enable(innerShadow, enabled); return;
|
||||
case LayerEffectKind::outerGlow: enable(outerGlow, enabled); return;
|
||||
case LayerEffectKind::innerGlow: enable(innerGlow, enabled); return;
|
||||
}
|
||||
throw std::logic_error("unknown effect kind");
|
||||
}
|
||||
|
||||
LayerEffects LayerEffects::visible() const
|
||||
{
|
||||
return LayerEffects{shown(stroke), shown(shadow), shown(colorOverlay), shown(innerShadow), shown(outerGlow)};
|
||||
return LayerEffects{shown(stroke), shown(shadow), shown(colorOverlay), shown(innerShadow), shown(outerGlow), shown(innerGlow)};
|
||||
}
|
||||
@@ -88,10 +88,24 @@ struct OuterGlowEffect {
|
||||
friend bool operator==(const OuterGlowEffect &, const OuterGlowEffect &) = default;
|
||||
};
|
||||
|
||||
// A glow inside the layer's edges, fading inward.
|
||||
struct InnerGlowEffect {
|
||||
std::optional<bool> enabled = std::nullopt;
|
||||
double size = 10;
|
||||
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 InnerGlowEffect &, const InnerGlowEffect &) = default;
|
||||
};
|
||||
|
||||
// Swift's raw values are the panel's and the menu's words.
|
||||
enum class LayerEffectKind { stroke, shadow, colorOverlay, innerShadow, outerGlow };
|
||||
inline constexpr std::array allLayerEffectKinds{LayerEffectKind::stroke, LayerEffectKind::shadow, LayerEffectKind::colorOverlay,
|
||||
LayerEffectKind::innerShadow, LayerEffectKind::outerGlow};
|
||||
enum class LayerEffectKind { stroke, shadow, colorOverlay, innerShadow, outerGlow, innerGlow };
|
||||
inline constexpr std::array allLayerEffectKinds{LayerEffectKind::stroke, LayerEffectKind::shadow, LayerEffectKind::colorOverlay,
|
||||
LayerEffectKind::innerShadow, LayerEffectKind::outerGlow, LayerEffectKind::innerGlow};
|
||||
QString rawValue(LayerEffectKind kind);
|
||||
|
||||
// What a layer draws round itself; its pixels stay untouched.
|
||||
@@ -101,7 +115,8 @@ struct LayerEffects {
|
||||
std::optional<ColorOverlayEffect> colorOverlay = std::nullopt;
|
||||
std::optional<InnerShadowEffect> innerShadow = std::nullopt;
|
||||
std::optional<OuterGlowEffect> outerGlow = std::nullopt;
|
||||
bool isEmpty() const { return !stroke && !shadow && !colorOverlay && !innerShadow && !outerGlow; }
|
||||
std::optional<InnerGlowEffect> innerGlow = std::nullopt;
|
||||
bool isEmpty() const { return !stroke && !shadow && !colorOverlay && !innerShadow && !outerGlow && !innerGlow; }
|
||||
bool isValid() const;
|
||||
std::vector<LayerEffectKind> kinds() const;
|
||||
bool contains(LayerEffectKind kind) const;
|
||||
|
||||
Reference in new issue
Block a user