Showing posts with label Computational geometry. Show all posts
Showing posts with label Computational geometry. Show all posts

Monday, 22 August 2011

Building my own Delphi Physics Engine part IX

I'm quite familiar with GDI, the graphics provided under Microsoft and which is widely used in Windows applications. TDPE is now ready to use Direct2D as now it's fully supported by Delphi 2010 and XE. According to MSDN, Direct2D is: a hardware-accelerated, immediate-mode, 2-D graphics API that provides high performance and high quality rendering for 2-D geometry, bitmaps, and text. The Direct2D API is designed to interoperate well with GDI, GDI+, and Direct3D.
As I'm testing this under Windows Vista, I've had to upgrade my windows version to Service Pack 2 and then I've installed the following update (KB971512 - The Windows Graphics, Imaging, and XPS Library contain the latest advancements in modern graphics technologies for gaming, multimedia, imaging and printing applications.). Direct2D is already installed in Windows 7.

We can use the same methods from TCanvas for our TDirect2DCanvas as they derive from the same ancestor TCustomCanvas. But you need to check the documentation for differences in the way they work.

A simple way to use Direct2D is as follows:

uses
    Direct2D, D2D1;

procedure Paint();
begin
    if TDirect2DCanvas.Supported then
    begin
        d2dCanvas := TDirect2DCanvas.Create(Canvas, ClientRect);
        try
            d2dCanvas.RenderTarget.beginDraw;
            d2dCanvas.RenderTarget.Clear(D2D1ColorF(clBlack));
            d2dCanvas.RenderTarget.SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
            // drawing goes here
            Paint(d2dCanvas);
            d2dCanvas.RenderTarget.EndDraw;
        finally
            FreeAndNil(d2dCanvas);
        end;
    end;
end;

Here is the result of the Physics Engine using a Direct2D Render and a GDI Render:





You will see the difference when switching from GDI to Direct2D as the performance is quite different. When it comes to Direct2D, my computer is getting quite slower as my graphic card is not powerful at all and we can see that it takes a while to render the image.


GDI uses Pixel graphics but Direct2D can supports vector graphics as well, in which mathematical formulas are used to draw the lines and curves. Vector graphics provides high quality rendering independent of resolution of the device, while the pixelated graphics has dependency with resolution which may results in choppy graphics.

Most of the GDI APIs are not using anti-aliasing and transparency. Ofcrouse there are functions to do so but always there’s programming cost for taking advantage of these features. Also if we apply transparency and anti-aliasing, the computations are done using CPU. Direct2D can take advantage of graphics hardware and delegate the computationally intensive tasks to GPU


Get the latest version here: ThundaxBallDemo v1.584.
Interaction:
'd' will drop a box with a random colour and a constant angular velocity.
'f' will drop a circle with a random colour and a constant angular velocity.
'mouse interaction' mouse is able to grab items.
'q' Enable/Disable GDI or Direct2D rendering.

Note: You need to have installed Direct2D to run the app using Direct2D, otherwise GDI will be used.

Related links:

Wednesday, 10 August 2011

Building my own Delphi Physics Engine part VIII

TDPE is almost ready and it will not take long to release the first stable library!. I am eager to continue working on the project trying to sort out different physic scenarios  to make my dynamic engine even more realistic. Do not hesitate to give it a try as you will find the experience quite interesting. I am still working on the n-edge object and the cutting tool and trying to refactor all the code to cope with a layer architecture. Using this concept, I want to structure my system in a way that every component works using a certain level of abstraction. As you see in the next figure, every object is allocated in the screen while there is another layer which is responsible for the collision detection and another one for the forces interaction. You will notice in the video that there are still little issues to tackle regarding the object interaction as sometimes we can experience overlapping.


In the figure above, you can see that now the items are not rigid and they interact with each other as they were real items. Notice that the square item will go through the round shape of the circle.



Enjoy the video!.

Get the latest executable here: ThundaxBallDemo v1.504.
Interaction:
'd' will drop a box with a random color and a constant angular velocity
'f' will drop a circle with a random color and a constant angular velocity
'mouse interaction' mouse is able to grab items

Related links:

Tuesday, 21 June 2011

Building my own Delphi Physics Engine part VII

After several weeks working on my TDPE I think I managed it to get the desired outcome and now the displayed items show more realistic features. In my previous versions, neither angular velocity nor mass was interacting in terms of moving an object and now, using different mathematical algorithms, the gravity and angular velocity are considered for every item. That means that as soon as a collision is originated, the resultant force will lean back or forward the object and it will be rotating or moving until the forces of the system reach a value close to zero.
This new approach has included a massive refactoring of the existing code to cope with this new behaviour that will deliver spectacular performances. During the following weeks I'm going to  focus on the cutting tool as now I only have to extent the 4-edges collision object to n-edges. Once I have the n-edges collision object then I would be able to generate small chunks of it and each piece would have its own behaviour giving you the feeling that the object has really been broken.


As it's shown in the image, when most or more than a half of the object is out of the base (in blue), the red box should lean forward and fall according to the laws of physics (falling using rotation instead of falling straight how it was before in previous versions).

The following video will show the results of applying the latest algorithms to my Engine:


Enjoy the video!.

Get the latest executable here: ThundaxBallDemo v1.349.
If you want to drop a box, press 'd' and a new red box will fall.

Related links:

Sunday, 1 May 2011

Polygonal approximation to circle

In order to be able to tackle the "cutting tool" for my TDPhysicsEngine, I need to work with polygon approximations instead of a full drawn circle using the Canvas.Ellipse Delphi function. To achieve this in the view layer is as easy as using Canvas.Polygon function to draw a polygon using a list of calculated points. To calculate the points, we need to divide the circle in discrete chunks and then let the function draw the lines between points. The main reason what I'm doing that is to be sure that I can cut up a polygon and then use the resulting pieces as new objects that will behave as independent objects:


The function below will help you to draw the polygon:

