How can I simulate a hanging cable in WPF? - c#

I have an application that is very "connection-based", i.e. multiple inputs/outputs.
The UI concept of a "cable" is exactly what I'm looking for to make the concept clear to the user. Propellerhead took a similar approach in their Reason software for audio components, illustrated in this YouTube video (fast forward to 2m:50s).
I can make this concept work in GDI by painting a spline from point A to point B, there's got to be a more elegant way to use Paths or something in WPF for this, but where do you start? Is there a good way to simulate the animation of the cable swing when you grab it and shake it?
I'm also open to control libraries (commercial or open source) if this wheel has already been invented for WPF.
Update: Thanks to the links in the answers so far, I'm almost there.
I've created a BezierCurve programmatically, with Point 1 being (0, 0), Point 2 being the bottom "hang" point, and Point 3 being wherever the mouse cursor is. I've created a PointAnimation for Point 2 with an ElasticEase easing function applied to it to give the "Swinging" effect (i.e., bouncing the middle point around a bit).
Only problem is, the animation seems to run a little late. I'm starting the Storyboard each time the mouse moves, is there a better way to do this animation? My solution so far is located here:
Bezier Curve Playground
Code:
private Path _path = null;
private BezierSegment _bs = null;
private PathFigure _pFigure = null;
private Storyboard _sb = null;
private PointAnimation _paPoint2 = null;
ElasticEase _eEase = null;
private void cvCanvas_MouseMove(object sender, MouseEventArgs e)
{
var position = e.GetPosition(cvCanvas);
AdjustPath(position.X, position.Y);
}
// basic idea: when mouse moves, call AdjustPath and draw line from (0,0) to mouse position with a "hang" in the middle
private void AdjustPath(double x, double y)
{
if (_path == null)
{
_path = new Path();
_path.Stroke = new SolidColorBrush(Colors.Blue);
_path.StrokeThickness = 2;
cvCanvas.Children.Add(_path);
_bs = new BezierSegment(new Point(0, 0), new Point(0, 0), new Point(0, 0), true);
PathSegmentCollection psCollection = new PathSegmentCollection();
psCollection.Add(_bs);
_pFigure = new PathFigure();
_pFigure.Segments = psCollection;
_pFigure.StartPoint = new Point(0, 0);
PathFigureCollection pfCollection = new PathFigureCollection();
pfCollection.Add(_pFigure);
PathGeometry pathGeometry = new PathGeometry();
pathGeometry.Figures = pfCollection;
_path.Data = pathGeometry;
}
double bottomOfCurveX = ((x / 2));
double bottomOfCurveY = (y + (x * 1.25));
_bs.Point3 = new Point(x, y);
if (_sb == null)
{
_paPoint2 = new PointAnimation();
_paPoint2.From = _bs.Point2;
_paPoint2.To = new Point(bottomOfCurveX, bottomOfCurveY);
_paPoint2.Duration = new Duration(TimeSpan.FromMilliseconds(1000));
_eEase = new ElasticEase();
_paPoint2.EasingFunction = _eEase;
_sb = new Storyboard();
Storyboard.SetTarget(_paPoint2, _path);
Storyboard.SetTargetProperty(_paPoint2, new PropertyPath("Data.Figures[0].Segments[0].Point2"));
_sb.Children.Add(_paPoint2);
_sb.Begin(this);
}
_paPoint2.From = _bs.Point2;
_paPoint2.To = new Point(bottomOfCurveX, bottomOfCurveY);
_sb.Begin(this);
}

