Grafikk
Børre Stenseth
Moduler>Grafikk>Brikker

Brikker

Hva
Flytte og rotere brikker

Vi skal lage en skisse til et brettspill der vi ønsker å flytte og rotere polygonformede brikker så intuitivt som mulig. Dersom vi peker midt i brikken (polygonet) så skal det tolkes som at vi vil flytte brikken, mens et punkt i en "rimelig" avstand fra sentrum innebærer en rotasjon om sentrum. Vi forutsetter konvekse polygoner.

MERK at dette er bare en skisse der vi leker med litt geometri. Vi er ganske langt fra en implementasjon av f.eks. et puslespill.

screen

Hvis vi starter med formen så er logikke rimelig enkel. Vi setter opp to brikker og vi plukker opp musebegivenheter.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace pieces
{
    public partial class Form1 : Form
    {
        List<brikke> brikker = new List<brikke>(2);
        brikke currentBrikke = null;
        Boolean isRotating = false;
        int lastX, lastY;
        
        public Form1()
        {
            InitializeComponent();
            // set up brikker: x,y,x,y,x,y
            // close polygons! (last=first)
            setupBrikke("10,1,10,100,50,100,10,1");
            setupBrikke("100,100,200,100,150,200,100,100");
        }
        private void setupBrikke(String pos)
        {
            List<PointF> pnts = new List<PointF>(5);
            String[] vals = pos.Split(',');
            for (int ix = 0; ix < vals.Length; ix = ix + 2)
                pnts.Add(new Point(Convert.ToInt32(vals[ix]),
                                   Convert.ToInt32(vals[ix + 1])));
            brikker.Add(new brikke(panel1, pnts.ToArray()));
        }
        private void panel1_Paint(object sender, PaintEventArgs e)
        {
            // bottom to top
            for (int ix = brikker.Count - 1; ix > -1; ix--)
                brikker[ix].Draw();
        }
        private void panel1_MouseDown(object sender, MouseEventArgs e)
        {
            // have we hit a polygon?
            foreach (brikke b in brikker)
            {
                if (b.polygonHit(new Point(e.X, e.Y)))
                {
                    currentBrikke = b;
                    // this one to top
                    brikker.Remove(currentBrikke);
                    brikker.Insert(0,currentBrikke);
                    currentBrikke.hiLite(true);
                    panel1.Invalidate(currentBrikke.getRegion());
                    lastX = e.X;
                    lastY = e.Y;
                    isRotating = currentBrikke.WillRotate(new Point(e.X, e.Y));
                    return;
                }
            }
        }
        private void panel1_MouseMove(object sender, MouseEventArgs e)
        {
            if (currentBrikke != null)
            {
                //keep inside
                int x = Math.Max(0,Math.Min(e.X, panel1.Width));
                int y = Math.Max(0,Math.Min(e.Y, panel1.Height));
                
                panel1.Invalidate(currentBrikke.getRegion()); 
                if (isRotating)
                    currentBrikke.rotate(x,lastX,y,lastY);
                else
                    currentBrikke.move(x-lastX,y-lastY);
                panel1.Invalidate(currentBrikke.getRegion()); 
                lastX = x;
                lastY = y;
            }
        }
        private void panel1_MouseUp(object sender, MouseEventArgs e)
        {
            if (currentBrikke != null)
            {
                currentBrikke.hiLite(false);
                panel1.Invalidate(currentBrikke.getRegion());
                currentBrikke = null;
            }
        }
    }
}

Vi ser at vi stiller noen spørsmål til en brikke: polygonHit, willRotate og vi bruker metodene: move, rotate, hilite, draw, getRegion. Vi ser dessuten at vi bruker en oppfriskingsstrategi der vi invaliderer det området en brikke dekker slik at den generell paint-metoden bare tegner det som er nødvendig. Dette for å unngå at hele brettet friskes opp.

Når vi initialiserer en brikke setter vi opp det omgivende rektangelet og vi bestemmer hva som er midten i brikken, polygonet.

// attributter
PointF[] polygon;
Rectangle box;
Point center;
static Brush HILITE_BRUSH = Brushes.Red;
static Brush NORMAL_BRUSH = Brushes.Blue;
Brush m_brush;
Control m_owner;
// current transformation matrix
Matrix m_Matrix=new Matrix();

