327 lines
11 KiB
C++
327 lines
11 KiB
C++
#include "Canvas.h"
|
|
#include "Blend2D_Convert.h"
|
|
#include "Font_Face_p.h"
|
|
#include "Image_p.h"
|
|
#include "blend2d/blend2d.h"
|
|
#include <array>
|
|
#include <filesystem>
|
|
#include <iostream>
|
|
#include <system_error>
|
|
namespace renderive {
|
|
|
|
namespace {
|
|
std::string& default_font_source_storage() {
|
|
static std::string source;
|
|
return source;
|
|
}
|
|
} // namespace
|
|
|
|
std::shared_ptr<Font_Face> Canvas::default_font_face() {
|
|
static std::shared_ptr<Font_Face> face = []() -> std::shared_ptr<Font_Face> {
|
|
static constexpr std::array<const char*, 5> candidates{
|
|
"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"
|
|
};
|
|
for (const char* path : candidates) {
|
|
std::error_code error;
|
|
if (!std::filesystem::exists(path, error))
|
|
continue;
|
|
auto loaded = std::make_shared<Font_Face>();
|
|
if (loaded->impl_->face.create_from_file(path) == BL_SUCCESS) {
|
|
default_font_source_storage() = path;
|
|
return loaded;
|
|
}
|
|
}
|
|
return {};
|
|
}();
|
|
return face;
|
|
}
|
|
|
|
Font Canvas::resolve_font(Font font) {
|
|
if (font.size <= 0.0)
|
|
font.size = 12.0;
|
|
if (!font.face || !font.face->valid())
|
|
font.face = default_font_face();
|
|
return font;
|
|
}
|
|
|
|
bool Canvas::can_use_font(const Font& font) {
|
|
return font.size > 0.0 && font.face && font.face->valid();
|
|
}
|
|
|
|
struct Canvas::Impl {
|
|
Image& image;
|
|
BLContext ctx;
|
|
Font current_font = Canvas::resolve_font({});
|
|
bool antialias = true;
|
|
Pen current_pen;
|
|
Brush current_brush;
|
|
Impl(Image& img) : image(img) {
|
|
if (!img.impl_->image.is_empty()) {
|
|
ctx.begin(img.impl_->image);
|
|
if (ctx)
|
|
ctx.set_comp_op(BL_COMP_OP_SRC_OVER);
|
|
}
|
|
}
|
|
~Impl() {
|
|
if (ctx)
|
|
ctx.end();
|
|
}
|
|
void apply_brush() {
|
|
if (!current_brush.enabled()) {
|
|
ctx.set_fill_style(BLRgba(0.0, 0.0, 0.0, 0.0));
|
|
return;
|
|
}
|
|
ctx.set_fill_style(Convert::to_bl(current_brush.color));
|
|
}
|
|
void apply_text_fill() {
|
|
if (!current_pen.enabled()) {
|
|
ctx.set_fill_style(BLRgba(0.0, 0.0, 0.0, 0.0));
|
|
return;
|
|
}
|
|
ctx.set_fill_style(Convert::to_bl(current_pen.color));
|
|
}
|
|
void apply_pen(double stroke_width = -1.0) {
|
|
if (!current_pen.enabled()) {
|
|
ctx.set_stroke_style(BLRgba(0.0, 0.0, 0.0, 0.0));
|
|
return;
|
|
}
|
|
ctx.set_stroke_style(Convert::to_bl(current_pen.color));
|
|
double w = stroke_width > 0.0 ? stroke_width : current_pen.width;
|
|
ctx.set_stroke_width(w);
|
|
if (current_pen.style == Line_Style::Dash || current_pen.style == Line_Style::Dot) {
|
|
BLArray<double> pattern;
|
|
if (current_pen.style == Line_Style::Dash) {
|
|
pattern.append(w * 4.0);
|
|
pattern.append(w * 2.0);
|
|
}
|
|
else {
|
|
pattern.append(w * 1.0);
|
|
pattern.append(w * 2.0);
|
|
}
|
|
ctx.set_stroke_dash_array(pattern);
|
|
}
|
|
else {
|
|
ctx.set_stroke_dash_array(BLArray<double>());
|
|
}
|
|
ctx.set_stroke_caps(static_cast<BLStrokeCap>(Convert::blend2d_line_cap(current_pen.cap)));
|
|
ctx.set_stroke_join(static_cast<BLStrokeJoin>(Convert::blend2d_line_join(current_pen.join)));
|
|
}
|
|
};
|
|
Canvas::Canvas(Image& image) : impl_(std::make_unique<Impl>(image)) {}
|
|
Canvas::~Canvas() = default;
|
|
void Canvas::save() {
|
|
impl_->ctx.save();
|
|
}
|
|
void Canvas::restore() {
|
|
impl_->ctx.restore();
|
|
}
|
|
void Canvas::set_pen(const Pen& pen) {
|
|
impl_->current_pen = pen;
|
|
}
|
|
void Canvas::set_brush(const Brush& brush) {
|
|
impl_->current_brush = brush;
|
|
}
|
|
void Canvas::set_font(const Font& font) {
|
|
impl_->current_font = resolve_font(font);
|
|
}
|
|
void Canvas::set_antialias(bool enabled) {
|
|
impl_->antialias = enabled;
|
|
}
|
|
void Canvas::set_image_interpolation(Image_Interpolation_Mode mode) {
|
|
switch (mode) {
|
|
case Image_Interpolation_Mode::Nearest:
|
|
impl_->ctx.set_pattern_quality(BL_PATTERN_QUALITY_NEAREST);
|
|
break;
|
|
case Image_Interpolation_Mode::Bilinear:
|
|
impl_->ctx.set_pattern_quality(BL_PATTERN_QUALITY_BILINEAR);
|
|
break;
|
|
case Image_Interpolation_Mode::Bicubic:
|
|
impl_->ctx.set_pattern_quality(BL_PATTERN_QUALITY_BILINEAR);
|
|
break;
|
|
}
|
|
}
|
|
void Canvas::translate(double x, double y) {
|
|
impl_->ctx.translate(x, y);
|
|
}
|
|
void Canvas::scale(double x, double y) {
|
|
impl_->ctx.scale(x, y);
|
|
}
|
|
void Canvas::rotate(double radians) {
|
|
impl_->ctx.rotate(radians);
|
|
}
|
|
void Canvas::transform(double m00, double m01, double m10, double m11, double m20, double m21) {
|
|
impl_->ctx.apply_transform(BLMatrix2D(m00, m01, m10, m11, m20, m21));
|
|
}
|
|
void Canvas::reset_transform() {
|
|
impl_->ctx.reset_transform();
|
|
}
|
|
void Canvas::set_clip_rect(const RectF& rect) {
|
|
impl_->ctx.clip_to_rect(Convert::to_bl(rect));
|
|
}
|
|
void Canvas::reset_clip() {
|
|
impl_->ctx.restore_clipping();
|
|
}
|
|
void Canvas::draw_line(PointF first, PointF second) {
|
|
impl_->apply_pen();
|
|
impl_->ctx.stroke_line(first.x, first.y, second.x, second.y);
|
|
}
|
|
void Canvas::draw_polyline(std::span<const PointF> points) {
|
|
if (points.size() < 2)
|
|
return;
|
|
impl_->apply_pen();
|
|
BLPath path;
|
|
path.move_to(points[0].x, points[0].y);
|
|
for (std::size_t i = 1; i < points.size(); ++i)
|
|
path.line_to(points[i].x, points[i].y);
|
|
impl_->ctx.stroke_path(path);
|
|
}
|
|
void Canvas::draw_point(PointF point) {
|
|
impl_->apply_pen();
|
|
impl_->ctx.stroke_line(point.x, point.y, point.x + 0.001, point.y);
|
|
}
|
|
void Canvas::draw_points(std::span<const PointF> points) {
|
|
for (auto& p : points)
|
|
draw_point(p);
|
|
}
|
|
void Canvas::draw_rect(const RectF& rect) {
|
|
impl_->apply_pen();
|
|
auto r = Convert::to_bl(rect);
|
|
impl_->ctx.stroke_rect(r);
|
|
}
|
|
void Canvas::fill_rect(const RectF& rect) {
|
|
impl_->apply_brush();
|
|
auto r = Convert::to_bl(rect);
|
|
impl_->ctx.fill_rect(r);
|
|
}
|
|
void Canvas::draw_ellipse(PointF center, double radius_x, double radius_y) {
|
|
impl_->apply_pen();
|
|
BLPath path;
|
|
path.add_ellipse(BLEllipse(center.x, center.y, radius_x, radius_y));
|
|
impl_->ctx.stroke_path(path);
|
|
}
|
|
void Canvas::draw_polygon(std::span<const PointF> points, bool fill) {
|
|
if (points.size() < 3)
|
|
return;
|
|
BLPath path;
|
|
path.move_to(points[0].x, points[0].y);
|
|
for (std::size_t i = 1; i < points.size(); ++i)
|
|
path.line_to(points[i].x, points[i].y);
|
|
path.close();
|
|
if (fill) {
|
|
impl_->apply_brush();
|
|
impl_->ctx.fill_path(path);
|
|
}
|
|
impl_->apply_pen();
|
|
impl_->ctx.stroke_path(path);
|
|
}
|
|
void Canvas::fill_polygon(std::span<const PointF> points) {
|
|
draw_polygon(points, true);
|
|
}
|
|
void Canvas::draw_image(const RectF& target, const Image& image) {
|
|
auto src = RectF{0, 0, static_cast<double>(image.width()), static_cast<double>(image.height())};
|
|
draw_image(target, image, src);
|
|
}
|
|
void Canvas::draw_image(const RectF& target, const Image& image, const RectF& source) {
|
|
if (image.empty())
|
|
return;
|
|
impl_->ctx.blit_image(
|
|
BLRect(target.x, target.y, target.width, target.height),
|
|
image.impl_->image,
|
|
BLRectI(static_cast<int>(source.x), static_cast<int>(source.y),
|
|
static_cast<int>(source.width), static_cast<int>(source.height)));
|
|
}
|
|
void Canvas::draw_text(PointF position, std::string_view text) {
|
|
if (text.empty() || !can_use_font(impl_->current_font))
|
|
return;
|
|
BLFont bl_font;
|
|
bl_font.create_from_face(impl_->current_font.face->impl_->face, static_cast<float>(impl_->current_font.size));
|
|
const BLFontMetrics& metrics = bl_font.metrics();
|
|
impl_->apply_text_fill();
|
|
impl_->ctx.set_stroke_style(BLRgba(0.0, 0.0, 0.0, 0.0));
|
|
impl_->ctx.fill_utf8_text(BLPoint(position.x, position.y + metrics.ascent), bl_font, text.data(), text.size());
|
|
}
|
|
void Canvas::draw_text(const RectF& rect, Text_Align align, std::string_view text) {
|
|
if (text.empty() || !can_use_font(impl_->current_font))
|
|
return;
|
|
auto metrics = measure_text(text);
|
|
double x = rect.x;
|
|
double y = rect.y;
|
|
if ((align & Text_Align::Horizontal_Center) == Text_Align::Horizontal_Center)
|
|
x += (rect.width - metrics.width) / 2.0;
|
|
else if ((align & Text_Align::Right) == Text_Align::Right)
|
|
x += rect.width - metrics.width;
|
|
if ((align & Text_Align::Vertical_Center) == Text_Align::Vertical_Center)
|
|
y += (rect.height - metrics.height) / 2.0;
|
|
else if ((align & Text_Align::Bottom) == Text_Align::Bottom)
|
|
y += rect.height - metrics.height;
|
|
draw_text(PointF{x, y}, text);
|
|
}
|
|
Text_Metrics Canvas::measure_text(std::string_view text) const {
|
|
return measure_text(impl_->current_font, text);
|
|
}
|
|
Text_Metrics Canvas::measure_text(const Font& font, std::string_view text) const {
|
|
Font resolved = resolve_font(font);
|
|
if (text.empty() || !can_use_font(resolved))
|
|
return {};
|
|
BLFont bl_font;
|
|
bl_font.create_from_face(resolved.face->impl_->face, static_cast<float>(resolved.size));
|
|
BLGlyphBuffer gb;
|
|
gb.set_utf8_text(text.data(), text.size());
|
|
bl_font.shape(gb);
|
|
BLTextMetrics tm{};
|
|
bl_font.get_text_metrics(gb, tm);
|
|
const BLFontMetrics& fm = bl_font.metrics();
|
|
return {
|
|
static_cast<double>(tm.bounding_box.x1 - tm.bounding_box.x0),
|
|
static_cast<double>(fm.ascent + fm.descent),
|
|
static_cast<double>(fm.ascent),
|
|
static_cast<double>(fm.descent),
|
|
static_cast<double>(fm.line_gap)
|
|
};
|
|
}
|
|
Image& Canvas::image() const {
|
|
return impl_->image;
|
|
}
|
|
|
|
Canvas::Font_Diagnostics Canvas::default_font_diagnostics() {
|
|
Font_Diagnostics diagnostics;
|
|
auto face = default_font_face();
|
|
diagnostics.source = default_font_source_storage();
|
|
diagnostics.face_valid = face && face->valid();
|
|
Font font;
|
|
font.face = face;
|
|
font.size = 12.0;
|
|
diagnostics.font_valid = can_use_font(font);
|
|
if (!diagnostics.font_valid)
|
|
return diagnostics;
|
|
|
|
Image image(1, 1);
|
|
image.fill(Color::transparent());
|
|
Canvas canvas(image);
|
|
canvas.set_font(font);
|
|
Text_Metrics metrics = canvas.measure_text("0123456789 ABC \xE4\xB8\xAD\xE6\x96\x87");
|
|
diagnostics.test_width = metrics.width;
|
|
diagnostics.test_height = metrics.height;
|
|
return diagnostics;
|
|
}
|
|
|
|
void Canvas::diagnose_default_font_once() {
|
|
static bool done = false;
|
|
if (done)
|
|
return;
|
|
done = true;
|
|
Font_Diagnostics diagnostics = default_font_diagnostics();
|
|
std::cout << "Renderive default font: source="
|
|
<< (diagnostics.source.empty() ? "<none>" : diagnostics.source)
|
|
<< " face_valid=" << (diagnostics.face_valid ? "true" : "false")
|
|
<< " font_valid=" << (diagnostics.font_valid ? "true" : "false")
|
|
<< " test_width=" << diagnostics.test_width
|
|
<< " test_height=" << diagnostics.test_height
|
|
<< std::endl;
|
|
}
|
|
} // namespace renderive
|