procedure GDIRenderer.CircleApproximation(xcenter, ycenter, Radius, Rotate: Double; style: TStyle);
var
    PArrow: array [1 .. points] of TPoint;
    i: integer;
    theta: Double;
    x, y : double;
    beforeBrushColor, beforePenColor: TColor;
    beforePenWidth: integer;
begin
    beforeBrushColor := FCanvas.Brush.color;
    beforePenColor := FCanvas.Pen.color;
    beforePenWidth := FCanvas.Pen.Width;

    FCanvas.Pen.color := style.PenColor;
    FCanvas.Pen.Width := style.penWidth;
    FCanvas.Brush.color := style.BrushColor;

    for i := 1 to points do
    begin
        theta := Pi * ((i-1)/(points/2));
        x := xcenter + (Radius* Cos(theta));
        y := ycenter + (Radius* Sin(theta));
        PArrow[i] := Point(Round(x), Round(y));
    end;
    FCanvas.Polygon(PArrow);

    FCanvas.Brush.color := beforeBrushColor;
    FCanvas.Pen.color := beforePenColor;
    FCanvas.Pen.Width := beforePenWidth;
end;

The big difficulty here is to model this behaviour in the model. I need to work more and think about it because for the moment it's adjusted as a sphere and it's not a good approximation.

Building my own Delphi Physics Engine part VI

I've been quite busy lately and in this article I've focused on the collision container. As you'll see in the following images and video, the spheres will be bouncing around, colliding with one another and with the surfaces inside the container that holds the entire motion of the system. With a simple tweak, the physics engine is able to display the unseen forces when objects are being "kicked". The key feature is that I want to show the resultant vectors that are involved in the collisions and the movement so the observant would have an idea of what's happening inside the model and which forces are being generated.

In the image below, you'll see the resultant force when the sphere has collided with the surface beneath. This leads me to the description of the Elastic collision equations


The following image shows all the vectors involved in the collision:


And the video showing the dynamics:


You can download from here the last version of the application: ThundaxBallsDemo v1.172 that is digitally signed (once the file is downloaded, right click, properties and go to the certificate and install it) to increase security. To add more spheres press 'a' and 's'. To cut one of the bridges press 'z' and play!.

I also have created the new logo for the Engine, let me know if you like it!.

In few months the library would be ready here:


Thanks for reading! and do not hesitate to leave any comment!.

Saturday, 5 February 2011

Building my own Delphi Physics Engine part V

Today I felt inspired (after parts I, II, III and IV) and I tried to do a cloth simulation in the Thundax Physics Engine. There are a lot of things to do, but at least I can start simulating different components very fast without too much implementation. I saw one amazing implementation of the "processing cloth" in JRC313.com. Even though the applet is written in JavaScript, the performance is quite impressive and it's a nice work.

but, How it works?
"Every line in the cloth simulation is technically called a constraint and every point is a point mass (an object with no dimension, just location and mass). All the constraints do is control the distance between each point mass. If two points move too far apart, it will pull them closer. If two points are too close together, it will push them apart. The cloth is really then just a collection of constraints and point masses in a never ending struggle."

using Relaxation in simple linear systems:
In the case of this cloth simulation all I needed to do was try satisfying the constraints as fast as I can. For things like simple rope simulations it may be necessary to satisfy several times (maybe 4 or 5). The more times you satisfy, the more rigid the constraint becomes. This process is known as relaxation and is amazing!. The displacement will then be of the form y(t) = Ae − t / Tcos(ÎĽt − δ). The constant T is called the relaxation time of the system and the constant ÎĽ is the quasi-frequency. (Wikipedia).

In the following videos you'll be able to check the performance of the simulated cloth. I took advantage of my previous bridge (spring + particles) and I've concatenated a series of bridges to set up a virtual cloth. Now the movement is quite astonishing:


In the second video I'm showing one of the new features for the next release: "the cutting tool". I still need to think about it, but for simple objects it could be simple to cut an object and see its reaction, like in the next video:


You can download the last version of the executable file here: thundax Balls demo v1.52. And maybe in a near future you'll be able to see something similar to the Puzzler for iPhone.


Other interesting video about physics and games is Crayon Physics from Petri Purho:

I hope you enjoy the videos!

Related Links:

Friday, 4 February 2011

Building my own Delphi Physics Engine part IV

I'm still working on my solution, but I can advance you a preview of the smoothness of the application. I have improved the bridge particle and its interaction with external events. I'm working on a TStyle class that will enhance the GDIRender allowing the different particles to have a custom style on the screen and refactor all the classes to improve the scalability and interoperability of the Engine. If you are interested on testing the application, you can get the latest version of it from here: Thundax Ball Demo v1.1. In this version you will notice the improved interaction with the mouse and the circle particles. I have to work out a solution for the other particles, but I'm still designing the whole product and I need to test some of the features that I want to release in the next version. Once the version is ready, I will upload it on Sourceforge just in case you feel the urge to play with it!.
Here you can see the new performance:


Wednesday, 2 February 2011

Building my own Delphi Physics Engine part III

Now I'm working on the interactive part (Controller). Since now I only have developed the model and the view section of the MVC pattern and I'm implementing and testing the controller part. The main problem is that the Engine is quite simple to implement and use until the visual control appears. Now I have to take into account all the events that come from the outside (mouse, keyboard) and add subscribers to the object particles. With the design that I've implemented it is easy to make extensible the model and the view section because the Engine follows the "low cohesion high coupling" principle. My sister challenged me by trying to do something similar to the ruicode project, where its ofxRuiPhysics2d project (Simple 2d physics addon for OF (OpenFrameworks) using the Verlet integrator where it includes particles, collisions and springs) is quite astonishing. Here you can see one of its videos:


And here is my project: Thundax balls demo.exe, a Delphi win32 application that will start a demo with balls, springs and collisions. In the following videos you'll see different performances of my tests. In the first video I'm trying to simulate the environment with all the forces playing at the same time, and in the second video you can see myself using the mouse interacting with the balls and the springs.