public brikke(Control owner,PointF[] p)
{
    // polygon is closed
    polygon = p;
    m_brush = NORMAL_BRUSH;
    m_owner = owner;
    makeBox();
    makeCenter();
}

private void makeBox()
{
    // surrounding rectangle
    float minx = Int32.MaxValue;
    float miny = Int32.MaxValue;
    float maxx = Int32.MinValue;
    float maxy = Int32.MinValue;
    for (int ix = 0; ix < polygon.Length; ix++)
    {
        minx = Math.Min(minx, polygon[ix].X);
        miny = Math.Min(miny, polygon[ix].Y);
        maxx = Math.Max(maxx, polygon[ix].X);
        maxy = Math.Max(maxy, polygon[ix].Y);
    }
    box = new Rectangle((int)minx - 2, (int)miny - 2,
        (int)maxx - (int)minx + 4, (int)maxy - (int)miny + 4);
}

private void makeCenter()
{
    // decide center
    float xsum=0;
    float ysum = 0;
    for (int ix = 0; ix < polygon.Length - 1; ix++)
    {
        xsum += polygon[ix].X-box.Left;
        ysum += polygon[ix].Y-box.Top;
    }
    center.X = Convert.ToInt32(box.Left+(xsum / (polygon.Length - 1)));
    center.Y = Convert.ToInt32(box.Top+(ysum / (polygon.Length - 1)));
}

Metodene som responderer på de kravene Form1 har er implementert slik:

public Region getRegion()
{
    Region r = new Region(new Rectangle(box.X - 2, box.Top - 2, box.Width + 4, box.Height + 4));
        r.Transform(m_Matrix);
        return r;
}
public void hiLite(Boolean on)
{
    if (on)
        m_brush = HILITE_BRUSH;
    else
        m_brush = NORMAL_BRUSH;
}
public void move(int dx, int dy)
{
    m_Matrix.Translate(dx, dy,MatrixOrder.Append);
}
public void rotate(int x, int lastX, int y, int lastY)
{
    Matrix m = m_Matrix.Clone();
    m.Invert();
    Point[] pts = new Point[2] { new Point(x, y), new Point(lastX, lastY) };
    m.TransformPoints(pts);
    // fixed anglesize
    float v = 2.0f;
    // direction based on quadrant and direction
    if (   ((pts[0].X > center.X)  && (pts[0].Y < pts[1].Y))
        || ((pts[0].X <= center.X) && (pts[0].Y >= pts[1].Y)))
        v = -v;            
    m_Matrix.RotateAt(v,center, MatrixOrder.Prepend);
}
public void Draw()
{
    Graphics DC = m_owner.CreateGraphics();
    DC.ResetTransform();
    DC.MultiplyTransform(m_Matrix);
    
    DC.FillPolygon(m_brush, polygon);
    DC.DrawLines(new Pen(Brushes.Black), polygon);
    // mark center
    DC.DrawLine(new Pen(Brushes.Black), center.X-10,center.Y,center.X+10,center.Y);
    DC.DrawLine(new Pen(Brushes.Black), center.X,center.Y-10,center.X,center.Y+10);
    // mark box
    DC.DrawRectangle(new Pen(Brushes.DarkGray), box);
    DC.ResetTransform();
}
public Boolean polygonHit(Point p)
{
    Matrix m = m_Matrix.Clone();
    m.Invert();
    Point[] pts = new Point[1] { new Point(p.X, p.Y) };
    m.TransformPoints(pts);
    return calculations.InsidePolygon(polygon, pts[0].X,pts[0].Y);
}
public Boolean WillRotate(Point p)
{
    Matrix m = m_Matrix.Clone();
    m.Invert();
    Point[] pts = new Point[1] { new Point(p.X, p.Y) };
    m.TransformPoints(pts);
    
    // reasonable distance from center ?
    double dist = Math.Sqrt(
        (pts[0].X - center.X) * (pts[0].X - center.X) + 
        (pts[0].Y - center.Y) * (pts[0].Y - center.Y));
    
    return dist > Math.Min(box.Width, box.Height) / 3;
}

Vi ser at metoden polygonHit baserer seg på en klasse, calculations, og en metode InsidePolygon. Klassen calculations er slik.

_calculations.cs
Referanser
  • Prosjektet:
    https://svn.hiof.no/svn/psource/Csharpspikes/brikker
Vedlikehold

B. Stenseth, januar 2011

(Velkommen) Moduler>Grafikk>Brikker (Histogram)