Building A Direct2D Framework, Part 3, Solid, Linear Gradient and Radial Gradient Brushes

About Direct2d Brushes

Direct2d has a number of different objects that you can use to compose an image. In this article, I'll discuss solid, linear gradient and radial gradient brushes.

Brushes

Direct2d Brush Wrapper Classes

Direct2d brushes are made up two kinds of data - the data that is used to create them, and the data for the brush itself in Direct2D. The creation data is up to the application, but the brush itself is a COM object whose lifetime is bound to that of the render context. When the render context is recreated, the brush must be recreated as well.

For simplicity, one might be tempted to create and release the brushes with every paint call, but this is inefficient. So, we make our engine a bit smarter and allow it recreate the brushes as needed behind the scenese for the application. This we do with a bit of help from some C++ generics. The below establishes our model of a brush as something that can be created, released, and recreated.

template  class deviceDependentAsset : public deviceDependentAssetBase {
protected:
	T asset;

public:

(snip)

	virtual bool create( direct2dContext *target ) = 0;

	bool recreate( direct2dContext *target )
	{
		release();
		create( target );
	}

	inline T getAsset() { return asset; }

protected:

	virtual void release()
	{
		if (asset) asset->Release();
		asset = NULL;
	}
};

All of the brushes inherit from the above in this fashion, with the engine calling create and release behind the scenes as needed.



Direct2d Solid Color Brushes

Solid color brushes are pretty easy. A solid color brush consists of a single RGBA color. Still, for consistency, this engine uses a DTO (Data Transfer Object) to create a solid color brush.

void directApplication::addSolidColorBrush( solidBrushDto *_solidBrushDto )
{
	solidColorBrush *brush = new solidColorBrush();
	brush->color = toColor( _solidBrushDto->color );
	brushes[ _solidBrushDto->name ] = brush;
	brush->create( this );
}

As discussed before, the brush wrapper is smart enough to create itself. The actual Direct2d call is fairly simple. Notice how the render target creates the brush, that color is a member of this brush wrapper, and asset is the Direct2d brush itself.

	bool create( direct2dContext *target )
	{
		HRESULT hr = target->renderTarget->CreateSolidColorBrush( color, &asset );
		return SUCCEEDED(hr);
	}

Direct2d Linear Gradient Brushes

Linear gradient brushes interpolate colors across a straight line. Linear gradient brushes take three basic types of data. There's a start point, an end point and a list of what's called a gradient stop.

The start point and stop point control the angle of the gradient, and its overall size. The gradient stops control what colors are included in the gradient. Note that the positions are seemingly relative to the object the brush is rendered in. Direct2d does the right thing - if you create a gradient brush and draw an image with at 20,20, you'll get the same gradient as you would if you drew the image at 500,200.

The gradient stop, and there can be more than one, has a position and a color. The position is a fraction from 0 to 1.0, indicating what percentage along the span that the selected color should be reached at. The color itself is an RGBA color. So, you can create some interesting effects by using building alpha gradients.

Here we create the linear gradient brush from our DTO.

void directApplication::addLinearGradientBrush( linearGradientBrushDto *_linearGradientBrushDto )
{
	D2D1_GRADIENT_STOP gradientStop;
	linearGradientBrush *brush = new linearGradientBrush();
	brush->start = toPoint( _linearGradientBrushDto->start );
	brush->stop = toPoint( _linearGradientBrushDto->stop );
	for (auto i = _linearGradientBrushDto->gradientStops.begin(); i != _linearGradientBrushDto->gradientStops.end(); i++) {
		gradientStop = toGradientStop( *i );
		brush->stops.push_back( gradientStop );
	}
	brushes[ _linearGradientBrushDto->name ] = brush;
	brush->create( this );
}