I hope you enjoy the videos!.

Related links:

Related frameworks:
It's amazing the large amount of physics Engine that are available on the net. I just google the words "Physics Engine" and here are the results:
Javascript, C, and Java Frameworks:
ActionScript Frameworks:

3D and Realistic Frameworks:

Friday, 10 December 2010

Building my own Delphi Physics Engine part II

Going on with my DPE (Delphi Physics Engine) I've improved my old version (Building my own Delphi Physics Engine Part I) of the Jansen Mechanism by creating the rest of the legs displaced 120Âş each one. I've been fixing the damping level of the framework just to be sure that the movements are as realistic as possible. In this version, the movement is quite realistic and we can trace the movement of the leg by plotting the kinetic analysis:

A physics engine is computer software that provides an approximate simulation of certain simple physical systems, such as rigid body dynamics (including collision detection), soft body dynamics, and fluid dynamics, of use in the domains of computer graphics, video games and film. Their main uses are in video games (typically as middleware), in which case the simulations are in real-time. The term is sometimes used more generally to describe any software system for simulating physical phenomena, such as high-performance scientific simulation. Wikipedia.

Here you can see the CGI result with the hanging version of the Jansen mechanism with one leg and with three legs:

And the final version with the development of the Jansen machine. In this version (Thundax Test Forces v2.exe) if you press 'p' the machine will start walking and if you want to change direction, you only need to press the 'z'.

What's next?. Now I'll try to reproduce different physics systems and show how it goes. The Easy Java Simulations tool, offers a wide range of examples that I would like to try.
Enjoy the learning!.

Related Links:

Friday, 3 December 2010

Building my own Delphi Physics Engine part I

 These days I've been working on my own Delphi Physics Engine based on the very well known APE (ActionScript Physics Engine) with some improvements that will help developers to build models very fast. In this beta version I've done the following improvements:
  • Enhance user performance.
  • Drastic reduction of Memory leaks.
  • Improve the interoperability between objects.
  • Improve maths algorithms to collision detection.
  • Use the VLO GDI Render to perform the drawing of the shapes.
In this first insight into the Engine, I'm going to show you a model of the Jansen mechanism, a kinetic sculpture that can move with the help of several pairs of legs.


I've built my own model taking advantage of the physic engine and imagination!.
Here you can see my own model:
The movement is still not very well performed, but it's due to the measures of the different parts of the sculpture. As for the calculation points, I've used the next template:
You can download the demo project from here (Thundax Test Forces). To play with the application, you only need to press 'z' to start the kinetic sculpture, and as for the objects below the sculpture, press 'd' and you'll force a collision between two objects.

The next days I'll be improving the model (you can see the kinetic analysis that is plotted below the sculpture) and adding the extra foot to perform a fully movement.

Enjoy the learning!.

Related links:

Tuesday, 29 September 2009

Primera aplicaciĂłn con Thundax Box Manager (Parte I)

Bueno, lo prometido es deuda y aquí tenéis la primera representación a la utilización de éste nuevo framework que me he creado from scratch. Ésta primera aplicación utilizando mi unnamed framework consiste como no, en el dibujado de un grafo (dirigido o no) y el correspondiente cálculo del camino mínimo aplicando el algoritmo de Dijkastra. He tenido que tirar de mis apuntes de matemática discreta, pero estoy muy contento al poder realizar ésta primera aplicación sin muchas complicaciones en la utilización de los objetos personalizados. Esta implementación sencilla es muy fácil de utilizar, simplemente dibujamos todo el grafo con sus conexiones (con dirección simple o doblemente dirigidas, ésto provoca que en la lista de adyacentes se publican las dos direcciones, tanto la ida como la vuelta). Luego tenemos que marcar el origen y el final mediante unos colores concretos y marcamos los vértices y aristas con números para identificarlos y sus pesos.

Lo veremos mejor con un ejemplo:

Como veis en el grafo, todas las aristas son de doble sentido y tiene marcado el peso de pasar por ése camino. Los vertices (TBox) estan numerados y tenemos marcado también con colores el origen (1 - clLime) y el final (6 - clFucshia). Hay que marcarlos siempre con éstos colores para que funcione. Una vez hecho ésto, vamos a Extension -> DijKstra Algorithm y nos devolverá el camino mínimo marcado en rojo.

Aún tengo que arreglar alguna cosa del algoritmo, porque aún no afina muy bien, pero de momento la cosa funciona correctamente, y permite de una manera rápida, sencilla y gráfica, montar nuestros propios grafos y luego calcular el camino mínimo.

AquĂ­ os dejo el cĂłdigo fuente del algoritmo:
  • Dijkstra algorithm with Delphi:


procedure TfrmMain.Dijkstra1Click(Sender: TObject);
var
AdjacentList: array[1..MaxVertexs, 1..MaxVertexs] of Integer;
VertexList: ArrayOfInteger;
predecessor: ArrayOfInteger;
visiteds: array[1..MaxVertexs] of boolean;
vertexs, edges, firstNode, LastNode: integer;
i, j, pos1, pos2, min: integer;
Conn: TConnector;
textList: string;
begin
vertexs := boxlist.count;
edges := connectorList.count;
firstNode := -1;
LastNode := -1;
for i := 0 to BoxList.count - 1 do
begin
if BoxList.items[i].boxColor = clLime then
firstNode := StrToInt(BoxList.items[i].getText);
if BoxList.items[i].boxColor = clFuchsia then
LastNode := StrToInt(BoxList.items[i].getText);
end;
if (firstNode = -1) or (LastNode = -1) then
exit;

for i := 1 to MaxVertexs do
for j := 1 to MaxVertexs do
begin
AdjacentList[i, j] := NoWeight;
if i = j then
AdjacentList[i, j] := 0
end;