If you want true dynamic motion (ie, when you "shake" the mouse pointer you can create waves that travel along the cord), you will need to use finite element techniques. However if you are ok with static behavior you can simply use Bezier curves.
First I'll briefly describe the finite element approach, then go into more detail on the static approach.
Dynamic approach
Divide your "cord" into a large number (1000 or so) "elements", each with a position and velocity Vector. Use the CompositionTarget.Rendering event to compute each element position as follows:
Compute the pull on each element along the "cord" from adjacent elements, which is proportional to the distance between elements. Assume the cord itself is massless.
Compute the net force vector on each "element" which consists of the pull from each adjacent element along the cord, plus the constant force of gravity.
Use a mass constant to convert the force vector to accelaration, and update the position and velocity using the equations of motion.
Draw the line using a StreamGeometry build with a BeginFigure followed by a PolyLineTo. With so many points there is little reason to do the extra computations to create a cubic bezier approximation.
Static approach
Divide your cord into perhaps 30 segments, each of which is a cubic bezier approximation to the catenary y = a cosh(x/a). Your end control points should be on the catenary curve, the parallels should tangent to the catenaries, and the control line lengths set based on the second derivative of the catenary.
In this case you will probably also want to render a StreamGeometry, using BeginFigure and PolyBezierTo to build it.
I would implement this as a custom Shape subclass "Catenary" similar to Rectangle and Ellipse. In that case, all you have to override the DefiningGeometry property. For efficiency I would also override CacheDefiningGeometry, GetDefiningGeometryBounds, and GetNaturalSize.
You would first decide how to parameterize your catenary, then add DependencyProperties for all your parameters. Make sure you set the AffectsMeasure and AffectsRender flags in your FrameworkPropertyMetadata.
One possible parameterization would be XOffset, YOffset, Length. Another might be XOffset, YOffset, SagRelativeToWidth. It would depend on what would be easiest to bind to.
Once your DependencyProperties are defined, implement your DefiningGeometry property to compute the cubic bezier control points, construct the StreamGeometry, and return it.
If you do this, you can drop a Catenary control anywhere and get a catenary curve.

User bezier curve segments in a path.
http://www.c-sharpcorner.com/UploadFile/dbeniwal321/WPFBezier01302009015211AM/WPFBezier.aspx

