diff --git a/src/effects/text.cpp b/src/effects/text.cpp index 460adf5..667382b 100644 --- a/src/effects/text.cpp +++ b/src/effects/text.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "core/parallel.hpp" #include "effects/source_fill.hpp" @@ -57,6 +58,10 @@ const std::vector& TextEffect::params() const { "Moves the text right by this many pixels (left when negative)."), ParamSpec::makeInt("text_offset_y", "Offset Y", 0, -20000, 20000, "Moves the text down by this many pixels (up when negative)."), + ParamSpec::makeDouble("text_rotation", "Rotation", 0.0, -360.0, 360.0, + "Turns the text and its box about their centre by this many " + "degrees, clockwise (anticlockwise when negative).", + 1.0, 1), ParamSpec::makeDouble("text_opacity", "Text opacity", 1.0, 0.0, 1.0, "How strongly the glyphs are mixed over what is behind them.") .styled(), @@ -264,6 +269,18 @@ Image TextEffect::render(const std::vector& sources, int width, int heigh const double boxY = (centre ? (height - boxHeight) / 2.0 : 0.0) + static_cast(paramInt(values, "text_offset_y", 0)); + // The turn is about the middle of the box, so it never moves the text away + // from where the centring and the offset put it. Cairo's y points down, + // which makes a positive angle clockwise on screen. + const double rotation = paramDouble(values, "text_rotation", 0.0); + if (rotation != 0.0) { + const double cx = boxX + boxWidth / 2.0; + const double cy = boxY + boxHeight / 2.0; + cairo_translate(cr.get, cx, cy); + cairo_rotate(cr.get, rotation * std::numbers::pi / 180.0); + cairo_translate(cr.get, -cx, -cy); + } + if (drawsBox) { cairo_save(cr.get); cairo_rectangle(cr.get, boxX, boxY, boxWidth, boxHeight); diff --git a/tests/core_tests.cpp b/tests/core_tests.cpp index 2dc172b..ffee49c 100644 --- a/tests/core_tests.cpp +++ b/tests/core_tests.cpp @@ -759,6 +759,20 @@ int main() { check(countRed(moved, 100, 60, 200, 120) > 0 && countRed(moved, 0, 0, 100, 60) == 0, "the offset moves it down and to the right"); + // A wide line turned a quarter stands upright: it reaches above and + // below the band it filled unturned, and no longer out to the sides. + ParamValues wide = plain; + wide["text"] = std::string("HHHHHH"); + wide["font_size"] = 40.0; + const auto flat = renderText(blue, 200, 120, wide); + ParamValues turned = wide; + turned["text_rotation"] = 90.0; + const auto upright = renderText(blue, 200, 120, turned); + check(countRed(flat, 0, 0, 200, 25) == 0 && countRed(upright, 0, 0, 200, 25) > 0, + "rotation turns the text about its centre"); + check(countRed(flat, 0, 0, 50, 120) > 0 && countRed(upright, 0, 0, 50, 120) == 0, + "and takes it in from the sides"); + // Weight and size are the two knobs the glyphs themselves answer to: a // bolder or a larger setting covers strictly more of the canvas. ParamValues bold = plain; @@ -877,7 +891,8 @@ int main() { carries("box_padding") && carries("box_opacity"), "the look is what it carries"); check(!carries("text") && !carries("text_wrap") && !carries("box_image") && - !carries("text_center") && !carries("text_offset_x") && !carries("text_offset_y"), + !carries("text_center") && !carries("text_offset_x") && !carries("text_offset_y") && + !carries("text_rotation"), "the words and the placement stay on the stage"); check(styleSpecs("").empty() && styleSpecs("nosuchkind").empty(), "a kind no effect claims carries nothing");