And then here, our linear gradient brush creates itself.

	virtual bool create( direct2dContext *target )
	{
		ID2D1GradientStopCollection *pGradientStops = NULL;

		HRESULT hr = target->renderTarget->CreateGradientStopCollection( &stops[0], stops.size(), &pGradientStops );

		if (SUCCEEDED(hr))
		{
			hr = target->renderTarget->CreateLinearGradientBrush(
				D2D1::LinearGradientBrushProperties( start, stop ),
				D2D1::BrushProperties(),
				pGradientStops,
				&asset
				);
			pGradientStops->Release();
		}
		return SUCCEEDED(hr);
	}


Direct2d Radial Gradient Brushes

Radial gradient brushes create interpolated colors in a elliptical arc. Radial gradient brushes take four kinds of data: the center point of an ellipse, an offset from that center which is the true center of the gradient, and the radius in both x and y. Finally, like its linear sibling, the radial gradient brush has a list of gradient stops.

Like the linear gradient brush, Direct2d does the right thing and the brush is consistently rendered for the same shape no matter where it is drawn. Gradient stops work the same way as in linear gradient brushes. The radial gradient brush has a collection of stops, each of which has a position and a color. The position is a fraction from 0 to 1.0, indicating what percentage along the radius that the selected color should be reached at. The color itself is an RGBA color.

Here, we create the radial gradient brush wrapper from a DTO

void directApplication::addRadialGradientBrush( radialGradientBrushDto *_radialGradientBrushDto )
{
	D2D1_GRADIENT_STOP gradientStop;
	radialGradientBrush *brush = new radialGradientBrush();
	brush->radialProperties.center = toPoint( _radialGradientBrushDto->center );
	brush->radialProperties.gradientOriginOffset = toPoint( _radialGradientBrushDto->offset );
	brush->radialProperties.radiusX = _radialGradientBrushDto->radiusX;
	brush->radialProperties.radiusY = _radialGradientBrushDto->radiusY;
	for (auto i = _radialGradientBrushDto->gradientStops.begin(); i != _radialGradientBrushDto->gradientStops.end(); i++) {
		gradientStop = toGradientStop( *i );
		brush->stops.push_back( gradientStop );
	}
	brushes[ _radialGradientBrushDto->name ] = brush;
	brush->create( this );
}

And here, we actually create the radial gradient brush object.

	bool create( direct2dContext *target )
	{
		ID2D1GradientStopCollection *pGradientStops = NULL;

		HRESULT hr = target->renderTarget->CreateGradientStopCollection( &stops[0], stops.size(), &pGradientStops );

		if (SUCCEEDED(hr))
		{
			hr = target->renderTarget->CreateRadialGradientBrush(
				radialProperties,
				D2D1::BrushProperties(),
				pGradientStops,
				&asset
				);
			pGradientStops->Release();
		}
		return SUCCEEDED(hr);
	}

Drawing with Direct2d Brushes

Unlike GDI, brushes in Direct2d are passed directly to the drawing primitive. In this snippet of code below, I grab the names of the brushes from a DTO, look them up in our engine implementation, and then draw with them, filling an interior, or drawing around the border, as needed.

void directApplication::drawPath( pathInstance2dDto *_pathInstanceDto )
{
	auto fill = brushes[ _pathInstanceDto->fillBrushName ];
	auto border = brushes[ _pathInstanceDto->borderBrushName ];
	auto p = paths[ _pathInstanceDto->pathName ];

	if ((!border && !fill) ||  !p)
		return;

	D2D1::Matrix3x2F product = D2D1::Matrix3x2F::Rotation(_pathInstanceDto->rotation) * D2D1::Matrix3x2F::Translation(_pathInstanceDto->position.x, _pathInstanceDto->position.y );
	renderTarget->SetTransform( product );

	if (fill) {
		renderTarget->FillGeometry( p->geometry, fill->getBrush() );
	}
	if (border && _pathInstanceDto->strokeWidth > 0.0) {
		renderTarget->DrawGeometry( p->geometry, border->getBrush(), _pathInstanceDto->strokeWidth );
	}
}

Wrap Up and Downloads

In this article we explored Direct2d brushes a little bit.

If you have Windows 7, you can download the EXE or source code. Note that Windows will howl about the files not being digitally signed. The source code requires Visual Studio 2010 C++. We'll talk about brushes in the next article.