phetsims / calculus-grapher

"Calculus Grapher" is an educational simulation in HTML5, by PhET Interactive Simulations.
GNU General Public License v3.0
4 stars 4 forks source link

Address behavior of FreeForm drag. #218

Closed veillette closed 1 year ago

veillette commented 1 year ago

After adding a drag listener to the graphNode instead of the curve themselves (see #210) we will need to address the behavior of the FreeForm curve manipulation mode as it makes an assumption that the initial drag event is on the curve.

veillette commented 1 year ago

The commit above fixes the issue above by setting the penultimatePosition to null.

Therefore, for the first drag event, there is only one point, which does not lie on the curve, such that you can create segments of curve as

image

Note the single dot that results from an exceedingly short drag event.

I'll address this next week to come up with a more acceptable behavior.

veillette commented 1 year ago

Note for self: New approach to average curve based on mollifying functions.

New approach to average curve for free form ````typescript /** * Allows the user to drag Points in the Curve to any desired position to create customs but smooth shapes. * This method will update the curve with the new position value. It attempts to create a smooth curve * between position and antepenultimatePosition. * The main goal of the drawToForm method is to create a curve segment that is smooth enough that it can be * twice differentiable without generating discontinuities. * * @param position - in model coordinates * @param penultimatePosition - in model coordinates * @param antepenultimatePosition - in model coordinates */ private drawFreeformToPosition( position: Vector2, penultimatePosition: Vector2 | null, antepenultimatePosition: Vector2 | null ): void { // Closest point associated with the position const closestPoint = this.getClosestPointAt( position.x ); // Amount to shift the CurvePoint closest to the passed-in position. closestPoint.y = position.y; // Point associated with the last drag event if ( penultimatePosition ) { const lastPoint = this.getClosestPointAt( penultimatePosition.x ); // We want to create a straight line between this point and the last drag event point const closestVector = closestPoint.getVector(); this.interpolate( closestVector.x, closestVector.y, lastPoint.x, penultimatePosition.y ); } else { // There is no position associated with the last drag event. // Let's create a hill with a narrow width at the closestPoint. // See https://github.com/phetsims/calculus-grapher/issues/218 this.createHillAt( WEE_WIDTH, closestPoint.x, closestPoint.y ); } if ( penultimatePosition && antepenultimatePosition ) { const lastPoint = this.getClosestPointAt( penultimatePosition.x ); // Point associated with the last drag event const nextToLastPoint = this.getClosestPointAt( antepenultimatePosition.x ); // Checks that lastPoint is in between closestPoint and lastPoint if ( ( closestPoint.x - lastPoint.x ) * ( nextToLastPoint.x - lastPoint.x ) < 0 ) { // Finds two control points that are approximately midway between our three points const cp1Point = this.getClosestPointAt( ( position.x + penultimatePosition.x ) / 2 ); const cp2Point = this.getClosestPointAt( ( penultimatePosition.x + antepenultimatePosition.x ) / 2 ); // Check that the lastPoint is between cp1 and cp2 if ( ( cp1Point.x - lastPoint.x ) * ( cp2Point.x - lastPoint.x ) < 0 ) { // x separation between two adjacent points in a curve array const deltaX = this.deltaX; const isDescending = cp1Point.x < cp2Point.x; const p1x = isDescending ? cp1Point.x : cp2Point.x; const p2x = isDescending ? cp2Point.x : cp1Point.x; const linearOne = this.linear( closestPoint.x, position.y, lastPoint.x, penultimatePosition.y ); const linearTwo = this.linear( lastPoint.x, penultimatePosition.y, nextToLastPoint.x, antepenultimatePosition.y ); const stepFunction: MathFunction = x => { if ( isDescending ) { return ( x < penultimatePosition.x ) ? linearOne( x ) : linearTwo( x ); } else { return ( x < penultimatePosition.x ) ? linearTwo( x ) : linearOne( x ); } }; const displacement = p2x - p1x; const mollifierFunction: MathFunction = x => { const width = 0.5 * displacement; if ( Math.abs( x ) < width ) { return Math.exp( -1 / ( 1 - ( x / width ) ** 2 ) ); } else { return 0; } }; for ( let x = p1x; x < p2x; x += deltaX ) { let weight = 0; let functionWeight = 0; for ( let dx = -displacement; dx < displacement; dx += deltaX ) { weight += mollifierFunction( dx ); functionWeight += mollifierFunction( dx ) * stepFunction( x + dx ); } this.getClosestPointAt( x ).y = functionWeight / weight; } } } } } private linear( x1: number, y1: number, x2: number, y2: number ): MathFunction { assert && assert( x1 !== x2, 'linear requires different x values' ); return x => ( x - x2 ) * ( y1 - y2 ) / ( x1 - x2 ) + y2; } ````
veillette commented 1 year ago

I committed the approach above (after some clean up).

It uses a very different approach on how to smooth functions based on a mollifier. We previously used short quadratic segments, but these had the unfortunate property to becomes a bunch of piecewise constants once we take the second derivative.

A typical result using the mollifying function gives image

For reference, here is a typical case using the quadratic segment algorithm. image

veillette commented 1 year ago

Even with mollifying approach, you can see that the second derivative is a somewhat bizarre function. However, that is probably the best I can do.

veillette commented 1 year ago

I'll assign this to @amanda-phet to test and review the free-form. I suggest you used the Lab Screen with the second derivative to truly test the free-form.

amanda-phet commented 1 year ago

Let's discuss this in a meeting. I'm not sure what exactly I'd like changed, but this seems difficult to interpret.

Ex 1: This situation wasn't possible in the flash version, so it's cool we can do it now. However the result is difficult to interpret. image

Ex 2: This situation is just drawing a random-ish curve, nothing too fancy or unusual. image WIth many presses of the "smooth" button, I expected the 2nd derivative to smooth out, but it didn't: image

veillette commented 1 year ago

I should add that the presence of discontinuity points in the derivatives is symptomatic of a failure of the discontinuity detection algorithm rather than an intrinsic problem with free form drag.

amanda-phet commented 1 year ago

Thanks for clarifying @veillette . I think the free form drag is working very well.