- 首先,我們可以想像將照片切分成大小相同的矩形。
- 接著將矩形的四個邊,轉換成拼圖所特有的曲線。
- 由於曲線的兩端固定在矩形的頂點上,利用亂數改變曲線的線條,便可以產生每個形狀都有些微差異的拼塊了。
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; }
不過,後續的事情還很多,只是技術上困難的部分大概都搞定了。切割時,還要考慮最外圍邊緣必須改為直線,而操作介面則是另一項複雜的學問,但我就不再談下去了。