for i := 0 to edges - 1 do
begin
Conn := ConnectorList.items[i];
pos1 := StrToInt(Conn.SourceBox.GetText);
pos2 := StrToInt(Conn.targetBox.GetText);
AdjacentList[pos1, pos2] := StrToInt(Conn.Line.GetText);
if Conn.Line.ClassNameIs('TAbstractDottedDoubleArrowLine') or
Conn.Line.ClassNameIs('TAbstractSimpleDoubleArrowLine') then
AdjacentList[pos2, pos1] := StrToInt(Conn.Line.GetText);
end;

for i := 1 to vertexs do
begin
VertexList[i] := AdjacentList[firstNode, i];
predecessor[i] := firstNode;
visiteds[i] := false;
end;
VertexList[firstNode] := 0;
visiteds[firstNode] := true;

j := firstNode;
repeat
for i := 1 to LastNode do
if (not visiteds[i]) and (VertexList[i] > VertexList[j] + AdjacentList[j, i]) then
begin
VertexList[i] := VertexList[j] + AdjacentList[j, i];
predecessor[i] := j;
end;
min := NoWeight;
for i := 1 to vertexs do
if (not visiteds[i]) and (VertexList[i] < min) then
begin
min := VertexList[i];
j := i;
end;
if (min <> NoWeight) then
visiteds[j] := true;
until (visiteds[LastNode]) or (min = NoWeight);

textList := '';
while (LastNode <> firstNode) do
begin
textList := textList + IntToStr(LastNode);
LastNode := predecessor[LastNode];
end;

textList := Swap(textList + IntToStr(FirstNode));

i := 1;
while i < length(textList) do
begin
for j := 0 to connectorList.count - 1 do
begin
Conn := ConnectorList.items[j];
if ((Conn.SourceBox.getText = textList[i]) and
(Conn.TargetBox.getText = textList[i + 1])) or
((Conn.SourceBox.getText = textList[i + 1]) and
(Conn.TargetBox.getText = textList[i])) then
begin
Conn.SourceBox.lineColor := clRed;
Conn.TargetBox.lineColor := clRed;
Conn.Line.LineColor := clRed;
end;
end;
i := i + 1;
end;
end;


Podéis descargar la última versión de la aplicación aquí: ThundaxBoxManager v2.0.0 build 65.exe.
  • Enlaces de interĂ©s:
Dijkstra applet Demos.


Mejorando el diseño de una flecha (Parte II)

He detectado un pequeño error en la composición de la flecha. Cuándo el ángulo representado es igual a pi/2 o -pi/2 la flecha no se dibuja correctamente, solo aparece la punta sin el relleno, es decir, el polígono no se dibuja correctamente. La solución (como siempre) es bastante fácil y solo hay que tener en cuenta la distribución geométrica de los puntos de representación de la punta de flecha. El problema reside en la comparación utilizando reales. Éstos no se pueden comparar realmente, ya que siempre son diferentes internamente, por lo tanto cuando el programa intenta comparar los valores de pi/2 con el valor calculado de pi/2 dice que no son iguales:

La funcion consiste en mirar si el valor absoluto del angulo difiere de un ángulo de pi/2, y si realizamos la comparación con el debugger no dirá que los valores no son iguales, y visualmente lo son.

Para evitar ésto utilizaré una función que os mostré hace tiempo en uno de mis posts para la comparación de reales: Comparando reales con delphi.

Mediante éste último arreglo, el framework (aún estoy pensando en que nombre darle, pero al final caerá algo como TWAIN Technology without an Interesting Name) tiene bastante resuelto todo el tema del cálculo para el repintado de las diferentes estructuras.

El cĂłdigo final del dibujado de la flecha es el siguiente:


procedure TAbstractLine.DrawFashionArrow(Source, Target: TPoint);
function CalcPoint(p: TPoint; angle: double; Distance: integer): TPoint;
var
X, Y, M: double;
begin
if Comparar(Abs(angle),(PI / 2),'<>') then
begin
if Comparar(Abs(angle),(PI / 2),'<') then
Distance := -Distance;
M := Tan(angle);
X := p.X + Distance / sqrt(1 + sqr(M));
Y := p.Y + M * (X - p.X);
Result := Point(round(X), round(Y));
end
else
begin
if angle > 0 then
Distance := -Distance;
Result := Point(p.X, p.Y + Distance);
end;
end;

var
angle: double;
PArrow: array[1..4] of TPoint;
restColor: TColor;
begin
if (Distance(Source, Target) < 20.0) or (Distance(Source, Target) > 2000.0) then
exit;
angle := ArcTan2((Target.Y - Source.Y), (Target.X - Source.X));
PArrow[1] := Target;
PArrow[2] := CalcPoint(Target, angle + PI / 9, LenArrow);
PArrow[3] := CalcPoint(Target, angle, 2 * LenArrow div 3);
PArrow[4] := CalcPoint(Target, angle - PI / 9, LenArrow);
FCanvas.Pen.Width := 1;
if FInside then
FCanvas.Pen.Color := FSelectedColor
else
FCanvas.Pen.Color := FLineColor;
FCanvas.Brush.Style := bsSolid;
restColor := FCanvas.Brush.Color;
if Ffilled then
FCanvas.Brush.Color := FFillColor;
FCanvas.Polygon(PArrow);
FCanvas.Brush.Color := restColor;
end;


La actualización de la función de comparación también la dejo ya que faltaba un operador de comparación:


uses types, Math;

function Comparar(Value1, Value2: double; MethodComp: string): boolean;

const
RealMargin = 0.000001; //1e-6

implementation

