public void AdvanceWithSample(StrokeSample incomingSample) { var sampleAfter = SampleAfter; if (sampleAfter != null) { SampleBefore = FromSample; FromSample = ToSample; ToSample = SampleAfter; SampleAfter = incomingSample; FromSampleIndex += 1; } }
public void Update(StrokeSample sample, int index) { if (index == 0) { hasUpdatesFromStartTo = 0; } else if (hasUpdatesFromStartTo.HasValue && index == hasUpdatesFromStartTo.Value + 1) { hasUpdatesFromStartTo = index; } else if (!hasUpdatesAtEndFrom.HasValue || hasUpdatesAtEndFrom.Value > index) { hasUpdatesAtEndFrom = index; } Samples [index] = sample; sampleIndicesExpectingUpdates.Remove(index); if (sampleIndicesExpectingUpdates.Count == 0) { ReceivedAllNeededUpdatesBlock?.Invoke(); ReceivedAllNeededUpdatesBlock = null; } }
public int Add(StrokeSample sample) { var resultIndex = Samples.Count; if (!hasUpdatesAtEndFrom.HasValue) { hasUpdatesAtEndFrom = resultIndex; } Samples.Add(sample); if (PreviousPredictedSamples.Count == 0) { PreviousPredictedSamples.AddRange(PredictedSamples); } if (sample.EstimatedPropertiesExpectingUpdates != 0) { sampleIndicesExpectingUpdates.Add(resultIndex); } PredictedSamples.Clear(); return(resultIndex); }
public void AddPredicted(StrokeSample sample) { PredictedSamples.Add(sample); }
public StrokeSegment(StrokeSample sample) { SampleAfter = sample; FromSampleIndex = -2; }
// Note: this is not a particularily efficient way to draw a great stroke path // with CoreGraphics.It is just a way to produce an interesting looking result. // For a real world example you would reuse and cache CGPaths and draw longer // paths instead of an aweful lot of tiny ones, etc.You would also respect the // draw rect to cull your draw requests.And you would use bezier paths to // interpolate between the points to get a smooother curve. void Draw(Stroke stroke) { var updateRanges = stroke.UpdatedRanges(); if (DisplayOptions == StrokeViewDisplayOptions.Debug) { for (int index = 0; index < DirtyRectViews.Count; index++) { var dirtyRectView = DirtyRectViews [index]; dirtyRectView.Alpha = 0; if (index < updateRanges.Length) { dirtyRectView.Alpha = 1; var range = updateRanges [index]; var strokes = stroke.Samples.Skip(range.LowerBound) .Take(range.UpperBound - range.LowerBound + 1); dirtyRectView.Frame = DirtyRectForSampleStride(strokes); } } } lastEstimatedSample = null; stroke.ClearUpdateInfo(); var sampleCount = stroke.Samples.Count; if (sampleCount <= 0) { return; } var context = UIGraphics.GetCurrentContext(); if (context == null) { return; } var strokeColor = UIColor.Black; Action lineSettings; Action forceEstimatedLineSettings; if (displayOptions == StrokeViewDisplayOptions.Debug) { lineSettings = () => { context.SetLineWidth(0.5f); context.SetStrokeColor(UIColor.White.CGColor); }; forceEstimatedLineSettings = () => { context.SetLineWidth(0.5f); context.SetStrokeColor(UIColor.Blue.CGColor); }; } else { lineSettings = () => { context.SetLineWidth(0.25f); context.SetStrokeColor(strokeColor.CGColor); }; forceEstimatedLineSettings = lineSettings; } Action azimuthSettings = () => { context.SetLineWidth(1.5f); context.SetStrokeColor(UIColor.Orange.CGColor); }; Action altitudeSettings = () => { context.SetLineWidth(0.5f); context.SetStrokeColor(strokeColor.CGColor); }; var forceMultiplier = 2f; var forceOffset = 0.1f; var fillColorRegular = UIColor.Black.CGColor; var fillColorCoalesced = UIColor.LightGray.CGColor; var fillColorPredicted = UIColor.Red.CGColor; CGVector?lockedAzimuthUnitVector = null; var azimuthLockAltitudeThreshold = NMath.PI / 2 * 0.8f; // locking azimuth at 80% altitude lineSettings(); Func <StrokeSample, nfloat> forceAccessBlock = sample => { return(sample.ForceWithDefault()); }; if (DisplayOptions == StrokeViewDisplayOptions.Ink) { forceAccessBlock = sample => { return(sample.PerpendicularForce()); }; } // Make the force influence less pronounced for the calligraphy pen. if (DisplayOptions == StrokeViewDisplayOptions.Calligraphy) { var prevGetter = forceAccessBlock; forceAccessBlock = sample => { return(NMath.Max(prevGetter(sample), 1)); }; // make force value less pronounced forceMultiplier = 1; forceOffset = 10; } var previousGetter = forceAccessBlock; forceAccessBlock = sample => { return(previousGetter(sample) * forceMultiplier + forceOffset); }; StrokeSample heldFromSample = null; CGVector? heldFromSampleUnitVector = null; Action <StrokeSegment> draw = segment => { var toSample = segment.ToSample; if (toSample != null) { StrokeSample fromSample = heldFromSample ?? segment.FromSample; // Skip line segments that are too short. var dist = Vector(fromSample.Location, toSample.Location).Quadrance(); if (dist < 0.003f) { if (heldFromSample == null) { heldFromSample = fromSample; heldFromSampleUnitVector = segment.FromSampleUnitNormal; } return; } if (toSample.Predicted) { if (displayOptions == StrokeViewDisplayOptions.Debug) { context.SetFillColor(fillColorPredicted); } } else { bool coalesced = displayOptions == StrokeViewDisplayOptions.Debug && fromSample.Coalesced; context.SetFillColor(coalesced ? fillColorCoalesced : fillColorRegular); } if (displayOptions == StrokeViewDisplayOptions.Calligraphy) { var fromAzimuthUnitVector = Stroke.CalligraphyFallbackAzimuthUnitVector; var toAzimuthUnitVector = Stroke.CalligraphyFallbackAzimuthUnitVector; if (fromSample.Azimuth.HasValue) { if (!lockedAzimuthUnitVector.HasValue) { lockedAzimuthUnitVector = fromSample.GetAzimuthUnitVector(); } fromAzimuthUnitVector = fromSample.GetAzimuthUnitVector(); toAzimuthUnitVector = toSample.GetAzimuthUnitVector(); if (fromSample.Altitude.Value > azimuthLockAltitudeThreshold) { fromAzimuthUnitVector = lockedAzimuthUnitVector.Value; } if (toSample.Altitude.Value > azimuthLockAltitudeThreshold) { toAzimuthUnitVector = lockedAzimuthUnitVector.Value; } else { lockedAzimuthUnitVector = toAzimuthUnitVector; } } // Rotate 90 degrees var calligraphyTransform = CGAffineTransform.MakeRotation(NMath.PI / 2); fromAzimuthUnitVector = fromAzimuthUnitVector.Apply(calligraphyTransform); toAzimuthUnitVector = toAzimuthUnitVector.Apply(calligraphyTransform); var fromUnitVector = fromAzimuthUnitVector.Mult(forceAccessBlock(fromSample)); var toUnitVector = toAzimuthUnitVector.Mult(forceAccessBlock(toSample)); context.BeginPath(); context.Move(fromSample.Location.Add(fromUnitVector)); context.AddLine(toSample.Location.Add(toUnitVector)); context.AddLine(toSample.Location.Sub(toUnitVector)); context.AddLine(fromSample.Location.Sub(fromUnitVector)); context.ClosePath(); context.DrawPath(CGPathDrawingMode.FillStroke); } else { var fromUnitVector = (heldFromSampleUnitVector.HasValue ? heldFromSampleUnitVector.Value : segment.FromSampleUnitNormal).Mult(forceAccessBlock(fromSample)); var toUnitVector = segment.ToSampleUnitNormal.Mult(forceAccessBlock(toSample)); var isForceEstimated = fromSample.EstimatedProperties.HasFlag(UITouchProperties.Force) || toSample.EstimatedProperties.HasFlag(UITouchProperties.Force); if (isForceEstimated) { if (lastEstimatedSample == null) { lastEstimatedSample = new EstimatedSample { Index = segment.FromSampleIndex + 1, Sample = toSample } } ; forceEstimatedLineSettings(); } else { lineSettings(); } context.BeginPath(); context.Move(fromSample.Location.Add(fromUnitVector)); context.AddLine(toSample.Location.Add(toUnitVector)); context.AddLine(toSample.Location.Sub(toUnitVector)); context.AddLine(fromSample.Location.Sub(fromUnitVector)); context.ClosePath(); context.DrawPath(CGPathDrawingMode.FillStroke); } var isEstimated = fromSample.EstimatedProperties.HasFlag(UITouchProperties.Azimuth); if (fromSample.Azimuth.HasValue && (!fromSample.Coalesced || isEstimated) && !fromSample.Predicted && displayOptions == StrokeViewDisplayOptions.Debug) { var length = 20f; var azimuthUnitVector = fromSample.GetAzimuthUnitVector(); var azimuthTarget = fromSample.Location.Add(azimuthUnitVector.Mult(length)); var altitudeStart = azimuthTarget.Add(azimuthUnitVector.Mult(length / -2)); var altitudeTarget = altitudeStart.Add((azimuthUnitVector.Mult(length / 2)).Apply(CGAffineTransform.MakeRotation(fromSample.Altitude.Value))); // Draw altitude as black line coming from the center of the azimuth. altitudeSettings(); context.BeginPath(); context.Move(altitudeStart); context.AddLine(altitudeTarget); context.StrokePath(); // Draw azimuth as orange (or blue if estimated) line. azimuthSettings(); if (isEstimated) { context.SetStrokeColor(UIColor.Blue.CGColor); } context.BeginPath(); context.Move(fromSample.Location); context.AddLine(azimuthTarget); context.StrokePath(); } heldFromSample = null; heldFromSampleUnitVector = null; } }; if (stroke.Samples.Count == 1) { // Construct a face segment to draw for a stroke that is only one point. var sample = stroke.Samples [0]; var tempSampleFrom = new StrokeSample { Timestamp = sample.Timestamp, Location = sample.Location.Add(new CGVector(-0.5f, 0)), Coalesced = false, Predicted = false, Force = sample.Force, Azimuth = sample.Azimuth, Altitude = sample.Altitude, EstimatedProperties = sample.EstimatedProperties }; var tempSampleTo = new StrokeSample { Timestamp = sample.Timestamp, Location = sample.Location.Add(new CGVector(0.5f, 0)), Coalesced = false, Predicted = false, Force = sample.Force, Azimuth = sample.Azimuth, Altitude = sample.Altitude, EstimatedProperties = sample.EstimatedProperties }; var segment = new StrokeSegment(tempSampleFrom); segment.AdvanceWithSample(tempSampleTo); segment.AdvanceWithSample(null); draw(segment); } else { foreach (var segment in stroke) { draw(segment); } } }
void Collect(Stroke stroke, UITouch touch, UIView view, bool coalesced, bool predicted) { if (view == null) { throw new ArgumentNullException(); } // Only collect samples that actually moved in 2D space. var location = touch.GetPreciseLocation(view); var previousSample = stroke.Samples.LastOrDefault(); if (Distance(previousSample?.Location, location) < 0.003) { return; } var sample = new StrokeSample { Timestamp = touch.Timestamp, Location = location, Coalesced = coalesced, Predicted = predicted }; bool collectForce = touch.Type == UITouchType.Stylus || view.TraitCollection.ForceTouchCapability == UIForceTouchCapability.Available; if (collectForce) { sample.Force = touch.Force; } if (touch.Type == UITouchType.Stylus) { var estimatedProperties = touch.EstimatedProperties; sample.EstimatedProperties = estimatedProperties; sample.EstimatedPropertiesExpectingUpdates = touch.EstimatedPropertiesExpectingUpdates; sample.Altitude = touch.AltitudeAngle; sample.Azimuth = touch.GetAzimuthAngle(view); if (stroke.Samples.Count == 0 && estimatedProperties.HasFlag(UITouchProperties.Azimuth)) { stroke.ExpectsAltitudeAzimuthBackfill = true; } else if (stroke.ExpectsAltitudeAzimuthBackfill && !estimatedProperties.HasFlag(UITouchProperties.Azimuth)) { for (int index = 0; index < stroke.Samples.Count; index++) { var priorSample = stroke.Samples [index]; var updatedSample = priorSample; if (updatedSample.EstimatedProperties.HasFlag(UITouchProperties.Altitude)) { updatedSample.EstimatedProperties &= ~UITouchProperties.Altitude; updatedSample.Altitude = sample.Altitude; } if (updatedSample.EstimatedProperties.HasFlag(UITouchProperties.Azimuth)) { updatedSample.EstimatedProperties &= ~UITouchProperties.Azimuth; updatedSample.Azimuth = sample.Azimuth; } stroke.Update(updatedSample, index); } stroke.ExpectsAltitudeAzimuthBackfill = false; } } if (predicted) { stroke.AddPredicted(sample); } else { var index = stroke.Add(sample); if (touch.EstimatedPropertiesExpectingUpdates != 0) { outstandingUpdateIndexes [touch.EstimationUpdateIndex] = new StrokeIndex { Stroke = stroke, Index = index }; } } }