|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
IntroductionA while back one of my web buddies sent me a link to an excellent image and video addin for a web browser. It is called
PicLens. And someone also left a note in the forum of
one of my other articles saying how simliar it was. At the time I had not seen PicLens
so didn't have a clue what the comment meant. But when I did install PicLens I was blown away.
Table Of ContentsThe rest of this article will cover the following areas
I guess the best way is to just crack on. One note though before we start, I will be using C# and Visual Studio 2008 The General IdeaThe general idea behind PicLens (which is what I am trying to imitate) is to offer the user the ability to search various online repositories for images/videos and then present these in a nice 3D plane. This is the basic idea. I feel that I have achieved all of these requirements within MarsaX, and it looks pretty cool to boot. Perhaps some screen shots would help show what the attached MarsaX code looks like when run Allow User To Specify Search This is avaible by using the +/- buttons in the top right hand side. When clicked on these buttons will show/hide the SearchArea element
Search Area When clicked on, these buttons conduct their search. If a query is not in progress the users new query will be used. The current Search is given a white glow, whilst the item under the mouse gets a blue glow.
Search Results Being Loaded When a search is started a loading section is shown (remember these images are being read/downloaded from the web). This is shown below
Results Shown After the loading has completed (A load timer has timed out) the images are shown to the user, along with some controls at the bottom of the page to scroll through the images in 3D space, and also to zoom in/out. Shown below is what MarsaX looks like after it first runs
Then when the user uses the scroll area control the 3D objects will be panned left/right
And they will rotate when the get the mouse over event (should you have this option turned on in the App.Config file)
User Controls You can see from here the user is able to run their mouse over an individaul image within 3D space, causing it to rotate in 3D space. The user is also able to move the images in 3D space using the pan control at the bottom, and also affect the zoom using the zoom control. It should also be noted that clicking on one of the 3D hosted images will show it in a new popup window
I have to say I am pretty happy about how this turned out, as its only really taken about 7 hours of actual work I think. So yeah I am pretty happy about it. So I'm done showing screen shots, so what I want to do now is explain MarsaX works internally The Nitty GrittyOk so now you have seen the screen shots, you probably want to know how it works. In order to exaplain this I think it is probably best to break it down into several areas, namely:
This should cover most of it, so let us crack on shall we SearchAreaThe search area is probably one of the easiest parts as its really just a bunch of
The search area element within the /// <summary>
/// If the SearchArea is not currently shown, Animates the SearchArea
/// to be shown on screen
/// </summary>
private void btnPlus_Click(object sender, RoutedEventArgs e)
{
if (!IsSearchAreaShown)
{
IsSearchAreaShown = true;
Storyboard HideSearchArea =
this.TryFindResource("OnShowSearchArea") as Storyboard;
if (HideSearchArea != null)
HideSearchArea.Begin(SearchArea);
}
}
/// <summary>
/// If the SearchArea is currently shown, Animates the SearchArea
/// to be hidden off screen
/// </summary>
private void btnMinus_Click(object sender, RoutedEventArgs e)
{
if (IsSearchAreaShown)
{
HideSearchArea();
}
}
/// <summary>
/// Animates the SearchArea to be hidden off screen
/// </summary>
private void HideSearchArea()
{
IsSearchAreaShown = false;
Storyboard HideSearchArea =
this.TryFindResource("OnHideSearchArea") as Storyboard;
if (HideSearchArea != null)
HideSearchArea.Begin(SearchArea);
}
And the <!-- Show Search Area Animation -->
<Storyboard x:Key="OnShowSearchArea">
<ThicknessAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="SearchArea"
Storyboard.TargetProperty="(FrameworkElement.Margin)">
<SplineThicknessKeyFrame KeyTime="00:00:00" Value="0,9,-500,-9"/>
<SplineThicknessKeyFrame KeyTime="00:00:00.5000000" Value="0,9,-10,-9"/>
<SplineThicknessKeyFrame KeyTime="00:00:01" Value="0,9,0,-9"/>
</ThicknessAnimationUsingKeyFrames>
</Storyboard>
<!-- Hide Search Area Animation -->
<Storyboard x:Key="OnHideSearchArea">
<ThicknessAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="SearchArea"
Storyboard.TargetProperty="(FrameworkElement.Margin)">
<SplineThicknessKeyFrame KeyTime="00:00:00" Value="0,9,0,-9"/>
<SplineThicknessKeyFrame KeyTime="00:00:00.5000000" Value="0,9,-490,-9"/>
<SplineThicknessKeyFrame KeyTime="00:00:01" Value="0,9,-500,-9"/>
</ThicknessAnimationUsingKeyFrames>
</Storyboard>
All thats really happening is that teh SearchArea element is starting off screen, by putting in a negative Left Margin setting, and the OnShowSearchArea The Actual SearchAs can be seen in the SearchArea element within the /// <summary>
/// Stores the newSearchType search request in an internal
/// field and proceeds to call the GetQueryResults on a background thread
/// </summary>
private void ConductSearch(string keyword, SearchTypes newSearchType)
{
currentSearchtype = newSearchType;
loadTimer.IsEnabled = true;
controlsArea.Visibility = Visibility.Collapsed;
ucLoader.Visibility = Visibility.Visible;
IsAnimating = true;
IsVisible = false;
//Load the images async, but assume that the loadTimer time will
//be enough to cover how long it will take to fetch and display
//all the images
ThreadPool.QueueUserWorkItem(x =>
{
var data = GetQueryResults(keyword);
//Create 3D models for the images
Dispatcher.BeginInvoke(DispatcherPriority.Normal,
((Action)delegate { CreateModelsForImages(data); }));
});
}
/// <summary>
/// returns a List<see cref="PhotoInfo">PhotoInfo</see>
/// which match the current search query based on the value
/// of internal search request
/// </summary>
private List<PhotoInfo> GetQueryResults(string keyword)
{
switch (currentSearchtype)
{
case SearchTypes.FlickrLatest:
return FlickerProvider.LoadLatestPictures();
case SearchTypes.FlickrInteresting:
return FlickerProvider.LoadInterestingPictures();
case SearchTypes.FlickrKey:
return FlickerProvider.LoadPicturesKey(keyword);
default:
return FlickerProvider.LoadLatestPictures();
}
}
What is actually happening here is that a new WaitCallBack is added to the ThreadPool so that the search is conducted in the background. Whilst the search is being performed the Loading screen is shown to show that the worker is busy fetching some images.
IMPORTANT NOTE : We had originally use a Glow BitmapEffect on this usercontrol, but this 1 BitmapEffect was enough to change how much CPU time was used from around 7% to 50-60%....Scary. There is a new Effects API within the .NET 3.5 SP1, but it doesn't include a Glow effect. Which is rather sad I think, considering how nice they look. The problem here was that the BitmapEffects are not hardware accelerated.
The UI thread is still responsive at this point, but this is little else the user can do but wait for the iamges. Never the less I consider it good practice to fetch the images in the background, leaving the UI responsive So where exactly do these images come from, well they come from an RSS feed available at Flickr. There is a single class that exposes some static search methods for obtaining feed results. This class is called
Anyway the upshot of all this is, that we have a class with some static methods on it that allow us to carry out Flickr searches and return some results. Let see one of these search methods /// <summary>
/// Returns a List<see cref="PhotoInfo">PhotoInfo</see> which represent
/// the latest Flickr images
/// </summary>
public static List<PhotoInfo> LoadLatestPictures()
{
try
{
var xraw = XElement.Load(MOST_RECENT);
var xroot = XElement.Parse(xraw.ToString());
var photos = (from photo in xroot.Element("photos").
Elements("photo")
select new PhotoInfo
{
ImageUrl =
string.Format("http://farm{0}.static.flickr.com/{1}/{2}_{3}_m.jpg",
(string)photo.Attribute("farm"),
(string)photo.Attribute("server"),
(string)photo.Attribute("id"),
(string)photo.Attribute("secret"))
}).Take(Constants.ROWS * Constants.COLUMNS);
return photos.ToList<PhotoInfo>();
}
catch (Exception e)
{
Trace.WriteLine(e.Message, "ERROR");
}
return null;
}
It can seen that we are using some XLINQ to query the RSS feed and select a /// <summary>
/// A simple data class
/// </summary>
public class PhotoInfo
{
#region Data
//The url to the actual image
public string ImageUrl { get; set; }
#endregion
}
Where each Organising The Search ResultsOk so we are now failr happy with what we have, we have a search area with some search buttons, that when clicked will load some image urls via querying some RSS feeds. Thats all well and good, but we need to do something with these returned Going back a step first, recall that part of the ConductSearch() used a ThreadPool to carry out some work. Lets just remind ourselves of that //Load the images async, but assume that the loadTimer time will
//be enough to cover how long it will take to fetch and display
//all the images
ThreadPool.QueueUserWorkItem(x =>
{
var data = GetQueryResults(keyword);
//Create 3D models for the images
Dispatcher.BeginInvoke(DispatcherPriority.Normal,
((Action)delegate { CreateModelsForImages(data); }));
});
It can be seen that this in turn calls the
In essence that is all that we are trying to achieve, so I guess it's time for some code. To ensure we create the /// <summary>
/// Creates a new ModelUIElement3D for each of the <See cref="PhotoInfo">PhotoInfo</See>
/// within the input List<See cref="PhotoInfo">PhotoInfo</See>.
/// </summary>
private void CreateModelsForImages(List<PhotoInfo> photos)
{
int photoNum = 0;
IsVisible = false;
container.Children.Clear();
modelToImageLookUp.Clear();
for (int rows = 0; rows < Constants.ROWS; rows++)
{
for (int col = 0; col < Constants.COLUMNS; col++)
{
container.Children.Add(CreateModel(photos[photoNum].ImageUrl, rows, col));
photoNum++;
}
}
}
This method in turn calls the /// <summary>
/// Creates a new ModelUIElement3D child which is added to the
/// ContainerUIElement3Ds Children. The row/col parameters are used to position the
/// ModelUIElement3D is 3D space, whilst the imageUri is used to create an Image
/// for the new ModelUIElement3D child
/// </summary>
private ModelUIElement3D CreateModel(string imageUri, int row, int col)
{
//Get a VisualBrush for the Url
VisualBrush vBrush = GetVisualBrush(imageUri);
//Create the model
ModelUIElement3D model3D = new ModelUIElement3D
{
Model = new GeometryModel3D
{
Geometry = new MeshGeometry3D
{
TriangleIndices = new Int32Collection(
new int[] { 0, 1, 2, 2, 3, 0 }),
TextureCoordinates = new PointCollection(
new Point[]
{
new Point(0, 1),
new Point(1, 1),
new Point(1, 0),
new Point(0, 0)
}),
Positions = new Point3DCollection(
new Point3D[]
{
new Point3D(-0.5, -0.5, 0),
new Point3D(0.5, -0.5, 0),
new Point3D(0.5, 0.5, 0),
new Point3D(-0.5, 0.5, 0)
})
},
Material = new DiffuseMaterial
{
Brush = vBrush
},
BackMaterial = new DiffuseMaterial
{
Brush = Brushes.Black
},
Transform = CreateGroup(row, col)
}
};
//hook up mouse events, and add to lookup and return the ModelUIElement3D
model3D.MouseEnter += ModelUIElement3D_MouseEnter;
model3D.MouseDown += model3D_MouseDown;
modelToImageLookUp.Add(model3D, imageUri);
return model3D;
}
This method above is responsible for creating a new /// <summary>
/// Creates a Border with an Image where the Image.Source is the url
/// input paarmeter. This is then made into a VisualBrush which is
/// retuned for use within a ModelUIElement3D
/// </summary>
private VisualBrush GetVisualBrush(string url)
{
Border bord = new Border();
bord.Width = 15;
bord.Height = 15;
bord.CornerRadius = new CornerRadius(0);
bord.BorderThickness = new Thickness(0.5);
bord.BorderBrush = Brushes.WhiteSmoke;
try
{
Image img = new Image
{
Source = new BitmapImage(new Uri(@url, UriKind.RelativeOrAbsolute)),
Stretch = Stretch.Fill,
Margin = new Thickness(0)
};
bord.Child = img;
}
catch (Exception e)
{
Trace.WriteLine(e.Message, "ERROR");
}
VisualBrush vBrush = new VisualBrush(bord);
return vBrush;
}
Once all the User ControlsOnce all the
The user is also able to mouse over an image to rotate it in 3D space, or click it to show its associated image in a new popup window. lets look at the Pan and Zoom now PanThere is a file that holds certain constants that are used within the application, this is called ///
And here is the /// <summary>
/// Animates to a partilcular ModelUIElement3D position, uses the col input parameter
/// to work out how far to move the ContainerUIElement3D which holds all
/// the ModelUIElement3Ds
/// </summary>
private void Animate(int col)
{
double move = col * -MODEL_OFFSET;
Storyboard storyboard = new Storyboard();
ParallelTimeline timeline = new ParallelTimeline();
timeline.BeginTime = TimeSpan.FromSeconds(0);
timeline.Duration = TimeSpan.FromSeconds(2);
//do move
DoubleAnimation daMove = new DoubleAnimation(move, new Duration(TimeSpan.FromSeconds(2)));
daMove.DecelerationRatio = 1.0;
Storyboard.SetTargetName(daMove, "contTrans");
Storyboard.SetTargetProperty(daMove, new PropertyPath(TranslateTransform3D.OffsetXProperty));
//do angle
double angle = col > Constants.COLUMNS / 2 ? -15 : 15;
DoubleAnimation daAngle = new DoubleAnimation(angle, new Duration(TimeSpan.FromSeconds(0.8)));
Storyboard.SetTargetName(daAngle, "contAngle");
Storyboard.SetTargetProperty(daAngle, new PropertyPath(AxisAngleRotation3D.AngleProperty));
DoubleAnimation daAngle2 = new DoubleAnimation(0, new Duration(TimeSpan.FromSeconds(1)));
daAngle2.BeginTime = daAngle.Duration.TimeSpan;
Storyboard.SetTargetName(daAngle2, "contAngle");
Storyboard.SetTargetProperty(daAngle2, new PropertyPath(AxisAngleRotation3D.AngleProperty));
timeline.Children.Add(daMove);
timeline.Children.Add(daAngle);
timeline.Children.Add(daAngle2);
storyboard.Children.Add(timeline);
storyboard.Begin(this);
}
Like I say the Pan is done using a Standard WPF Slider control, but it has just had its default
But with Marlons special
<!-- Slider to move ContainerUIElement3D -->
<Slider x:Name="slideImages" Minimum="0" Maximum="{x:Static local:Constants.COLUMNSTOSHOW}"
Background="Black" ValueChanged="slideImages_ValueChanged" Width="200"
Height="20" Margin="10,0,0,0" >
<Slider.Template>
<ControlTemplate TargetType="Slider">
<Grid>
<Grid.Background>
<!-- Tile background to show the boxes -->
<DrawingBrush
Viewport="0,0,5,5"
ViewportUnits="Absolute"
TileMode="Tile">
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="{StaticResource backgroundBrush}">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,100,100" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="White">
<GeometryDrawing.Geometry>
<GeometryGroup>
<RectangleGeometry Rect="50,50,50,50" />
</GeometryGroup>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Grid.Background>
<Track Name="PART_Track">
<Track.Resources>
<Style TargetType="RepeatButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RepeatButton">
<Border Background="{TemplateBinding Background}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Track.Resources>
<Track.DecreaseRepeatButton>
<RepeatButton Background="Transparent"
Command="Slider.DecreaseLarge" />
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb Width="20" Background="DarkGray" Opacity="0.7" />
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton
Background="Transparent"
Command="Slider.IncreaseLarge" />
</Track.IncreaseRepeatButton>
</Track>
</Grid>
</ControlTemplate>
</Slider.Template>
</Slider>
ZoomIs also a standard WPF Slider control, whos ValueChanged method simply moves the ViewPort3D cameras Z position in or out, which simulates a zoom /// <summary>
/// Changes the embedded ViewPort3D camera position between 4-10. Simulating a zoom
/// </summary>
private void slideZoom_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
CameraPosition = e.NewValue;
}
.....
.....
/// <summary>
/// The new Camera Z position. When set aninmates the camera position to the new
/// Z position. This is a simulated Zoom
/// </summary>
public double CameraPosition
{
set
{
Point3D newPosition = new Point3D(camera.Position.X, camera.Position.Y, value);
Point3DAnimation daZoom = new Point3DAnimation(newPosition, new Duration(TimeSpan.FromSeconds(1)));
camera.BeginAnimation(PerspectiveCamera.PositionProperty, daZoom);
}
}
You can also zoom in/out using the mouse wheel, which is achieved using the following code /// <summary>
/// Handle the Mouse wheel to Zoom in and out
/// </summary>
/// <param name="e"></param>
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
double value = Math.Max(0, e.Delta / 10);//divide the value by 10 so that it is more smooth
value = Math.Min(e.Delta, Constants.COLUMNSTOSHOW);
slideZoom.Value = value;
}
I think that concludes how it all hangs together, shown below are a few areas where I felt it could be improved, and where PicLens is actually much better Saving Photos You LikeAt the request of Josh Smith, we have also allowed users to save their pictures. Once they click on an image, the user will be presented with a popup window as shown below, from where they will be able to save the picture to a location of their choice
You would be wrong if you thought this code was easy to do. It actualy took a while to get right, and is mainly contained in a class called Working with Images in WPF is not as easy as working with GDI+. ConfigurationDue to some requests to be able to not have the rotating on the individual models, we have now added the ability to either use rotation or no. This and other user settings can be set up in the App.Config file where the following key/value pairs may be used to configure MarsaX <configuration>
<appSettings>
<!-- These cant be bigger than 2 and 25 as Yahoo ImageSearch
API has a limit of 50 (so ROWS * COLS can't be > 50) -->
<add key="rows" value="2"/>
<add key="columns" value="25"/>
<!-- Set this true if you want the 3D model to flip
around on mouse over -->
<add key="should3DModelFlipOnMouseOver" value="false"/>
<!-- Saved image default location -->
<add key="savedImageLocation" value="c:\"/>
<!-- Set this true to stretch images to fill 3D model,
otherwise images will be shown at natural ratio -->
<add key="stretchImagesFor3DModels" value="true"/>
</appSettings>
</configuration>
Areas For ImprovementVideo Support PicLens also supports videos for the items within the 3D plane. Whilst obtaining an image for a vidoe is faily easy to extract using the Which is all cool, but this type of element doesn't have Mouse events, so one would also need to do hit testing directly on the Viewport3D for these types of elements. Where for images
you can use a Virtualization One of the very cool things about PicLens is that it performs some sort of Virtualization, where it only loads what is visible on the screen. The MarsaX implementation doesn't have any Virtualization. Rather what it does do is load up a set ammount of images from reading the RSS Feed mentioned above, so it's not perfect....I know, I know History
Did You Like ItCould I just ask, if you liked this article could you please vote for it, and perhaps leave a comment
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||