function Comparar(Value1, Value2: double; MethodComp: string): boolean;
var
ret: boolean;
begin
ret := false;
if MethodComp = '=' then
begin
case CompareValue(Value1, Value2, RealMargin) of
EqualsValue: ret := true;
end;
end
else if MethodComp = '<>' then
begin
case CompareValue(Value1, Value2, RealMargin) of
LessThanValue: ret := true;
GreaterThanValue: ret := true;
end;
end
else if MethodComp = '>=' then
begin
case CompareValue(Value1, Value2, RealMargin) of
EqualsValue: ret := true;
GreaterThanValue: ret := true;
end;
end
else if MethodComp = '>' then
begin
case CompareValue(Value1, Value2, RealMargin) of
GreaterThanValue: ret := true;
end;
end
else if MethodComp = '<=' then
begin
case CompareValue(Value1, Value2, RealMargin) of
LessThanValue: ret := true;
EqualsValue: ret := true;
end;
end
else if MethodComp = '<' then
begin
case CompareValue(Value1, Value2, RealMargin) of
LessThanValue: ret := true;
end;
end;
result := ret;
end;


Ahora, el dibujado de las flechas es perfecto:

Espero que os sirva de ayuda.

Thursday, 24 September 2009

Corrigiendo el aliasing del texto inclinado

Prosiguiendo los cálculos realizados sobre mi aplicación Thundax Box Manager, en éste artículo daré importancia a la corrección del Aliasing que ocurre en el momento de inclinar el texto. La propia API de windows nos permite trabajar con las fuentes modificando los parámetros básicos de éstas y definiendo así su calidad. Si observamos la siguiente imagen sacada de la aplicación, notaréis como el sistema intenta dibujar las letras orientadas con una inclinación de la manera que puede. La verdad es que no se entienden muy bien y no queda muy bonito en la aplicación.

Si consultamos los parámetros de CreateFont, podemos ver que trabaja bastante bien con fuentes Truetype y que además permite corregir el aliasing ajustando un parámetro de su función. Mediante el parámetro fdwQuality [in]:
The output quality. The output quality defines how carefully GDI must attempt to match the logical-font attributes to those of an actual physical font. It can be one of the following values.
Podemos utilizar el valor: ANTIALIASED_QUALITY.
Font is antialiased, or smoothed, if the font supports it and the size of the font is not too small or too large.
Aquí podéis ver las diferentes pruebas realizadas cambiando el tipo de fuente y el tamaño:


Como podéis ver mediante el valor seleccionado podemos mejorar el aliasing de la fuente aplicando un filtro antialiasing sobre el texto rotado. Desde la aplicación, podemos ir probando las diferentes fuentes que tenemos instaladas pero he comprobado que hay muchas fuentes que no disponen de éste comportamiento. Al principio había dejado por defecto la fuente Tahoma, pero no funciona tan bien como la Calibri que es la que utiliza Microsoft Office. Ahora en la aplicación, las fuentes tienen mejor aspecto:


Para acabar de mejorar el método de pintado del texto, he arreglado un tema pendiente sobre centrar el texto en la línea. El cálculo es como siempre bastante fácil y solo tenemos que recurrir a la trigonometría para solucionar ésto.

El problema reside en que el texto empieza a dibujarse desde el centro de la línea, ya que se calcula el punto médio entre el origen y el final de la conexión. Pero claro, tenemos que tener en cuenta la longitud del texto escrito y desplazar el centro la mitad de ésta longitud para que el texto se muestre centrado:

En la siguiente imagen describo el cálculo con un poco más de precisión:

Utilizando la función Canvas.TextWidth podemos saber la longitud actual del texto en píxels, luego solo necesitamos dividir éste valor entre 2 para obtener la mitad de la longitud. Ahora sabiendo el ángulo de inclinación tenemos que calcular los valores de a y b para calcular el nuevo punto a partir de P.

La nueva función del cálculo de la orientación queda de la siguiente manera:


procedure TAbstractLine.DrawTextOrientation(Source, Target: TPoint);

function iif(condition: boolean; resultTrue: integer; resultFalse: integer): integer;
begin
Result := resultFalse;
if condition then
Result := resultTrue
end;

var
newFont, FontSelected: integer;
angle: double;
iAngle, i: integer;
Position: TPoint;
calc : double;
begin
if FDescription.text = '' then
exit;
angle := CalcAngleTwoPoints(Source, Target);
iAngle := round(angle);

if (iAngle >= 91) and (iAngle <= 180) then
iAngle := iAngle + 180;

if (iAngle >= 181) and (iAngle <= 269) then
iAngle := iAngle - 180;

for i := 0 to FDescription.Count - 1 do
begin
Position := Point((Target.X + Source.X) div 2, (Target.Y + Source.Y) div 2);
FCanvas.Font := FFontText;
calc := (FCanvas.TextWidth(FDescription[i]) div 2) * cos(DegToRad(iAngle));
Position.x := Position.x - round(calc);
calc := (FCanvas.TextWidth(FDescription[i]) div 2) * sin(DegToRad(iAngle));
Position.y := Position.y + round(calc);

SetBkMode(FCanvas.Handle, transparent);
newFont := CreateFont(-FfontText.Size, 0, iAngle * 10, 0, iif(fsBold in FfontText.Style, FW_BOLD, FW_NORMAL), iif(fsItalic in FfontText.Style, 1, 0), iif(fsUnderline in FfontText.Style, 1, 0), iif
(fsStrikeOut in FfontText.Style, 1, 0), ANSI_CHARSET, OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, ANTIALIASED_QUALITY, FF_DONTCARE, PChar(FfontText.Name));

Fcanvas.font.color := FfontText.color;
FontSelected := SelectObject(FCanvas.Handle, newFont);
TextOut(FCanvas.Handle, Position.X , Position.Y, PChar(FDescription[i]), Length(FDescription[i]));
SelectObject(FCanvas.Handle, FontSelected);
DeleteObject(newFont);
end;
end;


La última versión de la aplicación con éstos cambios la podéis encontrar aquí: Thundax Box Manager v1.0.0 build 159.exe.

AquĂ­ os dejo con una imagen de la aplicaciĂłn utilizando el corrector de aliasing:


  • Enlaces de interĂ©s:
Display rotated text.
Image Processing.
Graphic Algorithms.
Flicker Free TImage Delphi.
Win32 and COM Development.
Delphi DegToRad Command.


