/* ============================================================================== This file is part of the JUCE framework. Copyright (c) Raw Material Software Limited JUCE is an open source framework subject to commercial or open source licensing. By downloading, installing, or using the JUCE framework, or combining the JUCE framework with any other source code, object code, content or any other copyrightable work, you agree to the terms of the JUCE End User Licence Agreement, and all incorporated terms including the JUCE Privacy Policy and the JUCE Website Terms of Service, as applicable, which will bind you. If you do not agree to the terms of these agreements, we will not license the JUCE framework to you, and you must discontinue the installation or download process and cease use of the JUCE framework. JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ JUCE Privacy Policy: https://juce.com/juce-privacy-policy JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ Or: You may also use this code under the terms of the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.en.html THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { //============================================================================== struct D2DHelpers { static bool isTransformAxisAligned (const AffineTransform& transform) { return transform.mat01 == 0.0f && transform.mat10 == 0.0f; } static void pathToGeometrySink (const Path& path, ID2D1GeometrySink* sink, const AffineTransform& transform, D2D1_FIGURE_BEGIN figureMode) { class ScopedFigure { public: ScopedFigure (ID2D1GeometrySink* s, D2D1_POINT_2F pt, D2D1_FIGURE_BEGIN mode) : sink (s) { sink->BeginFigure (pt, mode); } ~ScopedFigure() { if (sink != nullptr) sink->EndFigure (end); } void setClosed() { end = D2D1_FIGURE_END_CLOSED; } private: ID2D1GeometrySink* sink = nullptr; D2D1_FIGURE_END end = D2D1_FIGURE_END_OPEN; JUCE_DECLARE_NON_COPYABLE (ScopedFigure) JUCE_DECLARE_NON_MOVEABLE (ScopedFigure) }; // Every call to BeginFigure must have a matching call to EndFigure. But - the Path does not necessarily // have matching startNewSubPath and closePath markers. D2D1_POINT_2F lastLocation{}; std::optional figure; Path::Iterator it (path); const auto doTransform = [&transform] (float x, float y) { transform.transformPoint (x, y); return D2D1_POINT_2F { x, y }; }; const auto updateFigure = [&] (float x, float y) { if (! figure.has_value()) figure.emplace (sink, lastLocation, figureMode); lastLocation = doTransform (x, y); }; while (it.next()) { switch (it.elementType) { case Path::Iterator::lineTo: updateFigure (it.x1, it.y1); sink->AddLine (lastLocation); break; case Path::Iterator::quadraticTo: updateFigure (it.x2, it.y2); sink->AddQuadraticBezier ({ doTransform (it.x1, it.y1), lastLocation }); break; case Path::Iterator::cubicTo: updateFigure (it.x3, it.y3); sink->AddBezier ({ doTransform (it.x1, it.y1), doTransform (it.x2, it.y2), lastLocation }); break; case Path::Iterator::closePath: if (figure.has_value()) figure->setClosed(); figure.reset(); break; case Path::Iterator::startNewSubPath: figure.reset(); lastLocation = doTransform (it.x1, it.y1); figure.emplace (sink, lastLocation, figureMode); break; } } } static D2D1_POINT_2F pointTransformed (Point pt, const AffineTransform& transform) { transform.transformPoint (pt.x, pt.y); return { (FLOAT) pt.x, (FLOAT) pt.y }; } static void rectToGeometrySink (const Rectangle& rect, ID2D1GeometrySink* sink, const AffineTransform& transform, D2D1_FIGURE_BEGIN figureMode) { const auto a = pointTransformed (rect.getTopLeft(), transform); const auto b = pointTransformed (rect.getTopRight(), transform); const auto c = pointTransformed (rect.getBottomRight(), transform); const auto d = pointTransformed (rect.getBottomLeft(), transform); sink->BeginFigure (a, figureMode); sink->AddLine (b); sink->AddLine (c); sink->AddLine (d); sink->EndFigure (D2D1_FIGURE_END_CLOSED); } static ComSmartPtr rectListToPathGeometry (ID2D1Factory* factory, const RectangleList& clipRegion, const AffineTransform& transform, D2D1_FILL_MODE fillMode, D2D1_FIGURE_BEGIN figureMode, [[maybe_unused]] Direct2DMetrics* metrics) { JUCE_D2DMETRICS_SCOPED_ELAPSED_TIME (metrics, createGeometryTime) ScopedGeometryWithSink objects { factory, fillMode }; if (objects.sink == nullptr) return {}; for (int i = clipRegion.getNumRectangles(); --i >= 0;) rectToGeometrySink (clipRegion.getRectangle (i), objects.sink, transform, figureMode); return objects.geometry; } static ComSmartPtr pathToPathGeometry (ID2D1Factory* factory, const Path& path, const AffineTransform& transform, D2D1_FIGURE_BEGIN figureMode, [[maybe_unused]] Direct2DMetrics* metrics) { JUCE_D2DMETRICS_SCOPED_ELAPSED_TIME (metrics, createGeometryTime) ScopedGeometryWithSink objects { factory, path.isUsingNonZeroWinding() ? D2D1_FILL_MODE_WINDING : D2D1_FILL_MODE_ALTERNATE }; if (objects.sink == nullptr) return {}; pathToGeometrySink (path, objects.sink, transform, figureMode); return objects.geometry; } static ComSmartPtr pathStrokeTypeToStrokeStyle (ID2D1Factory1* factory, const PathStrokeType& strokeType) { // JUCE JointStyle ID2D1StrokeStyle // --------------- ---------------- // mitered D2D1_LINE_JOIN_MITER // curved D2D1_LINE_JOIN_ROUND // beveled D2D1_LINE_JOIN_BEVEL // // JUCE EndCapStyle ID2D1StrokeStyle // ---------------- ---------------- // butt D2D1_CAP_STYLE_FLAT // square D2D1_CAP_STYLE_SQUARE // rounded D2D1_CAP_STYLE_ROUND auto lineJoin = D2D1_LINE_JOIN_MITER; switch (strokeType.getJointStyle()) { case PathStrokeType::JointStyle::mitered: // already set break; case PathStrokeType::JointStyle::curved: lineJoin = D2D1_LINE_JOIN_ROUND; break; case PathStrokeType::JointStyle::beveled: lineJoin = D2D1_LINE_JOIN_BEVEL; break; default: // invalid EndCapStyle jassertfalse; break; } auto capStyle = D2D1_CAP_STYLE_FLAT; switch (strokeType.getEndStyle()) { case PathStrokeType::EndCapStyle::butt: // already set break; case PathStrokeType::EndCapStyle::square: capStyle = D2D1_CAP_STYLE_SQUARE; break; case PathStrokeType::EndCapStyle::rounded: capStyle = D2D1_CAP_STYLE_ROUND; break; default: // invalid EndCapStyle jassertfalse; break; } D2D1_STROKE_STYLE_PROPERTIES1 strokeStyleProperties { capStyle, capStyle, capStyle, lineJoin, strokeType.getStrokeThickness(), D2D1_DASH_STYLE_SOLID, 0.0f, D2D1_STROKE_TRANSFORM_TYPE_NORMAL }; ComSmartPtr strokeStyle; factory->CreateStrokeStyle (strokeStyleProperties, nullptr, 0, strokeStyle.resetAndGetPointerAddress()); return strokeStyle; } }; //============================================================================== Direct2DGraphicsContext::Direct2DGraphicsContext() = default; Direct2DGraphicsContext::~Direct2DGraphicsContext() = default; bool Direct2DGraphicsContext::startFrame (float dpiScale) { const auto pimpl = getPimpl(); const auto paintAreas = pimpl->getPaintAreas(); currentState = pimpl->startFrame(); if (currentState == nullptr) return false; if (auto deviceContext = pimpl->getDeviceContext()) { resetPendingClipList(); clipToRectangleList (paintAreas); // Clear the buffer *after* setting the clip region clearTargetBuffer(); // Init font & brush setFont (currentState->font); currentState->updateCurrentBrush(); addTransform (AffineTransform::scale (dpiScale)); } return true; } void Direct2DGraphicsContext::endFrame() { getPimpl()->finishFrame(); currentState = nullptr; ++frame; } void Direct2DGraphicsContext::setOrigin (Point o) { applyPendingClipList(); currentState->currentTransform.setOrigin (o); resetPendingClipList(); } void Direct2DGraphicsContext::addTransform (const AffineTransform& transform) { // The pending clip list is based on the transform stored in currentState, so apply the pending clip list before adding the transform applyPendingClipList(); currentState->currentTransform.addTransform (transform); resetPendingClipList(); } bool Direct2DGraphicsContext::clipToRectangle (const Rectangle& r) { const auto& transform = currentState->currentTransform; auto& deviceSpaceClipList = currentState->deviceSpaceClipList; JUCE_TRACE_EVENT_INT_RECT_LIST (etw::clipToRectangle, etw::direct2dKeyword, getFrameId(), r); // The renderer needs to keep track of the aggregate clip rectangles in order to correctly report the // clip region to the caller. The renderer also needs to push Direct2D clip layers to the device context // to perform the actual clipping. The reported clip region will not necessarily match the Direct2D clip region // if the clip region is transformed, or the clip region is an image or a path. // // Pushing Direct2D clip layers is expensive and there's no need to clip until something is actually drawn. // So - pendingClipList is a list of the areas that need to actually be clipped. Each fill or // draw method then applies any pending clip areas before drawing. // // Also - calling ID2D1DeviceContext::SetTransform is expensive, so check the current transform to see // if the renderer can pre-transform the clip rectangle instead. if (transform.isOnlyTranslated) { // The current transform is only a translation, so save a few cycles by just adding the // offset instead of transforming the rectangle; the software renderer does something similar. auto translatedR = r.toFloat() + transform.offset.toFloat(); deviceSpaceClipList.clipTo (translatedR); pendingClipList.clipTo (translatedR); } else if (currentState->isCurrentTransformAxisAligned()) { // The current transform is a simple scale + translation, so pre-transform the rectangle auto transformedR = transform.boundsAfterTransform (r.toFloat()); deviceSpaceClipList.clipTo (transformedR); pendingClipList.clipTo (transformedR); } else { deviceSpaceClipList = getPimpl()->getFrameSize().toFloat(); // The current transform is too complex to pre-transform the rectangle, so just add the // rectangle to the clip list. The renderer will need to call ID2D1DeviceContext::SetTransform // before applying the clip layer. pendingClipList.clipTo (r.toFloat()); } return ! isClipEmpty(); } bool Direct2DGraphicsContext::clipToRectangleList (const RectangleList& newClipList) { JUCE_SCOPED_TRACE_EVENT_FRAME_RECT_I32 (etw::clipToRectangleList, etw::direct2dKeyword, getFrameId(), newClipList) const auto& transform = currentState->currentTransform; auto& deviceSpaceClipList = currentState->deviceSpaceClipList; // This works a lot like clipToRect // Just one rectangle? if (newClipList.getNumRectangles() == 1) return clipToRectangle (newClipList.getRectangle (0)); if (transform.isIdentity()) { deviceSpaceClipList.clipTo (newClipList); pendingClipList.clipTo (newClipList); } else if (currentState->currentTransform.isOnlyTranslated) { RectangleList offsetList (newClipList); offsetList.offsetAll (transform.offset); deviceSpaceClipList.clipTo (offsetList); pendingClipList.clipTo (offsetList); } else if (currentState->isCurrentTransformAxisAligned()) { RectangleList scaledList; for (auto& i : newClipList) scaledList.add (transform.boundsAfterTransform (i.toFloat())); deviceSpaceClipList.clipTo (scaledList); pendingClipList.clipTo (scaledList); } else { deviceSpaceClipList = getPimpl()->getFrameSize().toFloat(); pendingClipList.clipTo (newClipList); } return ! isClipEmpty(); } void Direct2DGraphicsContext::excludeClipRectangle (const Rectangle& userSpaceExcludedRectangle) { JUCE_SCOPED_TRACE_EVENT_FRAME_RECT_I32 (etw::excludeClipRectangle, etw::direct2dKeyword, getFrameId(), userSpaceExcludedRectangle) auto& transform = currentState->currentTransform; auto& deviceSpaceClipList = currentState->deviceSpaceClipList; const auto frameSize = getPimpl()->getFrameSize().toFloat(); if (transform.isOnlyTranslated) { // Just a translation; pre-translate the exclusion area const auto translatedR = transform.translated (userSpaceExcludedRectangle.toFloat()) .getLargestIntegerWithin() .toFloat(); deviceSpaceClipList.subtract (translatedR); pendingClipList.subtract (translatedR); } else if (currentState->isCurrentTransformAxisAligned()) { // Just a scale + translation; pre-transform the exclusion area const auto transformedR = transform.boundsAfterTransform (userSpaceExcludedRectangle.toFloat()) .getLargestIntegerWithin() .toFloat(); deviceSpaceClipList.subtract (transformedR); pendingClipList.subtract (transformedR); } else { applyPendingClipList(); deviceSpaceClipList = frameSize; pendingClipList.subtract (userSpaceExcludedRectangle.toFloat()); } } void Direct2DGraphicsContext::resetPendingClipList() { auto& transform = currentState->currentTransform; const auto frameSize = transform.isOnlyTranslated || currentState->isCurrentTransformAxisAligned() ? getPimpl()->getFrameSize() : getPimpl()->getFrameSize().transformedBy (transform.getTransform().inverted()); pendingClipList.reset (frameSize.toFloat()); } void Direct2DGraphicsContext::applyPendingClipList() { if (! pendingClipList.isClipApplied()) return; auto& transform = currentState->currentTransform; const auto axisAligned = currentState->isCurrentTransformAxisAligned(); const auto list = pendingClipList.getList(); // Clip if the pending clip list is not empty and smaller than the frame size if (! list.containsRectangle (getPimpl()->getFrameSize().toFloat()) && ! list.isEmpty()) { if (list.getNumRectangles() == 1 && axisAligned) { auto r = list.getRectangle (0); currentState->pushAliasedAxisAlignedClipLayer (r); } else { auto clipTransform = axisAligned ? AffineTransform{} : transform.getTransform(); if (auto clipGeometry = D2DHelpers::rectListToPathGeometry (getPimpl()->getDirect2DFactory(), list, clipTransform, D2D1_FILL_MODE_WINDING, D2D1_FIGURE_BEGIN_FILLED, metrics.get())) { currentState->pushGeometryClipLayer (clipGeometry); } } resetPendingClipList(); } } void Direct2DGraphicsContext::clipToPath (const Path& path, const AffineTransform& transform) { JUCE_SCOPED_TRACE_EVENT_FRAME (etw::clipToPath, etw::direct2dKeyword, getFrameId()); applyPendingClipList(); // Set the clip list to the full size of the frame to match // the software renderer auto pathTransform = currentState->currentTransform.getTransformWith (transform); auto transformedBounds = path.getBounds().transformedBy (pathTransform); currentState->deviceSpaceClipList.clipTo (transformedBounds); if (auto deviceContext = getPimpl()->getDeviceContext()) { currentState->pushGeometryClipLayer (D2DHelpers::pathToPathGeometry (getPimpl()->getDirect2DFactory(), path, pathTransform, D2D1_FIGURE_BEGIN_FILLED, metrics.get())); } } void Direct2DGraphicsContext::clipToImageAlpha (const Image& sourceImage, const AffineTransform& transform) { JUCE_SCOPED_TRACE_EVENT_FRAME (etw::clipToImageAlpha, etw::direct2dKeyword, getFrameId()); if (sourceImage.isNull()) return; applyPendingClipList(); // Put a rectangle clip layer under the image clip layer // The D2D bitmap brush will extend past the boundaries of sourceImage, so clip // to the sourceImage bounds auto brushTransform = currentState->currentTransform.getTransformWith (transform); if (D2DHelpers::isTransformAxisAligned (brushTransform)) { currentState->pushAliasedAxisAlignedClipLayer (sourceImage.getBounds().toFloat().transformedBy (brushTransform)); } else { const auto sourceImageRectF = D2DUtilities::toRECT_F (sourceImage.getBounds()); ComSmartPtr geometry; if (const auto hr = getPimpl()->getDirect2DFactory()->CreateRectangleGeometry (sourceImageRectF, geometry.resetAndGetPointerAddress()); SUCCEEDED (hr) && geometry != nullptr) { currentState->pushTransformedRectangleGeometryClipLayer (geometry, brushTransform); } } // Set the clip list to the full size of the frame to match // the software renderer currentState->deviceSpaceClipList = getPimpl()->getFrameSize().toFloat(); if (auto deviceContext = getPimpl()->getDeviceContext()) { const auto maxDim = (int) deviceContext->GetMaximumBitmapSize(); if (sourceImage.getWidth() > maxDim || sourceImage.getHeight() > maxDim) { // The Direct2D renderer doesn't currently support clipping to very large images jassertfalse; return; } const auto device = D2DUtilities::getDeviceForContext (deviceContext); const auto pagesAndArea = PagesAndArea::make (sourceImage, device); if (pagesAndArea.pages.empty()) return; const auto bitmap = pagesAndArea.pages.front().bitmap; if (bitmap == nullptr) return; // Make a transformed bitmap brush using the bitmap // As usual, apply the current transform first *then* the transform parameter ComSmartPtr brush; const auto pageTransform = AffineTransform::translation (pagesAndArea.area.getTopLeft()).inverted(); auto matrix = D2DUtilities::transformToMatrix (pageTransform.followedBy (brushTransform)); D2D1_BRUSH_PROPERTIES brushProps = { 1.0f, matrix }; auto bitmapBrushProps = D2D1::BitmapBrushProperties (D2D1_EXTEND_MODE_CLAMP, D2D1_EXTEND_MODE_CLAMP); auto hr = deviceContext->CreateBitmapBrush (bitmap, bitmapBrushProps, brushProps, brush.resetAndGetPointerAddress()); if (FAILED (hr)) return; // Push the clipping layer onto the layer stack // Don't set maskTransform in the LayerParameters struct; that only applies to geometry clipping // Do set the contentBounds member, transformed appropriately auto layerParams = D2D1::LayerParameters1(); auto transformedBounds = sourceImage.getBounds().toFloat().transformedBy (brushTransform); layerParams.contentBounds = D2DUtilities::toRECT_F (transformedBounds); layerParams.opacityBrush = brush; currentState->pushLayer (layerParams); } } bool Direct2DGraphicsContext::clipRegionIntersects (const Rectangle& r) { const auto rect = currentState->currentTransform.isOnlyTranslated ? currentState->currentTransform.translated (r.toFloat()) : currentState->currentTransform.boundsAfterTransform (r.toFloat()); return currentState->deviceSpaceClipList.intersectsRectangle (rect); } Rectangle Direct2DGraphicsContext::getClipBounds() const { return currentState->currentTransform.deviceSpaceToUserSpace (currentState->deviceSpaceClipList.getBounds()).getSmallestIntegerContainer(); } bool Direct2DGraphicsContext::isClipEmpty() const { return getClipBounds().isEmpty(); } void Direct2DGraphicsContext::saveState() { JUCE_SCOPED_TRACE_EVENT_FRAME (etw::saveState, etw::direct2dKeyword, getFrameId()); applyPendingClipList(); currentState = getPimpl()->pushSavedState(); } void Direct2DGraphicsContext::restoreState() { JUCE_SCOPED_TRACE_EVENT_FRAME (etw::restoreState, etw::direct2dKeyword, getFrameId()); currentState = getPimpl()->popSavedState(); currentState->updateColourBrush(); jassert (currentState); resetPendingClipList(); } void Direct2DGraphicsContext::beginTransparencyLayer (float opacity) { JUCE_SCOPED_TRACE_EVENT_FRAME (etw::beginTransparencyLayer, etw::direct2dKeyword, getFrameId()); applyPendingClipList(); if (auto deviceContext = getPimpl()->getDeviceContext()) currentState->pushTransparencyLayer (opacity); } void Direct2DGraphicsContext::endTransparencyLayer() { JUCE_SCOPED_TRACE_EVENT_FRAME (etw::endTransparencyLayer, etw::direct2dKeyword, getFrameId()); if (auto deviceContext = getPimpl()->getDeviceContext()) currentState->popTopLayer(); } void Direct2DGraphicsContext::setFill (const FillType& fillType) { JUCE_SCOPED_TRACE_EVENT_FRAME (etw::setFill, etw::direct2dKeyword, getFrameId()); if (auto deviceContext = getPimpl()->getDeviceContext()) { currentState->fillType = fillType; currentState->updateCurrentBrush(); } } void Direct2DGraphicsContext::setOpacity (float newOpacity) { JUCE_SCOPED_TRACE_EVENT_FRAME (etw::setOpacity, etw::direct2dKeyword, getFrameId()); currentState->setOpacity (newOpacity); if (auto deviceContext = getPimpl()->getDeviceContext()) currentState->updateCurrentBrush(); } void Direct2DGraphicsContext::setInterpolationQuality (Graphics::ResamplingQuality quality) { switch (quality) { case Graphics::ResamplingQuality::lowResamplingQuality: currentState->interpolationMode = D2D1_INTERPOLATION_MODE_NEAREST_NEIGHBOR; break; case Graphics::ResamplingQuality::mediumResamplingQuality: currentState->interpolationMode = D2D1_INTERPOLATION_MODE_LINEAR; break; case Graphics::ResamplingQuality::highResamplingQuality: currentState->interpolationMode = D2D1_INTERPOLATION_MODE_HIGH_QUALITY_CUBIC; break; } } Line Direct2DGraphicsContext::offsetShape (Line a, Point b) { return { a.getStart() + b, a.getEnd() + b }; } Rectangle Direct2DGraphicsContext::offsetShape (Rectangle a, Point b) { return a + b; } RectangleList Direct2DGraphicsContext::offsetShape (RectangleList a, Point b) { a.offsetAll (b); return a; } template void Direct2DGraphicsContext::paintPrimitive (const Shape& shape, Fn&& primitiveOp) { const auto& transform = currentState->currentTransform; applyPendingClipList(); auto deviceContext = getPimpl()->getDeviceContext(); if (deviceContext == nullptr) return; const auto fillTransform = transform.isOnlyTranslated ? SavedState::BrushTransformFlags::applyWorldAndFillTypeTransforms : SavedState::BrushTransformFlags::applyFillTypeTransform; const auto brush = currentState->getBrush (fillTransform); if (transform.isOnlyTranslated) { const auto translated = offsetShape (shape, transform.offset.toFloat()); if (currentState->doesIntersectClipList (translated)) primitiveOp (translated, deviceContext, brush); } else if (currentState->doesIntersectClipList (transform.boundsAfterTransform (shape))) { ScopedTransform scopedTransform { *getPimpl(), currentState }; primitiveOp (shape, deviceContext, brush); } } void Direct2DGraphicsContext::fillRect (const Rectangle& r, bool replaceExistingContents) { if (r.isEmpty()) return; if (replaceExistingContents) { applyPendingClipList(); const auto asRectF = D2DUtilities::toRECT_F (r.toFloat()); ComSmartPtr rectGeometry; getPimpl()->getDirect2DFactory()->CreateRectangleGeometry (asRectF, rectGeometry.resetAndGetPointerAddress()); const auto matrix = D2DUtilities::transformToMatrix (currentState->currentTransform.getTransform()); ComSmartPtr geo; getPimpl()->getDirect2DFactory()->CreateTransformedGeometry (rectGeometry, matrix, geo.resetAndGetPointerAddress()); const auto brush = currentState->fillType.isInvisible() ? currentState->currentBrush : currentState->getBrush(); currentState->layers.fillGeometryWithNoLayersActive (getPimpl()->getDeviceContext(), geo, brush); return; } const auto fill = [] (Rectangle rect, ComSmartPtr deviceContext, ComSmartPtr brush) { if (brush != nullptr) deviceContext->FillRectangle (D2DUtilities::toRECT_F (rect), brush); }; paintPrimitive (r.toFloat(), fill); } void Direct2DGraphicsContext::fillRect (const Rectangle& r) { if (r.isEmpty()) return; auto fill = [] (Rectangle rect, ComSmartPtr deviceContext, ComSmartPtr brush) { if (brush != nullptr) deviceContext->FillRectangle (D2DUtilities::toRECT_F (rect), brush); }; paintPrimitive (r, fill); } void Direct2DGraphicsContext::fillRectList (const RectangleList& list) { if (getPimpl()->fillSpriteBatch (list)) return; auto fill = [] (const RectangleList& l, ComSmartPtr deviceContext, ComSmartPtr brush) { if (brush != nullptr) for (const auto& r : l) deviceContext->FillRectangle (D2DUtilities::toRECT_F (r), brush); }; paintPrimitive (list, fill); } void Direct2DGraphicsContext::drawRect (const Rectangle& r, float lineThickness) { auto draw = [&] (Rectangle rect, ComSmartPtr deviceContext, ComSmartPtr brush) { // ID2D1DeviceContext::DrawRectangle centers the stroke around the edges of the specified rectangle, but // the software renderer contains the stroke within the rectangle // To match the software renderer, reduce the rectangle by half the stroke width if (brush != nullptr) deviceContext->DrawRectangle (D2DUtilities::toRECT_F (rect.reduced (lineThickness * 0.5f)), brush, lineThickness); }; paintPrimitive (r, draw); } void Direct2DGraphicsContext::fillPath (const Path& p, const AffineTransform& transform) { JUCE_SCOPED_TRACE_EVENT_FRAME (etw::fillPath, etw::direct2dKeyword, getFrameId()); if (p.getBounds().withZeroOrigin() == Rectangle{}) return; applyPendingClipList(); const auto deviceContext = getPimpl()->getDeviceContext(); const auto brush = currentState->getBrush (SavedState::applyFillTypeTransform); const auto factory = getPimpl()->getDirect2DFactory(); const auto geometry = D2DHelpers::pathToPathGeometry (factory, p, transform, D2D1_FIGURE_BEGIN_FILLED, metrics.get()); if (deviceContext == nullptr || brush == nullptr || geometry == nullptr) return; JUCE_D2DMETRICS_SCOPED_ELAPSED_TIME (metrics, fillGeometryTime) ScopedTransform scopedTransform { *getPimpl(), currentState }; deviceContext->FillGeometry (geometry, brush); } void Direct2DGraphicsContext::strokePath (const Path& p, const PathStrokeType& strokeType, const AffineTransform& transform) { JUCE_SCOPED_TRACE_EVENT_FRAME (etw::drawPath, etw::direct2dKeyword, getFrameId()); if (p.getBounds().withZeroOrigin() == Rectangle{}) return; applyPendingClipList(); const auto deviceContext = getPimpl()->getDeviceContext(); const auto brush = currentState->getBrush (SavedState::applyFillTypeTransform); const auto factory = getPimpl()->getDirect2DFactory(); const auto strokeStyle = D2DHelpers::pathStrokeTypeToStrokeStyle (factory, strokeType); const auto geometry = D2DHelpers::pathToPathGeometry (factory, p, transform, D2D1_FIGURE_BEGIN_HOLLOW, metrics.get()); if (deviceContext == nullptr || brush == nullptr || geometry == nullptr || strokeStyle == nullptr) return; JUCE_D2DMETRICS_SCOPED_ELAPSED_TIME (metrics, drawGeometryTime) ScopedTransform scopedTransform { *getPimpl(), currentState }; deviceContext->DrawGeometry (geometry, brush, strokeType.getStrokeThickness(), strokeStyle); } void Direct2DGraphicsContext::drawImage (const Image& imageIn, const AffineTransform& transform) { JUCE_D2DMETRICS_SCOPED_ELAPSED_TIME (metrics, drawImageTime) JUCE_SCOPED_TRACE_EVENT_FRAME (etw::drawImage, etw::direct2dKeyword, getFrameId()); if (imageIn.isNull()) return; applyPendingClipList(); if (auto deviceContext = getPimpl()->getDeviceContext()) { const auto device = D2DUtilities::getDeviceForContext (deviceContext); const auto pagesAndArea = PagesAndArea::make (imageIn, device); if (pagesAndArea.pages.empty()) { jassertfalse; return; } const auto imageTransform = currentState->currentTransform.getTransformWith (transform); auto drawTiles = [&] (auto&& getRect) { for (const auto& page : pagesAndArea.pages) { if (page.bitmap == nullptr) continue; const auto pageBounds = page.getBounds(); const auto intersection = pageBounds.toFloat().getIntersection (pagesAndArea.area.toFloat()); if (intersection.isEmpty()) continue; const auto src = intersection - pageBounds.getPosition().toFloat(); const auto dst = getRect (intersection - pagesAndArea.area.getPosition().toFloat()); const auto [srcConverted, dstConverted] = std::tuple (D2DUtilities::toRECT_F (src), D2DUtilities::toRECT_F (dst)); if (page.bitmap->GetPixelFormat().format == DXGI_FORMAT_A8_UNORM) { const auto lastColour = currentState->colourBrush->GetColor(); const auto lastMode = deviceContext->GetAntialiasMode(); currentState->colourBrush->SetColor (D2D1::ColorF (1.0f, 1.0f, 1.0f, currentState->fillType.getOpacity())); deviceContext->SetAntialiasMode (D2D1_ANTIALIAS_MODE_ALIASED); deviceContext->FillOpacityMask (page.bitmap, currentState->colourBrush, dstConverted, srcConverted); deviceContext->SetAntialiasMode (lastMode); currentState->colourBrush->SetColor (lastColour); } else { const auto lastMode = deviceContext->GetAntialiasMode(); if (pagesAndArea.pages.size() > 1) deviceContext->SetAntialiasMode (D2D1_ANTIALIAS_MODE_ALIASED); deviceContext->DrawBitmap (page.bitmap, dstConverted, currentState->fillType.getOpacity(), currentState->interpolationMode, srcConverted, {}); deviceContext->SetAntialiasMode (lastMode); } } }; if (imageTransform.isOnlyTranslation() || D2DHelpers::isTransformAxisAligned (imageTransform)) { drawTiles ([&] (auto intersection) { return intersection.transformedBy (imageTransform); }); return; } ScopedTransform scopedTransform { *getPimpl(), currentState, transform }; drawTiles ([] (auto intersection) { return intersection; }); } } void Direct2DGraphicsContext::drawLine (const Line& line) { drawLineWithThickness (line, 1.0f); } void Direct2DGraphicsContext::drawLineWithThickness (const Line& line, float lineThickness) { auto draw = [&] (Line l, ComSmartPtr deviceContext, ComSmartPtr brush) { if (brush == nullptr) return; const auto makePoint = [] (const auto& x) { return D2D1::Point2F (x.getX(), x.getY()); }; deviceContext->DrawLine (makePoint (l.getStart()), makePoint (l.getEnd()), brush, lineThickness); }; paintPrimitive (line, draw); } void Direct2DGraphicsContext::setFont (const Font& newFont) { JUCE_SCOPED_TRACE_EVENT_FRAME (etw::setFont, etw::direct2dKeyword, getFrameId()); currentState->setFont (newFont); } const Font& Direct2DGraphicsContext::getFont() { return currentState->font; } float Direct2DGraphicsContext::getPhysicalPixelScaleFactor() const { if (currentState != nullptr) return currentState->currentTransform.getPhysicalPixelScaleFactor(); // If this is hit, there's no frame in progress, so the scale factor isn't meaningful jassertfalse; return 1.0f; } void Direct2DGraphicsContext::drawRoundedRectangle (const Rectangle& area, float cornerSize, float lineThickness) { auto draw = [&] (Rectangle rect, ComSmartPtr deviceContext, ComSmartPtr brush) { if (brush == nullptr) return; D2D1_ROUNDED_RECT roundedRect { D2DUtilities::toRECT_F (rect), cornerSize, cornerSize }; deviceContext->DrawRoundedRectangle (roundedRect, brush, lineThickness); }; paintPrimitive (area, draw); } void Direct2DGraphicsContext::fillRoundedRectangle (const Rectangle& area, float cornerSize) { auto fill = [&] (Rectangle rect, ComSmartPtr deviceContext, ComSmartPtr brush) { if (brush == nullptr) return; D2D1_ROUNDED_RECT roundedRect { D2DUtilities::toRECT_F (rect), cornerSize, cornerSize }; deviceContext->FillRoundedRectangle (roundedRect, brush); }; paintPrimitive (area, fill); } void Direct2DGraphicsContext::drawEllipse (const Rectangle& area, float lineThickness) { auto draw = [&] (Rectangle rect, ComSmartPtr deviceContext, ComSmartPtr brush) { if (brush == nullptr) return; auto centre = rect.getCentre(); D2D1_ELLIPSE ellipse { { centre.x, centre.y }, rect.proportionOfWidth (0.5f), rect.proportionOfHeight (0.5f) }; deviceContext->DrawEllipse (ellipse, brush, lineThickness); }; paintPrimitive (area, draw); } void Direct2DGraphicsContext::fillEllipse (const Rectangle& area) { auto fill = [&] (Rectangle rect, ComSmartPtr deviceContext, ComSmartPtr brush) { if (brush == nullptr) return; auto centre = rect.getCentre(); D2D1_ELLIPSE ellipse { { centre.x, centre.y }, rect.proportionOfWidth (0.5f), rect.proportionOfHeight (0.5f) }; deviceContext->FillEllipse (ellipse, brush); }; paintPrimitive (area, fill); } void Direct2DGraphicsContext::drawGlyphs (Span glyphNumbers, Span> positions, const AffineTransform& transform) { jassert (glyphNumbers.size() == positions.size()); JUCE_D2DMETRICS_SCOPED_ELAPSED_TIME (metrics, drawGlyphRunTime) JUCE_SCOPED_TRACE_EVENT_FRAME (etw::drawGlyphRun, etw::direct2dKeyword, getFrameId()); if (currentState->fillType.isInvisible() || glyphNumbers.empty() || positions.empty()) return; const auto& font = currentState->font; const auto deviceContext = getPimpl()->getDeviceContext(); if (! deviceContext) return; const auto typeface = font.getTypefacePtr(); const auto fontFace = [&]() -> ComSmartPtr { if (auto* x = dynamic_cast (typeface.get())) return x->getIDWriteFontFace(); return {}; }(); if (fontFace == nullptr) return; const auto fontScale = font.getHorizontalScale(); const auto textTransform = AffineTransform::scale (fontScale, 1.0f).followedBy (transform); const auto worldTransform = currentState->currentTransform.getTransform(); const auto textAndWorldTransform = textTransform.followedBy (worldTransform); const auto onlyTranslated = textAndWorldTransform.isOnlyTranslation(); const auto fillTransform = onlyTranslated ? SavedState::BrushTransformFlags::applyWorldAndFillTypeTransforms : SavedState::BrushTransformFlags::applyFillTypeTransform; auto brush = currentState->getBrush (fillTransform); if (brush == nullptr) return; const auto getBrushTransform = [] (auto brushIn) -> AffineTransform { D2D1::Matrix3x2F matrix{}; brushIn->GetTransform (&matrix); return D2DUtilities::matrixToTransform (matrix); }; applyPendingClipList(); D2D1_POINT_2F baselineOrigin { 0.0f, 0.0f }; if (onlyTranslated) { baselineOrigin = { textAndWorldTransform.getTranslationX(), textAndWorldTransform.getTranslationY() }; } else { if (brush != currentState->colourBrush) { const auto brushTransform = getBrushTransform (brush); brush->SetTransform (D2DUtilities::transformToMatrix (brushTransform.followedBy (textTransform.inverted()))); } getPimpl()->setDeviceContextTransform (textAndWorldTransform); } // There's no need to transform a plain colour brush jassert (brush != currentState->colourBrush || getBrushTransform (brush).isIdentity()); auto& run = getPimpl()->glyphRun; run.replace (positions, fontScale); DWRITE_GLYPH_RUN directWriteGlyphRun; directWriteGlyphRun.fontFace = fontFace; directWriteGlyphRun.fontEmSize = font.getHeightInPoints(); directWriteGlyphRun.glyphCount = (UINT32) glyphNumbers.size(); directWriteGlyphRun.glyphIndices = glyphNumbers.data(); directWriteGlyphRun.glyphAdvances = run.getAdvances(); directWriteGlyphRun.glyphOffsets = run.getOffsets(); directWriteGlyphRun.isSideways = FALSE; directWriteGlyphRun.bidiLevel = 0; const auto tryDrawColourGlyphs = [&] { // There's a helpful colour glyph rendering sample at // https://github.com/microsoft/Windows-universal-samples/blob/main/Samples/DWriteColorGlyph/cpp/CustomTextRenderer.cpp const auto factory = getPimpl()->getDirectWriteFactory4(); if (factory == nullptr) return false; const auto ctx = deviceContext.getInterface(); if (ctx == nullptr) return false; ComSmartPtr enumerator; constexpr auto formats = DWRITE_GLYPH_IMAGE_FORMATS_TRUETYPE | DWRITE_GLYPH_IMAGE_FORMATS_CFF | DWRITE_GLYPH_IMAGE_FORMATS_COLR | DWRITE_GLYPH_IMAGE_FORMATS_PNG | DWRITE_GLYPH_IMAGE_FORMATS_JPEG | DWRITE_GLYPH_IMAGE_FORMATS_TIFF | DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8; if (const auto hr = factory->TranslateColorGlyphRun (baselineOrigin, &directWriteGlyphRun, nullptr, formats, DWRITE_MEASURING_MODE_NATURAL, nullptr, 0, enumerator.resetAndGetPointerAddress()); FAILED (hr) || enumerator == nullptr) { // NOCOLOR is expected if the font has no colour glyphs. Other errors are not expected. jassert (hr == DWRITE_E_NOCOLOR && enumerator == nullptr); return false; } for (BOOL gotRun = false; SUCCEEDED (enumerator->MoveNext (&gotRun)) && gotRun;) { const DWRITE_COLOR_GLYPH_RUN1* colourRun = nullptr; if (FAILED (enumerator->GetCurrentRun (&colourRun)) || colourRun == nullptr) break; JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wswitch-enum") switch (colourRun->glyphImageFormat) { case DWRITE_GLYPH_IMAGE_FORMATS_PNG: case DWRITE_GLYPH_IMAGE_FORMATS_JPEG: case DWRITE_GLYPH_IMAGE_FORMATS_TIFF: case DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8: ctx->DrawColorBitmapGlyphRun (colourRun->glyphImageFormat, { colourRun->baselineOriginX, colourRun->baselineOriginY }, &colourRun->glyphRun, colourRun->measuringMode); break; case DWRITE_GLYPH_IMAGE_FORMATS_TRUETYPE: case DWRITE_GLYPH_IMAGE_FORMATS_CFF: case DWRITE_GLYPH_IMAGE_FORMATS_COLR: default: { const auto useForeground = colourRun->paletteIndex == 0xffff; const auto lastColour = currentState->colourBrush->GetColor(); const auto colourBrush = currentState->colourBrush; if (! useForeground) colourBrush->SetColor (colourRun->runColor); const auto brushToUse = useForeground ? ComSmartPtr (brush) : ComSmartPtr (colourBrush); ctx->DrawGlyphRun ({ colourRun->baselineOriginX, colourRun->baselineOriginY }, &colourRun->glyphRun, colourRun->glyphRunDescription, brushToUse, colourRun->measuringMode); if (! useForeground) colourBrush->SetColor (lastColour); break; } } JUCE_END_IGNORE_WARNINGS_GCC_LIKE } return true; }; if (! tryDrawColourGlyphs()) deviceContext->DrawGlyphRun (baselineOrigin, &directWriteGlyphRun, brush); if (! onlyTranslated) getPimpl()->resetDeviceContextTransform(); } Direct2DGraphicsContext::ScopedTransform::ScopedTransform (Pimpl& pimplIn, SavedState* stateIn) : pimpl (pimplIn), state (stateIn) { pimpl.setDeviceContextTransform (stateIn->currentTransform.getTransform()); } Direct2DGraphicsContext::ScopedTransform::ScopedTransform (Pimpl& pimplIn, SavedState* stateIn, const AffineTransform& transform) : pimpl (pimplIn), state (stateIn) { pimpl.setDeviceContextTransform (stateIn->currentTransform.getTransformWith (transform)); } Direct2DGraphicsContext::ScopedTransform::~ScopedTransform() { pimpl.resetDeviceContextTransform(); } //============================================================================== //============================================================================== #if JUCE_UNIT_TESTS class Direct2DGraphicsContextTests : public UnitTest { public: Direct2DGraphicsContextTests() : UnitTest ("Direct2D Graphics Context", UnitTestCategories::graphics) {} void runTest() override { const auto imageWidth = 1 << 15; const auto imageHeight = 128; Image largeImageSoftware { Image::RGB, imageWidth, imageHeight, false, SoftwareImageType{} }; { Graphics g { largeImageSoftware }; g.setGradientFill ({ Colours::red, 0, 0, Colours::cyan, (float) largeImageSoftware.getWidth(), 0, false }); g.fillAll(); } constexpr auto targetDim = 512; const auto largeImageNative = NativeImageType{}.convert (largeImageSoftware); const auto subsection = largeImageNative.getClippedImage (largeImageNative.getBounds().withSizeKeepingCentre (1 << 14, 64)); beginTest ("Render large images"); { for (const auto& imageToDraw : { largeImageNative, subsection }) { const AffineTransform transformsToTest[] { {}, AffineTransform::translation ((float) targetDim - (float) imageToDraw.getWidth(), 0), AffineTransform::translation (0, (float) targetDim - (float) imageToDraw.getHeight()), AffineTransform::scale ((float) targetDim / imageWidth), AffineTransform::scale ((float) targetDim / imageWidth) .followedBy (AffineTransform::translation (32, 64)), AffineTransform::scale (1.1f), AffineTransform::scale ((float) targetDim / imageWidth, (float) targetDim / imageHeight), AffineTransform::rotation (MathConstants::pi * 0.25f), AffineTransform::rotation (MathConstants::pi * 0.25f, imageWidth * 0.5f, 0) .followedBy (AffineTransform::translation (-imageWidth * 0.5f, 0)), }; for (const auto& transform : transformsToTest) { Image targetNative { Image::RGB, targetDim, targetDim, true, NativeImageType{} }; Image targetSoftware { Image::RGB, targetDim, targetDim, true, SoftwareImageType{} }; for (auto& image : { &targetNative, &targetSoftware }) { Graphics g { *image }; g.drawImageTransformed (imageToDraw, transform); } auto pixelsToIgnore = createEdgeMask (imageToDraw.getWidth(), imageToDraw.getHeight(), targetNative.getWidth(), targetNative.getHeight(), transform); compareImages (targetNative, targetSoftware, 16, pixelsToIgnore); } } } beginTest ("Check that there is no seam between D2D image tiles"); { const auto width = 229; const auto height = 80 * width; Image filmStripSoftware { Image::RGB, width, height, true, SoftwareImageType{} }; { Graphics g { filmStripSoftware }; g.setGradientFill ({ Colours::red, 0, 0, Colours::cyan, (float) filmStripSoftware.getWidth(), 0, false }); g.fillAll(); } const auto filmStrip = NativeImageType{}.convert (filmStripSoftware); Image targetNative { Image::RGB, targetDim, targetDim, true, NativeImageType{} }; Image targetSoftware { Image::RGB, targetDim, targetDim, true, SoftwareImageType{} }; const auto transform = AffineTransform::scale (1.1f); for (auto* target : { &targetNative, &targetSoftware }) { Graphics g { *target }; g.setColour (Colours::orange); g.fillAll(); g.addTransform (transform); g.drawImage (filmStrip, 0, 0, width, width, 0, (16384 / width) * width, width, width); } auto pixelsToIgnore = createEdgeMask (width, width, targetNative.getWidth(), targetNative.getHeight(), transform); compareImages (targetNative, targetSoftware, 1, pixelsToIgnore); } beginTest ("Gradient fill transform should compose with world transform correctly"); { testGradientFillTransform (1.0f); testGradientFillTransform (1.5f); } beginTest ("Text gradient fill transform should compose with world transform correctly"); { testTextGradientFillTransform (2.0f); testTextGradientFillTransform (1.5f); testTextGradientFillTransform (1.0f); } } static Image createEdgeMask (int sourceWidth, int sourceHeight, int maskWidth, int maskHeight, const AffineTransform& transform) { Image mask { Image::SingleChannel, maskWidth, maskHeight, true, SoftwareImageType{} }; Graphics g { mask }; g.addTransform (transform); g.setColour (Colours::white); g.drawRect (Rectangle { 0, 0, sourceWidth + 1, sourceHeight + 1 }.toFloat(), 2.0f); return mask; } void compareImages (const Image& a, const Image& b, int stride, const Image& ignoreMask) { expect (a.getBounds() == b.getBounds()); const Image::BitmapData bitmapA { a, Image::BitmapData::readOnly }; const Image::BitmapData bitmapB { b, Image::BitmapData::readOnly }; int64_t maxAbsError{}; int64_t accumulatedError{}; int64_t numSamples{}; for (auto y = 0; y < a.getHeight(); y += stride) { for (auto x = 0; x < a.getWidth(); x += stride) { if (ignoreMask.getPixelAt (x, y) != Colour{}) continue; const auto expected = bitmapA.getPixelColour (x, y); const auto actual = bitmapB.getPixelColour (x, y); for (auto& fn : { &Colour::getRed, &Colour::getGreen, &Colour::getBlue, &Colour::getAlpha }) { const auto signedError = ((int64_t) (actual.*fn)() - (int64_t) (expected.*fn)()); const auto absError = std::abs (signedError); maxAbsError = std::max (maxAbsError, absError); accumulatedError += absError; ++numSamples; } } } const auto averageError = (double) accumulatedError / (double) numSamples; expect (std::abs (averageError) < 1.0 && maxAbsError < 10); } void testGradientFillTransform (float scale) { constexpr int size = 500; constexpr int circleSize = 100; constexpr int brushTranslation = 20; Image image { Image::RGB, roundToInt (size * scale), roundToInt (size * scale), true }; { for (int i = 0; i < size / circleSize; ++i) { Graphics g { image }; g.addTransform (AffineTransform::scale (scale)); g.addTransform (AffineTransform::translation ((float) i * circleSize, (float) i * circleSize)); const auto fillCol1 { Colours::red }; const auto fillCol2 { Colours::green }; const auto centreLoc = circleSize / 2.0f; FillType innerGlowGrad = ColourGradient { fillCol1, { centreLoc, centreLoc }, fillCol2, { centreLoc, 0.0f }, true }; innerGlowGrad.gradient->addColour (0.19, fillCol1); innerGlowGrad.transform = AffineTransform::scale (1.1f, 0.9f, centreLoc, centreLoc) .followedBy (AffineTransform::translation (brushTranslation, brushTranslation)); g.setFillType (innerGlowGrad); g.fillEllipse (0, 0, (float) circleSize, (float) circleSize); } } for (int i = 0; i < size / circleSize; ++i) { const auto getScaled = [scale] (Point p) { return p.toFloat().transformedBy (AffineTransform::scale (scale)).roundToInt(); }; const Point centre { circleSize / 2, circleSize / 2 }; const Point brushOffset { brushTranslation, brushTranslation }; const auto redPosition = getScaled (centre + brushOffset); expect (image.getPixelAt (redPosition.getX(), redPosition.getY()) == Colours::red); const auto mostlyRedPosition = getScaled (centre); expect (approximatelyEqual (image.getPixelAt (mostlyRedPosition.getX(), mostlyRedPosition.getY()), Colour { 138, 59, 0 })); const auto greenPosition = getScaled (centre.withY (2)); expect (image.getPixelAt (greenPosition.getX(), greenPosition.getY()) == Colours::green); const auto blackPosition = getScaled ({ circleSize - 2, 2 }); expect (image.getPixelAt (blackPosition.getX(), blackPosition.getY()) == Colours::black); } } void testTextGradientFillTransform (float scale) { const auto typeface = loadTypeface (FontBinaryData::Karla_Regular_Typo_Off_Offsets_Off); constexpr int size = 500; Image image { Image::RGB, roundToInt (size * scale), roundToInt (size * scale), true }; const auto fillCol1 = Colours::cyan; const auto fillCol2 = Colours::magenta; const auto fillColMiddle = fillCol1.interpolatedWith (fillCol2, 0.5f); { Graphics g { image }; g.addTransform (AffineTransform::scale (scale)); g.setFont (FontOptions { typeface }.withPointHeight (50)); g.setGradientFill ({ fillCol1, { size * 0.5f - 80, 0 }, fillCol2, { size * 0.5f + 80, 0.0f }, false }); for (auto i = 0; i != 10; ++i) { g.drawText (String::repeatedString ("-", 100), Rectangle { size * 2, size }.translated (i * 50 - 500, i * 50), Justification::topLeft, false); } } const auto getPixelAtScaled = [&image, scale] (Point p) { const auto scaled = p.toFloat().transformedBy (AffineTransform::scale (scale)).roundToInt(); return image.getPixelAt (scaled.x, scaled.y); }; expect (approximatelyEqual (getPixelAtScaled ({ 15, 27 }), fillCol1)); expect (approximatelyEqual (getPixelAtScaled ({ 485, 27 }), fillCol2)); expect (approximatelyEqual (getPixelAtScaled ({ 15, 77 }), fillCol1)); expect (approximatelyEqual (getPixelAtScaled ({ 485, 77 }), fillCol2)); expect (approximatelyEqual (getPixelAtScaled ({ 250, 77 }), fillColMiddle)); expect (approximatelyEqual (getPixelAtScaled ({ 15, 477 }), fillCol1)); expect (approximatelyEqual (getPixelAtScaled ({ 485, 477 }), fillCol2)); expect (approximatelyEqual (getPixelAtScaled ({ 250, 477 }), fillColMiddle)); } static bool approximatelyEqual (const Colour& a, const Colour& b) { return std::abs (a.getRed() - b.getRed()) < 2 && std::abs (a.getGreen() - b.getGreen()) < 2 && std::abs (a.getBlue() - b.getBlue()) < 2 && std::abs (a.getAlpha() - b.getAlpha()) < 2; } static Typeface::Ptr loadTypeface (Span data) { return Typeface::createSystemTypefaceFor (data.data(), data.size()); } }; static Direct2DGraphicsContextTests direct2DGraphicsContextTests; #endif } // namespace juce