Silverlight 1.0 Video Slidepuzzle
I decided to take some time to learn Microsoft Silverlight. Since version 1.1 is still in Alpha and 2.0 is about to come out, I wanted to make sure I understood the technology, from the ground up and decided to stick with version 1.0.
Version 1.0 works only with Javascript (JScript) while version 2.0 will work with any of the .NET languages. As usual, it started out with a small directionless project, just to try to get some experience under my belt; then I saw a cool video about a somewhat unrelated technology: Microsoft Surface. What a cool technology… they had a demo of a video playing that would divide itself in puzzle pieces.
Ok, back to Silverlight, so I started reading about using a VideoBrush object, which allows you to “paint” a surface of a shape with raw video. That sounds exactly what I need to emulate the effect I saw in that Microsoft Surface video. So my small project morphed into a video slidepuzzle, just like we all used to have as kids, but instead of an image, the pieces will have live video… cool, huh? (sliding live pieces around…)
This is the first draft and I have a few enhancements already in mind. The code was written in order to dynamically calculate the sizes and positions of the pieces, based on a factor variable. This factor variable determines the amount of pieces on each axis. So I could easily change the factor to, let’s say 8 and have it generate 63 (64-1) pieces, instead of 15(16-1). Check back soon, as I may make this my next project.
Go ahead and click on any of the pieces below:
[note color=”#FFFFCC”][/note]
UPDATE: Stefan Wick from the Microsoft Silverlight team contacted me to point out an error in my code: “Miguel, […] Your code currently calls createFromXaml() on a piece of markup that begins with
Indeed just changing
javascript code
//initialize global variables
var posArray = new Array();
var factor = 4;
var margin = 4;
var plugin = null;
var pWidth = 320;
var pHeight = 240;
var emptySlot = 0;
function onCanvasLoaded(sender) {
//------------------------------
//add rectangle dynamically
//------------------------------
// Retrieve a reference to the plug-in.
plugin = sender.getHost();
pWidth = 320;
pHeight = 240;
//build base array with positions of all rectangle positions
var i = 0; var j = 0; var k = 0;
for (i = 0; i < factor; i++) //Rows (y)
{
for (j = 0; j < factor; j++) //Columns (x)
{
posArray[k] = new Array();
posArray[k][0] = (pWidth / factor) * j; //x position
posArray[k][1] = (pHeight / factor) * i; //y position
posArray[k][2] = ((pWidth - margin) / factor) * (-j) //x video offset
posArray[k][3] = ((pHeight - margin) / factor) * (-i) //y video offset
++k;
}
}
//build the rectangles
var xamlRectangleFragment;
var rectangleFragment;
var xamlResourceFragment = "";
var resourceFragment;
for (var m = 0; m < posArray.length - 1; m++) {
//Add the pieces
xamlRectangleFragment =
'<Rectangle xmlns:x=
"http://schemas.microsoft.com/winfx/2006/xaml" x:Name="p' + m + '" ';
xamlRectangleFragment +=
'Width="' + (pWidth - margin) factor +
'" Height="' + (pHeight - margin) / factor + '" ';
xamlRectangleFragment += 'Tag="' + m + '" ';
xamlRectangleFragment += 'Canvas.Left="' + posArray[m][0] + '" ';
xamlRectangleFragment += 'Canvas.Top="' + posArray[m][1] + '" ';
xamlRectangleFragment += 'Stroke="Red" StrokeThickness="1" >';
xamlRectangleFragment += '<Rectangle.Fill>';
xamlRectangleFragment +=
'<VideoBrush SourceName="media" Stretch="None"
AlignmentX="Left" AlignmentY="Top">';
xamlRectangleFragment += '<VideoBrush.Transform>';
xamlRectangleFragment +=
'<TranslateTransform X="' + (posArray[m][2] - margin) + Y =
"' + (posArray[m][3] - margin) + '" />';
xamlRectangleFragment += '</VideoBrush.Transform>';
xamlRectangleFragment += '</VideoBrush>';
xamlRectangleFragment += '</Rectangle.Fill>';
xamlRectangleFragment += '</Rectangle>';
rectangleFragment = null;
rectangleFragment = plugin.content.createFromXaml(xamlRectangleFragment, false);
// Add the XAML fragmento as a child of the root Canvas object.
sender.children.add(rectangleFragment);
rectangleFragmentInstance = null;
rectangleFragmentInstance = sender.findName("p" + m);
//piece eventhandlers
rectangleFragmentInstance.addEventListener(
"MouseLeftButtonDown", onMouseDown);
//-----------------------------------------------
//adds the storyboards for animation for each piece
xamlResourceFragment += '<Storyboard
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" ';
xamlResourceFragment += 'x:Name="storyboard_x_' + m + '" >';
xamlResourceFragment += '<DoubleAnimation x:Name="animation_x_' + m + '" ';
xamlResourceFragment += 'Storyboard.TargetName="p' + m + '" ';
xamlResourceFragment += 'Storyboard.TargetProperty="(Canvas.Left)" ';
xamlResourceFragment += 'From="0" To="400" Duration="0:0:0.1"
AutoReverse="False" />'
xamlResourceFragment += '</Storyboard>'
xamlResourceFragment += '<Storyboard
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" ';
xamlResourceFragment += 'x:Name="storyboard_y_' + m + '" >';
xamlResourceFragment += '<DoubleAnimation x:Name="animation_y_' + m + '" ';
xamlResourceFragment += 'Storyboard.TargetName="p' + m + '" ';
xamlResourceFragment += 'Storyboard.TargetProperty="(Canvas.Top)" ';
xamlResourceFragment += 'From="0" To="400" Duration="0:0:0.1"
AutoReverse="False" />'
xamlResourceFragment += '</Storyboard>'
}
//for some reason, I can't do sender.children.add here,
//but I can do sender.Resources =...
//so I have to concatenate the strings and then set
//this value outside of the loop
xamlResourceFragment = '<ResourceDictionary>' +
xamlResourceFragment + '</ResourceDictionary>';
resourceFragment = null;
resourceFragment = plugin.content.createFromXaml(xamlResourceFragment, false);
sender.Resources = (resourceFragment);
emptySlot = m; // sets the emptySlot id to the last item
}
// Start slide operation.
function onMouseDown(sender, mouseEventArgs) {
var selectedPiece = sender;
if (isPieceMovable(selectedPiece)) {
slidePieceAndReset(selectedPiece);
}
}
function isPieceMovable(selectedPiece) {
if (selectedPiece != null) {
//check adjacent pieces; to the right, left, below and above
//only one condition should match...
//if nothing matches, fall through and return false...
if (
(
selectedPiece["Canvas.Left"] !=
//if not on the last column, then ok
(pWidth / factor) * (factor - 1) &&
selectedPiece["Canvas.Left"] + (pWidth / factor) ==
posArray[emptySlot][0] && //if spot matches X EmptySlot
//if spot matches Y EmptySlot
selectedPiece["Canvas.Top"] == posArray[emptySlot][1]
) || (
//if not on the first column, then ok
selectedPiece["Canvas.Left"] != 0 &&
selectedPiece["Canvas.Left"] - (pWidth / factor) ==
posArray[emptySlot][0] && //if spot matches X EmptySlot
//if spot matches Y EmptySlot
selectedPiece["Canvas.Top"] == posArray[emptySlot][1]
) || (
//if not on last row, then ok
selectedPiece["Canvas.Top"] != (pHeight / factor) * (factor - 1) &&
//if spot matches X EmptySlot
selectedPiece["Canvas.Left"] == posArray[emptySlot][0] &&
selectedPiece["Canvas.Top"] + (pHeight / factor) ==
posArray[emptySlot][1] //if spot matches Y EmptySlot
) || (
//if not on top row, then ok
selectedPiece["Canvas.Top"] != 0 &&
//if spot matches X EmptySlot
selectedPiece["Canvas.Left"] == posArray[emptySlot][0] &&
//if spot matches Y EmptySlot
selectedPiece["Canvas.Top"] - (pHeight / factor) ==
posArray[emptySlot][1]
)
) {
return true;
}
else {
return false;
}
}
}
function slidePieceAndReset(selectedPiece) {
//find the array position of this piece spot
//(which will be the empty location, once we move it)
for (p = 0; p < posArray.length; p++) {
if (posArray[p][0] == selectedPiece["Canvas.Left"] &&
posArray[p][1] == selectedPiece["Canvas.Top"]) {
break;
}
}
//set the origins and destinations for the storyboard animations
var animationX = selectedPiece.findName("animation_x_" + selectedPiece.Tag);
var animationY = selectedPiece.findName("animation_y_" + selectedPiece.Tag);
animationY["From"] = selectedPiece["Canvas.Top"];
animationY["To"] = posArray[emptySlot][1];
animationX["From"] = selectedPiece["Canvas.Left"];
animationX["To"] = posArray[emptySlot][0];
var storyboardX = selectedPiece.findName("storyboard_x_" + selectedPiece.Tag);
var storyboardY = selectedPiece.findName("storyboard_y_" + selectedPiece.Tag);
//ready? animate!
storyboardX.begin();
storyboardY.begin();
//now set the empty space spot left by the piece to be the emptySlot
emptySlot = p;
}
function onMediaEnded(sender, eventArgs) {
var myMediaElement = sender.findName("media");
myMediaElement.Position = "00:00:00";
myMediaElement.Play();
}
function toggleHintLayer(sender, eventArgs) {
var myMediaElement = sender.findName("media");
var hintLayerCheck = sender.findName("toggleHintLayerCheckbox")
if (myMediaElement.Opacity == "0.0") {
myMediaElement.Opacity = "0.2";
hintLayerCheck.Indices = "59";
} else {
myMediaElement.Opacity = "0.0";
hintLayerCheck.Indices = "134";
}
}
//-------------------------------------------------------------------------
var interval = null;
function toggleAutoplay(sender, eventArgs) {
var autoPlayCheck = sender.findName("toggleAutoplayCheckbox")
if (interval) {
clearInterval(interval);
interval = null;
autoPlayCheck.Indices = "134";
} else {
interval = setInterval("moveRandomPiece()", 200);
autoPlayCheck.Indices = "59";
}
}
function moveRandomPiece() {
//obtain a random piece that isMovable
var randomNumber;
var randomPiece;
for (p = 0; p < posArray.length - 1; p++) {
randomNumber = Math.floor(Math.random() * posArray.length);
randomPiece = plugin.content.findName("p" + randomNumber);
if (isPieceMovable(randomPiece)) {
slidePieceAndReset(randomPiece);
break;
}
}
}
//----------------------------------------------------------------------
function toggleMediaPause(sender, eventArgs) {
sender.findName("media").pause();
var mediaPauseCheck = sender.findName("toggleMediaPauseCheckbox");
var mediaPlayCheck = sender.findName("toggleMediaPlayCheckbox");
mediaPauseCheck.Indices = "59";
mediaPlayCheck.Indices = "134";
}
function toggleMediaPlay(sender, eventArgs) {
sender.findName("media").play();
var mediaPauseCheck = sender.findName("toggleMediaPauseCheckbox");
var mediaPlayCheck = sender.findName("toggleMediaPlayCheckbox");
mediaPauseCheck.Indices = "134";
mediaPlayCheck.Indices = "59";
}
xaml code
<Canvas x:Name="parentCanvas"
xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Loaded="onCanvasLoaded"
Width="320"
Height="240"
Background="#EDF1F2"
Canvas.Left="40"
Canvas.Top="20">
<MediaElement x:Name="media"
Source="/private/experiments/videopuzzle/Acoustic.wmv"
Opacity="0.2"
Volume="0.8"
MediaEnded="onMediaEnded" />
<Canvas Canvas.Top="245" Canvas.Left="0"
Cursor="Hand"
MouseLeftButtonDown="toggleHintLayer" >
<Glyphs x:Name="toggleHintLayerCheckbox" OriginX="0" OriginY="15"
Fill="Black"
FontUri="/private/experiments/videopuzzle/wingding.ttf"
FontRenderingEmSize="16"
Indices="134" />
<TextBlock x:Name="toggleHintLayerCheckboxText"
FontSize="12"
Canvas.Left="17"
Foreground="Black"
Text="Hint layer" />
</Canvas>
<!-- Autoplay toggle-->
<Canvas Canvas.Top="245" Canvas.Left="100" Cursor="Hand"
MouseLeftButtonDown="toggleAutoplay" >
<Glyphs x:Name="toggleAutoplayCheckbox" OriginX="0" OriginY="15"
Fill="Black"
FontUri="/private/experiments/videopuzzle/wingding.ttf"
FontRenderingEmSize="16"
Indices="134" />
<TextBlock x:Name="toggleAutoplayCheckboxText"
FontSize="12"
Canvas.Left="15"
Foreground="Black"
Text="Autoslide" />
</Canvas>
<!-- mediaPlay toggle-->
<Canvas Canvas.Top="245" Canvas.Left="200" Cursor="Hand"
MouseLeftButtonDown="toggleMediaPlay" >
<Glyphs x:Name="toggleMediaPlayCheckbox" OriginX="0" OriginY="15"
Fill="Black"
FontUri="/sandbox/videopuzzle/wingding.ttf"
FontRenderingEmSize="16"
Indices="59" />
<TextBlock x:Name="toggleMediaPlayCheckboxText"
FontSize="12"
Canvas.Left="15"
Foreground="Black"
Text="Play video" />
</Canvas>
<!-- mediaPause toggle-->
<Canvas Canvas.Top="260" Canvas.Left="200" Cursor="Hand"
MouseLeftButtonDown="toggleMediaPause" >
<Glyphs x:Name="toggleMediaPauseCheckbox" OriginX="0" OriginY="15"
Fill="Black"
FontUri="/sandbox/videopuzzle/wingding.ttf"
FontRenderingEmSize="16"
Indices="134" />
<TextBlock x:Name="toggleMediaPauseCheckboxText"
FontSize="12"
Canvas.Left="15"
Foreground="Black"
Text="Pause video" />
</Canvas>
</Canvas>