Monday, 21 September 2009

Mostrando texto inteligible en la rotaciĂłn sobre un canvas

Continuando con el diseño apasionante de Thundax Box Manager, si os fijáis en la última imagen que hay en el post anterior, veréis que el texto no se entiende bien, pero por motivos de rotación. Si calculamos el ángulo entre 2 puntos del plano, éste es correcto al dibujar líneas, pero no al dibujar el texto, por eso tenemos que hacer una pequeña conversión para que el texto siempre se pueda leer. En un artículo anterior os comenté una función para poder dibujar el texto en la orientación que queramos, pero tenemos que tener en cuenta siempre la visión del observador. A la máquina le da igual si le escribes el texto del derecho o del revés, pero a nosotros no.

Si nos fijamos en la siguiente imagen representando los 4 cuadrantes:

Podemos ver que los cuadrantes 1 y 3 los tenemos que evitar en la medida de lo posible, y esto lo haremos realizando un sencillo cálculo posicionandonos en el cuadrante que más nos interese. Por lo tanto siempre que el sistema tienda a trabajar con el cuadrante 1, hay que pasarlo automáticamente al cuadrante 4. Hay que hacer lo mismo con el 3 hacia el 2.

Por lo tanto el cálculo a realizar sería el siguiente:


procedure TAbstractLine.DrawTextOrientation(Source, Target: TPoint);
function iif(condition: boolean; resultTrue: integer; resultFalse: integer): integer;
begin
Result := resultFalse;
if condition then
Result := resultTrue
end;

var
newFont, FontSelected: integer;
angle: double;
iAngle, i: integer;
Position: TPoint;
begin
if FDescription.text = '' then
exit;
angle := CalcAngleTwoPoints(Source, Target);
iAngle := round(angle);

if (iAngle >= 91) and (iAngle <= 180) then
iAngle := iAngle + 180;

if (iAngle >= 181) and (iAngle <= 269) then
iAngle := iAngle - 180;

for i := 0 to FDescription.Count - 1 do
begin
Position := Point((Target.X + Source.X) div 2, (Target.Y + Source.Y) div 2);
SetBkMode(FCanvas.Handle, transparent);
newFont := CreateFont(-FfontText.Size, 0, iAngle * 10, 0, iif(fsBold in FfontText.Style, FW_BOLD, FW_NORMAL), iif(fsItalic in FfontText.Style, 1, 0), iif(fsUnderline in FfontText.Style, 1, 0), iif
(fsStrikeOut in FfontText.Style, 1, 0), ANSI_CHARSET, OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_DONTCARE, PChar(FfontText.Name));

FontSelected := SelectObject(FCanvas.Handle, newFont);
TextOut(FCanvas.Handle, Position.X, Position.Y + (i * FfontText.Size), PChar(FDescription[i]), Length(FDescription[i]));
SelectObject(FCanvas.Handle, FontSelected);
DeleteObject(newFont);
end;
end;


Al final obtendremos un dibujo entendible al utilizar la aplicación ThundaxBoxManager, la cúal podéis descargar desde aquí: ThundaxBoxManager v1.0.0 build 125.exe.


Tuesday, 15 September 2009

ImplementaciĂłn de un Canvas para modificar Objetos en Delphi parte XI

En éste artículo me dedicaré a explicaros como he resuelto el problema de las etiquetas. Dado que las líneas expresan un concepto, deberíamos poder indicarlo de alguna manera mediante una etiqueta (label) indicando una pequeña descripción de ésta. El problema empieza por saber dónde tengo que poner ese texto (ubicación exacta en coordenadas) y el ángulo exacto de inclinación que existe sobre la recta. El cálculo es relativamente fácil, ya que solo tenemos que calcular el angulo formado por dos puntos del plano, y buscar el punto médio de un segmento. Mediante éstos 2 cálculos ya tenemos el 50% del trabajo hecho. El otro 50% lo tenemos que completar buscando alguna forma para dibujar éste texto en ése punto con la inclinación dada. Pero para ésto tampoco hay ningún problema. Si os acordáis de mi post : Mostrar el texto en la orientación que queramos en un canvas, aquí describo como funciona el tema de las fuentes para dibujarlas en la orientación que queramos.

  • 1. Calcular el ángulo formado por dos puntos del plano:
Mediante éste pequeña función obtendremos el cálculo:



function TAbstractLine.CalcAngleTwoPoints(Source, Target: TPoint): double;
var
angle: double;
begin
angle := 180 * (1 + ArcTan2(Source.y - Target.y, Target.x - Source.x) / PI);
if angle >= 360.0 then
angle := angle - 360.0;
result := angle;
end;


  • 2. Calcular el punto medio de un segmento:
Con éste cálculo obtenemos el punto medio:



Position := Point((target.x + source.x) div 2, (target.y + source.y) div 2);


Ahora, la funciĂłn que nos permite dibujar el tema de las descripciones es el siguiente:


procedure TAbstractLine.DrawTextOrientation(Source, Target: TPoint);

function iif(condition: boolean; resultTrue: integer; resultFalse: integer): integer;
begin
result := resultFalse;
if Condition then
result := resultTrue
end;
var
newFont, FontSelected: integer;
ft: TFont;
angle: double;
iAngle: integer;
Position: TPoint;
begin
ft := TFont.Create;
ft.Name := 'Arial';
ft.Size := 12;
ft.Style := ft.Style - [fsBold, fsItalic, fsUnderline, fsStrikeOut];
angle := CalcAngleTwoPoints(Source, Target);
iAngle := Round(angle);
Position := Point((target.x + source.x) div 2, (target.y + source.y) div 2);
if FDescription = '' then
exit;
SetBkMode(Fcanvas.handle, transparent);
newFont := CreateFont(
-ft.Size,
0,
iAngle * 10,
0,
iif(fsBold in ft.Style, FW_BOLD, FW_NORMAL),
iif(fsItalic in ft.Style, 1, 0),
iif(fsUnderline in ft.Style, 1, 0),
iif(fsStrikeOut in ft.Style, 1, 0),
ANSI_CHARSET,
OUT_TT_PRECIS,
CLIP_DEFAULT_PRECIS,
PROOF_QUALITY,
FF_DONTCARE,
PChar(ft.Name));

