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...
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.
br> br>
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? (slide the 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 will make this my next project.
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 <Canvas.Resources>.
This is not actually supported as we don’t have an object of type Canvas.Resources. In Silverlight 1.0 it worked “by accident”,
but with Silverlight 2 Beta2, we now detect the invalid XAML and throw an error.
You’d have to use <ResourceDictionary> instead as this is the proper type of the collection you are trying to create."
Indeed just changing <Canvas.Resource> for <ResourceDictionary> makes the app work with Silverlight 2.0 Beta2
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="ClientBin/myvideo.wmv"
Opacity="0.3"
MediaEnded="onMediaEnded"
Volume="1.0"/>
</Canvas>
The xaml code is pretty simple since pretty much everything is generated via script at the onLoaded event. So not much to see here.
And here is the entire Javascript code to make this gem run. Enjoy!
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 rectangles 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 xamlRectFrag;
var rectFrag;
var xamlResFrag = "";
var resFrag;
for(var m=0; m < posArray.length-1; m++)
{
//Add the pieces
xamlRectFrag= '<Rectangle xmlns:x="http://schem..." ';
xamlRectFrag+= 'x:Name="p'+ m + '" ';
xamlRectFrag+= 'Width="'+(pWidth-margin)/factor+'" ';
xamlRectFrag+= 'Height="'+(pHeight-margin)/factor+'" ';
xamlRectFrag+= 'Tag="' + m + '" ';
xamlRectFrag+= 'Canvas.Left="'+posArray[m][0] + '" ';
xamlRectFrag+= 'Canvas.Top="'+posArray[m][1]+'" ';
xamlRectFrag+= 'Stroke="Red" StrokeThickness="1" >';
xamlRectFrag+= '<Rectangle.Fill>';
xamlRectFrag+= '<VideoBrush SourceName="media"...>';
xamlRectFrag+= '<VideoBrush.Transform>';
xamlRectFrag+= '<TranslateTransform+'" ';
xamlRectFrag+= 'X="'+ (posArray[m][2]-margin) +'" ';
xamlRectFrag+= 'Y="'+ (posArray[m][3]-margin) +'" />';
xamlRectFrag+= '</VideoBrush.Transform>';
xamlRectFrag+= '</VideoBrush>';
xamlRectFrag+= '</Rectangle.Fill>';
xamlRectFrag+= '</Rectangle>';
rectFrag = null;
rectFrag = plugin.content.createFromXaml(xamlRectFrag,false);
// Add the XAML fragments as a child of the Canvas object.
sender.children.add(rectFrag);
rectFragInst = null;
rectFragInst = sender.findName("p" + m);
//piece eventhandlers
rectFragInst.addEventListener(
"MouseLeftButtonDown",onMouseDown);
//-----------------------------------------------
//adds the storyboards for animation
//for each piece into the Canvas.Resources
xamlResFrag+= '<Storyboard xmlns:x="http://schema..." ';
xamlResFrag+= 'x:Name="storyboard_x_' + m + '" >';
xamlResFrag+= '<DoubleAnimation +'" ';
xamlResFrag+= 'x:Name="animation_x_'+m+'" ';
xamlResFrag+= 'Storyboard.TargetName="p' + m +'" ';
xamlResFrag+= 'Storyboard.TargetProperty="(Canvas.Left)" ';
xamlResFrag+= 'From="0" To="400" Duration="0:0:0.1" />'
xamlResFrag+= '</Storyboard>'
xamlResFrag+= '<Storyboard xmlns:x="http://schema..." ';
xamlResFrag+= 'x:Name="storyboard_y_' + m + '" >';
xamlResFrag+= '<DoubleAnimation +'" ';
xamlResFrag+= 'x:Name="animation_y_'+m +'" ';
xamlResFrag+= 'Storyboard.TargetName="p' + m +'" ';
xamlResFrag+= 'Storyboard.TargetProperty="(Canvas.Top)" ';
xamlResFrag+= 'From="0" To="400" Duration="0:0:0.1" />'
xamlResFrag+= '</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
//changed based on suggestion of Stefan Wick (Microsoft)
xamlResFrag=
'<ResourceDictionary>'+xamlResFrag+'</ResourceDictionary>';
resFrag = null;
resFrag = plugin.content.createFromXaml(xamlResFrag, false);
sender.Resources = (resFrag);
// sets the emptySlot id to the last item
emptySlot = m;
}
// Start slide operation when clicked.
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((
//if not on the last column, then ok
selectedPiece["Canvas.Left"]!=
(pWidth/factor)*(factor-1)&&
//if spot matches X EmptySlot
selectedPiece["Canvas.Left"]+(pWidth/factor)==
posArray[emptySlot][0]&&
//if spot matches Y EmptySlot
selectedPiece["Canvas.Top"]==
posArray[emptySlot][1]
)||(
//if not on the first column, then ok
selectedPiece["Canvas.Left"]!= 0 &&
//if spot matches X EmptySlot
selectedPiece["Canvas.Left"]-(pWidth/factor)==
posArray[emptySlot][0] &&
//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] &&
//if spot matches Y EmptySlot
selectedPiece["Canvas.Top"]+(pHeight/factor)==
posArray[emptySlot][1]
)||(
//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";
}