Introduction
If you are creating fun and attractive Web applications with animations, graphics and different attractive and fun services, then sometimes you need the user to create images online. For example, to post it on the page of another user or something like this. This article describes a control that you can use for such purposes. This control is a Silverlight 4 based.
Application Overview
As I said before, this application in attachments contains a control that allow a web user to create simple images online. This control doesn't create any JPEGs or PNGs, but it returns ImageSource
to developer, and developer can use this ImageSource
for his purposes: create images, backgrounds, etc.
The following tools and features are there:
- Round brush
- Size of brush
- Color of brush
- Brush opacity
- Clear and undo features
The following screenshot shows you the layout of editor:
At the top, there are Undo and Clear command buttons. In the middle is a canvas with image. At the bottom, there is color, size, opacity and brush preview.
Also, in the application, there is a button "Get Snapshot" that demonstrates the feature that converts drawn image to ImageSource
. There is Border
at the bottom, which background is that ImageSource
. The example of such a preview is below:
How to Use
This control has the following public
properties and methods that can be used outside:
BrushColor
- Dependency property. Brush color
BrushSize
- Dependency property. Brush size
BrushAlpha
- Dependency property. Brush opacity
Clear()
-Clear canvas
HideTools()
- Hide bottom tools
ShowTools()
- Show bottom tools
Undo()
- Undo if available
GetImage()
- Get current image
Max count of undoable actions are stored in const maxUndo
in EditorCanvas
class. If anyone needs it to be a property - just do it. :)
HideTools()
and ShowTools()
use animations that are defined in the related XAML file.
Code
Let's look into the sources.
First, I want to describe how an image is drawn. There are 3 layers:
<Grid x:Name="Sheet" Background="White" SizeChanged="Sheet_SizeChanged">
<Grid.Clip>
<RectangleGeometry />
</Grid.Clip>
</Grid>
<Canvas x:Name="CursorCanvas" Background="Transparent" Cursor="None">
<Ellipse x:Name="Cursor" Canvas.ZIndex="100" Visibility="Collapsed"
Opacity="{Binding BrushAlpha}" Width="{Binding BrushSize}"
Height="{Binding BrushSize}">
<Ellipse.Fill>
<SolidColorBrush Color="{Binding BrushColor}" />
</Ellipse.Fill>
</Ellipse>
<Path Stroke="Black" Canvas.ZIndex="101" StrokeThickness="1" x:Name="Cross"
VerticalAlignment="Center" HorizontalAlignment="Center"
Visibility="{Binding ElementName=Cursor, Path=Visibility}">
<Path.Data>
<GeometryGroup>
<LineGeometry StartPoint="3,0" EndPoint="8,0"/>
<LineGeometry StartPoint="-3,0" EndPoint="-8,0"/>
<LineGeometry StartPoint="0,3" EndPoint="0,8"/>
<LineGeometry StartPoint="0,-3" EndPoint="0,-8"/>
</GeometryGroup>
</Path.Data>
</Path>
</Canvas>
<Canvas x:Name="InputCanvas" Background="Transparent" Cursor="None"
MouseLeftButtonDown="InputCanvas_MouseLeftButtonDown"
MouseLeftButtonUp="InputCanvas_MouseLeftButtonUp" MouseEnter="InputCanvas_MouseEnter"
MouseMove="InputCanvas_MouseMove" MouseLeave="InputCanvas_MouseLeave">
</Canvas>
Sheet
- It is a grid
that contains geometries that have been drawn.
CursorCanvas
- Contains brush cursor and cross cursor
InputCanvas
- Collects all mouse inputs. We need it, because otherwise mouse will always be on the Ellipse
(brush cursor) that is inside CursorCanvas
and there will no MouseEnters
and MouseLeave
and MouseMove
events.
When user presses the left button, the following code is performed:
InputCanvas.CaptureMouse();
geometry = new GeometryGroup();
geometry.FillRule = FillRule.Nonzero;
figure = new Path();
figure.Fill = new SolidColorBrush(BrushColor) { Opacity = BrushAlpha };
Sheet.Children.Add(figure);
First, we need to capture the mouse, to avoid drawing to be finished after user moves out of the control. Next, we create a path with selected color and opacity and drop it into Sheet
. Geometry variable contains geometry that is drawn right now.
When user moves the mouse, then Ellipses
are added to locations where "MouseMove
" event occurs. Ellipses
are connected by rectangles. I call these rectangles "connectors
". The idea is explained in the image below:
To draw that rectangle, I have to know the size of ellipse
s and its center coords. And I know it. So it is easy. The following code draws one connector between 2 ellipse
s:
Point a, b, c, d;
double x1 = mousePosition.X;
double y1 = mousePosition.Y;
double x2 = prevMousePosition.Value.X;
double y2 = prevMousePosition.Value.Y;
double l = BrushSize / 2;
PathGeometry conntector = new PathGeometry();
conntector.FillRule = FillRule.Nonzero;
double alpha = Math.Atan2(y2 - y1, x2 - x1);
double beta = Math.PI / 2 - alpha;
a = new Point(x1 - l * Math.Cos(beta), y1 + l * Math.Sin(beta));
b = new Point(x2 - l * Math.Cos(beta), y2 + l * Math.Sin(beta));
c = new Point(x2 + l * Math.Cos(beta), y2 - l * Math.Sin(beta));
d = new Point(x1 + l * Math.Cos(beta), y1 - l * Math.Sin(beta));
PointCollection points = new PointCollection();
points.Add(d);
points.Add(c);
points.Add(b);
conntector.Figures.Add(new PathFigure()
{
IsClosed = true,
IsFilled = true,
StartPoint = a,
Segments = { new PolyLineSegment() { Points = points } }
});
Variables a
, b
, c
, d
are vertices of the rectangle that describe the connector. And here is one important thing. I wasted some time because of it. This important thing is an order of points.
Add()
commands. For correct geometry filling, you have to add point in counterclockwise order! Otherwise filling will work like you use EvenOdd fill
method, even if you choose NonZero
.
Ok, what do we have? Path that contains set of ellipses and connectors. User releases left button and we need to process this data. The easiest way is remain in this Path
in the Sheet
and draw next figures. But, some users like to draw very complicated images. :) So, if there will be a lot of geometry, you can get a slowdown. Not good. So, I decided to render this Path
and set the rendered image as a Background
of the Sheet
. And it showed me good results. No slowdown.
The following method is called when user releases leftbutton
:
private void EndFigure()
{
mouseLeftButton = false;
InputCanvas.ReleaseMouseCapture();
prevMousePosition = null;
Sheet.Background = new ImageBrush() { ImageSource = ConvertToImage() };
Sheet.Children.Clear();
}
ConvertToImage()
is a method that converts the current look of Sheet
into ImageSource
. It is also used in GetImage()
method and I wouldn't return to its descriptions in the future. So, here it is:
private ImageSource ConvertToImage()
{
WriteableBitmap bitmap = new WriteableBitmap(Sheet, new TranslateTransform());
bitmap.Render(Sheet, new TranslateTransform());
return bitmap;
}
Rather easy. Isn't it?
Another thing that I would like to show you is a color picker. In the app, it looks like:
I want to show you the algorithm that builds such palette.
int x,y, count;
x = y = count = 0;
for (int r = 0; r <= 255; r += 51)
{
for (int g = 0; g <= 255; g += 51)
{
for (int b = 0; b <= 255; b += 51)
{
Border brd = new Border()
{
Background = new SolidColorBrush
(Color.FromArgb(255, (byte)r, (byte)g, (byte)b)),
BorderThickness = new Thickness(0),
Margin = new Thickness(1),
Width = 15,
Height = 15,
Cursor = Cursors.Hand
};
brd.MouseLeftButtonDown += delegate
{
SelectedColor = ((SolidColorBrush)brd.Background).Color;
ppColors.IsOpen = false;
};
count++;
cnvColors.Children.Add(brd);
Canvas.SetLeft(brd, x);
Canvas.SetTop(brd, y);
if (count >= 6)
{
y = 0;
x += 17;
count = 0;
}
else
{
y += 17;
}
}
}
}
The idea is to go through all possible sets of R and G and B values and add border with such RGB to container. Since R, G and B are bytes, their max is 255. So, the total count of colors will be 255*255*255 = 16581375... Heh, too much. :) So, let's reduce the number of steps: and increase component values with 51 instead of 1. So, for one color component, we will have only 255/51=5 values.
Total number of colors is 5*5*5 = 125. It's ok for this application. To make the palette look nice, I choose offset values, such that the same colors are in the same column and in the same row.
Results
In this application, I tried to create an easy to use and easy to modify control, that allows you to create simple image editor in your application. You can use it as-is, or re-use only some parts of it: for example ColorPicker
.
Also, here you can get some interesting information about working with geometries, mouse capturing and optimization by rendering a lot of controls into the image.
History
- 26th April, 2011: Initial post