mirror of
https://github.com/amyinspace/MagicSetEditor2.git
synced 2026-06-10 04:57:00 -04:00
418 lines
15 KiB
C++
418 lines
15 KiB
C++
//+----------------------------------------------------------------------------+
|
|
//| Description: Magic Set Editor - Program to make card games |
|
|
//| Copyright: (C) Twan van Laarhoven and the other MSE developers |
|
|
//| License: GNU General Public License 2 or later (see file COPYING) |
|
|
//+----------------------------------------------------------------------------+
|
|
|
|
// ----------------------------------------------------------------------------- : Includes
|
|
|
|
#include <util/prec.hpp>
|
|
#include <util/rotation.hpp>
|
|
#include <gfx/gfx.hpp>
|
|
#include <data/font.hpp>
|
|
#include <wx/rawbmp.h>
|
|
|
|
// ----------------------------------------------------------------------------- : Rotation
|
|
|
|
Rotation::Rotation(Radians angle, const RealRect& rect, double zoom, double stretch, RotationFlags flags)
|
|
: angle(constrain_radians(angle))
|
|
, size(rect.size())
|
|
, origin(rect.position())
|
|
, zoomX(zoom * stretch)
|
|
, zoomY(zoom)
|
|
{
|
|
if (stretch != 1.0) {
|
|
size.width /= stretch;
|
|
}
|
|
// set origin
|
|
if (flags & ROTATION_ATTACH_TOP_LEFT) {
|
|
origin -= boundingBoxCorner(size);
|
|
}
|
|
}
|
|
|
|
void Rotation::setStretch(double s) {
|
|
size.width *= s * getStretch();
|
|
zoomX = zoomY * s;
|
|
}
|
|
|
|
|
|
RealPoint Rotation::tr(const RealPoint& p) const {
|
|
double s = sin(angle), c = cos(angle);
|
|
double x = p.x * zoomX, y = p.y * zoomY;
|
|
return RealPoint(c * x + s * y + origin.x,
|
|
-s * x + c * y + origin.y);
|
|
}
|
|
RealPoint Rotation::trPixel(const RealPoint& p) const {
|
|
double s = sin(angle), c = cos(angle);
|
|
double x = p.x * zoomX + 0.5, y = p.y * zoomY + 0.5;
|
|
return RealPoint(c * x + s * y + origin.x - 0.5,
|
|
-s * x + c * y + origin.y - 0.5);
|
|
}
|
|
RealPoint Rotation::trNoZoom(const RealPoint& p) const {
|
|
double s = sin(angle), c = cos(angle);
|
|
double x = p.x, y = p.y;
|
|
return RealPoint(c * x + s * y + origin.x,
|
|
-s * x + c * y + origin.y);
|
|
}
|
|
RealPoint Rotation::trPixelNoZoom(const RealPoint& p) const {
|
|
double s = sin(angle), c = cos(angle);
|
|
double x = p.x + 0.5, y = p.y + 0.5;
|
|
return RealPoint(c * x + s * y + origin.x - 0.5,
|
|
-s * x + c * y + origin.y - 0.5);
|
|
}
|
|
|
|
RealSize Rotation::trSizeToBB(const RealSize& size) const {
|
|
if (is_straight(angle)) {
|
|
if (is_sideways(angle)) {
|
|
return RealSize(size.height * zoomY, size.width * zoomX);
|
|
} else {
|
|
return RealSize(size.width * zoomX, size.height * zoomY);
|
|
}
|
|
} else {
|
|
double s = sin(angle), c = cos(angle);
|
|
double x = size.width * zoomX, y = size.height * zoomY;
|
|
return RealSize(fabs(c * x) + fabs(s * y), fabs(s * x) + fabs(c * y));
|
|
}
|
|
}
|
|
|
|
RealRect Rotation::trRectToBB(const RealRect& r) const {
|
|
const bool special_case_optimization = true;
|
|
double x = r.x * zoomX, y = r.y * zoomY;
|
|
double w = r.width * zoomX, h = r.height * zoomY;
|
|
if (special_case_optimization && is_rad0(angle)) {
|
|
return RealRect(origin.x + x, origin.y + y, w, h);
|
|
} else if (special_case_optimization && is_rad180(angle)) {
|
|
return RealRect(origin.x - x - w, origin.y - y - h, w, h);
|
|
} else if (special_case_optimization && is_rad90(angle)) {
|
|
return RealRect(origin.x + y, origin.y - x - w, h, w);
|
|
} else if (special_case_optimization && is_rad270(angle)) {
|
|
return RealRect(origin.x - y - h, origin.y + x, h, w);
|
|
} else {
|
|
double s = sin(angle), c = cos(angle);
|
|
RealRect result(c * x + s * y + origin.x,
|
|
-s * x + c * y + origin.y,
|
|
0,0);
|
|
if (c > 0) {
|
|
result.width += c * w;
|
|
result.height += c * h;
|
|
} else {
|
|
result.x += c * w;
|
|
result.width -= c * w;
|
|
result.y += c * h;
|
|
result.height -= c * h;
|
|
}
|
|
if (s > 0) {
|
|
result.width += s * h;
|
|
result.y -= s * w;
|
|
result.height += s * w;
|
|
} else {
|
|
result.x += s * h;
|
|
result.width -= s * h;
|
|
result.height -= s * w;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
wxRegion Rotation::trRectToRegion(const RealRect& r) const {
|
|
if (is_straight(angle)) {
|
|
return trRectToBB(r).toRect();
|
|
} else {
|
|
wxPoint points[4] = {trPixel(RealPoint(r.left(), r.top() ))
|
|
,trPixel(RealPoint(r.left(), r.bottom()))
|
|
,trPixel(RealPoint(r.right(), r.bottom()))
|
|
,trPixel(RealPoint(r.right(), r.top() ))};
|
|
return wxRegion(4,points);
|
|
}
|
|
}
|
|
|
|
RealPoint Rotation::trInv(const RealPoint& p) const {
|
|
double s = sin(angle), c = cos(angle);
|
|
double x = p.x - origin.x, y = p.y - origin.y;
|
|
return RealPoint((c * x - s * y) / zoomX,
|
|
(s * x + c * y) / zoomY);
|
|
}
|
|
RealSize Rotation::trInv(const RealSize& x) const {
|
|
double s = sin(angle), c = cos(angle);
|
|
return RealSize((c * x.width - s * x.height) / zoomX,
|
|
(s * x.width + c * x.height) / zoomY);
|
|
}
|
|
|
|
RealPoint Rotation::boundingBoxCorner(const RealSize& size) const {
|
|
// This function is a bit tricky,
|
|
// I derived it by drawing the four cases.
|
|
// Two succeeding cases must agree where they overlap (0,90,180,270 degrees)
|
|
double s = sin(angle), c = cos(angle);
|
|
double w = size.width * zoomX, h = size.height * zoomY;
|
|
if (angle <= rad90) return RealPoint(0, -w * s);
|
|
if (angle <= rad180) return RealPoint(w * c, h * c - w * s);
|
|
if (angle <= rad270) return RealPoint(w * c + h * s, h * c);
|
|
else return RealPoint(h * s, 0);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------- : Rotater
|
|
|
|
Rotater::Rotater(Rotation& rot, const Rotation& by)
|
|
: old(rot)
|
|
, rot(rot)
|
|
{
|
|
// apply rotation
|
|
rot.origin = rot.tr(by.origin);
|
|
rot.size = by.size;
|
|
rot.angle = constrain_radians(rot.angle + by.angle);
|
|
// zooming is not really correct if rot.zoomX != rot.zoomY
|
|
rot.zoomX *= by.zoomX;
|
|
rot.zoomY *= by.zoomY;
|
|
}
|
|
|
|
Rotater::~Rotater() {
|
|
rot = old; // restore
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------- : RotatedDC
|
|
|
|
RotatedDC::RotatedDC(DC& dc, Radians angle, const RealRect& rect, double zoom, RenderQuality quality, RotationFlags flags)
|
|
: Rotation(angle, rect, zoom, 1.0, flags)
|
|
, dc(dc), quality(quality)
|
|
{}
|
|
|
|
RotatedDC::RotatedDC(DC& dc, const Rotation& rotation, RenderQuality quality)
|
|
: Rotation(rotation)
|
|
, dc(dc), quality(quality)
|
|
{}
|
|
|
|
// ----------------------------------------------------------------------------- : RotatedDC : Drawing
|
|
|
|
void RotatedDC::DrawText(const String& text, const RealPoint& pos, int blur_radius, Color stroke_color, int stroke_radius, double stretch) {
|
|
DrawText(text, pos, dc.GetTextForeground(), blur_radius, stroke_color, stroke_radius, stretch);
|
|
}
|
|
|
|
void RotatedDC::DrawText(const String& text, const RealPoint& pos, Color color, int blur_radius, Color stroke_color, int stroke_radius, double stretch) {
|
|
if (text.empty()) return;
|
|
if (color.Alpha() == 0) return;
|
|
if (quality >= QUALITY_AA) {
|
|
RealRect r(pos, GetTextExtent(text));
|
|
RealRect r_ext = trRectToBB(r);
|
|
RealPoint pos2 = tr(pos);
|
|
stretch *= getStretch();
|
|
if (fabs(stretch - 1) > 1e-6) {
|
|
r.width *= stretch;
|
|
RealRect r_ext2 = trRectToBB(r);
|
|
pos2.x += r_ext2.x - r_ext.x;
|
|
pos2.y += r_ext2.y - r_ext.y;
|
|
r_ext.x = r_ext2.x;
|
|
r_ext.y = r_ext2.y;
|
|
}
|
|
draw_resampled_text(dc, text, pos2, r_ext, angle, color, blur_radius, stroke_color, stroke_radius, stretch);
|
|
} else if (quality >= QUALITY_SUB_PIXEL) {
|
|
RealPoint p_ext = tr(pos)*text_scaling;
|
|
double usx,usy;
|
|
dc.GetUserScale(&usx, &usy);
|
|
dc.SetUserScale(usx/text_scaling, usy/text_scaling);
|
|
dc.SetTextForeground(color);
|
|
dc.DrawRotatedText(text, (int) p_ext.x, (int) p_ext.y, rad_to_deg(angle));
|
|
dc.SetUserScale(usx, usy);
|
|
} else {
|
|
RealPoint p_ext = tr(pos);
|
|
dc.SetTextForeground(color);
|
|
dc.DrawRotatedText(text, (int) p_ext.x, (int) p_ext.y, rad_to_deg(angle));
|
|
}
|
|
}
|
|
|
|
void RotatedDC::DrawTextWithShadowOrStroke(const String& text, const FontRef& font, const RealPoint& pos, double scale, double stretch) {
|
|
double s_scale = scale * dc.GetFont().GetPointSize() / text_scaling / 15.;
|
|
if (font.hasShadow() && !font.hasStroke()) {
|
|
RealSize shadow_displacement = trInvS(RealSize(font.shadow_displacement_x, font.shadow_displacement_y) * s_scale);
|
|
DrawText(text, pos + shadow_displacement, font.shadow_color, lround(font.shadow_blur * s_scale), Color(0,0,0), 0, stretch);
|
|
}
|
|
DrawText(text, pos, font.color, lround(font.stroke_blur * s_scale), font.stroke_color, lround(font.stroke_radius * s_scale), stretch);
|
|
}
|
|
|
|
void RotatedDC::DrawBitmap(const Bitmap& bitmap, const RealPoint& pos) {
|
|
if (is_rad0(angle)) {
|
|
RealPoint p_ext = tr(pos);
|
|
dc.DrawBitmap(bitmap, to_int(p_ext.x), to_int(p_ext.y), true);
|
|
} else {
|
|
DrawImage(bitmap.ConvertToImage(), pos);
|
|
}
|
|
}
|
|
void RotatedDC::DrawImage(const Image& image, const RealPoint& pos, ImageCombine combine) {
|
|
Image rotated = rotate_image(image, angle);
|
|
DrawPreRotatedImage(rotated, RealRect(pos,trInvS(RealSize(image))), combine);
|
|
}
|
|
void RotatedDC::DrawPreRotatedBitmap(const Bitmap& bitmap, const RealRect& rect) {
|
|
RealPoint p_ext = tr(rect.position()) + boundingBoxCorner(rect.size());
|
|
dc.DrawBitmap(bitmap, to_int(p_ext.x), to_int(p_ext.y), true);
|
|
}
|
|
void RotatedDC::DrawPreRotatedImage (const Image& image, const RealRect& rect, ImageCombine combine) {
|
|
RealPoint p_ext = tr(rect.position()) + boundingBoxCorner(rect.size());
|
|
draw_combine_image(dc, to_int(p_ext.x), to_int(p_ext.y), image, combine);
|
|
}
|
|
|
|
void RotatedDC::DrawLine (const RealPoint& p1, const RealPoint& p2) {
|
|
wxPoint p1_ext = tr(p1), p2_ext = tr(p2);
|
|
dc.DrawLine(p1_ext.x, p1_ext.y, p2_ext.x, p2_ext.y);
|
|
}
|
|
|
|
void RotatedDC::DrawRectangle(const RealRect& r) {
|
|
if (is_straight(angle)) {
|
|
wxRect r_ext = trRectToBB(r);
|
|
dc.DrawRectangle(r_ext.x, r_ext.y, r_ext.width, r_ext.height);
|
|
} else {
|
|
wxPoint points[4] = {trPixel(RealPoint(r.left(), r.top() ))
|
|
,trPixel(RealPoint(r.left(), r.bottom()))
|
|
,trPixel(RealPoint(r.right(), r.bottom()))
|
|
,trPixel(RealPoint(r.right(), r.top() ))};
|
|
dc.DrawPolygon(4,points);
|
|
}
|
|
}
|
|
|
|
void RotatedDC::DrawRoundedRectangle(const RealRect& r, double radius) {
|
|
if (is_straight(angle)) {
|
|
wxRect r_ext = trRectToBB(r);
|
|
dc.DrawRoundedRectangle(r_ext.x, r_ext.y, r_ext.width, r_ext.height, trS(radius));
|
|
} else {
|
|
// TODO
|
|
DrawRectangle(r);
|
|
}
|
|
}
|
|
|
|
void RotatedDC::DrawInvertRectangle(const RealRect& r) {
|
|
wxRect r_ext = trRectToBB(r);
|
|
|
|
wxBitmap bmp(r_ext.width, r_ext.height, 24);
|
|
wxMemoryDC memDC(bmp);
|
|
memDC.Blit(0, 0, r_ext.width, r_ext.height, &dc, r_ext.x, r_ext.y);
|
|
memDC.SelectObject(wxNullBitmap);
|
|
|
|
wxNativePixelData data(bmp);
|
|
if (!data) return; // Raw access not available on this platform/bitmap
|
|
wxNativePixelData::Iterator p(data);
|
|
for (int j = 0; j < r_ext.height; ++j) {
|
|
wxNativePixelData::Iterator rowStart = p;
|
|
for (int i = 0; i < r_ext.width; ++i) {
|
|
// Invert each channel
|
|
p.Red() = 255 - p.Red();
|
|
p.Green() = 255 - p.Green();
|
|
p.Blue() = 255 - p.Blue();
|
|
++p; // Move to next pixel in the row
|
|
}
|
|
p = rowStart;
|
|
p.OffsetY(data, 1); // Move to the next row
|
|
}
|
|
|
|
dc.DrawBitmap(bmp, RealPoint(r_ext.x, r_ext.y));
|
|
}
|
|
|
|
void RotatedDC::DrawCircle(const RealPoint& center, double radius) {
|
|
wxPoint p = tr(center);
|
|
dc.DrawCircle(p.x + 1, p.y + 1, int(trS(radius)));
|
|
}
|
|
|
|
void RotatedDC::DrawEllipse(const RealPoint& center, const RealSize& size) {
|
|
wxPoint c_ext = tr(center - size/2);
|
|
wxSize s_ext = trSizeToBB(size);
|
|
dc.DrawEllipse(c_ext.x, c_ext.y, s_ext.x, s_ext.y);
|
|
}
|
|
void RotatedDC::DrawEllipticArc(const RealPoint& center, const RealSize& size, Radians start, Radians end) {
|
|
wxPoint c_ext = tr(center - size/2);
|
|
wxSize s_ext = trSizeToBB(size);
|
|
dc.DrawEllipticArc(c_ext.x, c_ext.y, s_ext.x, s_ext.y, rad_to_deg(start + angle), rad_to_deg(end + angle));
|
|
}
|
|
void RotatedDC::DrawEllipticSpoke(const RealPoint& center, const RealSize& size, Radians angle) {
|
|
wxPoint c_ext = tr(center - size/2);
|
|
wxSize s_ext = trSizeToBB(size);
|
|
Radians rot_angle = angle + this->angle;
|
|
Radians sin_angle = sin(rot_angle), cos_angle = cos(rot_angle);
|
|
// position of center and of point on the boundary can vary because of rounding errors,
|
|
// this code matches DrawEllipticArc (at least on windows xp).
|
|
dc.DrawLine(
|
|
c_ext.x + int( 0.5 * (s_ext.x + cos_angle) ), // center
|
|
c_ext.y + int( 0.5 * (s_ext.y - sin_angle) ),
|
|
c_ext.x + int( 0.5 + 0.5 * (s_ext.x-1) * (1 + cos_angle) ), // boundary
|
|
c_ext.y + int( 0.5 + 0.5 * (s_ext.y-1) * (1 - sin_angle) )
|
|
);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------- : Forwarded properties
|
|
|
|
void RotatedDC::SetPen(const wxPen& pen) { dc.SetPen(pen); }
|
|
void RotatedDC::SetBrush(const wxBrush& brush) { dc.SetBrush(brush); }
|
|
void RotatedDC::SetTextForeground(const Color& color) { dc.SetTextForeground(color); }
|
|
void RotatedDC::SetLogicalFunction(wxRasterOperationMode function) { dc.SetLogicalFunction(function); }
|
|
|
|
void RotatedDC::SetFont(const wxFont& font) {
|
|
if (quality == QUALITY_LOW && zoomX == 1 && zoomY == 1) {
|
|
dc.SetFont(font);
|
|
} else {
|
|
wxFont scaled = font;
|
|
if (quality == QUALITY_LOW) {
|
|
scaled.SetPointSize((int) trY(font.GetPointSize()));
|
|
} else {
|
|
scaled.SetPointSize((int) (trY(font.GetPointSize()) * text_scaling));
|
|
}
|
|
dc.SetFont(scaled);
|
|
}
|
|
}
|
|
void RotatedDC::SetFont(const FontRef& font, double scale) {
|
|
dc.SetFont(font.toWxFont(trS(scale) * (quality == QUALITY_LOW ? 1 : text_scaling)));
|
|
}
|
|
|
|
double RotatedDC::getFontSizeStep() const {
|
|
if (quality == QUALITY_LOW) {
|
|
return 1;
|
|
} else {
|
|
return 1. / text_scaling;
|
|
}
|
|
}
|
|
|
|
RealSize RotatedDC::GetTextExtent(const String& text) const {
|
|
int w, h;
|
|
dc.GetTextExtent(text, &w, &h);
|
|
#ifdef __WXGTK__
|
|
// HACK: Some fonts don't get the descender height set correctly.
|
|
int charHeight = dc.GetCharHeight();
|
|
if (charHeight != h)
|
|
h += h - charHeight;
|
|
#endif
|
|
if (quality == QUALITY_LOW) {
|
|
return RealSize(w / zoomX, h / zoomY);
|
|
} else {
|
|
return RealSize(w / (zoomX * text_scaling), h / (zoomY * text_scaling));
|
|
}
|
|
}
|
|
double RotatedDC::GetCharHeight() const {
|
|
int h = dc.GetCharHeight();
|
|
#ifdef __WXGTK__
|
|
// See above HACK
|
|
int extent;
|
|
dc.GetTextExtent(_("H"), 0, &extent);
|
|
if (h != extent)
|
|
h = 2 * extent - h;
|
|
#endif
|
|
if (quality == QUALITY_LOW) {
|
|
return h / zoomY;
|
|
} else {
|
|
return h / (zoomY * text_scaling);
|
|
}
|
|
}
|
|
|
|
void RotatedDC::SetClippingRegion(const RealRect& rect) {
|
|
dc.SetDeviceClippingRegion(trRectToRegion(rect));
|
|
}
|
|
void RotatedDC::DestroyClippingRegion() {
|
|
dc.DestroyClippingRegion();
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------- : Other
|
|
|
|
Bitmap RotatedDC::GetBackground(const RealRect& r) {
|
|
wxRect wr = trRectToBB(r);
|
|
Bitmap background(wr.width, wr.height);
|
|
wxMemoryDC mdc;
|
|
mdc.SelectObject(background);
|
|
mdc.Blit(0, 0, wr.width, wr.height, &dc, wr.x, wr.y);
|
|
mdc.SelectObject(wxNullBitmap);
|
|
return background;
|
|
}
|