JSXGraph - Animation of Polygon slow and rough - javascript

In JSXGraph I graph a plate (polygon) with 4 forces (arrows) acting on it. I intend to use the code for a variable number of forces (that's why it is more general than necessary for just 4 forces).
Force components can be shown and hidden. When a force arrow (not passing through the pivot point) is dragged, the "drag" event triggers an animation:
1st: showing the force and its arm, waiting for 2s
2nd: rotate the polygon in the direction of the force component (either clockwise or anti-clockwise).
Problem 1: The animation is slow and rough. I tried to use "suspendUpdate" and "unsuspendUpdate", but it made things even worse. Probably, I did not use it at the right place.
Problem 2: (probably related) I did not find a good way to clone the polygon for the animation. Or would it be best to store its coordinates? But if so, how would I reset the polygon (plus force arrow) to its original position after the rotation/animation?
My complete code is here: https://jsfiddle.net/bujowi/j1etgLsp/3/
You find the function called for the animation from line 244 onwards:
function animateRotation(x1, y1, x2, y2, sign)
Any comments and suggestions for improvements are welcome.

In the jsfiddle the animation appears to be rough since the the drag event is fired multiple times. Therefore, the animation runs in parallel multiple times. In the code below this is prevented by the flag is_animated. If true no new animation can be started.
A minor, more JSXGraph-ish issue is that the transformation is added to the JSXGraph elements in every step of the animation in setInterval. This may produce strange effects. It is much more clean to bind the transformation once to an element, but make the transformation dynamic, e.g. dependent on an angle variable:
var angle = 0.03;
let tRot = b1.create('transform', [function() { return sign * angle; }, pivot], { type: 'rotate' });
Additionally, this would make it possible to rotate the polygon back to the zero angle at the end of the animation. So, in setInterval only the angle variable is increased and the board is updated:
let idInterval = setInterval(function() {
angle += 0.03;
b1.update();
// ...
}, refreshTime);
Here is the complete new animation code (without rotating back at the end):
function animateRotation(x1, y1, x2, y2, sign) {
if (this.is_animated) {
return;
}
this.is_animated = true;
// Force arrow and arm (distance from pivot)
let Rot1 = b1.create('point', [x1, y1], { visible: false })
let Rot2 = b1.create('point', [x2, y2], { visible: false })
let rotArrow = b1.create('arrow', [Rot1, Rot2], { strokeColor: '#000', strokeWidth: 3, visible: true })
// Polygon
let PolyCopy = []
for (i = 0; i < poly_coord.length; i++) {
PolyCopy.push(b1.create('point', [poly_coord[i][0], poly_coord[i][1]], at))
}
let polyCopy = b1.create('polygon', PolyCopy, { strokeColor: myBlue, fillColor: myBlue })
// Hide all other force arrows
showAllElements(false)
// Show line of action and distance from pivot for 1s
let lineOfAction = b1.create('line', [[x1, y1], [x2, y2]], { strokeColor: '#000', strokeWidth: 2, dash: 1 })
let perpLine = b1.create('perpendicular', [lineOfAction, Pivot], { withLabel: false, visible: false })
let isecP = b1.create('intersection', [perpLine, lineOfAction, 0], { withLabel: false, visible: false })
let dist = b1.create('segment', [Pivot, isecP], { strokeColor: '#000', strokeWidth: 4 })
var that = this; // make "this" available in the interval function
// This is the animation: rotating the polygon for how many 'frames', refreshTime in ms
setTimeout(function() {
b1.removeObject(lineOfAction)
b1.removeObject(perpLine)
b1.removeObject(isecP)
b1.removeObject(dist)
var angle = 0.03;
let tRot = b1.create('transform', [function() { return sign * angle; }, pivot], { type: 'rotate' });
tRot.bindTo(PolyCopy);
tRot.bindTo([Rot1, Rot2]);
let frames = 0;
const refreshTime = 50;
let idInterval = setInterval(function() {
angle += 0.03;
b1.update();
if (frames++ > 20) {
that.is_animated = false;
b1.removeObject(polyCopy);
b1.removeObject(rotArrow);
showAllElements(true);
clearInterval(idInterval);
}
}, refreshTime)
}, 1000); // time for showing the line of action and distance
}
See it live at https://jsfiddle.net/fbdzx0ht/3/
Best wishes,
Alfred

Related

Is it possible to access the exact xaxis position of the cursor in a highcharts tooltip?

I'm working on a highcharts solution which includes several different graph types in one single chart. Is it possible to have the exact time of mouse position displayed in the tooltip instead of a calculated range? (We're using highstock together with the boost and xrange module)
Also i need to always show the newest value of a series left from cursor position. Due to having an xRange Series i needed to refresh the tooltip with my own implementation since stickytracking doesnt work with xrange.
But right now the display of the green temperature series constantly switches from the actual value to the first value in the series (e.g when hovering around March 11th it constantly changes from 19.85°C back to 19.68°C which is the very first entry)
So i'm having 2 issues:
displaying the exact time in tooltip
displaying specific values in tooltip
I guess both could be solved with having the exact x position of cursor and for displaying the values i guess i did somewhat the right thing already with refreshing the tooltip on mousemove. Still the values won't always display properly.
I understand that Highcharts makes a best guess on the tooltip time value by displaying the range but to me it seems like it orients itself around the xRange Series.
I already tries to tinker around with the plotoptions.series.stickyTracking and tooltip.snap values but this doesn't really help me at all.
I understand too that this.x in tooltip formatter function will be bound to the closest point. Still i need it to use the current mouse position.
In a first attempt i was filtering through the series in the tooltip formatter itself before i changed to calculating the points on the mousemove event. But there i also couldn't get the right values since x was a rough estimate anyways.
Is there any solution to that?
At the moment i'm using following function onMouseMove to refresh the tooltip:
chart.container.addEventListener('mousemove', function(e) {
const xValue = chart.xAxis[0].toValue(chart.pointer.normalize(e).chartX);
const points = [];
chart.series.filter(s => s.type === "xrange").forEach(s => {
s.points.forEach(p => {
const { x, x2 } = p;
if (xValue >= x && xValue <= x2) points.push(p);
})
})
chart.series.filter(s => s.type !== "xrange").forEach(s => {
const point = s.points.reverse().find(p => p.x <= xValue);
if(point) points.push(point);
})
if (points.length) chart.tooltip.refresh(points, chart.pointer.normalize(e));
})
also i'm using this tooltip configuration and formatter:
tooltip: {
shared: true,
followPointer: true,
backgroundColor: "#FFF",
borderColor: "#AAAAAA",
borderRadius: 5,
shadow: false,
useHTML: true,
formatter: function(){
const header = createHeader(this.x)
return `
<table>
${header}
</table>
`
}
},
const createHeader = x => {
const headerFormat = '%d.%m.%Y, %H:%M Uhr';
const dateWithOffSet = x - new Date(x).getTimezoneOffset() * 60 * 1000;
return `<tr><th colspan="2" style="text-align: left;">${Highcharts.dateFormat(headerFormat, dateWithOffSet)}</th></tr>`
}
See following jsFiddle for my current state (just remove formatter function to see the second issue in action): jsFiddle
(including the boost module throws a script error in jsFiddle. Don't know if this is important so i disabled it for now)
finally found a solution to have access to mouse position in my tooltip:
extending Highcharts with a module (kudos to Torstein Hønsi):
(function(H) {
H.Tooltip.prototype.getAnchor = function(points, mouseEvent) {
var ret,
chart = this.chart,
inverted = chart.inverted,
plotTop = chart.plotTop,
plotLeft = chart.plotLeft,
plotX = 0,
plotY = 0,
yAxis,
xAxis;
points = H.splat(points);
// Pie uses a special tooltipPos
ret = points[0].tooltipPos;
// When tooltip follows mouse, relate the position to the mouse
if (this.followPointer && mouseEvent) {
if (mouseEvent.chartX === undefined) {
mouseEvent = chart.pointer.normalize(mouseEvent);
}
ret = [
mouseEvent.chartX - chart.plotLeft,
mouseEvent.chartY - plotTop
];
}
// When shared, use the average position
if (!ret) {
H.each(points, function(point) {
yAxis = point.series.yAxis;
xAxis = point.series.xAxis;
plotX += point.plotX + (!inverted && xAxis ? xAxis.left - plotLeft : 0);
plotY += (point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY) +
(!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151
});
plotX /= points.length;
plotY /= points.length;
ret = [
inverted ? chart.plotWidth - plotY : plotX,
this.shared && !inverted && points.length > 1 && mouseEvent ?
mouseEvent.chartY - plotTop : // place shared tooltip next to the mouse (#424)
inverted ? chart.plotHeight - plotX : plotY
];
}
// Add your event to Tooltip instances
this.event = mouseEvent;
return H.map(ret, Math.round);
}
})(Highcharts)
http://jsfiddle.net/2h951hdj/
Also you can wrap dragStart on the pointer and get exactly mouse position, in this case when you click on the chart area you will have the mouse position on the x-axis.
(function(H) {
H.wrap(H.Pointer.prototype, 'dragStart', function(proceed, e) {
let chart = this.chart;
chart.mouseIsDown = e.type;
chart.cancelClick = false;
chart.mouseDownX = this.mouseDownX = e.chartX;
chart.mouseDownY = this.mouseDownY = e.chartY;
chart.isZoomedByDrag = true;
console.log(chart.mouseDownX);
});
}(Highcharts));
Highcharts.chart('container', {
chart: {
events: {
load: function() {
let chart = this,
tooltip = chart.tooltip;
console.log(tooltip);
}
}
},
series: [{
data: [2, 5, 2, 3, 6, 5]
}],
});
Live demo: https://jsfiddle.net/BlackLabel/1b8rf9hc/

Leaflet.js wrap LatLng positions of line to nearest line

I want to have the user click two positions and then draw a line between them. This is totally functional and easy. And it also works if the click is outside the -180;180 range.
createPolyLine(outside2, userLocation);
//draw polyline
function createPolyLine(loc1, loc2) {
var latlongs = [loc1, loc2];
console.log(loc1);
var polyline = new L.Polyline(latlongs, {
color: 'white',
opacity: 1,
weight: 1,
clickable: false
}).addTo(map);
}
Now If a user clicks say on longitude of -600 I want to have it automatically wrap into the -180;180 area but only if its the closest path. If its closer to keep the click in the -360:-180 area then it should wrap into the -360;-180 area. Same for the other side in positive direction ofcourse.
Example image of what i mean:
Example of when its closer to use -360;-180 region and not wrap it into -180;180
Example of when its closer to use -180;180 and it would be wrong now
Based on second example but correctly wrapped now
What would be the most efficient way to achieve this auto wrap into either -360;-180 / -180;180 / 180;360 depending on where the closest line between two points would be?
Thanks to the response of Corey Alix I came up with a solution which feels kinda dirty but it works for all the test cases I tried against. In my scenario one point is always in -180;180 range.
// a is always in -180;180
function getPositions(a, b) {
b.lng %= 360;
let abs = Math.abs(a.lng - b.lng);
let absWrap = Math.abs(a.lng - b.wrap().lng);
if(absWrap < abs) {
b = b.wrap();
abs = absWrap;
}
let left = new L.LatLng(b.lat, b.lng - 360);
let right = new L.LatLng(b.lat, b.lng + 360);
let leftAbs = Math.abs(a.lng - left.lng);
if(leftAbs < abs) {
b = left;
abs = leftAbs;
}
if(Math.abs(a.lng - right.lng) < abs) {
b = right;
}
return [a, b];
}
//draw polyline
function createPolyLine(loc1, loc2) {
var latlongs = getPositions(loc2, loc1);
polyline = new L.Polyline(latlongs, {
color: 'white',
opacity: 1,
weight: 1,
clickable: false
}).addTo(map);
}

Paper.js - How to set the duration of drawing vector or segment?

I need to set vectors(or segments) related to some routes, starting from point A and ending to point B in a certain time. Like a trip for example. Unfortunly I can't find how to set the time of drawing passing a value:
<script src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.12.7/paper-core.js"></script>
var point1 = new Point(0, 0);
var point2 = new Point(110, 200);
var x = point2.x - point1.x;
// = 110 - 50 = 60
var y = point2.y - point1.y;
// = 200 - 50 = 150;
var vector = point2 - point1;
// Create a Paper.js Path to draw a line into it:
var path = new Path();
// Give the stroke a color
path.strokeColor = 'red';
var start = vector;
function onFrame(event) {
if (event.count < 101) {
path.add(start);
start += new Point(1, 1);
}
}
If I understand your case well, here is a sketch demonstrating a possible implementation.
The idea is to keep a reference path from which we calculate a temporary path on each frame, in order to produce the animation.
The advantage of this solution is that you can apply it to any kind of path.
// Create a path to animate.
const path = new Path.Circle({
center: view.center,
radius: 50,
selected: true,
closed: false
});
// Initialize the time variable that will control the animation.
let time = 0;
// On each frame...
function onFrame() {
// ...if the animation is not finished yet...
if (time <= 1) {
// ...animate.
time += 0.01;
drawTmpPath(time);
}
}
// Initialize the temporary path that will display our animation.
let tmpPath;
function drawTmpPath(t) {
// Make sure that t is never over 1.
t = Math.min(t, 1);
// Remove the previously drawn temporary path.
if (tmpPath) {
tmpPath.remove();
}
// Draw the new temporary path from the reference one.
tmpPath = path.clone().set({
selected: false,
strokeColor: 'orange',
strokeWidth: 5
});
// Split it at the appropriate location.
const remainingPath = tmpPath.splitAt(tmpPath.length * t);
// Remove the eventual remaining part.
if (remainingPath) {
remainingPath.remove();
}
}
// Scale things up.
project.activeLayer.fitBounds(view.bounds.scale(0.8));
Edit
In answer to your comment, in order to have control over the animation time, you can store your animation starting time and, on each frame, calculate the relative time that the update function needs from the current time.
Here is a sketch demonstrating this as an extension of the above example.
Note that you could also rely on an external animation library like GreenSock to handle the timing more easily.
// Total animation time in milliseconds.
const totalTime = 10000;
// Create a path to animate.
const path = new Path.Circle({
center: view.center,
radius: 50,
selected: true,
closed: false
});
// Initialize the time variable that will control the animation.
const startTime = Date.now();
let animationDone = false;
// On each frame...
function onFrame() {
// ...if the animation is not finished yet...
if (!animationDone) {
// ...calculate the relative time needed to draw the tmp path.
const relativeTime = (Date.now() - startTime) / totalTime;
// ...animate.
if (relativeTime < 1) {
drawTmpPath(relativeTime);
} else {
drawTmpPath(1);
animationDone = true;
}
}
}
// Initialize the temporary path that will display our animation.
let tmpPath;
function drawTmpPath(t) {
// Make sure that t is never over 1.
t = Math.min(t, 1);
// Remove the previously drawn temporary path.
if (tmpPath) {
tmpPath.remove();
}
// Draw the new temporary path from the reference one.
tmpPath = path.clone().set({
selected: false,
strokeColor: 'orange',
strokeWidth: 5
});
// Split it at the appropriate location.
const remainingPath = tmpPath.splitAt(tmpPath.length * t);
// Remove the eventual remaining part.
if (remainingPath) {
remainingPath.remove();
}
}
// Scale things up.
project.activeLayer.fitBounds(view.bounds.scale(0.8));

Famo.us Balls Drag and Drop, set velocity when released

I'm trying to achieve something similar to an air hockey table effect with Famo.us. The idea is to have multiple circle bodies that can collide (see battle).
Right now my first concern is getting the ball's vector attributes to zero out on drag start.
I'm trying to use the 'reset()' method from Particle.reset, but am running into tons of issues. Here's some of the code from the codepen I have so far:
ball.draggable.on("start", function(pos) {
var zeroV = new Vector(0, 0, 0);
// need to remove force here
ball.circle.reset(pos.position, zeroV, ball.circle.orientation, zeroV);
});
Any idea how to best zero out the force on the ball once I begin a drag? Also how might I determine velocity on release relative to how fast the user is dragging before release?
The answer to both of your questions lie in the adding and removing a body particle from the physics engine in Famo.us.
Here is example code: jsBin code
Note: This example does not solve your whole solution, but does answer your questions and should help you to get to your desired effect.
Any idea how to best zero out the force on the ball once I begin a drag?
Rather than zero out the force, you will detach the particle from the engine temporarily.
physicsEngine.removeBody(this.particle);
In the example, I am doing this on click of a created circle surface.
ball.particle = new Circle({
radius: radius,
position: [x, y, 0]
});
ball.physicsID = physicsEngine.addBody(ball.particle);
physicsEngine.attach(collision, balls, ball.particle);
ball.on('click', function(){
if (!this.stopped) {
physicsEngine.removeBody(this.particle);
} else {
this.physicsID = physicsEngine.addBody(this.particle);
physicsEngine.attach(collision, balls, this.particle);
balls.push(this.particle);
}
console.log('balls', balls);
this.stopped = !this.stopped;
});
How might I determine velocity on release relative to how fast the user is dragging before release?
When you drag the square surface and on('end'... you pass the velocity to the creation of your particle. You use the velocity from the drag end to start your particle in motion with setVelocity.
ball.particle.setVelocity(velocity);
As you can see in the example code:
sync.on('end', function(data){
if (!surface.createdBall && data.velocity){
var velocity = data.velocity;
surface.createdBall = true;
var endX = position[0] + 0;
var endY = position[1] + 0;
createBall(endX, endY, velocity);
}
});
...
function createBall(x, y, velocity) {
var ball = new Surface({
size: [radius * 2, radius * 2],
properties: {
backgroundColor: 'blue',
borderRadius: (radius * 2) + 'px'
}
});
ball.particle = new Circle({
radius: radius,
position: [x, y, 0]
});
ball.physicsID = physicsEngine.addBody(ball.particle);
physicsEngine.attach(collision, balls, ball.particle);
node.add(ball.particle).add(ball);
balls.push(ball.particle);
console.log('created ball', velocity);
ball.particle.setVelocity(velocity);
surface.createdBall = false;
ball.on('click', function(){
if (!this.stopped) {
physicsEngine.removeBody(this.particle);
} else {
this.physicsID = physicsEngine.addBody(this.particle);
physicsEngine.attach(collision, balls, this.particle);
balls.push(this.particle);
}
console.log('balls', balls);
this.stopped = !this.stopped;
});
}

how to make countdown timer rotate for each second?

I want to change this jsfiddle jsfiddle. It uses jQueryCountDown (http://plugins.jquery.com/countdown360/)
It currently rotates an outer ring, by one increment every second, but this shows as an anti-clockwise rotation of the ring as it counts downs.
I want the countdown to still rotate each second, but the direction of the outer ring rotation should be clockwise (currently the ring rotates anti-clockwise). Please see the example:
Example code:
var countdown = $("#countdown").countdown360({
radius: 60,
seconds: 20,
label: ['sec', 'secs'],
fontColor: '#FFFFFF',
autostart: false,
onComplete: function () {
console.log('done');
}
});
countdown.start();
$('#countdown').click(function() {
countdown.extendTimer(2);
});
That plugin is hard-coded to rotate anti-clockwise. I updated the original plugin so that it now has an option to have clockwise: true.
The new demo looks like this:
This example has one running clockwise and the second running normally (anti-clockwise):
JSFiddle: http://jsfiddle.net/TrueBlueAussie/gs3WY/246/
In the defaults I added clockwise:
defaults = {
...[snip]...
clockwise: false
};
In the start method I made it conditionally draw the "stroke" (the outer ring):
start: function () {
...[snip]...
this._drawCountdownShape(Math.PI * 3.5, !this.settings.clockwise);
...[snip]...
},
and in the _draw method I added a conditional calculation of the angle. Also when the timer ends, it conditionally draws the outer ring again based on the clockwise flag (previously it always redrew the entire outer ring before rendering the changes):
_draw: function () {
var secondsElapsed = Math.round((new Date().getTime() - this.startedAt.getTime()) / 1000);
if (this.settings.clockwise) {
var endAngle = (((Math.PI * 2) / this.settings.seconds) * secondsElapsed) - (Math.PI * .5);
} else {
var endAngle = (Math.PI * 3.5) - (((Math.PI * 2) / this.settings.seconds) * secondsElapsed);
}
this._clearRect();
if (secondsElapsed < this.settings.seconds) {
this._drawCountdownShape(Math.PI * 3.5, false);
this._drawCountdownShape(endAngle, true);
this._drawCountdownLabel(secondsElapsed);
} else {
this._drawCountdownShape(Math.PI * 3.5, this.settings.clockwise);
this._drawCountdownLabel(this.settings.seconds);
this.stop();
this.settings.onComplete();
}
}
You simply add a clockwise: true when you create the countdown:
var countdown = $("#countdown").countdown360({
radius: 60,
seconds: 20,
label: ['sec', 'secs'],
fontColor: '#FFFFFF',
autostart: false,
clockwise: true, // <<<<< Added this
onComplete: function () {
console.log('done');
}
});

Categories

Resources