Reactive Programming with RxJS - can this scroll function be simplified? - javascript

I'm pretty new to reactive programming (and RxJS) and all these operators are heavy to understand.
Anyway, I've successfully written this function that handles scrolling of the document while dragging something. I now wonder if this can be simplified.
Basically, onMouseDown I need to check the position of the mouse every 10ms and I need the updated clientY when the mouse moved, thats why I setup an Rx.Oberservable.interval(10) which I combine with the mouseMove observer. This will scroll the page, whether you move your mouse or not (as intended).
Here's the code:
handleWindowScrollOnDrag() {
var dragTarget = this.getDOMNode()
var scrollTarget = document.body
var wHeight = window.innerHeight
var maxScroll = document.documentElement.scrollHeight - wHeight
// Get the three major events
var mouseup = Rx.Observable.fromEvent(document, 'mouseup');
var mousemove = Rx.Observable.fromEvent(document, 'mousemove');
var mousedown = Rx.Observable.fromEvent(dragTarget, 'mousedown');
var mousedrag = mousedown.flatMap(function (md) {
var y = scrollTarget.scrollTop
var multiplier = 1
// Scroll every 10ms until mouseup when we can
var intervalSource = Rx.Observable.interval(10).takeUntil(mouseup);
// Get actual clientY until mouseup
var movement = mousemove.map(function (mm) {
return {
y: mm.clientY
};
}).takeUntil(mouseup);
return Rx.Observable
.combineLatest(movement, intervalSource, function (s1) {
multiplier = 1
if (s1.y < 100 && y >= 0) {
if (s1.y < 75) multiplier = 3;
if (s1.y < 50) multiplier = 5;
if (s1.y < 25) multiplier = 10;
if (s1.y < 15) multiplier = 20;
y -= (1 * multiplier)
}
if (s1.y > wHeight - 100 && y <= (maxScroll)) {
if (s1.y > wHeight - 75) multiplier = 3;
if (s1.y > wHeight - 50) multiplier = 5;
if (s1.y > wHeight - 25) multiplier = 10;
if (s1.y > wHeight - 15) multiplier = 20;
y += (1 * multiplier)
}
return {
y: y
};
});
});
// Update position
this.subscription = mousedrag.subscribe(function (pos) {
document.body.scrollTop = pos.y
});
},

