private void HandleTimelineWidgetTouch(Touch touch) { /* * The timeline interaction code and statemachine is a little bit complex and has several conditional checks for each widget that constraint the timeline * Each timeline has three widgets, LEFT_WIDGET, MID_WIDGET, RIGHT_WIDGET. * LEFT and RIGHT _WIDGET control the left and right zoom and operate symmetrically - the equations are nearly the same and their state machine is identical. * The constraints ensure that the L_RANGE should not be greater than 0, and the R_RANGE should not be less than 1. * This constraint ensures that the timeline range is never out of bounds, inverted (e.g. R_RANGE less than L_RANGE), etc. * The timeline works on a single touch interaction. Whenever the user slides their finger along the timeline, something should happen. * Either it should zoom or it should slide. * * Here is how the LEFT_WIDGET operates (from here on out called L_W). * In the constraint condition, when L_RANGE = 0: * (1) - if the user drags L_W to the left, the timeline should "zoom in" by moving L_RANGE to the left. * (2) - if the user drags L_W to the right, it should zoom the other way, moving R_RANGE to the right. * In the arbitrary state (L_RANGE < 0): * (3) - if the user drags L_W either left or right, timeline should zoom between L_RANGE and 1 (not between L_RANGE & R_RANGE) * in other words, if year 2000 is at the very rightmost edge of the timeline (at position 1), year 2000 should stay at that position. * This means that R_RANGE has to be moved along with L_RANGE. The trivial situation is if R_RANGE = 1, then it stays where it is. * Bug if 1 < R_RANGE, then it has to be moved to keep 2000 at position 1, following this equation that can be derived without much difficulty: * Let L & R be the old L_RANGE and R_RANGE values. _L & _R are the new L_RANGE and R_RANGE values. In this case, we are solving for _R, all others are known. * Let x = (1 - L) / (R - L) be the proportion of the timeline that is between L and 1 (the year at 1 should be clamped) * When L_RANGE moves, this proportion should stay the same, so that x = (1 - _L) / (_R - _L) giving: * _R = _L + (R - L) * (1 - _L) / (1 - L); * (4) - there is a special condition when L_RANGE starts < 0 and reaches its constraint L_RANGE = 0. * In this case, the timeline should start zooming by moving R_RANGE to the right. There is a minor bug related to (touch.X - touch.OriginX) * that causes R_RANGE to suddenly jump when this condition is reached. If we add catalogTimelineRange[L_OFFSET] to the R_RANGE, then it behaves smoothly. * * The behavior of RIGHT_WIDGET is identical to L_W. The equation in (3) is slightly different but the derivation is identical. * Condition (4) is also changed. We must add (catalogTimelineRange[R_OFFSET] - 1) to ensure continuity of L_RANGE. * * MIDDLE_WIDGET has simple behavior because it changes L_RANGE and R_RANGE by the same values causing the timeline to slide. * If L_RANGE or R_RANGE reach their constraint, they are simply clamped to either 0 or 1 but the other value continues to move, creating the situation of zooming. */ float L, R; // old L & R range values float _L, _R; // new L & R range values // Catalog widget // copy the current RANGE values, these are now the "old" values L = catalogTimelineRange[L_RANGE]; R = catalogTimelineRange[R_RANGE]; // Only LEFT_WIDGET behavior is documented, RIGHT_WIDGET has symmetrical behavior. if (touch.OriginObject == catalogWidgets[LEFT_WIDGET]) { // compute new L range _L = catalogTimelineRange[L_OFFSET] + (touch.X - touch.OriginX) / size.X; if (0 < _L) { // L_RANGE can't be greater than 0. this handles state (2) and (4) _L = 0; _R = catalogTimelineRange[R_OFFSET] + (touch.X - touch.OriginX) / size.X + catalogTimelineRange[L_OFFSET]; if (_R < 1) { // sanity check, make SURE that R_RANGE is not less than 1. _R = 1; } } else { // state (3) _R = _L + (R - L) * (1 - _L) / (1 - L); } // copy the new RANGE values to the array catalogTimelineRange[L_RANGE] = _L; catalogTimelineRange[R_RANGE] = _R; UpdateTopCurves(); } else if (touch.OriginObject == catalogWidgets[RIGHT_WIDGET]) { _R = catalogTimelineRange[R_OFFSET] + (touch.X - touch.OriginX) / size.X; if (_R < 1) { _R = 1; _L = catalogTimelineRange[L_OFFSET] + (touch.X - touch.OriginX) / size.X + (catalogTimelineRange[R_OFFSET] - 1); if (0 < _L) { _L = 0; } } else { _L = _R - _R * (R - L) / R; } catalogTimelineRange[L_RANGE] = _L; catalogTimelineRange[R_RANGE] = _R; UpdateTopCurves(); } else if (touch.OriginObject == catalogWidgets[MID_WIDGET]) { // move L_RANGE and R_RANGE by the same amount _L = catalogTimelineRange[L_OFFSET] + (touch.X - touch.OriginX) / size.X; _R = catalogTimelineRange[R_OFFSET] + (touch.X - touch.OriginX) / size.X; // check the constraint on each if (0 < _L) { _L = 0; } if (_R < 1) { _R = 1; } catalogTimelineRange[L_RANGE] = _L; catalogTimelineRange[R_RANGE] = _R; UpdateTopCurves(); } // update tick scale catalogTickSkipScale = ComputeTickScale(catalogTicks.Count / (catalogTimelineRange[R_RANGE] - catalogTimelineRange[L_RANGE]), 1, 40, 80, 200); //Console.WriteLine("C_Tick: " + catalogTickSkipScale + "\tC_R: " + catalogTimelineRange[R_RANGE] + "\tC_L: " + catalogTimelineRange[L_RANGE]); // use widgets L = useTimelineRange[L_RANGE]; R = useTimelineRange[R_RANGE]; if (touch.OriginObject == useWidgets[LEFT_WIDGET]) { _L = useTimelineRange[L_OFFSET] + (touch.X - touch.OriginX) / size.X; if (0 < _L) { _L = 0; _R = useTimelineRange[R_OFFSET] + (touch.X - touch.OriginX) / size.X + useTimelineRange[L_OFFSET]; if (_R < 1) { _R = 1; } } else { _R = _L + (R - L) * (1 - _L) / (1 - L); } useTimelineRange[L_RANGE] = _L; useTimelineRange[R_RANGE] = _R; UpdateTopCurves(); UpdateBottomCurves(); } else if (touch.OriginObject == useWidgets[RIGHT_WIDGET]) { _R = useTimelineRange[R_OFFSET] + (touch.X - touch.OriginX) / size.X; if (_R < 1) { _R = 1; _L = useTimelineRange[L_OFFSET] + (touch.X - touch.OriginX) / size.X + (useTimelineRange[R_OFFSET] - 1); if (0 < _L) { _L = 0; } } else { _L = _R - _R * (R - L) / R; } useTimelineRange[L_RANGE] = _L; useTimelineRange[R_RANGE] = _R; UpdateTopCurves(); UpdateBottomCurves(); } else if (touch.OriginObject == useWidgets[MID_WIDGET]) { _L = useTimelineRange[L_OFFSET] + (touch.X - touch.OriginX) / size.X; _R = useTimelineRange[R_OFFSET] + (touch.X - touch.OriginX) / size.X; if (0 < _L) { _L = 0; } if (_R < 1) { _R = 1; } useTimelineRange[L_RANGE] = _L; useTimelineRange[R_RANGE] = _R; UpdateTopCurves(); UpdateBottomCurves(); } useTickSkipScale = ComputeTickScale(useTicks.Count / (useTimelineRange[R_RANGE] - useTimelineRange[L_RANGE]), 5, 40, 80, 200); //Console.WriteLine("Use_T: " + useTickSkipScale + "\tUse_R: " + useTimelineRange[R_RANGE] + "\tUse_L: " + useTimelineRange[L_RANGE]); // manufacture widgets L = manufactureTimelineRange[L_RANGE]; R = manufactureTimelineRange[R_RANGE]; if (touch.OriginObject == manufactureWidgets[LEFT_WIDGET]) { _L = manufactureTimelineRange[L_OFFSET] + (touch.X - touch.OriginX) / size.X; if (0 < _L) { _L = 0; _R = manufactureTimelineRange[R_OFFSET] + (touch.X - touch.OriginX) / size.X + manufactureTimelineRange[L_OFFSET]; if (_R < 1) { _R = 1; } } else { _R = _L + (R - L) * (1 - _L) / (1 - L); } manufactureTimelineRange[L_RANGE] = _L; manufactureTimelineRange[R_RANGE] = _R; UpdateBottomCurves(); } else if (touch.OriginObject == manufactureWidgets[RIGHT_WIDGET]) { _R = manufactureTimelineRange[R_OFFSET] + (touch.X - touch.OriginX) / size.X; if (_R < 1) { _R = 1; _L = manufactureTimelineRange[L_OFFSET] + (touch.X - touch.OriginX) / size.X + (manufactureTimelineRange[R_OFFSET] - 1); if (0 < _L) { _L = 0; } } else { _L = _R - _R * (R - L) / R; } manufactureTimelineRange[L_RANGE] = _L; manufactureTimelineRange[R_RANGE] = _R; UpdateBottomCurves(); } else if (touch.OriginObject == manufactureWidgets[MID_WIDGET]) { _L = manufactureTimelineRange[L_OFFSET] + (touch.X - touch.OriginX) / size.X; _R = manufactureTimelineRange[R_OFFSET] + (touch.X - touch.OriginX) / size.X; if (0 < _L) { _L = 0; } if (_R < 1) { _R = 1; } manufactureTimelineRange[L_RANGE] = _L; manufactureTimelineRange[R_RANGE] = _R; UpdateBottomCurves(); } manufactureTickSkipScale = ComputeTickScale(manufactureTicks.Count / (manufactureTimelineRange[R_RANGE] - manufactureTimelineRange[L_RANGE]), 5, 40, 80, 200); }
void touchTarget_TouchDown(object sender, TouchEventArgs e) { lock (touchPoints) { if (touchPoints.Count == 0) { Touch touch = new Touch(e); touchPoints.Add(e.Id, touch); } /* extra safety checks if (touchPoints.ContainsKey(t.Id) == false) { touchPoints.Add(e.TouchPoint.Id, e.TouchPoint); } //*/ } }