2010年9月28日

Jigsaw Puzzle by Silverlight

我打算使用 Silverlight 來寫拼圖遊戲,並以網友自己的照片,來做為圖案。為了要完成這項工作,需要將複雜的過程,分成幾個步驟,並找到對應的解決方法。
  1. 首先,我們可以想像將照片切分成大小相同的矩形。
  2. 接著將矩形的四個邊,轉換成拼圖所特有的曲線。
  3. 由於曲線的兩端固定在矩形的頂點上,利用亂數改變曲線的線條,便可以產生每個形狀都有些微差異的拼塊了。

執行上列的步驟,我們需要了解 Path 的語法,以及切割照片的方法。

Cubic Bezier Curve

利用 Cubic Beizer Curver 可以很容易設計出我們所要的曲線。上面的解說圖,說明 Cubic Bezier Curve 的語法,如果是直接寫在 xaml 上,結果如下:
<Path Data="Mx1,y1 Cx2,y2 x3,y3 x4,y4 C....", ..>

如果用程式來產生,程式碼如下:
            Path path = new Path();
            PathGeometry pathGeometry = new PathGeometry(); 

            path.Data = pathGeometry; 
            pathGeometry.Figures = new PathFigureCollection(); 

            PathFigure figur = new PathFigure(); 
            pathGeometry.Figures.Add(figur); 
            figur.StartPoint = new Point(x1, y1);

            BezierSegment seg1 = new BezierSegment();
            seg1.Point1 = new Point(x2, y2);
            seg1.Point2 = new Point(x3, y3);
            seg1.Point3 = new Point(x4, y4);
            figur.Segments.Add(seg1); 
            :            
            :            
            path.Stroke = new SolidColorBrush(Colors.Blue);
            path.StrokeThickness = 3.0;
            this.LayoutRoot.Children.Add(path);


Piece Sharp

接下來,可以利用 Expression Blend 設計一段邊線的模板 (PathNodes),並調整數值,使左右能夠對稱,並利用旋轉、鏡射,產出完整的拼塊圖案。
        
Point[] PathNodes = new Point[]
        {
            new Point( 33,8),  // C1-1
            new Point(69,9),   // C1-2
            new Point(77,-8),   // C1-3

            new Point( 85,-23),  // C2-1
            new Point(54,-39),   // C2-2 
            new Point(65,-58),   // C2-3

            new Point( 77,-76),          // C3-1
            new Point(200-77,-76),    // C3-2
            new Point(200-65,-58),   // C3-3

            new Point(200-54,-39),   // C4-1
            new Point(200-85,-23),   // C4-2
            new Point(200-77,-8),     // C4-3

            new Point(200-69,9),  // C5-1 
            new Point(200-33,8),  // C5-2
            new Point(200,0)        // C5-3
        };

        private enum EdgeOrientation
        {
            Horizontal = 0,
            Vertical = 1
        }

       private PathGeometry CreateJigsawPuzzlePathGrometry(Point startPoint, Size pieceSize, EdgeOrientation orientation)
        {            
            PathGeometry pathGeometry = new PathGeometry();
            pathGeometry.Figures = new PathFigureCollection();

            PathFigure figur = new PathFigure();
            figur.StartPoint = startPoint;
            pathGeometry.Figures.Add(figur);

            Point lastPoint = startPoint;

            for (int edge = 0; edge < 4; edge++)
            {
                TransformGroup group = new TransformGroup();
                double scaleX = (edge % 2 == 0) ? pieceSize.Width / 200 : pieceSize.Height / 200;
                double scaleY = (edge % 2 == 0) ? pieceSize.Height / 200 : pieceSize.Width / 200;
                scaleY = (edge % 2 == (int)orientation) ? -scaleY : scaleY;

                ScaleTransform st = new ScaleTransform() { ScaleX = scaleX, ScaleY = scaleY };
                group.Children.Add(st);

                TranslateTransform tt = new TranslateTransform() { X = lastPoint.X, Y = lastPoint.Y };
                group.Children.Add(tt);

                RotateTransform rt = new RotateTransform() { Angle = edge * 90, CenterX = lastPoint.X, CenterY = lastPoint.Y };
                group.Children.Add(rt);

                for (int i = 0; i < PathNodes.Length / 3; i++)
                {
                    BezierSegment seg = new BezierSegment();
                    seg.Point1 = group.Transform(PathNodes[i * 3]);
                    seg.Point2 = group.Transform(PathNodes[i * 3 + 1]);
                    seg.Point3 = group.Transform(PathNodes[i * 3 + 2]);

                    figur.Segments.Add(seg);
                    lastPoint = seg.Point3;
                }
            }
            return pathGeometry;
        }


CreateJigsawPuzzlePathGrometry 有三個參數:
startPoint - 我們將一張圖,以矩形劃分,每個矩形的左上角,距離圖片左上角的位置,即是每一片小拼塊開始進行切割的 startPoint
pieceSize - 指劃分圖片矩形大小
orientation - 由於鄰近的小拼塊,凹凸位置剛好相反,orientation 應交互變換



Clip Image

我們利用上列方法所產生的 PathGrometry 來進行圖片切割的工作,並讓切割後的圖案成為獨立的 element,並有一致的原始點位置,方便使用者移動與操作。
Silverlight 並沒有 sprite 的功能,ImageBrush.ImageSource 也沒辦法設定位移與範圍,所以我利用了 WriteableBitmap 來達成這項工作。

   
        private Canvas CreatePiece(BitmapImage bitmap, Point startPoint, Size pieceSize, EdgeOrientation orientation)
        {
            Image imageTemp = new Image();
            imageTemp.Source = bitmap;
            imageTemp.Clip = CreateJigsawPuzzlePathGrometry(startPoint, pieceSize, orientation);

            Size panelSize = new Size(pieceSize.Width * 2, pieceSize.Height * 2);
            WriteableBitmap wb = new WriteableBitmap((int)panelSize.Width, (int)panelSize.Height);
            TranslateTransform translate = new TranslateTransform() { X = -startPoint.X + pieceSize.Width / 2, Y = -startPoint.Y + pieceSize.Height / 2 };
            wb.Render(imageTemp, translate);
            wb.Invalidate();

            Image image = new Image();
            image.Source = wb;
            Canvas.SetLeft(image, -pieceSize.Width / 2);
            Canvas.SetTop(image, -pieceSize.Height / 2);

            Canvas canvas = new Canvas();
            canvas.Children.Add(image);

            Path path = new Path() { StrokeThickness = 2, Stroke = new SolidColorBrush(Colors.Black) };
            path.Data = CreateJigsawPuzzlePathGrometry(startPoint, pieceSize, orientation);
            Canvas.SetLeft(path, -startPoint.X);
            Canvas.SetTop(path, -startPoint.Y);
            canvas.Children.Add(path);
            
            return canvas;
        }
     


不過,後續的事情還很多,只是技術上困難的部分大概都搞定了。切割時,還要考慮最外圍邊緣必須改為直線,而操作介面則是另一項複雜的學問,但我就不再談下去了。

沒有留言:

Deploying Vue & .NET with Google OAuth on GCP Cloud Run

Deploying Vue & .NET with Google OAuth on GCP Cloud Run Deploying Vue & .NET with Google OAuth on GCP Cloud Run...