You can remove the extra takeUntils you only need one on the movement stream, since the combineLatest operator will clean up and dispose of all the underlying subscriptions when it completes.
Next, you can remove some extra state by using .scan to manage accumulated state instead of closure variables.
You can also simplify the event body by simply passing the mouse move's y value instead of incurring the overhead of an object allocation in both the map and the combineLatest operators.
Finally, I would change to use .withLatestFrom instead of combineLatest. Since you are already essentially polling at 100 fps with interval combineLatest would emit even faster if the mouse was moving at the same time.
As a side note, while I am not terribly familiar with how DOM scrolling works (and I don't actually know how well your code works in the wild), spamming the page's scroll value faster than the actual render rate of the page seems like overkill. It might be better to lower the interval rate and use something like jQuery.animate to smooth the scrolling in between.
;tldr
handleWindowScrollOnDrag() {
var dragTarget = this.getDOMNode()
var scrollTarget = document.body;
var wHeight = window.innerHeight;
var maxScroll = document.documentElement.scrollHeight - wHeight;
// Get the three major events
var mouseup = Rx.Observable.fromEvent(document, 'mouseup');
var mousemove = Rx.Observable.fromEvent(document, 'mousemove');
var mousedown = Rx.Observable.fromEvent(dragTarget, 'mousedown');
var mousedrag = mousedown.flatMap(function (md) {
// Scroll every 10ms until mouseup when we can
var intervalSource = Rx.Observable.interval(10, Rx.Scheduler.requestAnimationFrame);
return intervalSource
.takeUntil(mouseup)
.withLatestFrom(mousemove, function (s1, s2) {
return s2.clientY;
})
.scan(scrollTarget.scrollTop, function(y, delta) {
var multiplier = 1;
if (delta < 100 && y >= 0) {
if (delta < 75) multiplier = 3;
if (delta < 50) multiplier = 5;
if (delta < 25) multiplier = 10;
if (delta < 15) multiplier = 20;
y -= (1 * multiplier);
}
if (delta > wHeight - 100 && y <= (maxScroll)) {
if (delta > wHeight - 75) multiplier = 3;
if (delta > wHeight - 50) multiplier = 5;
if (delta > wHeight - 25) multiplier = 10;
if (delta > wHeight - 15) multiplier = 20;
y += (1 * multiplier);
}
return y;
});
});
// Update position
this.subscription = mousedrag.subscribe(function (pos) {
document.body.scrollTop = pos;
});
},
Edit
A mistake in the original actually allows one to simplify the code even further. You can remove the movement stream all together (and with it an extra call to map) by passing mousemove to withLatestFrom and using that result selector to grab the clientY
Additionally I added where you would likely want to add a scheduler if you were trying to synchronize interval with the render loop of the page, though I would still say you probably want to use 15ms (60fps) rather than 10ms, it again comes down to how the scroll rendering is done.
If setting the scroll position will only be updated at the end of each render loop then it will work fine. However, if it greedily recomputes after every set then it would be better to have an intermediary like React to write to Virtual DOM first rather than flushing all changes directly to the DOM.

Related

Increment to max number and then decrement to minimum

I want to make a Javascript function that will eventually cause a div to animate by updating a calculation as the user scrolls.
My aim is to do this by incrementing a number, initially from 0 up to 20. Once 20 is the value this number needs to be recognised and then the value needs to decrement down from 20 to -20 (For example)
At the moment I have a function that will count up as I scroll down the page and then when I scroll up will count down again but I'm not sure how best to get the values to also update when the numbers reach 20 and -20 as the user scrolls.
let scrollCount = 0;
window.addEventListener("mousewheel", function(e){
if(e.wheelDelta < 0 && scrollCount < 20){
scrollCount++
}
else if(e.wheelDelta > 0 && scrollCount > -20){
scrollCount--
}
let x = scrollCount * window.innerWidth
let y = 30 * window.innerHeight
moveEye(irisLeft, x, y)
moveEye(irisRight, x, y)
console.log(scrollCount)
});
I think using PageOffset would be a better option.
var scrollTop = window.pageYOffset;
This will give you an absolute px value of scroll position. Though it is not in range [-20,20], you can scale it mathematically(min-max scaling).
Scaling Answered Here
Round off scaled values to integers and you're done.
As I said in my comment - this behavior is quire weird as once 20 is reached your div will start trembling/bouncing and will throw your user in a epileptic shock.
let scrollCount = 0;
let direction = 1;
window.addEventListener("mousewheel", function (e) {
if (e.wheelDelta < 0) {
scrollCount += direction
if (scrollCount === 20 || scrollCount === -20) {
direction *= -1
}
}
else if (e.wheelDelta > 0) {
scrollCount -= direction
if (scrollCount === 20 || scrollCount === -20) {
direction *= -1
}
}
let x = scrollCount * window.innerWidth
let y = 30 * window.innerHeight
// moveEye(irisLeft, x, y)
// moveEye(irisRight, x, y)
console.log(scrollCount)
});

How to get angle of swipe from javascript touch events

I have the following code:
swipeDetect = function(el, callback){
var touchsurface = el,
swipedir,
startX,
startY,
distX,
distY,
threshold = 7, //required min distance traveled to be considered swipe
restraint = 100, // maximum distance allowed at the same time in perpendicular direction
allowedTime = 500, // maximum time allowed to travel that distance
maxangle=40,
elapsedTime,
startTime,
touchinprocess=false,
handleswipe = callback || function(swipedir){};
touchsurface.addEventListener('touchstart', function(e){
touchinprogress = true;
var touchobj = e.changedTouches[0];
swipedir = 'none';
dist = 0;
startX = touchobj.pageX;
startY = touchobj.pageY;
startTime = new Date().getTime();
}, false);
touchsurface.addEventListener('touchmove', function(e){
var touchobj = e.changedTouches[0];
distX = startX - touchobj.pageX;
distY = startY - touchobj.pageY;
rads = Math.atan(distY,distX);
var deg = rads * (180 / 3.14);
console.log(deg);
if (Math.abs(distY) > restraint && Math.abs(deg) > maxangle) e.preventDefault();
}, false);
touchsurface.addEventListener('touchend', function(e){
var touchobj = e.changedTouches[0];
distX = touchobj.pageX - startX;
distY = touchobj.pageY - startY;
elapsedTime = new Date().getTime() - startTime;
if (elapsedTime <= allowedTime){ // first condition for awipe met
if (Math.abs(distX) >= threshold && Math.abs(distY) <= restraint){ // 2nd condition for horizontal swipe met
swipedir = (distX < 0)? 'left' : 'right'; // if dist traveled is negative, it indicates left swipe
}
else if (Math.abs(distY) >= threshold && Math.abs(distX) <= restraint){ // 2nd condition for vertical swipe met
swipedir = (distY < 0)? 'up' : 'down'; // if dist traveled is negative, it indicates up swipe
};
};
handleswipe(e,swipedir);
return true;
}, false);
},
I'm trying to get the angle in degrees of a touch swipe in the touch move function (and kill the default scroll if the swipe is above a certain number of degrees). But It keeps outputting numbers around 80. No matter how I swipe. What is wrong here?
I'm using this for image swiping and I want to let the user swipe and not have it scroll and swipe at the same time.
Maybe i'm going about this all wrong.
Thanks for your help.
The Math.atan(x)[1] function takes a single argument, which in your case should be the ratio of y over x:
rads = Math.atan(distY/distX);
It might be hard to see the difference, but in your initial code you have a , rather than a /. I ran your code with this change and it worked. Keep in mind that Math.atan2(y,x) does take two arguments.
[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan
I did some testing and I think the issue may be with your atan function. Give atan2 a try?
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan2
It seems to essentially work for me.

Three.js object moving to target

I have a object that moves to a target. The problem is that the x position is quicker than the z position or the z position is quicker than the x position.
What can I do that my object slows down for the x position if the z position needs more time to move?
My code in the animation function:
var distanceX = objectX - targetX;
var distanceZ = objectZ - targetZ;
if( distanceX < 0) {
visitor.translateX( 0.05 );
}else {
if( distanceX > 0) {
visitor.translateX( -0.05 );
}
}
if( distanceZ < 0) {
visitor.translateZ( 0.05 );
}else{
if( distanceZ > 0) {
visitor.translateZ( -0.05 );
}
}
I would recommend to compute the current value as an absolute value instead of incrementally. What you are doing is basically something like this:
x = xLast + constantValue;
Instead, you could do this without needing the last value (this is called linear interpolation):
progress = Math.min((Date.now() - startTime) / duration, 1);
x = xStart + progress * (xEnd - xStart);
The progress value in this case is a value from 0 (animation start) to 1 (animation end).
If you write your animations like this, you can just set the duration for both parts to the same value. Using the three.js math utils you can even do this all at once:
var end = new THREE.Vector3(1, 0, 2);
var start = new THREE.Vector3(2, 0, -1);
// in animation-loop
var tmp = new THREE.Vector3();
var progress = Math.min((Date.now() - startTime) / duration, 1);
// tmp = progress * (end - start)
tmp.copy(end).sub(start).multiplyScalar(progress);
object.position.copy(from).add(tmp);
That way you will have all components of the position move at a constant speed that arrives at the destination exactly at the same time.
You might want to have a look at animation-libaries like tween.js. They can handle a lot of this quite comfortably.

Realistic mouse movement coordinates in javascript?

In javascript, is there a way I can create a variable and a function that "simulates" smooth mouse movement? i.e., say the function simulates a user starts from lower left corner of the browser window, and then moves mouse in a random direction slowly...
The function would return x and y value of the next position the mouse would move each time it is called (would probably use something like setInterval to keep calling it to get the next mouse position). Movement should be restricted to the width and height of the screen, assuming the mouse never going off of it.
What I don't want is the mouse to be skipping super fast all over the place. I like smooth movements/positions being returned.
A "realistic mouse movement" doesn't mean anything without context :
Every mouse user have different behaviors with this device, and they won't even do the same gestures given what they have on their screen.
If you take an FPS game, the movements will in majority be in a small vertical range, along the whole horizontal screen.
Here is a "drip painting" I made by recording my mouse movements while playing some FPS game.
If we take the google home page however, I don't even use the mouse. The input is already focused, and I just use my keyboard.
On some infinite scrolling websites, my mouse can stay at the same position for dozens of minutes and just go to a link at some point.
I think that to get the more realistic mouse movements possible, you would have to record all your users' gestures, and repro them.
Also, a good strategy could be to get the coordinates of the elements that will attract user's cursor the more likely (like the "close" link under SO's question) and make movements go to those elements' coordinates.
Anyway, here I made a snippet which uses Math.random() and requestAnimationFrame() in order to make an object move smoothly, with some times of pausing, and variable speeds.
// Canvas is here only to show the output of function
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
document.body.appendChild(canvas);
var maxX = canvas.width = window.innerWidth;
var maxY = canvas.height = window.innerHeight;
window.onresize = function(){
maxX = canvas.width = window.innerWidth;
maxY = canvas.height = window.innerHeight;
}
gc.onclick = function(){
var coords = mouse.getCoords();
out.innerHTML = 'x : '+coords.x+'<br>y : '+coords.y;
}
var Mouse = function() {
var that = {},
size = 15,
border = size / 2,
maxSpeed = 50, // pixels per frame
maxTimePause = 5000; // ms
that.draw = function() {
if (that.paused)
return;
that.update();
// just for the example
ctx.clearRect(0, 0, canvas.width, canvas.height);
if(show.checked){
ctx.drawImage(that.img, that.x - border, that.y - border, size, size)
}
// use requestAnimationFrame for smooth update
requestAnimationFrame(that.draw);
}
that.update = function() {
// take a random position, in the same direction
that.x += Math.random() * that.speedX;
that.y += Math.random() * that.speedY;
// if we're out of bounds or the interval has passed
if (that.x <= border || that.x >= maxX - border || that.y <= 0 || that.y >= maxY - border || ++that.i > that.interval)
that.reset();
}
that.reset = function() {
that.i = 0; // reset the counter
that.interval = Math.random() * 50; // reset the interval
that.speedX = (Math.random() * (maxSpeed)) - (maxSpeed / 2); // reset the horizontal direction
that.speedY = (Math.random() * (maxSpeed)) - (maxSpeed / 2); // reset the vertical direction
// we're in one of the corner, and random returned farther out of bounds
if (that.x <= border && that.speedX < 0 || that.x >= maxX - border && that.speedX > 0)
// change the direction
that.speedX *= -1;
if (that.y <= border && that.speedY < 0 || that.y >= maxY - border && that.speedY > 0)
that.speedY *= -1;
// check if the interval was complete
if (that.x > border && that.x < maxX - border && that.y > border && that.y < maxY - border) {
if (Math.random() > .5) {
// set a pause and remove it after some time
that.paused = true;
setTimeout(function() {
that.paused = false;
that.draw();
}, (Math.random() * maxTimePause));
}
}
}
that.init = function() {
that.x = 0;
that.y = 0;
that.img = new Image();
that.img.src ="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAABJUlEQVRIic2WXbHEIAyFI6ESKgEJkVIJlYCTSqiESIiESqiEb19gL9Od3f5R5mbmPPHwBTgnIPJfChiAGbCkCQgtG7BpmgAWIALaDDyOI2bGuq40BasqIoKZATgwNAWHEEjHbkBsBhYRVJUYIwBNwVlFaVOwiDDPMylmQ1OwquY7d0CBrglYkuEeidoeOKt61I6Cq0ftKFhqR+0MOKuo2BQsInnndvnOr4JvR+0qWO5G7Q44K0XtOXDf96jqh9z9WXAy1FJ8l0qd+zbtvU7lWs7wIzkuh8SvpqqDi3zGndPQauDkzvdESm8xZvbh4mVZ7k8ud/+aR0C3YPk7mVvgkCZPVrdZV3dHVem6bju1roMPNmbAmq8kG+/ynD7ZwNsAVVz9dL0AhBrZq7F+CSQAAAAASUVORK5CYII=";
that.reset();
}
that.getCoords = function(){
return {x: that.x, y:that.y};
}
that.init()
return that;
}
var mouse = new Mouse()
mouse.draw();
html,body {margin: 0}
canvas {position: absolute; top:0; left:0;z-index:-1}
#out{font-size: 0.8em}
<label for="show">Display cursor</label><input name="show" type="checkbox" id="show" checked="true"/><br>
<button id="gc">get cursor Coords</button>
<p id="out"></p>
Last I heard the browser's mouse position cannot be altered with JavaScript, so the question really has no answer "as is". The mouse position can be locked though. I'm not certain whether it would be possible to implement a custom cursor that allows setting the position. This would include hiding and perhaps locking the stock cursor.
Having something smoothly follow the cursor is quite straight forward. You may be able to reverse this process to achieve what you need. Here's a code snippet which simply calculates the distance between the cursor and a div every frame and then moves the div 10% of that distance towards the cursor:
http://jsfiddle.net/hpp0qb0d/
var p = document.getElementById('nextmove')
var lastX,lastY,cursorX,cursorY;
window.addEventListener('mousemove', function(e){
cursorX = e.pageX;
cursorY = e.pageY;
})
setInterval(function(){
var newX = p.offsetLeft + (cursorX - lastX)/10
var newY = p.offsetTop + (cursorY - lastY)/10
p.style.left = newX+'px'
p.style.top = newY+'px'
lastX = p.offsetLeft
lastY = p.offsetTop
},20)

Circle animation when area become touched

I try to create a animation where a circle become moved by a touchevent:
function initi() {
console.log('init');
// Get a reference to our touch-sensitive element
var touchzone = document.getElementById("myCanvas");
// Add an event handler for the touchstart event
touchzone.addEventListener("mousedown", startTouch, false);
// You need mouseup to capture the stop event
touchzone.addEventListener("mouseup", stopTouch, false);
}
function startTouch(event) {
// You can access the event here too..
// But for demo I will only get the current Time
startTime = new Date().getTime();
}
function stopTouch(event) {
// Get a reference to our coordinates div
var can = document.getElementById("myCanvas");
// Write the coordinates of the touch to the div
if (event.pageX < x * 100 && event.pageY > y * 10) {
// Calculate the duration, only if needed
var duration = new Date().getTime() - startTime;
bally -= 0.005 * duration; // use duration
}
else if ( event.pageX > x * 100) {
}
// I hope bally if defined in the outer scope somewhere ;)
console.log(event, x, bally);
draw();
}
So my question is how can I make the bally smaller when this area: event.pageX < x * 100 && event.pageY > y * 10 become touched, after bally -= 0.005 * duration; become executed? I try it with arrays to save if its touched and then give it to update function and know that localstorage and cookies would work too but I think thats not the best solution. Any hints to realize that?
see fiddle

Categories