private void ScaleKeyframes() { var boundaries = GridSelection.GetSelectionBoundaries(); if (boundaries.HasValue || Scale < Mathf.ZeroTolerance) { var saved = new List <IKeyframe>(); for (int i = boundaries.Value.Top; i <= boundaries.Value.Bottom; ++i) { if (!(Document.Current.Rows[i].Components.Get <NodeRow>()?.Node is IAnimationHost animable)) { continue; } foreach (var animator in animable.Animators.ToList()) { saved.Clear(); IEnumerable <IKeyframe> keys = animator.ReadonlyKeys.Where(k => k.Frame >= boundaries.Value.Left && k.Frame < boundaries.Value.Right ).ToList(); if (Scale < 1) { keys = keys.Reverse().ToList(); } foreach (var key in keys) { saved.Add(key); RemoveKeyframe.Perform(animator, key.Frame); } foreach (var key in saved) { // The formula should behave similiar to stretching animation with mouse int newFrame = (int)( boundaries.Value.Left + (key.Frame - boundaries.Value.Left) * (1 + (boundaries.Value.Left - boundaries.Value.Right) * Scale) / (1 + boundaries.Value.Left - boundaries.Value.Right) ); var newKey = key.Clone(); newKey.Frame = newFrame; SetAnimableProperty.Perform( animable, animator.TargetPropertyPath, newKey.Value, createAnimatorIfNeeded: true, createInitialKeyframeForNewAnimator: false, newKey.Frame ); SetKeyframe.Perform(animable, animator.TargetPropertyPath, Document.Current.AnimationId, newKey); } } } ClearGridSelection.Perform(); for (int i = boundaries.Value.Top; i <= boundaries.Value.Bottom; ++i) { SelectGridSpan.Perform(i, boundaries.Value.Left, (int)(boundaries.Value.Left + (boundaries.Value.Right - boundaries.Value.Left) * Scale)); } } else { Document.Current.History.RollbackTransaction(); } }
public static void Perform() { var Boundaries = GetSelectionBoundaries(); if (Boundaries == null) { AlertDialog.Show("Can't invert animation in a non-rectangular selection. The selection must be a single rectangle."); return; } using (Document.Current.History.BeginTransaction()) { for (int i = Boundaries.Value.Top; i <= Boundaries.Value.Bottom; ++i) { if (!(Document.Current.Rows[i].Components.Get <NodeRow>()?.Node is IAnimationHost animable)) { continue; } foreach (var animator in animable.Animators.ToList()) { var saved = animator.Keys.Where(k => Boundaries.Value.Left <= k.Frame && k.Frame < Boundaries.Value.Right).ToList(); foreach (var key in saved) { RemoveKeyframe.Perform(animator, key.Frame); } foreach (var key in saved) { SetProperty.Perform(key, nameof(IKeyframe.Frame), Boundaries.Value.Left + Boundaries.Value.Right - key.Frame - 1); SetKeyframe.Perform(animable, animator.TargetPropertyPath, animator.AnimationId, key); } } } Document.Current.History.CommitTransaction(); } }
private void CreateSpriteAnimatedImage(List <string> files) { onBeforeDrop?.Invoke(); using (Document.Current.History.BeginTransaction()) { var node = CreateNode.Perform(typeof(Image)); SetProperty.Perform(node, nameof(Widget.Pivot), Vector2.Half); SetProperty.Perform(node, nameof(Widget.Id), "Temp"); postProcessNode?.Invoke(node); var i = 0; ITexture first = null; foreach (var file in files) { if (!Utils.ExtractAssetPathOrShowAlert(file, out var assetPath, out var assetType)) { continue; } var text = new SerializableTexture(assetPath); first = first ?? text; SetKeyframe.Perform(node, nameof(Widget.Texture), Document.Current.AnimationId, new Keyframe <ITexture> { Value = text, Frame = i++, Function = KeyFunction.Steep, }); } SetProperty.Perform(node, nameof(Widget.Size), (Vector2)first.ImageSize); Document.Current.History.CommitTransaction(); } }
public static void Perform(IntVector2 offset, bool removeOriginals) { var rows = Document.Current.Rows.ToList(); if (offset.Y > 0) { rows.Reverse(); } foreach (var row in rows) { var track = row.Components.Get <AnimationTrackRow>()?.Track; if (track?.EditorState().Locked != false) { continue; } var clips = track.Clips.Where(i => i.IsSelected).ToList(); var keys = new List <IKeyframe>(); if (track.Animators.TryFind(nameof(AnimationTrack.Weight), out var weightAnimator, Document.Current.AnimationId)) { keys = weightAnimator.ReadonlyKeys.Where(k => clips.Any(c => c.BeginFrame <= k.Frame && k.Frame <= c.EndFrame)).ToList(); } if (removeOriginals) { foreach (var key in keys) { RemoveKeyframe.Perform(weightAnimator, key.Frame); } } foreach (var clip in clips) { if (removeOriginals) { AnimationClipToolbox.RemoveClip(track, clip); } else { SetProperty.Perform(clip, nameof(AnimationClip.IsSelected), false); } } int numRows = Document.Current.Rows.Count; var destRow = Document.Current.Rows[(row.Index + offset.Y).Clamp(0, numRows - 1)]; var destTrack = destRow.Components.Get <AnimationTrackRow>()?.Track; foreach (var clip in clips) { var newClip = clip.Clone(); newClip.BeginFrame += offset.X; newClip.EndFrame += offset.X; newClip.IsSelected = true; AnimationClipToolbox.InsertClip(destTrack, newClip); } foreach (var k in keys) { var key = k.Clone(); key.Frame += offset.X; SetKeyframe.Perform(destTrack, nameof(AnimationTrack.Weight), Document.Current.AnimationId, key); } } }
private void ScaleKeyframes() { if (GridSelection.GetSelectionBoundaries(out var boundaries) && Scale > Mathf.ZeroTolerance) { var processed = new HashSet <IAnimator>(); var saved = new List <IKeyframe>(); foreach (var animable in GridSelection.EnumerateAnimators(boundaries)) { foreach (var animator in animable.Animators) { if (animator.AnimationId != Document.Current.AnimationId || processed.Contains(animator)) { continue; } processed.Add(animator); saved.Clear(); IEnumerable <IKeyframe> keys = animator.ReadonlyKeys.Where(k => k.Frame >= boundaries.Left && k.Frame < boundaries.Right ).ToList(); if (Scale < 1) { keys = keys.Reverse().ToList(); } foreach (var key in keys) { saved.Add(key); RemoveKeyframe.Perform(animator, key.Frame); } foreach (var key in saved) { // The formula should behave similiar to stretching animation with mouse int newFrame = (int)( boundaries.Left + (key.Frame - boundaries.Left) * (1 + (boundaries.Left - boundaries.Right) * Scale) / (1 + boundaries.Left - boundaries.Right) ); var newKey = key.Clone(); newKey.Frame = newFrame; SetAnimableProperty.Perform( animable.Host, animator.TargetPropertyPath, newKey.Value, createAnimatorIfNeeded: true, createInitialKeyframeForNewAnimator: false, newKey.Frame ); SetKeyframe.Perform(animable.Host, animator.TargetPropertyPath, Document.Current.AnimationId, newKey); } } } ClearGridSelection.Perform(); for (int i = boundaries.Top; i <= boundaries.Bottom; ++i) { SelectGridSpan.Perform(i, boundaries.Left, (int)(boundaries.Left + (boundaries.Right - boundaries.Left) * Scale)); } }
private static void RestoreChildrenPositions(Widget widget, Matrix32 transform) { foreach (var child in widget.Nodes.OfType <Widget>()) { var newPosition = transform.TransformVector(child.Position); SetProperty.Perform(child, nameof(Widget.Position), newPosition); if (child.Animators.TryFind(nameof(Widget.Position), out var animator)) { foreach (var key in animator.ReadonlyKeys.ToList()) { var newKey = key.Clone(); newKey.Value = transform.TransformVector((Vector2)key.Value); SetKeyframe.Perform(animator, newKey); } } } }
public static void TransformPropertyAndKeyframes <T>(Node node, string propertyId, Func <T, T> transformer) { var value = new Property <T>(node, propertyId).Value; SetProperty.Perform(node, propertyId, transformer(value)); foreach (var animation in node.Animators) { if (animation.TargetPropertyPath == propertyId) { foreach (var keyframe in animation.Keys.ToList()) { var newKeyframe = keyframe.Clone(); newKeyframe.Value = transformer((T)newKeyframe.Value); SetKeyframe.Perform(node, animation.TargetPropertyPath, animation.AnimationId, newKeyframe); } } } }
public static void Perform() { var keys = KeyframeClipboard.Keys; if (keys == null || !Document.Current.TopLevelSelectedRows().Any()) { return; } int startRow = Document.Current.TopLevelSelectedRows().First().Index; var spans = Document.Current.Rows[startRow].Components.Get <GridSpanListComponent>()?.Spans; if (spans == null || !spans.Any()) { return; } int startCol = spans.First().A; Document.Current.History.DoTransaction(() => { foreach (var key in keys) { int rowIndex = startRow + key.Row; int colIndex = startCol + key.Frame; if (rowIndex >= Document.Current.Rows.Count || colIndex < 0) { continue; } var animable = Document.Current.Rows[rowIndex].Components.Get <NodeRow>()?.Node as IAnimationHost; if (animable == null) { continue; } var property = animable.GetType().GetProperty(key.Property); if (property == null) { continue; } var keyframe = key.Keyframe.Clone(); keyframe.Frame = colIndex; SetKeyframe.Perform(animable, key.Property, key.AnimationId, keyframe); } }); }
private static void SetKeyframes(Dictionary <Node, BoneAnimationData> keyframeDictionary) { foreach (var pair in keyframeDictionary) { if (pair.Value.NoParentKeyframes) { TransformPropertyAndKeyframes(pair.Key, nameof(Bone.Position), pair.Value.PositionTransformer); } else { SetProperty.Perform(pair.Key, nameof(Bone.Position), pair.Value.CurrentPosition); SetProperty.Perform(pair.Key, nameof(Bone.Rotation), pair.Value.CurrentRotation); foreach (var keyframe in pair.Value.PositionKeyframes) { SetKeyframe.Perform(pair.Key, nameof(Bone.Position), Document.Current.AnimationId, keyframe.Value); } foreach (var keyframe in pair.Value.RotationKeyframes) { SetKeyframe.Perform(pair.Key, nameof(Bone.Rotation), Document.Current.AnimationId, keyframe.Value); } SetAnimableProperty.Perform(pair.Key, nameof(Bone.BaseIndex), 0); } } }
/// <summary> /// Handles files drop. /// </summary> /// <param name="files">Dropped files.</param> public void Handle(List <string> files) { using (Document.Current.History.BeginTransaction()) { foreach (var file in files.Where(f => Path.GetExtension(f) == ".ogg").ToList()) { files.Remove(file); if (!Utils.ExtractAssetPathOrShowAlert(file, out var assetPath, out var assetType)) { continue; } var node = CreateNode.Perform(typeof(Audio)); var sample = new SerializableSample(assetPath); SetProperty.Perform(node, nameof(Audio.Sample), sample); SetProperty.Perform(node, nameof(Node.Id), Path.GetFileNameWithoutExtension(assetPath)); SetProperty.Perform(node, nameof(Audio.Volume), 1); var key = new Keyframe <AudioAction> { Frame = Document.Current.AnimationFrame, Value = AudioAction.Play }; SetKeyframe.Perform(node, nameof(Audio.Action), Document.Current.AnimationId, key); } Document.Current.History.CommitTransaction(); } }
public void Handle(List <string> files) { var grid = Timeline.Instance.Grid; var rowLocationUnderMouseOnFilesDrop = SelectAndDragRowsProcessor.MouseToRowLocation(grid.RootWidget.Input.MousePosition); var handled = new List <string>(); var cellUnderMouseOnFilesDrop = grid.CellUnderMouse(); var animateTextureCellOffset = 0; using (Document.Current.History.BeginTransaction()) { foreach (var file in files.ToList()) { if (Document.Current.Animation.IsCompound) { try { // Dirty hack: using a file drag&drop mechanics for dropping animation clips on the grid. var decodedAnimationId = Encoding.UTF8.GetString(Convert.FromBase64String(file)); AddAnimationClip.Perform( new IntVector2( cellUnderMouseOnFilesDrop.X + animateTextureCellOffset, cellUnderMouseOnFilesDrop.Y), decodedAnimationId); return; } catch { } } if (!Utils.ExtractAssetPathOrShowAlert(file, out var assetPath, out var assetType)) { continue; } switch (assetType) { case ".png": { if (Document.Current.Rows.Count == 0) { continue; } var widget = Document.Current.Rows[cellUnderMouseOnFilesDrop.Y].Components.Get <NodeRow>()?.Node as Widget; if (widget == null) { continue; } var key = new Keyframe <ITexture> { Frame = cellUnderMouseOnFilesDrop.X + animateTextureCellOffset, Value = new SerializableTexture(assetPath), Function = KeyFunction.Steep, }; SetKeyframe.Perform(widget, nameof(Widget.Texture), Document.Current.AnimationId, key); animateTextureCellOffset++; break; } case ".ogg": { var node = CreateNode.Perform(typeof(Audio)); if (rowLocationUnderMouseOnFilesDrop.HasValue) { var location = rowLocationUnderMouseOnFilesDrop.Value; var row = Document.Current.Rows.FirstOrDefault(r => r.Components.Get <Core.Components.NodeRow>()?.Node == node); if (row != null) { if (location.Index >= row.Index) { location.Index++; } SelectAndDragRowsProcessor.Probers.Any(p => p.Probe(row, location)); } } var sample = new SerializableSample(assetPath); SetProperty.Perform(node, nameof(Audio.Sample), sample); SetProperty.Perform(node, nameof(Node.Id), assetPath); SetProperty.Perform(node, nameof(Audio.Volume), 1); var key = new Keyframe <AudioAction> { Frame = cellUnderMouseOnFilesDrop.X, Value = AudioAction.Play }; SetKeyframe.Perform(node, nameof(Audio.Action), Document.Current.AnimationId, key); break; } } files.Remove(file); } Document.Current.History.CommitTransaction(); } }
private void ShowContextMenu() { SelectAnimationBasedOnMousePosition(); var menu = new Menu(); var rootNode = Document.Current.RootNode; menu.Add(new Command("Add", () => AddAnimation(rootNode, false))); menu.Add(new Command("Add Compound", () => AddAnimation(rootNode, true))); menu.Add(new Command("Add ZeroPose", () => AddZeroPoseAnimation(rootNode)) { Enabled = !rootNode.Animations.TryFind(Animation.ZeroPoseId, out _) }); var path = GetNodePath(Document.Current.Container); if (!string.IsNullOrEmpty(path)) { var container = Document.Current.Container; menu.Add(new Command($"Add To '{path}'", () => AddAnimation(container, false))); menu.Add(new Command($"Add Compound To '{path}'", () => AddAnimation(container, true))); menu.Add(new Command($"Add ZeroPose To '{path}'", () => AddZeroPoseAnimation(container)) { Enabled = !container.Animations.TryFind(Animation.ZeroPoseId, out _) }); } menu.Add(Command.MenuSeparator); menu.Add(new Command("Rename", RenameAnimation)); menu.Add(new Command("Duplicate", DuplicateAnimation)); menu.Add(Command.Delete); menu.Popup(); void AddAnimation(Node node, bool compound) { Document.Current.History.DoTransaction(() => { var animation = new Animation { Id = GenerateAnimationId("NewAnimation"), IsCompound = compound }; Core.Operations.InsertIntoList.Perform(node.Animations, node.Animations.Count, animation); SelectAnimation(GetAnimations().IndexOf(animation)); if (compound) { var track = new AnimationTrack { Id = "Track1" }; Core.Operations.InsertIntoList <AnimationTrackList, AnimationTrack> .Perform(animation.Tracks, 0, track); } }); // Schedule animation rename on the next update, since the widgets are not built yet panelWidget.Tasks.Add(DelayedRenameAnimation()); } void AddZeroPoseAnimation(Node node) { Document.Current.History.DoTransaction(() => { var animation = new Animation { Id = Animation.ZeroPoseId }; InsertIntoList.Perform(node.Animations, node.Animations.Count, animation); foreach (var a in node.Descendants.SelectMany(n => n.Animators).ToList()) { var(propertyData, animable, index) = AnimationUtils.GetPropertyByPath(a.Owner, a.TargetPropertyPath); var zeroPoseKey = Keyframe.CreateForType(propertyData.Info.PropertyType); zeroPoseKey.Value = index == -1 ? propertyData.Info.GetValue(animable) : propertyData.Info.GetValue(animable, new object[] { index }); zeroPoseKey.Function = KeyFunction.Steep; SetKeyframe.Perform(a.Owner, a.TargetPropertyPath, Animation.ZeroPoseId, zeroPoseKey); } SelectAnimation(GetAnimations().IndexOf(animation)); }); } IEnumerator <object> DelayedRenameAnimation() { yield return(null); RenameAnimation(); } }
private void Stretch(IntRectangle boundaries, DragSide side, int newPos, bool stretchMarkers) { int length; if (side == DragSide.Left) { length = boundaries.Right - newPos - 1; } else { length = newPos - boundaries.Left - 1; } int oldLength = boundaries.Right - boundaries.Left - 1; var processed = new HashSet <IAnimator>(); foreach (var animable in GridSelection.EnumerateAnimators(boundaries)) { foreach (var animator in animable.Animators) { if (animator.AnimationId != Document.Current.AnimationId || processed.Contains(animator) || !savedKeyframes.ContainsKey(animator)) { continue; } processed.Add(animator); IEnumerable <IKeyframe> saved = savedKeyframes[animator]; if ( side == DragSide.Left && length < oldLength || side == DragSide.Right && length > oldLength ) { saved = saved.Reverse(); } foreach (var key in saved) { RemoveKeyframe.Perform(animator, key.Frame); } foreach (var key in saved) { double relpos = savedPositions[key]; int newFrame; if (side == DragSide.Left) { newFrame = (int)Math.Round(newPos + relpos * length); } else { newFrame = (int)Math.Round(boundaries.Left + relpos * length); } var newKey = key.Clone(); newKey.Frame = newFrame; SetAnimableProperty.Perform( animable.Host, animator.TargetPropertyPath, newKey.Value, createAnimatorIfNeeded: true, createInitialKeyframeForNewAnimator: false, newKey.Frame ); SetKeyframe.Perform(animable.Host, animator.TargetPropertyPath, Document.Current.AnimationId, newKey); } } } if (stretchMarkers) { foreach (var marker in savedMarkers) { DeleteMarker.Perform(marker, removeDependencies: false); } foreach (var marker in savedMarkers) { double relpos = savedMarkerPositions[marker]; int newFrame; if (side == DragSide.Left) { newFrame = (int)Math.Round(newPos + relpos * length); } else { newFrame = (int)Math.Round(boundaries.Left + relpos * length); } var newMarker = marker.Clone(); newMarker.Frame = newFrame; SetMarker.Perform(newMarker, removeDependencies: false); } } }
public static void Perform(IntVector2 offset, bool removeOriginals) { var processedKeys = new HashSet <IKeyframe>(); var operations = new List <Action>(); foreach (var row in Document.Current.Rows) { var spans = row.Components.GetOrAdd <GridSpanListComponent>().Spans.GetNonOverlappedSpans(offset.X > 0); foreach (var span in spans) { var node = row.Components.Get <NodeRow>()?.Node ?? row.Components.Get <PropertyRow>()?.Node; if (node == null) { continue; } var property = row.Components.Get <PropertyRow>()?.Animator.TargetPropertyPath; foreach (var a in node.Animators.ToList()) { if (property != null && a.TargetPropertyPath != property) { continue; } IEnumerable <IKeyframe> keysEnumerable = a.Keys.Where(k => k.Frame >= span.A && k.Frame < span.B); if (offset.X > 0) { keysEnumerable = keysEnumerable.Reverse(); } foreach (var k in keysEnumerable) { if (processedKeys.Contains(k)) { continue; } processedKeys.Add(k); var destRow = row.Index + offset.Y; if (!CheckRowRange(destRow)) { continue; } var destRowComponents = Document.Current.Rows[destRow].Components; var destNode = destRowComponents.Get <NodeRow>()?.Node ?? destRowComponents.Get <PropertyRow>()?.Node; if (destNode == null || !ArePropertiesCompatible(node, destNode, a.TargetPropertyPath)) { continue; } if (k.Frame + offset.X >= 0) { var k1 = k.Clone(); k1.Frame += offset.X; // The same logic is used to create keyframes as everywhere, but extended by setting // all parameters from a particular keyframe. Yes, this creates some overhead. operations.Add(() => SetAnimableProperty.Perform(destNode, a.TargetPropertyPath, k1.Value, true, false, k1.Frame)); operations.Add(() => SetKeyframe.Perform(destNode, a.TargetPropertyPath, Document.Current.AnimationId, k1)); } // Order is importent. RemoveKeyframe must be after SetKeyframe, // to prevent animator clean up if all keys were removed. if (removeOriginals) { operations.Add(() => RemoveKeyframe.Perform(a, k.Frame)); } } } } } foreach (var o in operations) { o(); } }
private void Handle(IEnumerable <string> files) { Handling?.Invoke(); using (Document.Current.History.BeginTransaction()) { pendingImages = new List <string>(); foreach (var file in files) { try { string assetPath, assetType; if (!Utils.ExtractAssetPathOrShowAlert(file, out assetPath, out assetType) || !Utils.AssertCurrentDocument(assetPath, assetType)) { continue; } var nodeCreatingEventArgs = new NodeCreatingEventArgs(assetPath, assetType); NodeCreating?.Invoke(nodeCreatingEventArgs); if (nodeCreatingEventArgs.Cancel) { continue; } var fileName = Path.GetFileNameWithoutExtension(assetPath); switch (assetType) { case ".png": pendingImages.Add(assetPath); break; case ".ogg": { var node = CreateNode.Perform(typeof(Audio)); var sample = new SerializableSample(assetPath); SetProperty.Perform(node, nameof(Audio.Sample), sample); SetProperty.Perform(node, nameof(Node.Id), fileName); SetProperty.Perform(node, nameof(Audio.Volume), 1); var key = new Keyframe <AudioAction> { Frame = Document.Current.AnimationFrame, Value = AudioAction.Play }; SetKeyframe.Perform(node, nameof(Audio.Action), Document.Current.AnimationId, key); OnNodeCreated(node); break; } case ".tan": case ".model": case ".scene": DropSceneContextMenu.Create(assetPath, assetType, NodeCreated); break; } } catch (System.Exception e) { AlertDialog.Show(e.Message); } } if (pendingImages.Count > 0) { var menu = new Menu(); foreach (var kv in imageDropCommands.Commands) { if (NodeCompositionValidator.Validate(Document.Current.Container.GetType(), kv.Value)) { menu.Add(kv.Key); } } menu.Popup(); } Document.Current.History.CommitTransaction(); } }
public static void Perform() { var keys = KeyframeClipboard.Keys; if (keys == null || !Document.Current.TopLevelSelectedRows().Any()) { return; } int startRow = Document.Current.TopLevelSelectedRows().First().Index; var spans = Document.Current.Rows[startRow].Components.Get <GridSpanListComponent>()?.Spans; if (spans == null || !spans.Any()) { return; } int startCol = spans.First().A; Document.Current.History.DoTransaction(() => { var rows = Document.Current.Rows; int rowIndex = startRow; int animationHostIndex = 0; IAnimationHost animationHost = null; Node node = null; foreach (var key in keys) { int colIndex = startCol + key.Frame; if (rowIndex >= Document.Current.Rows.Count || colIndex < 0) { continue; } while (rowIndex < rows.Count) { node = rows[rowIndex].Components.Get <NodeRow>()?.Node; animationHost = node; if (animationHost != null) { if (animationHostIndex == key.AnimationHostOrderIndex) { break; } animationHostIndex++; } ++rowIndex; } if (rowIndex >= rows.Count) { break; } if (node.EditorState().Locked) { continue; } var(pd, _, _) = AnimationUtils.GetPropertyByPath(animationHost, key.Property); if (pd.Info == null) { continue; } var keyframe = key.Keyframe.Clone(); keyframe.Frame = colIndex; SetKeyframe.Perform(animationHost, key.Property, Document.Current.AnimationId, keyframe); } }); }
private void Stretch(Boundaries boundaries, DragSide side, int newPos, bool stretchMarkers) { int length; if (side == DragSide.Left) { length = boundaries.Right - newPos - 1; } else { length = newPos - boundaries.Left - 1; } int oldLength = boundaries.Right - boundaries.Left - 1; for (int i = boundaries.Top; i <= boundaries.Bottom; ++i) { if (!(Document.Current.Rows[i].Components.Get <NodeRow>()?.Node is IAnimationHost animable)) { continue; } foreach (var animator in animable.Animators.ToList()) { IEnumerable <IKeyframe> saved = savedKeyframes[animator]; if ( side == DragSide.Left && length < oldLength || side == DragSide.Right && length > oldLength ) { saved = saved.Reverse(); } foreach (var key in saved) { RemoveKeyframe.Perform(animator, key.Frame); } foreach (var key in saved) { double relpos = savedPositions[key]; int newFrame; if (side == DragSide.Left) { newFrame = (int)Math.Round(newPos + relpos * length); } else { newFrame = (int)Math.Round(boundaries.Left + relpos * length); } var newKey = key.Clone(); newKey.Frame = newFrame; SetAnimableProperty.Perform( animable, animator.TargetPropertyPath, newKey.Value, createAnimatorIfNeeded: true, createInitialKeyframeForNewAnimator: false, newKey.Frame ); SetKeyframe.Perform(animable, animator.TargetPropertyPath, Document.Current.AnimationId, newKey); } } } if (stretchMarkers) { foreach (var marker in savedMarkers) { DeleteMarker.Perform(Document.Current.Container, marker, removeDependencies: false); } foreach (var marker in savedMarkers) { double relpos = savedMarkerPositions[marker]; int newFrame; if (side == DragSide.Left) { newFrame = (int)Math.Round(newPos + relpos * length); } else { newFrame = (int)Math.Round(boundaries.Left + relpos * length); } var newMarker = marker.Clone(); newMarker.Frame = newFrame; SetMarker.Perform(Document.Current.Container, newMarker, removeDependencies: false); } } }