Stabilize touch events coordinates on touchmove - javascript

I'm working on a small application that allows users to draw freehand in 3d with three.js (the result is somewhat similar to Blender's grease pencil). The application works in any browser but I'm struggling with my main target platform: the iPad with the Apple pencil.
Compared to a mouse, the Apple pencil fires a very high amount of touchmove events and that causes my lines to be "jagged" in some scenarios, especially when the line is drawn slowly.
Here's a jsfiddle that shows the functionality. The only thing I'm doing here is this:
//convert coordinate to three.js space
x = (x / window.innerWidth) * 2 - 1;
y = -(y / window.innerHeight) * 2 + 1;
var vNow = new THREE.Vector3(x, y, 0);
//unproject the coordinates based on the camera
vNow.unproject(camera);
//push a new line into the line geometry
line.geometry.vertices.push(vNow);
//re-render the line at every mousemove
setGeometry()
If you try it with a mouse, the result are pretty okay. But once you switch to Apple pencil… this is what I tend to get 👇🏻 the lines are mostly "jagged"
I tried many things, but nothing satisfied me yet.
I tried generating curves from some or all of the coordinates and re-rendering the line on touchend but I do not like the effect and controlling the quality of the output is very difficult.
I tried simplifying the line with simplify.js and while close to the desired result it produces inconsistent results.
So, I thought that instead of manipulating the output (the line) I could manipulate the input (the points) and stabilise them before drawing. What would be the best approach to "stabilize" mouse events, given an array of points? I googled left and right but did not find a good answer. Most solutions for drawing smooth curves are for the canvas, which is very different from 3d geometry.
Thanks!

Related

how to detect minimum rotation in a JavaScript gesture library (eg alloyfinger.js)

I have a mobile web app written in JavaScript, using alloyfinger.js.
I tried Hammer.js, but it didn't work with certain iPhone models (eg iPhone 7+).
I suspect my question is the same for various gesture detection libraries.
My app detects rotate events, but I can't seem to write the correct code to ignore rotate events below a minimum angle of rotation. The code below hides the rotated element, even when the amount of rotation is very small.
handleRotatEvent(evt) {
const angle = evt.angle;
const absAngle = Math.abs(angle);
const minAngle = (90 * Math.PI / 180);
// convert 90 degrees to radians
if (absAngle < minAngle)
return;
hideItem(evt);
// use the event to find the target DOM element and hide it
}
My intent is to ignore rotations smaller than 90 degrees. Otherwise, hide the rotated element.
What am I misunderstanding or doing wrong here?
Thanks!
Adam Leffert
https://www.leffert.com
#dntzhang, the author of AlloyFinger.js answered this question on Twitter. He said that the angle resets as the user rotates the element. My more fundamental intent was to detect small rotations as a way to distinguish rotate events from swipe or pan events. He said that the correct way to do that is to check the number of fingers touching the element. One for swipe or pan. Two for rotate. Much simpler.

How to get the position of object in Three.js relative to the camera to draw a trail?

I have an object (a pen) in my scene, which is rotating around its axis in the render loop.
groupPen.rotation.y += speed;
groupPen.rotation.x += speed;
and I have also a TrackballControls, which allows the user to rotate the whole scene.
What I now want is to get the "real" position of the pen (or its pick) and place small spheres to create a trail behind it.
This means I need to know where the camera is looking at and place the trail spheres behind the peak of the pen and exclude them from the animation and the TrackballControls.
What I tried is:
groupSphereTrail.lookAt(camera.position);
didn't work. Means no reaction at all.
camera.add(groupSphereTrail);
didn't work. groupSphereTrail is than not in the view area, couldn't make it visible - manipulating position.z didn't help.
Then I tried something like sending a tray with traycaster. The idea was to send a ray from the center of the camera through the peak of the pen and then draw the trail there. But then I still doesn't have the "real" position.
Another idea was to create a 2d vector of the current position of the pen peak and just draw an html element on top of the canvas:
var p = penPeak.position.clone();
var vector = p.project(camera);
vector.x = (vector.x + 1) / 2 * width;
vector.y = -(vector.y - 1) / 2 * height;
but this also doesn't work.
What could be another working solution?
Current progress:
https://zhaw.swissmade.xyz
(click on the cap of the pen to see the writing - this writing trail should stay at its place when you rotate the camera)
If i understood the question right, you want to show the trail as if it were draw on the screen itself (screen space)?
yourTrailParticle.position.project(camera)
camera.add(yourTrailParticle)
That's the basic idea, but it gets a bit tricky with PerspectiveCamera. You could set up a whole new THREE.Scene to hold the trail, and render it with a fixed size orthographic camera.
The point is .project() will give you a normalized screen space coordinate of a world space vector, and you need to keep it somehow in sync with that camera (since the screen is too). The perspective camera has distortion so you need to figure out the appropriate distance to map the coordinate to. With a separate scene, this may become easier.

