OmaPhoto 1.0.0: the Linux port of Compositor
A layered image editor for Linux and Omarchy, ported from Compositor (github.com/robbietilton/Compositor), whose Swift source is its specification. It reads and writes Compositor's .comp projects. Packages for Ubuntu, Fedora and Arch come from the release workflow; scripts/install/ holds one install script per distribution family, and flake.nix serves NixOS. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
c864304695
564 files changed
+100804
No files matched your search
@@ -0,0 +1,246 @@
|
||||
#include "Document/HueSaturation.h"
|
||||
#include "Document/BrushStroke.h"
|
||||
#include "Document/LayerTransform.h"
|
||||
#include "Rendering/RasterSnapshot.h"
|
||||
#include <QPainter>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
|
||||
QString rawValue(ColorRange range)
|
||||
{
|
||||
switch (range) {
|
||||
case ColorRange::master: return QStringLiteral("Master");
|
||||
case ColorRange::reds: return QStringLiteral("Reds");
|
||||
case ColorRange::yellows: return QStringLiteral("Yellows");
|
||||
case ColorRange::greens: return QStringLiteral("Greens");
|
||||
case ColorRange::cyans: return QStringLiteral("Cyans");
|
||||
case ColorRange::blues: return QStringLiteral("Blues");
|
||||
case ColorRange::magentas: return QStringLiteral("Magentas");
|
||||
}
|
||||
throw std::logic_error("no such colour range");
|
||||
}
|
||||
|
||||
HueBand defaultBand(ColorRange range)
|
||||
{
|
||||
switch (range) {
|
||||
case ColorRange::master: return {0, 0, 360, 360};
|
||||
case ColorRange::reds: return {315, 345, 15, 45};
|
||||
case ColorRange::yellows: return {15, 45, 75, 105};
|
||||
case ColorRange::greens: return {75, 105, 135, 165};
|
||||
case ColorRange::cyans: return {135, 165, 195, 225};
|
||||
case ColorRange::blues: return {195, 225, 255, 285};
|
||||
case ColorRange::magentas: return {255, 285, 315, 345};
|
||||
}
|
||||
throw std::logic_error("no such colour range");
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Swift's truncatingRemainder, brought into 0 to 360.
|
||||
double wrap(double value)
|
||||
{
|
||||
const double remainder = std::fmod(value, 360);
|
||||
return remainder < 0 ? remainder + 360 : remainder;
|
||||
}
|
||||
}
|
||||
|
||||
double HueBand::forward(double from, double to)
|
||||
{
|
||||
const double delta = std::fmod(to - from, 360);
|
||||
return delta < 0 ? delta + 360 : delta;
|
||||
}
|
||||
|
||||
double HueBand::weight(double hue) const
|
||||
{
|
||||
const double span = forward(falloffStart, falloffEnd);
|
||||
// Master covers everything.
|
||||
if (!(span > 0))
|
||||
return 1;
|
||||
const double position = forward(falloffStart, hue);
|
||||
if (!(position <= span))
|
||||
return 0;
|
||||
const double rampIn = forward(falloffStart, rangeStart), plateauEnd = forward(falloffStart, rangeEnd);
|
||||
if (position < rampIn)
|
||||
return rampIn > 0 ? position / rampIn : 1;
|
||||
if (position <= plateauEnd)
|
||||
return 1;
|
||||
const double rampOut = span - plateauEnd;
|
||||
return rampOut > 0 ? (span - position) / rampOut : 1;
|
||||
}
|
||||
|
||||
std::array<double, 4> HueBand::handles() const
|
||||
{
|
||||
return {falloffStart, rangeStart, rangeEnd, falloffEnd};
|
||||
}
|
||||
|
||||
HueBand HueBand::centered(double hue) const
|
||||
{
|
||||
const double core = forward(rangeStart, rangeEnd), leading = forward(falloffStart, rangeStart), trailing = forward(rangeEnd, falloffEnd);
|
||||
const double start = wrap(hue - core / 2);
|
||||
return {wrap(start - leading), start, wrap(start + core), wrap(start + core + trailing)};
|
||||
}
|
||||
|
||||
void HueBand::include(double hue)
|
||||
{
|
||||
if (!(weight(hue) < 1))
|
||||
return;
|
||||
const double shoulderIn = forward(falloffStart, rangeStart), shoulderOut = forward(rangeEnd, falloffEnd);
|
||||
if (forward(hue, rangeStart) <= forward(rangeEnd, hue)) {
|
||||
rangeStart = hue;
|
||||
falloffStart = hue - shoulderIn;
|
||||
} else {
|
||||
rangeEnd = hue;
|
||||
falloffEnd = hue + shoulderOut;
|
||||
}
|
||||
normalize();
|
||||
}
|
||||
|
||||
void HueBand::exclude(double hue)
|
||||
{
|
||||
if (!(weight(hue) > 0))
|
||||
return;
|
||||
const double shoulderIn = forward(falloffStart, rangeStart), shoulderOut = forward(rangeEnd, falloffEnd);
|
||||
if (forward(falloffStart, hue) <= forward(hue, falloffEnd)) {
|
||||
falloffStart = hue + 1;
|
||||
rangeStart = hue + 1 + shoulderIn;
|
||||
} else {
|
||||
falloffEnd = hue - 1;
|
||||
rangeEnd = hue - 1 - shoulderOut;
|
||||
}
|
||||
normalize();
|
||||
}
|
||||
|
||||
void HueBand::normalize()
|
||||
{
|
||||
falloffStart = wrap(falloffStart);
|
||||
rangeStart = wrap(rangeStart);
|
||||
rangeEnd = wrap(rangeEnd);
|
||||
falloffEnd = wrap(falloffEnd);
|
||||
// Under a full circle.
|
||||
if (forward(falloffStart, falloffEnd) > 350)
|
||||
falloffEnd = wrap(falloffStart + 350);
|
||||
}
|
||||
|
||||
void HueBand::setHandle(int index, double degrees)
|
||||
{
|
||||
HueBand updated = *this;
|
||||
const double value = std::fmod(std::fmod(degrees, 360) + 360, 360);
|
||||
(index == 0 ? updated.falloffStart : index == 1 ? updated.rangeStart : index == 2 ? updated.rangeEnd : updated.falloffEnd) = value;
|
||||
const double span = forward(updated.falloffStart, updated.falloffEnd), toStart = forward(updated.falloffStart, updated.rangeStart),
|
||||
toEnd = forward(updated.falloffStart, updated.rangeEnd);
|
||||
if (span > 1 && span <= 350 && toStart <= toEnd && toEnd <= span)
|
||||
*this = updated;
|
||||
}
|
||||
|
||||
QString rawValue(HueSampleMode mode)
|
||||
{
|
||||
switch (mode) {
|
||||
case HueSampleMode::replace: return QStringLiteral("Sample");
|
||||
case HueSampleMode::add: return QStringLiteral("Add");
|
||||
case HueSampleMode::remove: return QStringLiteral("Remove");
|
||||
}
|
||||
throw std::logic_error("no such sample mode");
|
||||
}
|
||||
|
||||
QString help(HueSampleMode mode)
|
||||
{
|
||||
switch (mode) {
|
||||
case HueSampleMode::replace: return QStringLiteral("Click the image to center this range on that color");
|
||||
case HueSampleMode::add: return QStringLiteral("Click the image to widen this range to include that color");
|
||||
case HueSampleMode::remove: return QStringLiteral("Click the image to narrow this range to exclude that color");
|
||||
}
|
||||
throw std::logic_error("no such sample mode");
|
||||
}
|
||||
|
||||
HueSaturationSettings::HueSaturationSettings(double hue, double saturation, double lightness, bool colorize, ColorRange range)
|
||||
: range(range), colorize(colorize)
|
||||
{
|
||||
for (const ColorRange each : allColorRanges)
|
||||
bands.emplace(each, defaultBand(each));
|
||||
adjustments[range] = RangeAdjustment{hue, saturation, lightness};
|
||||
}
|
||||
|
||||
double HueSaturationSettings::hue() const
|
||||
{
|
||||
return adjustments.contains(range) ? adjustments.at(range).hue : 0;
|
||||
}
|
||||
|
||||
void HueSaturationSettings::setHue(double value)
|
||||
{
|
||||
adjustments[range].hue = value;
|
||||
}
|
||||
|
||||
double HueSaturationSettings::saturation() const
|
||||
{
|
||||
return adjustments.contains(range) ? adjustments.at(range).saturation : 0;
|
||||
}
|
||||
|
||||
void HueSaturationSettings::setSaturation(double value)
|
||||
{
|
||||
adjustments[range].saturation = value;
|
||||
}
|
||||
|
||||
double HueSaturationSettings::lightness() const
|
||||
{
|
||||
return adjustments.contains(range) ? adjustments.at(range).lightness : 0;
|
||||
}
|
||||
|
||||
void HueSaturationSettings::setLightness(double value)
|
||||
{
|
||||
adjustments[range].lightness = value;
|
||||
}
|
||||
|
||||
HueBand HueSaturationSettings::band() const
|
||||
{
|
||||
return bands.contains(range) ? bands.at(range) : defaultBand(range);
|
||||
}
|
||||
|
||||
void HueSaturationSettings::setBand(const HueBand &value)
|
||||
{
|
||||
bands[range] = value;
|
||||
}
|
||||
|
||||
HueSaturationSettings HueSaturationSettings::colorizeStart()
|
||||
{
|
||||
return HueSaturationSettings(0, 25, 0, true);
|
||||
}
|
||||
|
||||
bool HueSaturationSettings::isIdentity() const
|
||||
{
|
||||
return !colorize && std::all_of(adjustments.begin(), adjustments.end(), [](const auto &entry) { return entry.second == RangeAdjustment(); });
|
||||
}
|
||||
|
||||
double HueSaturationSettings::weight(ColorRange colorRange, double hue) const
|
||||
{
|
||||
if (colorRange == ColorRange::master)
|
||||
return 1;
|
||||
const double inBand = (bands.contains(colorRange) ? bands.at(colorRange) : defaultBand(colorRange)).weight(hue);
|
||||
return invertRange && colorRange == range ? 1 - inBand : inBand;
|
||||
}
|
||||
|
||||
HueSaturationEdit::HueSaturationEdit(QUuid layerID, const ImportedImage &original, std::optional<SelectionClip> selection,
|
||||
const LayerTransform &transform)
|
||||
: layerID(layerID), original(original), selection(std::move(selection)),
|
||||
pixelToDocument(BrushRaster::pixelToDocument(transform, original.size().width(), original.size().height()))
|
||||
{
|
||||
const QSize size = original.size();
|
||||
const double factor = std::min(1.0, double(previewLimit) / std::max(size.width(), size.height()));
|
||||
if (factor < 1) {
|
||||
const int width = std::max(1, int(size.width() * factor)), height = std::max(1, int(size.height() * factor));
|
||||
previewSource = BrushRaster::context(width, height, false);
|
||||
QPainter painter(&previewSource);
|
||||
const QRectF rect(0, 0, width, height);
|
||||
if (original.raster)
|
||||
original.raster->draw(rect, painter);
|
||||
else
|
||||
BrushRaster::draw(original.image(), rect, painter);
|
||||
previewPixelToDocument = BrushRaster::pixelToDocument(transform, width, height);
|
||||
} else {
|
||||
previewSource = original.image();
|
||||
previewPixelToDocument = pixelToDocument;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<QImage> HueSaturationEdit::previewImage(QUuid layer) const
|
||||
{
|
||||
return layer == layerID ? m_preparedPreview : std::nullopt;
|
||||
}
|
||||
Reference in new issue
Block a user