This is the second of series of posts that explore features across different platforms such as JavaFX Script or Flex. The first post can be found here.
Code is this post is based on Silverlight 2 Beta 2.
The example
As described in the previous post:
A little program that calculates and plots a polynomial that touches four points selected by the user. The user can move any of the given points in the screen and the polynomial will be recalculated and replotted while moving it.
The Neville's algorithm is used in order to find the polynomial that touches all the given points.
Dynamic languages and Silverlight
The Silverlight web site provides documentation and examples to get you started using dynamic languages with Silverlight also the Silverlight Dynamic Languages SDK provides examples and useful code templates.
However, most of the documentation that talks about specific features of Silverlight focused in working with C#.
One of the thing that I wanted to explore was to use the data binding feature, however I didn't find a way to use it in conjunction with IronRuby classes.
John Lam's blog is a good place to find specific samples for working with IronRuby and Silverlight.
One of the benefits of working with dynamic languages and Silveright is that it's very easy to just start trying examples and experiment with it. You only need the Silverlight 2 SDK beta 2 and any text editor (no Visual Studio is required). The Chiron local web server is a very useful tool that let's you execute the Silverlight program and let's you see the changes just by just refreshing the page.
The program
The final program looks like this:
The running version of this program can be found here.
Polynomial class
The following program shows the implementation of the
Polynomial
class.
class Polynomial
def initialize(coefficients)
@coefficients = coefficients
end
def evaluate(x)
i = @coefficients.length - 1
result = 0.0
while (i >= 0)
result += (x**i)*@coefficients[i]
i = i - 1
end
return result
end
def coefficients
@coefficients
end
def add(p)
if @coefficients.length > p.coefficients.length
c1 = @coefficients
c2 = p.coefficients
else
c1 = p.coefficients
c2 = @coefficients
end
length1 = c1.length
length2 = c2.length
result = (0..length1-1).map do |i|
if i < length2
c1[i]+c2[i]
else
c1[i]
end
end
return Polynomial.new(result)
end
def add_destructive(p)
if @coefficients.length > p.coefficients.length
c1 = @coefficients
c2 = p.coefficients
else
c1 = p.coefficients
c2 = @coefficients
end
length2 = c2.length
(0..length2-1).each do |i|
c1[i] = c1[i]+c2[i]
end
return Polynomial.new(c1)
end
def multiply(p)
results = Array.new(p.coefficients.length+@coefficients.length - 1,0.0)
current_exponent = 0
p.coefficients.each do |coefficient|
if coefficient.abs > 0.000001
current_self_exponent = 0
@coefficients.each do |self_coefficient|
results[current_self_exponent+current_exponent] = (self_coefficient*coefficient) + results[current_self_exponent+current_exponent]
current_self_exponent = current_self_exponent + 1
end
end
current_exponent = current_exponent + 1
end
Polynomial.new(results)
end
end
This class contains a couple of methods for polynomial evaluation, addition and multiplication. These operations are required to implement the algorithm.
Algorithm
The following code shows the implementation of Neville's algorithm.
class NevilleCalculator
def initialize(converter)
@converter = converter
end
def create_polynomial(points)
number_of_points = points.length
matrix = (0..number_of_points-1).map {|i|(0..i).map{|j| Polynomial.new([])}}
(0..number_of_points-1).each do |i|
matrix[i][0] = Polynomial.new([@converter.convert_to_plane_y(points[i].y)])
end
xs = points.map {|point| @converter.convert_to_plane_x(point.x) }
(1..number_of_points-1).each do |i|
(1..i).each do |j|
q = xs[i]-xs[i-j]
p1 = Polynomial.new [-1.0*xs[i-j]/q,1.0/q]
p2 = Polynomial.new [xs[i]/q,-1.0/q]
matrix[i][j] = p1.multiply(matrix[i][j-1]).add_destructive(p2.multiply(matrix[i-1][j-1]))
end
end
return matrix[number_of_points-1][number_of_points-1]
end
end
The
create_polynomial
method receives an array of user selected points in the screen. The converter
object (described below) converts coordinates from the screen to the Cartesian coordinate system.Screen to plane coordinate conversion
In order to convert the coordinates the
Converter
class was used.
class PlaneToScreenConverter
def initialize(screen_max_x,screen_min_x,screen_max_y,screen_min_y,
plane_max_x,plane_min_x,plane_max_y,plane_min_y)
...
@m_to_plane_x = ((@plane_max_x - @plane_min_x)/(@screen_max_x - @screen_min_x))
@b_to_plane_x = @plane_min_x- @screen_min_x*@m_to_plane_x
@m_to_plane_y = ((@plane_max_y - @plane_min_y)/(@screen_max_y - @screen_min_y))
@b_to_plane_y = @plane_min_y - @screen_min_y *@m_to_plane_y
end
...
def convert_to_screen_x(x)
m = ((@screen_max_x - @screen_min_x)/(@plane_max_x - @plane_min_x))
b = @screen_min_x - @plane_min_x*m
return (m*x + b)
end
def convert_to_screen_y(y)
m = ((@screen_max_y - @screen_min_y)/(@plane_max_y - @plane_min_y))
b = @screen_min_y - @plane_min_y*m
return (m*y + b)
end
def convert_to_plane_x(x)
return (@m_to_plane_x*x + @b_to_plane_x)
end
def convert_to_plane_y(y)
return (@m_to_plane_y*y + @b_to_plane_y)
end
end
Also a class to represent a user selected point was created:
class ScreenPoint
def x
@x
end
def x=(v)
@x = v
end
def y
@y
end
def y=(v)
@y = v
end
def initialize(x,y)
@x = x
@y = y
end
end
For some reason I couldn't use
attr_accessor
with the version of IronRuby shipped with the Silverlight SDK 2 Beta 2.Plotting the polynomial
In order to plot the polynomial a Polyline object will be used. The
FunctionPlotter
class is used to create the list of points to create the list of Polyline
points.
class FunctionPlotter
def initialize(converter,steps,polynomial)
@converter = converter
@steps = steps
@polynomial = polynomial
@points = []
end
def points
@points
end
def polynomial
@polynomial
end
def recalculate_points
increment = ((@converter.plane_max_x - @converter.plane_min_x)/@steps).abs
current_x = [@converter.plane_max_x,@converter.plane_min_x].min
@points = (0..@steps - 1).map do
x = current_x
y = @polynomial.evaluate(x)
current_x = current_x+increment
[x,y]
end
end
end
Dragging elements
Since the user can modify the points across the screen, a way to drag shapes is required. The Implementing Drag-and-Drop Functionality with Mouse Events and Capture (Silverlight 2) article provides a complete guide on how add dragging capabilities to a Silverlight shape.
Here's the implementation of a helper class that implements this functionality.
class ElementDraggingTracker
def is_captured
return @captured
end
def initialize
@captured = false
@last_position = ScreenPoint.new(0,0)
end
def handle_mouse_down(sender,e)
current_pos = e.GetPosition(nil)
@last_position.x = current_pos.X
@last_position.y = current_pos.Y
@captured = true
sender.CaptureMouse()
end
def handle_mouse_move(sender,e)
if @captured
new_position = e.GetPosition(nil)
deltav = new_position.Y - @last_position.y
deltah = new_position.X - @last_position.x
sender.Data.Center = Point.new(sender.Data.Center.X+deltah,sender.Data.Center.Y+deltav)
@last_position.x = new_position.X
@last_position.y = new_position.Y
end
end
def handle_mouse_up(sender,e)
sender.ReleaseMouseCapture()
@captured = false
end
end
This implementation was made to work only with Path objects that have an EllipseGeometry as the Data. That's why the
Data
and Center
properties are used. This code could be improved to be independent of the object being dragged.Presenting the polynomial
In order to show a textual representation of the calculated polynomial, a sequence of TextBlock objects was used. This was necessary since I wanted to show the exponent of each term of the polynomial. The only way I found to do that was to add an horizontal StackPanel and add
TextBlock
for each term and use a TranslateTransform for the exponents. Here's the code:
def create_polynomial_text_elements(p)
first_time = false
panel = polyTextPanel
panel.Children.Clear
length = p.coefficients.length
(0..length-1).each do |pos|
i = (length - 1) - pos
c = p.coefficients[i]
exponent = i
if c.abs > 0.000001 then
text = ""
text = text + ("%.4g" % c)
text = text + "x" if exponent > 0
b = create_text_block text,"FormulaTextStyle"
panel.Children.Add b
if exponent > 1
b = create_text_block exponent.to_s,"FormulaExponentTextStyle"
t = TranslateTransform.new
t.Y = -2
b.RenderTransform = t
panel.Children.Add b
end
if !first_time and exponent != 0
b = create_text_block " + " ,"FormulaTextStyle"
panel.Children.Add b
else
first_time = false
end
end
end
end
end
The main program
The XAML for this program is very simple:
<UserControl x:Class="System.Windows.Controls.UserControl"
xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="ucontrol">
<UserControl.Resources>
...
</UserControl.Resources>
<StackPanel>
<StackPanel x:Name="polyTextPanel" Orientation="Horizontal">
</StackPanel>
<Canvas x:Name="theCanvas" Width="480" Height="480" Background="White">
<Polyline x:Name="polynomialPolyline" Style="{StaticResource PolylineStyle}"/>
<Line Style="{StaticResource AxisLineStyle}" X1="0" Y1="240" X2="480" Y2="240"/>
<Line Style="{StaticResource AxisLineStyle}" X1="240" Y1="0" X2="240" Y2="480"/>
</Canvas>
</StackPanel>
</UserControl>
The
App
class implements the entry point of the program:
require "Silverlight"
require 'screen_point'
require 'element_dragging_tracker'
require 'plane_to_screen_converter'
require 'neville_calculator'
require 'function_plotter'
require 'polynomial'
include System::Windows
include System::Windows::Controls
include System::Windows::Media
include System::Windows::Shapes
class App < SilverlightApplication
use_xaml
...
end
App.new
The
SilverlightApplication
class and the referenced Silverlight.rb
file are provided as part of a template for IronRuby/Silverlight project from the Silvelight Dynamic SDK.The
initialize
method of the App
class looks like this:
def initialize
(0..(480/24)).each do |i|
theCanvas.Children.Add create_line(i*24,0,i*24,480,"GridLineStyle")
end
(0..(480/24)).each do |i|
theCanvas.Children.Add create_line(0,i*24,480,i*24,"GridLineStyle")
end
@points = [ScreenPoint.new(100,300),
ScreenPoint.new(200,400),
ScreenPoint.new(350,300),
ScreenPoint.new(400,200)
]
@converter = PlaneToScreenConverter.new(480.0,0.0,0.0,480.0,20.0,-20.0,20.0,-20.0)
@tracker = ElementDraggingTracker.new
canvas = theCanvas
@points.each do |p|
c = create_circle p.x,p.y,10,"ControlPointStyle"
t = create_text_block "","PointTextStyle"
adjust_point_text_block(p,t)
c.mouse_left_button_down do |sender,args|
@tracker.handle_mouse_down(sender,args)
end
c.mouse_left_button_up do |sender,args|
@tracker.handle_mouse_up(sender,args)
p.x = sender.Data.Center.X
p.y = sender.Data.Center.Y
adjust_point_text_block(p,t)
replot
end
c.mouse_move do |sender,args|
@tracker.handle_mouse_move(sender,args)
if @tracker.is_captured
p.x = sender.Data.Center.X
p.y = sender.Data.Center.Y
adjust_point_text_block(p,t)
replot
end
end
canvas.Children.Add c
canvas.Children.Add t
end
replot
end
Two important things happen in this method. First the grid lines are added to the canvas and then the control points are added to the canvas and the dragging event handlers are also associated with each point. The event handlers also take care of synchronizing the update of each point's label and to recalculate and re-plot the polynomial.
The
replot
method executes the Neville's algorithm and create the new points for the Polyline
.
def replot
@calc = NevilleCalculator.new(@converter)
@plotter = FunctionPlotter.new(@converter,200,@calc.create_polynomial(@points))
@plotter.recalculate_points
pc = PointCollection.new
@plotter.points.each do |aP|
pc.Add(Point.new(@converter.convert_to_screen_x(aP[0]),
@converter.convert_to_screen_y(aP[1])))
end
polynomialPolyline.Points = pc
create_polynomial_text_elements @plotter.polynomial
end
Additional methods for creating Silverlight shapes were required:
def create_line(x1,y1,x2,y2,style_key)
p = Path.new
p.Data = LineGeometry.new
p.Data.StartPoint = Point.new x1,y1
p.Data.EndPoint = Point.new x2,y2
p.Style = ucontrol.Resources.Item(style_key.ToString())
return p
end
def create_circle(x1,y1,radius,style_key)
p = Path.new
p.Data = EllipseGeometry.new
p.Data.Center = Point.new x1,y1
p.Data.RadiusX = radius
p.Data.RadiusY = radius
p.Style = ucontrol.Resources.Item(style_key.ToString())
return p
end
def create_text_block(text,style_key)
t = TextBlock.new
t.Text = text
t.Style = ucontrol.Resources.Item(style_key.ToString())
return t
end
def adjust_point_text_block(p,t)
t.Text = "("+("%.3f" % @converter.convert_to_plane_x(p.x)) +
","+("%.3f" % @converter.convert_to_plane_y(p.y)) + ")"
Canvas.SetTop(t,p.y+5)
Canvas.SetLeft(t,p.x-5)
end
Separating the styles
Silverlight style separationprovides a way to separate style definitions for UI elements. For this program, all the style definitions are defined in a separate section of the XAML file.
<UserControl.Resources>
<Style TargetType="TextBlock" x:Key="FormulaTextStyle">
<Setter Property="FontSize" Value="13"/>
</Style>
<Style TargetType="TextBlock" x:Key="FormulaExponentTextStyle">
<Setter Property="FontSize" Value="12"/>
</Style>
<Style TargetType="TextBlock" x:Key="PointTextStyle">
<Setter Property="FontSize" Value="10"/>
</Style>
<Style TargetType="Line" x:Key="AxisLineStyle">
<Setter Property="Stroke" Value="Black" />
<Setter Property="StrokeThickness" Value="3" />
</Style>
<Style TargetType="Polyline" x:Key="PolylineStyle">
<Setter Property="Stroke" Value="Black" />
</Style>
<Style TargetType="Path" x:Key="GridLineStyle">
<Setter Property="Stroke" Value="Cyan" />
</Style>
<Style TargetType="Path" x:Key="ControlPointStyle">
<Setter Property="Fill" Value="Blue" />
</Style>
</UserControl.Resources>
In order to assign styles to elements in IronRuby the following code was used:
p.Style = ucontrol.Resources.Item(style_key.ToString())
Given that
style_key
is an IronRuby variable with a string. Note that I had to use the ToString
method. If the ToString
method as not used I received a runtime error saying: ArgumentException: key must be a string
. Deploying the application
The creation of the XAP file to be deployed is VERY simple. The following command creates an
app.xap
file from a existing app
folder. Chiron.exe /z:app.xap /d:app
The
/z
parameter adds all the libraries required for executing IronRuby.Final words
The only issue I see is the lack of documentation for using Silverlight features with scripting languages. The main thing missing is how to use data binding (if possible) which is a very nice feature which I enjoyed using in JavaFX Script for the previous post.
Because of the inclusion of the IronRuby support libraries, the final XAP size was be bigger than I expected (~700K in this case). However it was not as big as the one from the JavaFX example which included all the JavaFX runtime libraries.
For this example I had to take care of the performance. I stared using an almost direct port of the JavaFX script code of the previous post for the algorithm and the polynomial operations which lead to bad performance.
The integration of IronRuby with .NET is very nice, translating examples from C# was very easy, for example with the element dragging code described above. One thing that can be improved is to give more information on where an exception occurred in a program.
Overall the experience of working with IronRuby and Silverlight was very nice. I was very easy to get feedback of code changes just by hitting refresh in the browser.
Code for this post can be found here.
The running version of the example can be found here.