Get started with animated typography/particles in javascript (mapping particles to a word)?

Alright, so I have a good deal of experience with HTML and CSS, and some experience with Javascript (I can write basic functions and have coded in similar languages).
I'm looking to start some visual projects and am specifically interested in getting into particle systems. I have an idea for something similar to Codecademy's name generator here (https://www.codecademy.com/courses/animate-your-name/0/1) where particles are mapped to a word and move if hovered over. It seems as though alphabet.js is what's really behind Codecademy's demo however I can't understand exactly how they mapped the particles to a word, etc.
I've done some basic tutorials just creating rudimentary particles in a canvas but I'm not sure a canvas is the best way to go - demos that utilize one of the many libraries available (such as http://soulwire.github.io/sketch.js/examples/particles.html) don't use a canvas.
So my question is - what is the best way for a beginner/intermediate in Javascript to start with particle systems? Specifically to accomplish the Codecademy name effect or similar? Should I try to use canvas or which library would be best to start with and how would you recommend starting?
The code for this project is achievable for your intermediate JS programmer status.
How the CodeAcademy project works ...
Start by building each letter out of circles and saving each circle's centerpoint in an array. The alphabet.js script holds that array of circle centerpoints.
On mousemove events, test which circles are within a specified radius of the mouse position. Then animate each of those discovered circles radially outward from the mouse position using simple trigonometry.
When the mouse moves again, test which circles are no longer within the specified radius of the current mouse position. Then animate each of those "outside" circles back towards their original positions.
You can also use native html5 canvas without any libraries...
Another approach allowing any text to be "dissolved" and reassembled
Start by drawing the text on the canvas. BTW, this approach will "dissolve" any drawing, not just text.
Use context.getImageData to fetch the opacity value of every pixel on the canvas. Determine which pixels on the canvas contain parts of the text. You can tell if a pixel is part of the text because it will be opaque rather than transparent.
Now do the same procedure that CodeAcademy did with their circles -- but use your pixels:
On mousemove events, test which pixels are within a specified radius of the mouse position. Then animate each of those discovered pixels radially outward from the mouse position using simple trigonometry.
When the mouse moves again, test which pixels are no longer within the specified radius of the current mouse position. Then animate each of those "outside" pixels back towards their original positions.
[Addition: mousemove event to test if circles are within mouse distance]
Note: You probably want to keep an animation frame running that moves circles closer or further from their original positions based on a flag (isInside) for each circle.
function handleMouseMove(e){
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// calc the current mouse position
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
// test each circle to see if it's inside or outside
// radius of 40px to current mouse position
// circles[] is an array of circle objects shaped like this
// {x:,y:,r:,originalX:,originalY:,isInside:}
var radius=40;
for(var i=0;i<circles.length;i++){
var c=circles[i];
var dx=c.x-mouseX;
var dy=c.y-mouseY;
if(dx*dx+dy*dy<radius*radius){
c.isInside=true;
// move c.x & c.y away from its originalX & originalY
}else{
c.isInside=false;
// if the circle is not already back at it's originalX, originalY
// then move c.x & c.y back towards its originalX, originalY
}
}
}

How to calculate the index of the tile underneath the mouse in an isometric world taking into account tile elevation

I have a tile-based isometric world and I can calculate which tile is underneath specific (mouse) coordinates by using the following calculations:
function isoTo2D(pt:Point):Point{
var tempPt:Point = new Point(0, 0);
tempPt.x = (2 * pt.y + pt.x) / 2;
tempPt.y = (2 * pt.y - pt.x) / 2;
return(tempPt);
}
function getTileCoordinates(pt:Point, tileHeight:Number):Point{
var tempPt:Point = new Point(0, 0);
tempPt.x = Math.floor(pt.x / tileHeight);
tempPt.y = Math.floor(pt.y / tileHeight);
return(tempPt);
}
(Taken from http://gamedevelopment.tutsplus.com/tutorials/creating-isometric-worlds-a-primer-for-game-developers--gamedev-6511, this is a flash implementation but the maths is the same)
However, my problem comes in when I have tiles that have different elevation levels:
In these scenarios, due to the height of some tiles which have a higher elevation, the tiles (or portions of tiles) behind are covered up and shouldn't be able to be selected by the mouse, instead selecting the tile which is in front of it.
How can I calculate the tile by mouse coordinates taking into account the tiles' elevation?
I'm using a javascript and canvas implementation.
There is a technique of capturing object under the mouse on a canvas without needing to recalculate mouse coordinates into your "world" coordinates. This is not perfect, has some drawbacks and restrictions, yet it does it's job in some simple cases.
1) Position another canvas atop of your main canvas and set it's opacity to 0. Make sure your second canvas has the same size and overlaps your main one.
2) Whenever you draw your interactive objects to the main canvas, draw and fill the same objects on the second canvas, but using one unique color per object (from #000000 to #ffffff)
3) Set mouse event handling to the second canvas.
4) Use getPixel on the second canvas at mouse position to get the "id" of the object clicked/hovered over.
Main advantage is WYSIWYG principle, so (if everything is done properly) you can be sure, that objects on the main canvas are in the same place as on the second canvas, so you don't need to worry about canvas resizing or object depth (like in your case) calculations to get the right object.
Main drawback is need to "double-render" the whole scene, yet it can be optimized by not drawing on the second canvas when it's not necessary, like:
in "idling" scene state, when interactive objects are staying on their places and wait for user action.
in "locked" scene state, when some stuff is animated or smth. and user is not allowed to interact with objects.
Main restriction is a maximum number of interactive objects on the scene (up to #ffffff or 16777215 objects).
So... Not reccomended for:
Games with big amount of interactive objects on a scene. (bad performance)
Fast-paced games, where interactive objects are constantly moved/created/destroyed.(bad performance, issues with re-using id's)
Good for:
GUI's handling
Turn-based games / slow-paced puzzle games.
Your hit test function will need to have access to all your tiles in order to determine which one is hit. It will then perform test hits starting with the tallest elevation.
Assuming that you only have discreet (integer) tile heights, the general algorithm would be like this (pseudo code, assuming that tiles is a two-dimensional array of object with an elevation property):
function getTile(mousePt, tiles) {
var maxElevation = getMaxElevation(tiles);
var minElevation = getMinElevation(tiles);
var elevation;
for (elevation = maxElevation; elevation >= minElevation; elevation--) {
var pt = getTileCoordinates(mousePt, elevation);
if (tiles[pt.x][pt.y].elevation === elevation) {
return pt;
}
}
return null; // not tile hit
}
This code would need to be adjusted for arbitrary elevations and could be optimized to skip elevation that don't contain any tiles.
Note that my pseudocode ignores vertical sides of a tile and clicks on them will select the (lower elevation) tile obscured by the vertical side. If vertical tiles need to be accounted for, then a more generic surface hit detection approach will be needed. You could visit every tile (from closest to farthest away) and test whether the mouse coordinates are in the "roof" or in one of the viewer facing "wall" polygons.
If map is not rotatable and exatly same as picture you posted here,
When you are drawing polygons, save each tile's polygon(s) in a polygon array. Then sort the array only once using distance of them(their tile) to you(closest first, farthest last) while keeping them grouped by tile index.
When click event happens, get x,y coordinates of mouse, and do point in polygon test starting from first element array until last element. When hit, stop at that element.
No matter how high a tile is, will not hide any tile that is closer to you(or even same distance to you).
Point in polygon test is already solved:
Point in Polygon Algorithm
How can I determine whether a 2D Point is within a Polygon?
Point in polygon
You can even check every pixel of canvas once with this function and save results into an 2d array of points, vect2[x][y] which gives i,j indexes of tiles from x,y coordinates of mouse, then use this as a very fast index finder.
Pros:
fast and parallelizable using webworkers(if there are millions of tiles)
scalable to multiple isometric maps using arrays of arrays of polygons sorted by distance to you.
Elevation doesnt decrease performance because of only 3 per tile maximum.
Doesn't need any conversion to isometric to 2d. Just the coordinates of corners of polygons on canvas and coordinates of mouse on the same canvas.
Cons:
You need coordinates of each corner if you haven't already.
Clicking a corner will pick closest tile to you while it is on four tiles at the same time.
The answer, oddly, is written up in the Wikipedia page, in the section titled "Mapping Screen to World Coordinates". Rather than try to describe the graphics, just read the section three times.
You will need to determine exactly which isomorphic projection you are using, often by measuring the tile size on the screen with a ruler.

CreateJS - scaling the canvas does not scale the mouse coordinates

I am working on a big project where exercises in Canvas are created through JSON-data and CreateJS. The purpose of having it in HTML 5 is to not have to use a separate app for your phone, you can always use the website.
Everything works fine, however in mobile the Canvas is rescaled to full screen. This is done through checking the screen size, and if it's small enough to be mobile the canvas is scaled through this code:
// browser viewport size
var w = window.innerWidth;
var h = window.innerHeight;
// stage dimensions
var ow = canvasWidth;
var oh = canvasHeight;
// keep aspect ratio
var scale = Math.min(w / ow, h / oh);
stage.scaleX = scale;
stage.scaleY = scale;
// adjust canvas size
stage.canvas.width = ow * scale;
stage.canvas.height = oh * scale;
This works great for most of the exercises, like quizzes and such, where all you have to do is click on a button. However we also have some drag and drop-exercises, and an exercise where you can color a drawing. These of course rely on the mouse coordinates to work properly. The problem is, when the canvas is scaled the mouse coordinates are not. So when you drag an item or try to draw, there is an offset happening. So your drawing appears way left of your click, and when picking up a draggable object it doesn't quite follow your click correctly.
Had I made the code from the beginning I'm fairly sure how I would have recalculated the coordinates, but since they are calculated by CreateJS I don't really know how I should go about this.
This was reported as a problem by someone about a year ago here, where this solution was suggested:
I was able to work around this by adding a top-level container and attaching my Bitmaps to that and scaling it.
The whole exercise is inside a container which I have tried to scale but to no avail. I have also tried sending the scale as a parameter to the parts of the exercise created (for example the menu, background images and such) and not scale it all together, and it seems to work okay since then I can exclude the drawing layer. But since it is a large project and many different exercises and parts to be scaled it would take quite some time to implement, and I'm not sure it's a viable solution.
Is there a good and easy way to rescale the mouse coordinates along with the canvas size in CreateJS? I have found pure Javascript examples here on SO, but nothing for CreateJS in particular.
Continued searching and finally stumbled upon this, which I hadn't seen before:
EaselJS - dragging children of scaled parent. It was exactly what I was looking for. I needed to change the coordinates I drew with this:
var coords = e.target.globalToLocal(e.stageX, e.stageY);
Then I could use the coords.x and coords.y instead of directly using e.stageX and e.stageY like before.

Categories