383 lines
13 KiB
C++
383 lines
13 KiB
C++
#include "Blend2D_Cache.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <initializer_list>
|
|
#include <limits>
|
|
#include <mutex>
|
|
#include <numbers>
|
|
#include <vector>
|
|
|
|
namespace renderive::detail {
|
|
namespace {
|
|
|
|
void clear_image(BLImage& image) {
|
|
BLImageData data{};
|
|
if (image.get_data(&data) != BL_SUCCESS)
|
|
return;
|
|
for (int y = 0; y < data.size.h; ++y) {
|
|
auto* row = static_cast<std::byte*>(data.pixel_data) +
|
|
static_cast<std::ptrdiff_t>(y) * data.stride;
|
|
std::memset(row, 0, static_cast<std::size_t>(data.size.w) * sizeof(Pixel));
|
|
}
|
|
}
|
|
|
|
void load_font_face(BLFontFace& face, std::initializer_list<const char*> candidates) {
|
|
for (const char* path : candidates) {
|
|
std::error_code error;
|
|
if (std::filesystem::exists(path, error) && face.create_from_file(path) == BL_SUCCESS)
|
|
return;
|
|
}
|
|
}
|
|
|
|
BLFontFace& default_font_face(const Font& font) {
|
|
static std::array<BLFontFace, 4> faces;
|
|
static std::once_flag once;
|
|
std::call_once(once, [] {
|
|
load_font_face(faces[0], {
|
|
"C:/Windows/Fonts/msyh.ttc",
|
|
"C:/Windows/Fonts/simsun.ttc",
|
|
"C:/Windows/Fonts/segoeui.ttf",
|
|
"C:/Windows/Fonts/arial.ttf",
|
|
"C:/Windows/Fonts/tahoma.ttf"
|
|
});
|
|
load_font_face(faces[1], {
|
|
"C:/Windows/Fonts/msyhbd.ttc",
|
|
"C:/Windows/Fonts/segoeuib.ttf",
|
|
"C:/Windows/Fonts/arialbd.ttf",
|
|
"C:/Windows/Fonts/tahomabd.ttf"
|
|
});
|
|
load_font_face(faces[2], {
|
|
"C:/Windows/Fonts/segoeuii.ttf",
|
|
"C:/Windows/Fonts/ariali.ttf",
|
|
"C:/Windows/Fonts/tahomai.ttf"
|
|
});
|
|
load_font_face(faces[3], {
|
|
"C:/Windows/Fonts/segoeuiz.ttf",
|
|
"C:/Windows/Fonts/arialbi.ttf",
|
|
"C:/Windows/Fonts/tahomabi.ttf"
|
|
});
|
|
});
|
|
const std::size_t index = (font.weight >= 600 ? 1u : 0u) | (font.italic ? 2u : 0u);
|
|
return faces[index] ? faces[index] : faces[0];
|
|
}
|
|
|
|
BLStrokeCap stroke_cap(Line_Cap cap) noexcept {
|
|
switch (cap) {
|
|
case Line_Cap::Square:
|
|
return BL_STROKE_CAP_SQUARE;
|
|
case Line_Cap::Round:
|
|
return BL_STROKE_CAP_ROUND;
|
|
case Line_Cap::Butt:
|
|
default:
|
|
return BL_STROKE_CAP_BUTT;
|
|
}
|
|
}
|
|
|
|
BLStrokeJoin stroke_join(Line_Join join) noexcept {
|
|
switch (join) {
|
|
case Line_Join::Bevel:
|
|
return BL_STROKE_JOIN_BEVEL;
|
|
case Line_Join::Round:
|
|
return BL_STROKE_JOIN_ROUND;
|
|
case Line_Join::Miter:
|
|
default:
|
|
return BL_STROKE_JOIN_MITER_CLIP;
|
|
}
|
|
}
|
|
|
|
double cubic_weight(double distance) noexcept {
|
|
constexpr double a = -0.5;
|
|
distance = std::abs(distance);
|
|
if (distance < 1.0)
|
|
return (a + 2.0) * distance * distance * distance -
|
|
(a + 3.0) * distance * distance + 1.0;
|
|
if (distance < 2.0)
|
|
return a * distance * distance * distance - 5.0 * a * distance * distance +
|
|
8.0 * a * distance - 4.0 * a;
|
|
return 0.0;
|
|
}
|
|
|
|
std::uint8_t pixel_channel(Pixel pixel, int shift) noexcept {
|
|
return static_cast<std::uint8_t>((pixel >> shift) & 0xFFu);
|
|
}
|
|
|
|
std::vector<Pixel> bicubic_resample(std::span<const Pixel> source,
|
|
int source_width,
|
|
int source_height,
|
|
int destination_width,
|
|
int destination_height) {
|
|
std::vector<Pixel> destination(
|
|
static_cast<std::size_t>(destination_width) * destination_height);
|
|
for (int y = 0; y < destination_height; ++y) {
|
|
const double source_y = (static_cast<double>(y) + 0.5) * source_height /
|
|
destination_height -
|
|
0.5;
|
|
const int base_y = static_cast<int>(std::floor(source_y));
|
|
for (int x = 0; x < destination_width; ++x) {
|
|
const double source_x = (static_cast<double>(x) + 0.5) * source_width /
|
|
destination_width -
|
|
0.5;
|
|
const int base_x = static_cast<int>(std::floor(source_x));
|
|
double channels[4]{};
|
|
double total_weight{};
|
|
for (int offset_y = -1; offset_y <= 2; ++offset_y) {
|
|
const int sample_y = std::clamp(base_y + offset_y, 0, source_height - 1);
|
|
const double weight_y = cubic_weight(source_y - (base_y + offset_y));
|
|
for (int offset_x = -1; offset_x <= 2; ++offset_x) {
|
|
const int sample_x = std::clamp(base_x + offset_x, 0, source_width - 1);
|
|
const double weight = weight_y *
|
|
cubic_weight(source_x - (base_x + offset_x));
|
|
const Pixel pixel = source[static_cast<std::size_t>(sample_y) *
|
|
source_width +
|
|
sample_x];
|
|
channels[0] += pixel_channel(pixel, 0) * weight;
|
|
channels[1] += pixel_channel(pixel, 8) * weight;
|
|
channels[2] += pixel_channel(pixel, 16) * weight;
|
|
channels[3] += pixel_channel(pixel, 24) * weight;
|
|
total_weight += weight;
|
|
}
|
|
}
|
|
const auto channel = [total_weight](double value) {
|
|
if (total_weight != 0.0)
|
|
value /= total_weight;
|
|
return static_cast<std::uint32_t>(std::clamp(std::lround(value), 0L, 255L));
|
|
};
|
|
destination[static_cast<std::size_t>(y) * destination_width + x] =
|
|
channel(channels[0]) | (channel(channels[1]) << 8) |
|
|
(channel(channels[2]) << 16) | (channel(channels[3]) << 24);
|
|
}
|
|
}
|
|
return destination;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void Blend2D_Color_Cache::clear() {
|
|
clear_image(image_);
|
|
}
|
|
|
|
void Blend2D_Color_Cache::composite(const ::Color_Cache& source) {
|
|
const auto& typed = dynamic_cast<const Blend2D_Color_Cache&>(source);
|
|
if (typed.image_.is_empty())
|
|
return;
|
|
const Size source_size = typed.size();
|
|
if (image_.is_empty() || size() != source_size) {
|
|
ensure_size(source_size);
|
|
clear_image(image_);
|
|
}
|
|
|
|
BLContext context(image_);
|
|
if (!context)
|
|
return;
|
|
context.set_comp_op(BL_COMP_OP_SRC_OVER);
|
|
context.blit_image(BLRect(0.0, 0.0,
|
|
static_cast<double>(source_size.width),
|
|
static_cast<double>(source_size.height)),
|
|
typed.image_,
|
|
BLRectI(0, 0, source_size.width, source_size.height));
|
|
context.end();
|
|
}
|
|
|
|
void Blend2D_Color_Cache::ensure_size(Size requested) {
|
|
if (requested.empty()) {
|
|
image_.reset();
|
|
return;
|
|
}
|
|
if (size() == requested)
|
|
return;
|
|
image_.create(requested.width, requested.height, BL_FORMAT_PRGB32);
|
|
clear_image(image_);
|
|
}
|
|
|
|
Size Blend2D_Color_Cache::size() const noexcept {
|
|
return {image_.width(), image_.height()};
|
|
}
|
|
|
|
Image_View Blend2D_Color_Cache::view() const noexcept {
|
|
BLImageData data{};
|
|
if (image_.get_data(&data) != BL_SUCCESS)
|
|
return {};
|
|
return {
|
|
static_cast<const std::byte*>(data.pixel_data),
|
|
data.size.w,
|
|
data.size.h,
|
|
static_cast<int>(data.stride),
|
|
Pixel_Format::Premultiplied_32
|
|
};
|
|
}
|
|
|
|
Painter::Painter(Blend2D_Color_Cache& cache, Size size) {
|
|
cache.ensure_size(size);
|
|
if (!cache.image_.is_empty() && context_.begin(cache.image_) == BL_SUCCESS) {
|
|
context_.set_comp_op(BL_COMP_OP_SRC_OVER);
|
|
active_ = true;
|
|
}
|
|
}
|
|
|
|
Painter::~Painter() {
|
|
if (active_)
|
|
context_.end();
|
|
}
|
|
|
|
BLRgba Painter::rgba(Color color) noexcept {
|
|
constexpr double scale = 1.0 / 255.0;
|
|
return BLRgba(color.r * scale, color.g * scale, color.b * scale, color.a * scale);
|
|
}
|
|
|
|
void Painter::apply_pen(const Pen& pen) {
|
|
context_.set_stroke_style(rgba(pen.color));
|
|
context_.set_stroke_width(std::max(0.1, pen.width));
|
|
context_.set_stroke_caps(stroke_cap(pen.cap));
|
|
context_.set_stroke_join(stroke_join(pen.join));
|
|
BLArray<double> pattern;
|
|
if (pen.style == Line_Style::Dash) {
|
|
pattern.append(std::max(1.0, pen.width * 4.0));
|
|
pattern.append(std::max(1.0, pen.width * 2.0));
|
|
} else if (pen.style == Line_Style::Dot) {
|
|
pattern.append(std::max(1.0, pen.width));
|
|
pattern.append(std::max(1.0, pen.width * 2.0));
|
|
}
|
|
context_.set_stroke_dash_array(pattern);
|
|
}
|
|
|
|
void Painter::line(PointF first, PointF second, const Pen& pen) {
|
|
if (!active_ || !pen.enabled())
|
|
return;
|
|
apply_pen(pen);
|
|
context_.stroke_line(first.x, first.y, second.x, second.y);
|
|
}
|
|
|
|
void Painter::polyline(std::span<const PointF> points, const Pen& pen) {
|
|
if (!active_ || !pen.enabled() || points.size() < 2)
|
|
return;
|
|
BLPath path;
|
|
path.move_to(points.front().x, points.front().y);
|
|
for (std::size_t index = 1; index < points.size(); ++index)
|
|
path.line_to(points[index].x, points[index].y);
|
|
apply_pen(pen);
|
|
context_.stroke_path(path);
|
|
}
|
|
|
|
void Painter::polygon(std::span<const PointF> points, const Pen& pen, const Brush& brush) {
|
|
if (!active_ || points.size() < 3)
|
|
return;
|
|
BLPath path;
|
|
path.move_to(points.front().x, points.front().y);
|
|
for (std::size_t index = 1; index < points.size(); ++index)
|
|
path.line_to(points[index].x, points[index].y);
|
|
path.close();
|
|
if (brush.enabled()) {
|
|
context_.set_fill_style(rgba(brush.color));
|
|
context_.fill_path(path);
|
|
}
|
|
if (pen.enabled()) {
|
|
apply_pen(pen);
|
|
context_.stroke_path(path);
|
|
}
|
|
}
|
|
|
|
void Painter::rect(RectF value, const Pen& pen, const Brush& brush) {
|
|
if (!active_)
|
|
return;
|
|
value = value.normalized();
|
|
if (value.empty())
|
|
return;
|
|
const BLRect rect(value.x, value.y, value.width, value.height);
|
|
if (brush.enabled()) {
|
|
context_.set_fill_style(rgba(brush.color));
|
|
context_.fill_rect(rect);
|
|
}
|
|
if (pen.enabled()) {
|
|
apply_pen(pen);
|
|
context_.stroke_rect(rect);
|
|
}
|
|
}
|
|
|
|
void Painter::circle(PointF center, double radius, const Pen& pen, const Brush& brush) {
|
|
if (!active_ || radius <= 0.0)
|
|
return;
|
|
BLPath path;
|
|
path.add_ellipse(BLEllipse(center.x, center.y, radius, radius));
|
|
if (brush.enabled()) {
|
|
context_.set_fill_style(rgba(brush.color));
|
|
context_.fill_path(path);
|
|
}
|
|
if (pen.enabled()) {
|
|
apply_pen(pen);
|
|
context_.stroke_path(path);
|
|
}
|
|
}
|
|
|
|
BLFont Painter::make_font(const Font& font) {
|
|
BLFont result;
|
|
auto& face = default_font_face(font);
|
|
if (face)
|
|
result.create_from_face(face, static_cast<float>(std::max(1.0, font.size)));
|
|
return result;
|
|
}
|
|
|
|
void Painter::text(PointF position, std::string_view value, const Font& font, const Pen& pen,
|
|
double rotation_degrees) {
|
|
if (!active_ || value.empty() || !pen.enabled())
|
|
return;
|
|
BLFont bl_font = make_font(font);
|
|
if (!bl_font)
|
|
return;
|
|
context_.set_fill_style(rgba(pen.color));
|
|
const auto& metrics = bl_font.metrics();
|
|
const double baseline = position.y + metrics.ascent;
|
|
const bool rotated = std::isfinite(rotation_degrees) && rotation_degrees != 0.0;
|
|
if (rotated) {
|
|
context_.save();
|
|
context_.rotate(rotation_degrees * std::numbers::pi / 180.0, position.x, baseline);
|
|
}
|
|
context_.fill_utf8_text(BLPoint(position.x, baseline),
|
|
bl_font, value.data(), value.size());
|
|
if (rotated)
|
|
context_.restore();
|
|
}
|
|
|
|
void Painter::heatmap(RectF target,
|
|
int width,
|
|
int height,
|
|
std::span<const Pixel> pixels,
|
|
Image_Interpolation_Mode interpolation) {
|
|
target = target.normalized();
|
|
if (!active_ || target.empty() || width <= 0 || height <= 0 ||
|
|
pixels.size() < static_cast<std::size_t>(width) * static_cast<std::size_t>(height))
|
|
return;
|
|
|
|
std::vector<Pixel> resampled;
|
|
if (interpolation == Image_Interpolation_Mode::Bicubic &&
|
|
target.width <= static_cast<double>(std::numeric_limits<int>::max()) &&
|
|
target.height <= static_cast<double>(std::numeric_limits<int>::max())) {
|
|
const int destination_width = std::max(1, static_cast<int>(std::lround(target.width)));
|
|
const int destination_height = std::max(1, static_cast<int>(std::lround(target.height)));
|
|
resampled = bicubic_resample(pixels, width, height,
|
|
destination_width, destination_height);
|
|
pixels = resampled;
|
|
width = destination_width;
|
|
height = destination_height;
|
|
}
|
|
|
|
BLImage image(width, height, BL_FORMAT_PRGB32);
|
|
BLImageData data{};
|
|
if (image.get_data(&data) != BL_SUCCESS)
|
|
return;
|
|
for (int y = 0; y < height; ++y) {
|
|
auto* destination = reinterpret_cast<Pixel*>(
|
|
static_cast<std::byte*>(data.pixel_data) + static_cast<std::ptrdiff_t>(y) * data.stride);
|
|
std::copy_n(pixels.data() + static_cast<std::size_t>(y) * width, width, destination);
|
|
}
|
|
context_.set_pattern_quality(interpolation == Image_Interpolation_Mode::Bilinear
|
|
? BL_PATTERN_QUALITY_BILINEAR
|
|
: BL_PATTERN_QUALITY_NEAREST);
|
|
context_.blit_image(BLRect(target.x, target.y, target.width, target.height),
|
|
image, BLRectI(0, 0, width, height));
|
|
}
|
|
|
|
} // namespace renderive::detail
|