FontSelected := SelectObject(Fcanvas.handle, newFont);
TextOut(Fcanvas.handle, position.x, position.y, PChar(FDescription), length(FDescription));
SelectObject(Fcanvas.handle, FontSelected);
DeleteObject(newFont);
FreeAndNil(ft);
end;


El resultado final, lo podéis comprobar en la siguiente imagen:

Aquí os dejo la última versión de la aplicación ThundaxBoxManager v1.0.0 Build 71.exe para que le echéis un vistazo y comprobéis el estado de la aplicación, que poco a poco va dando sus frutos.

Tuesday, 1 September 2009

ImplementaciĂłn de un Canvas para modificar Objetos en Delphi parte V

En éste artículo os describo las diferentes mejoras que trae la aplicación. Llevo un rato trabajando sobre el proyecto y he conseguido diferentes cosas. En ésta versión, he implementado la mejora anterior del movimiento del mouse (ahora ya no se escapa ningún evento), también la mejora con el redimensionado de las cajas (ocurría lo mismo que con lo de mover la caja), y además he incorporado el dibujado de flechas entre cajas. Antes se dibujaban líneas de centro a centro de la cajas y ahora hago un pequeño cálculo para encontrar la intersección entre las 2 rectas. No es muy complicado, sabiendo un poco de matemáticas se resuelve rápido. En la wikipedia podréis encontrar información sobre las rectas.

Si observamos una imagen de la aplicación veréis de lo que hablo:

Un cálculo parecido al que hay que hacer es el siguiente:

m := 0;
if (RectaP2.x - RectaP1.x) <> 0 then
    m := (RectaP2.y - RectaP1.y) / (RectaP2.x - RectaP1.x);
d := RectaP1.y - m * (RectaP1.x);

m2 := 0;
if (Recta2P2.x - Recta2P1.x) <> 0 then
    m2 := (Recta2P2.y - Recta2P1.y) / (Recta2P2.x - Recta2P1.x);
d2 := Recta2P1.y - m2 * (Recta2P1.x);

x := 0;
if (m - m2) <> 0 then
    x := (d2 - d) / (m - m2);
y := m * x + d;

s := false;
if ((RectaP1.x - x) * (x - RectaP2.x) >= 0)
    and ((RectaP1.y - y) * (y - RectaP2.y) >= 0)
    and ((Recta2P1.x - x) * (x - Recta2P2.x) >= 0)
    and ((Recta2P1.y - y) * (y - Recta2P2.y) >= 0) then
    s := true;

Solo hay que adaptarlo para los 4 laterales de la caja, ya que las rectas son conocidas. Hay que buscar la intersección entre la recta 1 = Entre centros y le recta 2 que será uno de los laterales, vértices 1-2, 2-3, 3-4, 4-1. Una vez encontramos el que intersecciona dibujamos la flecha.

La flecha, ya la tenĂ­a implementada en uno de mis post antiguos: Drawing an Arrow.

Aquí podréis descargar la última versión de la aplicación ThundaxBoxManager v1.0.0 build 16.exe, espero que la disfrutéis.

Además de las modificaciones anteriores, también he cambiado el tema del solapamiento. Cuando tenemos un objeto seleccionado, sus eventos no interaccionan con los objetos que hay no seleccionados. Antes ocurría que ibas moviendo una caja y cuando pasabas por encima de otra se movía también. Ahora queda todo el conjunto bien definido con la posibilidad de cambiar el orden.

Algo que también se me olvidó en el artículo anterior es que he utilizado el DobleBuffering en la aplicación para evitar el parpadeo o flickering.

  • Enlaces de interĂ©s:
EcuaciĂłn de la recta que pasa por 2 puntos.
Intersecting Lines algorithm.

Wednesday, 29 July 2009

Jugando con Elipses en Delphi

Hoy os traigo una pequeña aplicación didáctica en delphi que dibuja elipses (mejor dicho círculos, pero utilizo el metodo elipse del TCanvas para dibujarlo) y detecta las colisiones entre estas arrastrando el mouse por encima. Observareis que es muy simple pero sirve para jugar un poco con el canvas y el ratón y ver que tipo de aplicaciones visuales podemos llegar a hacer con delphi. Al pasar el mouse por encima del objeto detecta su paso y muestra un color de colisión. Además al mover una de las elipses hacia otro objeto, se detecta la colisión entre estos y se frena el movimiento si dejarte mover el objeto. La detección es muy sencilla, mediante la posición del mouse se comprueba que la distancia de éste con el centro de cualquiera de las otras elipses sea mayor que la distancia del objeto a arrastrar y las otras elipses, un principio muy simple pero que funciona muy bien y sin tener que hacer cálculos desorbitados. Aquí os dejo la aplicación Thundax Draw Ellipses, y parte del código fuente y un pantallazo de la aplicación:


La clase TEllipse:

unit ThundaxEllipse;

interface

uses
    types, Graphics, Classes, Controls;