IMHO 'hanging' (physically simulated) cables are a case of over-doing it - favouring looks over usability.
Are you sure you're not just cluttering the user-experience ?
In a node/connection-based UI I find clear connections (like in Quartz Composer : http://ellington.tvu.ac.uk/ma/wp-content/uploads/2006/05/images/Quartz%20Composer_screenshot_011.png ) way more important than eye-candy like swinging cables that head in a different direction (down due to gravity) than where the actually connection-point is. (And in the mean time eat up CPU-cycles for the simulation that could be more useful elsewhere)
Just my $0.02

Related

How to animate using Path with Storyboard in C#

So, I am trying to move my rectangular boxes around a grid like this -
For this, I am using a Story board.
I am using DoubleAnimation to move the boxes on X-Axis and Y-Axis in one of my Class. I am calling this class from MainWindow class. But, for every box, and for every turn, I have to create a new Double animation, assign the offset values, the start time, the duration etc. like this -
//Code to move Boxes 1-4 to first grid point in their path
TranslateTransform moveTransform = new TranslateTransform();
moveTransform.X = 0;
moveTransform.Y = 0;
x.RenderTransform = moveTransform;
Storyboard s = new Storyboard();
DoubleAnimation Move1= new DoubleAnimation();
Move1.From = 0;
Move1.To = xPosition; // calculate correct offset here
Move1.Duration = new Duration(TimeSpan.FromSeconds(hops));
if (x==Box2)
{
Move1.BeginTime = TimeSpan.FromSeconds(5);//For Box 2, the first move will be across Y-Axis and hence X-Axis move will be delayed by 5 seconds.
}
else
{
Move1.BeginTime = TimeSpan.Zero;
}
Storyboard.SetTarget(Move1, x);
Storyboard.SetTargetProperty(Move1, new PropertyPath("(UIElement.RenderTransform).(X)"));
s.Children.Add(Move1);
I know there is a way to define the path to reach the destination from source, but I am not sure how to do it? Also, I am not sure if what I am doing here is the optimal way.
So, my question is -
What is the better way to do this? How can we define paths for animation?
I am a newbie to C# so please do not mind if this sounds silly.
Thank you!
I was able to do it without using path.
For every move, I created a new double animation.
So for Left - Down - Right movement, I created 3 double animations with
X-Axis, Y-Axis and X-Axis again.

WPF Path's Geometry Transform performance gets worse with LineGeometries' length increase

I'm currently trying to create a little plot interactive editor, using WPF.
On maximized window the plot dragging with mouse is not responsive enough because of the plot grid.
I got a path for my plot grid lying inside a Canvas control (render transform just shifts it to the bottom of the canvas)
<Path Name="VisualGrid" RenderTransform="{StaticResource PlotTechnicalAdjust}" Style="{DynamicResource ResourceKey=GridStyle}" Panel.ZIndex="1"/>
Here is how grid is created; _curState has actual camera "viewport" metadata
if (_curState.Changes.ScaleStepXChanged)
{
foreach (TextBlock item in _xLabels)
{
DeleteLabel(item);
}
_xLabels.Clear();
double i = _curState.LeftEdgeLine;
_gridGeom.Children[(int)GridGeomIndexes.VerticalLines] = new GeometryGroup { Transform = _verticalLinesShift};
var verticalLines =(GeometryGroup)_gridGeom.Children[(int)GridGeomIndexes.VerticalLines];
while (i <= _curState.RightEdgeLine * (1.001))
{
verticalLines.Children.Add(new LineGeometry(new Point(i * _plotParameters.PixelsPerOneX, 0),
new Point(i * _plotParameters.PixelsPerOneX,
-_wnd.ContainerGeneral.Height)));
_xLabels.Add(CreateLabel(i, Axis.X));
i += _curState.CurrentScaleStepX;
}
_curState.Changes.ScaleStepXChanged = false;
}
if (_curState.Changes.ScaleStepYChanged)
{
foreach (TextBlock item in _yLabels)
{
DeleteLabel(item);
}
_yLabels.Clear();
double i = _curState.BottomEdgeLine;
_gridGeom.Children[(int)GridGeomIndexes.HorizontalLines] = new GeometryGroup { Transform = _horizontalLinesShift};
var horizontalLines = (GeometryGroup)_gridGeom.Children[(int)GridGeomIndexes.HorizontalLines];
while (i <= _curState.TopEdgeLine * (1.001))
{
horizontalLines.Children.Add(new LineGeometry(new Point(0, -i * _plotParameters.PixelsPerOneY),
new Point(_wnd.ContainerGeneral.Width,
-i * _plotParameters.PixelsPerOneY)));
_yLabels.Add(CreateLabel(i, Axis.Y));
i += _curState.CurrentScaleStepY;
}
_curState.Changes.ScaleStepYChanged = false;
}
Where Transforms are composition of TranslateTransform and ScaleTransform (for vertical lines I only use X components and only Y for horizontal lines).
After beeing created those GeometryGroups are only edited if a new line apears into camera or an existing line exits viewable space. Grid is only recreated when axis graduations have to be changed after zooming.
I have a dragging option implemented like this:
private Point _cursorOldPos = new Point();
private void OnDragPlotMouseMove(object sender, MouseEventArgs e)
{
if (e.Handled)
return;
Point cursorNewPos = e.GetPosition(ContainerGeneral);
_plotView.TranslateShiftX.X += cursorNewPos.X - _cursorOldPos.X;
_plotView.TranslateShiftY.Y += cursorNewPos.Y - _cursorOldPos.Y;
_cursorOldPos = cursorNewPos;
e.Handled = true;
}
This works perfectly smooth with a small window (1200x400 units) for a large amount of points (like 100+).
But for a large window (fullscreen 1920x1080) it happens pretty jittery even without any data-point controls on canvas.
The strange moment is that lags don't appear when I order my GridGenerator to keep around 100+ lines for small window and drag performance suffers when I got less than 50 lines on maximezed. It makes me think that it might somehow depend not on a number of elements inside a geometry, but on their linear size.
I suppose I should mention that OnSizeChanged I adjust the ContainerGeneral canvas' height and width and simply re-create the grid.
Checked the number of lines stored in runtime to make sure I don't have any extras. Tried using Image with DrawingVisual instead of Path. Nothing helped.
Appearances for clearer understanding
It was all about stroke dashes and WPF's unhealthy desire to count them all while getting hit test bounds for DrawingContext.
The related topic is Why does use of pens with dash patterns cause huge (!) performance degredation in WPF custom 2D drawing?

WPF c# Drawing Thick curve with Lines or Alternative

I'm currently plotting XY data on a canvas and drawing a curve with it. So far it is simple and working for a thin line but when I increase the thickness a peculiar effect happens due to how the lines are drawn to form a curve.
I've attached an example image that shows a nice smooth line that works fine when the line is thin. But when the line is thicker you can obviously see the problem.
Is there a way to connect these endpoints to make a nice smooth line?
If not, is there another drawing tool that is useful in creating a nice line?
I'm not happy about the implementation as is because quickly the canvas becomes cluttered by hundreds if not thousands of line objects on the Canvas. This seems like an awful way of doing this but I haven't found a better way as of yet. I'd much rather go with another route that would create a single curve object.
Any help is appreciated as always.
Thanks!
Point previousPoint;
public void DrawLineToBox(DrawLineAction theDrawAction, Point drawPoint)
{
Line myLine = new Line();
myLine.Stroke = new SolidColorBrush(Color.FromArgb(255, 0, 0, 0));
myLine.StrokeThickness = 29;
if(theDrawAction == DrawLineAction.KeepDrawing)
{
myLine.X1 = previousPoint.X; //draw from this point
myLine.Y1 = previousPoint.Y;
}
else if(theDrawAction == DrawLineAction.StartDrawing)
{
myLine.X1 = drawPoint.X; //draw from same point
myLine.Y1 = drawPoint.Y;
}
myLine.X2 = drawPoint.X; //draw to this point
myLine.Y2 = drawPoint.Y;
canvasToDrawOn.Children.Add(myLine); //add to canvas
previousPoint.X = drawPoint.X; //set current point as last point
previousPoint.Y = drawPoint.Y;
}
Try adding the following two lines:
myLine.StrokeStartLineCap = PenLineCap.Round;
myLine.StrokeEndLineCap = PenLineCap.Round;
Also, you really should use the Polylne or Path object to do what you are currently doing. Personally, I always set StrokeStartLineCap and StrokeEndLineCap to PenLineCap.Round and StrokeLineJoin to PenLineJoin.Round for the Polyline objects I used.

WPF DrawingContext draw behind my objects

I have a canvas with some images, and i'm also using DrawGeometry, to draw a circle that is filling when the time is passing.
This is how i draw the circle in my DrawingContext:
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
MyUtils.RenderProgressClock(drawingContext, clockPosition, 50, gameTime / totalTime);
}
And calling the InvalidateVisual(); to call it.
But Doing this my circle is behind my images and i cant see it, how can i draw it infront of them?
Im totally new to WPF and its giving me a hard Time....
This is the other method code as requested:
private static PathGeometry GetClockGeometry(Point position, double percentage, double radius)
{
const double innerFactor = 0.90;
double innerRadius = radius * innerFactor;
PathGeometry pie = new PathGeometry();
PathFigure pathFigure = new PathFigure();
pathFigure.StartPoint = new Point(0, -innerRadius);
pathFigure.IsClosed = true;
if (percentage > kMaxClockPercentage)
{
percentage = kMaxClockPercentage;
}
double angle = 360.0 * percentage;
// Starting Point
LineSegment inOutLine = new LineSegment(new Point(0, -radius), true);
// Arc
ArcSegment outerArc = new ArcSegment();
outerArc.IsLargeArc = angle >= 180.0;
outerArc.Point = new Point(Math.Cos((angle - 90) * Math.PI / 180.0) * radius, Math.Sin((angle - 90) * Math.PI / 180.0) * radius);
outerArc.Size = new Size(radius, radius);
outerArc.SweepDirection = SweepDirection.Clockwise;
LineSegment outInLine = new LineSegment(new Point(outerArc.Point.X * innerFactor, outerArc.Point.Y * innerFactor), true);
ArcSegment innerArc = new ArcSegment();
innerArc.IsLargeArc = angle >= 180.0;
innerArc.Point = pathFigure.StartPoint;
innerArc.Size = new Size(innerRadius, innerRadius);
innerArc.SweepDirection = SweepDirection.Counterclockwise;
pathFigure.Segments.Add(inOutLine);
pathFigure.Segments.Add(outerArc);
pathFigure.Segments.Add(outInLine);
pathFigure.Segments.Add(innerArc);
pie.Transform = new TranslateTransform(position.X, position.Y);
pie.Figures.Add(pathFigure);
return pie;
}
OK, now that I understand a little better what is going on, I see that my initial answer won't directly work for you. However, I also see that you have a bit of a problem.
Just the general nature of the way OnRender works means that what you draw is always going to end up behind the images and whatnot that you add to the window.
Add to that the fact that you're putting all this drawing code for a specific feature (the progress clock) into the window itself, and this solution feels a little off.
You might want to explore some alternatives.
A simple one would be to create a UserControl to draw the Clock. That UserControl could have a DependencyProperty for the % that it should be filled. You could use your (roughly) same OnRender code in the UserControl or you could do it some other fancy ways (I'm sure there's some way to do it in all XAML, though I don't know it off the top of my head). Then you just put that clock into the window like all your other images/controls.
You could also do it creating a CustomControl, though that takes a little bit more knowledge about WPF and Resources and whatnot to understand how it works. Since you're new to WPF, that might be a bit much right now.
You need to show us how your circle is added to the Canvas.
WPF is a retained drawing system, so the order the controls appear in it's visual-tree dictates their stacking order.. OnRender() really means AccumulateDrawingObjects() as it doesn't directly draw, it just creates a set of objects to draw.
Also, you don't need to InvalidateVisual() if an object is staying the same size, as it causes a very expensive re-layout.
More efficient ways to re-render are to use a DependencyProperty with AffectsRender... Or to create a DrawingGroup, add it to the DrawingContext during OnRender(), then anytime later you can DrawingGroup.Open() to change the drawing commands in the DrawingGroup.

WPF TranslateTransform

Im trying to use the TranslateTransform class to move a image on a Grid on Y axis. I need this movment to be smooth so i can not use SetMargin or SetCanvas. I try this in code behind:
public void MoveTo(Image target, double oldY, double newY)
{
var trans = new TranslateTransform();
var anim2 = new DoubleAnimation(0, newY, TimeSpan.FromSeconds(2))
{EasingFunction = new SineEase()};
target.RenderTransform = trans;
trans.BeginAnimation(TranslateTransform.YProperty, anim2);
}
The object i want to use (a Image control) is placed on a Grid.
For the first time everything works fine.
The problems comes when i try to move the object again using the same function.
The object (a Image control) first move to the start position (initial Y coordinate) then the animation begins.
Is it not suposed for the TranslateTransform to change the coordinates (in my case the Margin property) too?
Thank you.
The transform does not change the original values.they are your point of origin. If you want a new point of origin each time you move you can handle the animation completed event. Or from your transform you can get your current offset and make that your new start point for the animation.
In other words your start values would always be your last move to values
The TranslateTransform is a specific kind of render transformation. Rather that changing properties of the control (such as the Margin property), it simply affects how the control is displayed on the screen.
You have to use the By property of DoubleAnimation.
Try that:
//everytime you execute this anmation your object will be moved 2.0 further
double offset = 2.0
var anim2 = new DoubleAnimation(newY, TimeSpan.FromSeconds(2));
anim2.To = null;
anim2.By = offset;
You've explicitly told the animation to start from 0. It's doing what you've told it.
Just remove the explicit zero fromvalue and everything will work.
var anim2 = new DoubleAnimation(newY, TimeSpan.FromSeconds(2))
{ EasingFunction = new SineEase() };

Categories

Resources