Files
react-native-svg/windows/RNSVG/Utils.h
Andrew Coates d02c997352 fix: react-native-windows implementation for new architecture (#2527)
# Summary
There are two main things going on with the PR. Both involve a reworking
of the new arch implementation of rn-svg for windows.
The current implementation attempts to implement a svg renderer from
scratch. There are numerous edge cases that it wasn't handling
correctly, and the performance had some serious issues. This
implementation switches to use the svg rendering path built into
Direct2D. This brings significant performance improvements.

The 2nd issue is there have been various breaking changes in
react-native-windows for how new arch native components are implemented.
This brings the rn-svg implementation in line with those latest changes.

## Test Plan

Primary testing right now is loading up the example app in the repo.
New arch on react-native-windows is still in somewhat early days - so
there are not really current users of this code. I am integrating this
code into Microsoft Office, where I have tested some scenarios. But we
will get expanded testing as we roll out the new arch. I expect there to
be some follow-ups as we expand our usage. The version of rn-svg before
this PR doesn't build with the latest new arch react-native-windows
versions. - So its hard to get worse than that.

### What's required for testing (prerequisites)?

### What are the steps to reproduce (after prerequisites)?

## Compatibility

| OS      | Implemented |
| ------- | :---------: |
| iOS     |    N/A  |
| MacOS   |    N/A      |
| Android |    N/A      |
| Web     |          |

## Checklist

- [x] I have tested this on a device and a simulator
- [ ] I added documentation in `README.md`
- [ ] I updated the typed files (typescript)
- [ ] I added a test for the API in the `__tests__` folder
2024-11-15 12:17:12 +01:00

321 lines
10 KiB
C++

#pragma once
#include "pch.h"
#include <winrt/Windows.Foundation.Numerics.h>
#include <UI.Text.h>
#include "JSValueReader.h"
#include "D2DHelpers.h"
#include "D2DBrush.h"
#include "D2DDeviceContext.h"
#define _USE_MATH_DEFINES
#include <math.h>
using namespace winrt::Microsoft::ReactNative;
namespace winrt::RNSVG {
struct Utils {
public:
static std::vector<float> GetAdjustedStrokeArray(IVector<SVGLength> const &value, float strokeWidth, float canvasDiagonal) {
std::vector<float> result;
for (auto const item : value) {
float absValue{GetAbsoluteLength(item, canvasDiagonal)};
// Win2D sets the length of each dash as the product of the element value in array and stroke width,
// we divide each value in the dashArray by StrokeWidth to account for this.
// http://microsoft.github.io/Win2D/WinUI2/html/P_Microsoft_Graphics_Canvas_Geometry_CanvasStrokeStyle_CustomDashStyle.htm
result.push_back(absValue / (strokeWidth == 0.0f ? 1.0f : strokeWidth));
}
return result;
}
static float GetCanvasDiagonal(Size const &size) {
float powX{std::powf(size.Width, 2)};
float powY{std::powf(size.Height, 2)};
return std::sqrtf(powX + powY) * static_cast<float>(M_SQRT1_2);
}
static float GetAbsoluteLength(SVGLength const &length, double relativeTo) {
return GetAbsoluteLength(length, static_cast<float>(relativeTo));
}
static float GetAbsoluteLength(SVGLength const &length, float relativeTo) {
auto value{length.Value};
auto unit{length.Unit};
// 1in = 2.54cm = 96px
auto inch{96.0f};
auto cm{inch / 2.54f};
switch (unit) {
case RNSVG::LengthType::Percentage:
return value / 100.0f * relativeTo;
case RNSVG::LengthType::Centimeter:
// 1cm = 96px/2.54
return value * cm;
case RNSVG::LengthType::Millimeter:
// 1mm = 1/10th of 1cm
return value * cm / 10.0f;
case RNSVG::LengthType::Inch:
// 1in = 2.54cm = 96px
return value * inch;
case RNSVG::LengthType::Point:
// 1pt = 1/72th of 1in
return value * inch / 72.0f;
case RNSVG::LengthType::Pica:
// 1pc = 1/6th of 1in
return value * inch / 6.0f;
case RNSVG::LengthType::Pixel:
default:
return value;
}
}
static Numerics::float3x2 GetRotationMatrix(float degrees) {
// convert to radians
auto radians{degrees * static_cast<float>(M_PI) / 100.0f};
return Numerics::make_float3x2_rotation(radians);
}
static Numerics::float3x2 GetViewBoxTransform(Rect const &vbRect, Rect const &elRect, std::string align, RNSVG::MeetOrSlice const &meetOrSlice) {
// based on https://svgwg.org/svg2-draft/coords.html#ComputingAViewportsTransform
// Let vb-x, vb-y, vb-width, vb-height be the min-x, min-y, width and height values of the viewBox attribute
// respectively.
float vbX = vbRect.X;
float vbY = vbRect.Y;
float vbWidth = vbRect.Width;
float vbHeight = vbRect.Height;
// Let e-x, e-y, e-width, e-height be the position and size of the element respectively.
float eX = elRect.X;
float eY = elRect.Y;
float eWidth = elRect.Width;
float eHeight = elRect.Height;
// Initialize scale-x to e-width/vb-width.
float scaleX = eWidth / vbWidth;
// Initialize scale-y to e-height/vb-height.
float scaleY = eHeight / vbHeight;
// If align is not 'none' and meetOrSlice is 'meet', set the larger of scale-x and scale-y to the smaller.
// Otherwise, if align is not 'none' and meetOrSlice is 'slice', set the smaller of scale-x and scale-y to the
// larger.
if (align != "none" && meetOrSlice == RNSVG::MeetOrSlice::Meet) {
scaleX = scaleY = std::min(scaleX, scaleY);
} else if (align != "none" && meetOrSlice == RNSVG::MeetOrSlice::Slice) {
scaleX = scaleY = std::max(scaleX, scaleY);
}
// Initialize translate-x to e-x - (vb-x * scale-x).
float translateX = eX - (vbX * scaleX);
// Initialize translate-y to e-y - (vb-y * scale-y).
float translateY = eY - (vbY * scaleY);
// If align contains 'xMid', add (e-width - vb-width * scale-x) / 2 to translate-x.
if (align.find("xMid") != std::string::npos) {
translateX += (eWidth - vbWidth * scaleX) / 2.0f;
}
// If align contains 'xMax', add (e-width - vb-width * scale-x) to translate-x.
if (align.find("xMax") != std::string::npos) {
translateX += (eWidth - vbWidth * scaleX);
}
// If align contains 'yMid', add (e-height - vb-height * scale-y) / 2 to translate-y.
if (align.find("YMid") != std::string::npos) {
translateY += (eHeight - vbHeight * scaleY) / 2.0f;
}
// If align contains 'yMax', add (e-height - vb-height * scale-y) to translate-y.
if (align.find("YMax") != std::string::npos) {
translateY += (eHeight - vbHeight * scaleY);
}
// The transform applied to content contained by the element is given by
// translate(translate-x, translate-y) scale(scale-x, scale-y).
auto const &translate{Numerics::make_float3x2_translation(translateX, translateY)};
auto const &scale{Numerics::make_float3x2_scale(scaleX, scaleY)};
return scale * translate;
}
static D2D1_MATRIX_3X2_F GetViewBoxTransformD2D(Rect const &vbRect, Rect const &elRect, std::string align, RNSVG::MeetOrSlice const &meetOrSlice) {
return D2DHelpers::AsD2DTransform(GetViewBoxTransform(vbRect, elRect, align, meetOrSlice));
}
static RNSVG::MeetOrSlice GetMeetOrSlice(JSValue const &value) {
if (value.IsNull()) {
return RNSVG::MeetOrSlice::Meet;
}
switch (value.AsInt8()) {
case 2:
return RNSVG::MeetOrSlice::None;
case 1:
return RNSVG::MeetOrSlice::Slice;
case 0:
default:
return RNSVG::MeetOrSlice::Meet;
}
}
static std::string JSValueAsBrushUnits(JSValue const &value, std::string defaultValue = "objectBoundingBox") {
if (value.IsNull()) {
return defaultValue;
} else {
switch (value.AsInt32()) {
case 1:
return "userSpaceOnUse";
case 0:
default:
return "objectBoundingBox";
}
}
}
static float JSValueAsFloat(JSValue const &value, float defaultValue = 0.0f) {
return value.IsNull() ? defaultValue : value.AsSingle();
}
static std::string JSValueAsString(JSValue const &value, std::string defaultValue = "") {
return value.IsNull() ? defaultValue : value.AsString();
}
static Windows::UI::Color JSValueAsColor(JSValue const &value, Windows::UI::Color const &defaultValue = Colors::Transparent()) {
if (value.IsNull()) {
return defaultValue;
} else if (auto const &brush{value.To<xaml::Media::Brush>()}) {
if (auto const &scb{brush.try_as<xaml::Media::SolidColorBrush>()}) {
return scb.Color();
}
}
return defaultValue;
}
static SVGLength JSValueAsSVGLength(JSValue const &value, SVGLength const &defaultValue = {}) {
return value.IsNull() ? defaultValue : value.To<RNSVG::SVGLength>();
}
static Numerics::float3x2 JSValueAsTransform(JSValue const &value, Numerics::float3x2 const &defaultValue = {}) {
if (value.IsNull()) {
return defaultValue;
} else {
auto const &matrix{value.AsArray()};
return Numerics::float3x2(
matrix.at(0).AsSingle(),
matrix.at(1).AsSingle(),
matrix.at(2).AsSingle(),
matrix.at(3).AsSingle(),
matrix.at(4).AsSingle(),
matrix.at(5).AsSingle());
}
}
static D2D1::Matrix3x2F JSValueAsD2DTransform(JSValue const &value, D2D1::Matrix3x2F const defaultValue = {}) {
if (value.IsNull()) {
return defaultValue;
} else {
auto const &matrix{value.AsArray()};
return D2D1::Matrix3x2F(
matrix.at(0).AsSingle(),
matrix.at(1).AsSingle(),
matrix.at(2).AsSingle(),
matrix.at(3).AsSingle(),
matrix.at(4).AsSingle(),
matrix.at(5).AsSingle());
}
}
static std::vector<D2D1_GRADIENT_STOP> JSValueAsStops(JSValue const &value) {
if (value.IsNull()) {
return {};
}
auto const &stops{value.AsArray()};
std::vector<D2D1_GRADIENT_STOP> gradientStops;
for (size_t i = 0; i < stops.size(); ++i) {
D2D1_GRADIENT_STOP stop{};
stop.position = Utils::JSValueAsFloat(stops.at(i));
stop.color = D2DHelpers::AsD2DColor(Utils::JSValueAsColor(stops.at(++i)));
gradientStops.emplace_back(stop);
}
return gradientStops;
}
static winrt::Windows::UI::Color JSValueAsD2DColor(float value) {
auto color = static_cast<int32_t>(value);
auto alpha = static_cast<uint8_t>(color >> 24);
auto red = static_cast<uint8_t>((color >> 16) & 0xff);
auto green = static_cast<uint8_t>((color >> 8) & 0xff);
auto blue = static_cast<uint8_t>(color & 0xff);
return winrt::Windows::UI::ColorHelper::FromArgb(alpha, red, green, blue);
}
static com_ptr<ID2D1Brush> GetCanvasBrush(
hstring const &brushId,
Windows::UI::Color const &color,
RNSVG::SvgView const &root,
com_ptr<ID2D1Geometry> const &geometry,
RNSVG::D2DDeviceContext const &context) {
com_ptr<ID2D1Brush> brush;
com_ptr<ID2D1DeviceContext> deviceContext{get_self<RNSVG::implementation::D2DDeviceContext>(context)->Get()};
auto winColor{Windows::UI::Colors::Transparent()};
if (root && brushId != L"") {
if (brushId == L"currentColor") {
com_ptr<ID2D1SolidColorBrush> scb;
winColor = root.CurrentColor();
deviceContext->CreateSolidColorBrush(D2DHelpers::AsD2DColor(winColor), scb.put());
brush = scb.as<ID2D1Brush>();
} else if (auto const &brushView{root.Brushes().TryLookup(brushId)}) {
brushView.CreateBrush();
if (geometry) {
D2D1_RECT_F bounds;
geometry->GetBounds(nullptr, &bounds);
brushView.SetBounds(D2DHelpers::FromD2DRect(bounds));
}
brush = get_self<RNSVG::implementation::D2DBrush>(brushView.Brush())->Get();
}
}
if (!brush) {
com_ptr<ID2D1SolidColorBrush> scb;
assert(root != nullptr);
winColor = color;
deviceContext->CreateSolidColorBrush(D2DHelpers::AsD2DColor(winColor), scb.put());
brush = scb.as<ID2D1Brush>();
}
return brush;
}
static D2D1_VECTOR_2F GetScale(D2D1_MATRIX_3X2_F const matrix) {
auto scaleX = std::sqrt(matrix.m11 * matrix.m11 + matrix.m12 * matrix.m12);
auto scaleY = std::sqrt(matrix.m21 * matrix.m21 + matrix.m22 * matrix.m22);
return {scaleX, scaleY};
}
static D2D1_VECTOR_2F GetScale(Numerics::float3x2 const &matrix) {
return GetScale(D2DHelpers::AsD2DTransform(matrix));
}
};
} // namespace winrt::RNSVG