Pages Menu
TwitterRssFacebook
Categories Menu

Posted by on 27th February, 2008

Silverlight 1.0 Video Slidepuzzle

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 . 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 instead as this is the proper type of the collection you are trying to create.”

Indeed just changing for makes the app work with Silverlight 2.0 Beta2

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>