type
    TEllipse = class(TObject)
    private
        FborderColor: TColor;
        Flength: integer;
        FlineWidth: integer;
        FCollisionColor: TColor;
        FColor: TColor;
        Fposition: TPoint;
        FCanvas: TCanvas;
        FDrag: boolean;
        Fid: string;
        Fradium: integer;
        FCenter: TPoint;
        procedure SetborderColor(const Value: TColor);
        procedure SetCollisionColor(const Value: TColor);
        procedure SetColor(const Value: TColor);
        procedure Setlength(const Value: integer);
        procedure SetlineWidth(const Value: integer);
        procedure Setposition(const Value: TPoint);
        procedure SetDrag(const Value: boolean);
        procedure SetCenter(const Value: TPoint);
        procedure Setid(const Value: string);
        procedure Setradium(const Value: integer);
    public
        property position: TPoint read Fposition write Setposition;
        property id: string read Fid write Setid;
        property Drag: boolean read FDrag write SetDrag;
        property length: integer read Flength write Setlength;
        property Color: TColor read FColor write SetColor;
        property borderColor: TColor read FborderColor write SetborderColor;
        property lineWidth: integer read FlineWidth write SetlineWidth;
        property CollisionColor: TColor read FCollisionColor write SetCollisionColor;
        constructor Create(Canvas: TCanvas; center: TPoint; length: integer);
        function PointInside(x, y: integer): boolean;
        procedure SetMousePosition(x, y: integer);
        procedure Draw(x, y: integer);
        procedure EnableDrag(x, y: integer);
        property Center: TPoint read FCenter write SetCenter;
        property radium: integer read Fradium write Setradium;
    end;
implementation

{ TEllipse }

constructor TEllipse.Create(Canvas: TCanvas; center: TPoint; length: integer);
begin
    FCanvas := Canvas;
    Fposition := center;
    FLength := length;
    FDrag := false;
    FCenter := point(Fposition.x + (Flength div 2), Fposition.y + (Flength div 2));
    FRadium := FLength div 2;
end;

procedure TEllipse.Draw(x, y: integer);
begin
    if FDrag then
        SetMousePosition(x, y);
    if PointInside(x, y) then
        FCanvas.Brush.Color := FCollisionColor
else
        FCanvas.Brush.Color := FColor;

    FCanvas.Pen.Color := FborderColor;
    FCanvas.Pen.Width := 1;
    FCanvas.Ellipse(Fposition.x, Fposition.y, Fposition.x + FLength, Fposition.y + FLength);
    FCanvas.Font.Color := clWhite;
    FCanvas.TextOut(Fposition.x + (Flength div 2) - 5, Fposition.y + (Flength div 2) - 7, FId);
end;

procedure TEllipse.EnableDrag(x, y: integer);
begin
    if PointInside(x, y) then
        Fdrag := true;
end;

function TEllipse.PointInside(x, y: integer): boolean;
var
    x0, y0, a, b: integer;
    point2: TPoint;
begin
    point2 := Point(Fposition.x + Flength, FPosition.y + Flength);
    x0 := (Fposition.x + point2.x) div 2;
    y0 := (Fposition.y + point2.y) div 2;
    a := (point2.x - Fposition.x) div 2;
    b := (point2.y - Fposition.y) div 2;
    result := (Sqr((x - x0) / a) + Sqr((y - y0) / b) <= 1);
end;

procedure TEllipse.SetborderColor(const Value: TColor);
begin
    FborderColor := Value;
end;

procedure TEllipse.SetCenter(const Value: TPoint);
begin
    FCenter := Value;
end;

procedure TEllipse.SetCollisionColor(const Value: TColor);
begin
    FCollisionColor := Value;
end;

procedure TEllipse.SetColor(const Value: TColor);
begin
    FColor := Value;
end;

procedure TEllipse.SetDrag(const Value: boolean);
begin
    FDrag := Value;
end;

procedure TEllipse.Setid(const Value: string);
begin
    Fid := Value;
end;

procedure TEllipse.Setlength(const Value: integer);
begin
    Flength := Value;
end;

procedure TEllipse.SetlineWidth(const Value: integer);
begin
    FlineWidth := Value;
end;

procedure TEllipse.SetMousePosition(x, y: integer);
begin
    FPosition := point(x - (Flength div 2), y - (Flength div 2));
    FCenter := point(Fposition.x + (Flength div 2), Fposition.y + (Flength div 2));
end;

procedure TEllipse.Setposition(const Value: TPoint);
begin
    Fposition := Value;
end;

procedure TEllipse.Setradium(const Value: integer);
begin
    Fradium := Value;
end;

end.


El cálculo de las colisiones:

procedure TForm3.Image1MouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
var
    i, j: integer;
    Collide: boolean;
    textCollision: string;
    Collisions: array[1..20] of integer;
begin
    DrawRectangle(clWhite, clWhite);
    Memo1.lines.Clear;
    textCollision := '';
    Collide := false;
    for i := 1 to 20 do
        Collisions[i] := -1;

    for i := 1 to number do
    begin
        for j := 1 to number do
            if Ellipses[i].drag and
                distanciaMouse(Point(x, y), Ellipses[i].center, Ellipses[j].center) and
                Collision(ellipses[i], ellipses[j]) then
            begin
                Memo1.lines.add('Collide True ->' + inttostr(i) + ' ' + inttostr(j));
                Collide := true;
                Collisions[i] := i;
            end;
    end;

    for i := 1 to number do
    begin
        if Collisions[i] = -1 then
            ellipses[i].Draw(x, y)
        else
            ellipses[i].draw(ellipses[i].center.x, ellipses[i].center.y);
        Memo1.lines.add('Collision ' + inttostr(Collisions[i]));
    end;

    if collide then
        textCollision := 'Collision!';

    StatusBar1.Panels[0].Text := 'x : ' + inttostr(x) + ' y : ' + inttostr(y) + ' ' + textCollision;
end;

function TForm3.DistanciaMouse(p1, p2, p3: TPoint): boolean;
begin
    result := false;
    if (p2.x <> p3.x) and (p2.y <> p3.y) then
        result := (distancia(p1, p3) < distancia(p2, p3))
end;

function TForm3.Collision(p1, p2: TEllipse): boolean;
begin
    result := false;
    if p1 <> p2 then
        result := distancia(p1.center, p2.center) < (2 * (p1.radium));
end;

function TForm3.distancia(p1, p2: TPoint): double;
begin
    result := sqrt(sqr(p1.x - p2.x) + sqr(p1.y - p2.y));
end;