Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(): path animation #9077

Closed
wants to merge 16 commits into from
Closed

feat(): path animation #9077

wants to merge 16 commits into from

Conversation

ShaMan123
Copy link
Contributor

@ShaMan123 ShaMan123 commented Jul 8, 2023

Motivation

#9071
I use this kind of animation in my apps and this is 4x cleaner

Description

  fabric.util.animate({
    path,
    duration: 3000,
    endValue: '85%',
    easing: fabric.util.ease.easeInOutBack,
    onChange({ x, y }, v, t) {
      circle.setXY(new fabric.Point(x, y), 'center', 'center');
      canvas.renderAll();
    },
  });

Changes

Apart from PathAnimation:

  • refactored animation types:
    • moved XXXAnimationOptions to its file
    • removed TAnimation, AnimationOptions in favor of maps => AnimationRegistry, AimationOptionsRegistry

Gist

In Action

https://codesandbox.io/p/sandbox/fabric-nextjs-sandbox-forked-4vqpvd?file=%2Fpages%2Findex.tsx%3A52%2C5-77%2C6

Parcel.Sandbox.-.Google.Chrome.2023-07-07.11-46-58_Trim.mp4

ShaMan123 and others added 6 commits July 7, 2023 13:52
rename

Update PathAnimation.ts

Update PathAnimation.ts

cleaner

Update PathAnimation.ts

percent

expose distance to callback

FIX!

Update PathAnimation.ts

Update index.ts

expose

init
Copy link
Contributor Author

@ShaMan123 ShaMan123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated desc

@@ -3,8 +3,8 @@ import { capValue } from '../util/misc/capValue';

const RE_PERCENT = /^(\d+\.\d+)%|(\d+)%$/;

export function isPercent(value: string | null) {
return value && RE_PERCENT.test(value);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checked that all usages are with a defined value

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we put const RE_PERCENT = /^(\d+\.\d+)%|(\d+)%$/; inside the function isPercent ? i m not sure there is reason to keep a regex alive all the time. The parser is not hot code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought you wanted all regex to be const to be created once.
Can we not hold back on these things for now?

Comment on lines -121 to -137
export type ValueAnimationOptions = TAnimationOptions<number>;

export type ArrayAnimationOptions = TAnimationOptions<number[]>;

export type ColorAnimationOptions = TAnimationOptions<
TColorArg,
string,
number[]
>;

export type AnimationOptions<T extends number | number[] | TColorArg> =
T extends TColorArg
? ColorAnimationOptions
: T extends number[]
? ArrayAnimationOptions
: ValueAnimationOptions;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to the files

@ShaMan123 ShaMan123 requested a review from asturur July 8, 2023 12:34
@ShaMan123
Copy link
Contributor Author

ShaMan123 commented Jul 8, 2023

I want to write a test with playwright but not sure of the best way to do so

Copy link
Contributor Author

@ShaMan123 ShaMan123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ready

@github-actions
Copy link
Contributor

github-actions bot commented Jul 8, 2023

Coverage after merging feat-path-animation into master will be

82.91%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
index.node.ts7.69%100%0%14.29%17, 20, 23, 35, 38, 41
src
   ClassRegistry.ts100%100%100%100%
   Collection.ts94.71%94.64%86.67%97.09%101, 104, 207–208, 233–234
   CommonMethods.ts96.55%87.50%100%100%10
   Intersection.ts100%100%100%100%
   Observable.ts87.23%85.29%84.62%89.36%144–145, 170–171, 39–40, 48, 57, 91, 99
   Point.ts100%100%100%100%
   Shadow.ts98.36%95.65%100%100%178
   cache.ts97.06%90%100%100%57
   config.ts75%66.67%66.67%82.76%130, 138, 138, 138, 138, 138–140, 151–153
   constants.ts100%100%100%100%
src/Pattern
   Pattern.ts92.31%91.89%90%93.55%118, 129, 138, 31, 94
src/brushes
   BaseBrush.ts100%100%100%100%
   CircleBrush.ts0%0%0%0%108, 108, 108, 110, 112, 114–116, 118–121, 128–129, 13, 136, 138, 23–24, 32–36, 40–44, 51–54, 62–66, 68, 76, 76, 76, 76, 76–77, 79, 79, 79–82, 84, 92–93, 95, 97–99
   PatternBrush.ts97.06%87.50%100%100%21
   PencilBrush.ts91.06%82.35%100%93.81%122–123, 152, 152–154, 176, 176, 276, 280, 285–286, 68–69, 84–85
   SprayBrush.ts0%0%0%0%107, 107, 107, 107, 107–108, 110–111, 118–119, 121, 123–127, 136, 140–141, 141, 149, 149, 149–152, 154–157, 161–162, 164, 166–169, 17, 172, 179, 18, 180, 182, 184–185, 187, 194–195, 197–198, 20, 201, 201, 208, 208, 21, 212, 22, 22, 22–24, 28, 32, 39, 46, 53, 60, 67, 84–86, 94–96, 98–99
src/canvas
   Canvas.ts79.05%77.54%83.05%79.57%1000–1001, 1001, 1001–1003, 1005–1006, 1006, 1006, 1008, 1016, 1016, 1016–1018, 1018, 1018, 1024–1025, 1033–1034, 1034, 1034–1035, 1040, 1042, 1073–1075, 1078–1079, 1083–1084, 1197–1199, 1202–1203, 1276, 1395, 1515, 161, 186, 296–297, 300–304, 309, 332–333, 338–343, 36, 363, 363, 363–364, 364, 364–365, 373, 378–379, 379, 379–380, 382, 391, 397–398, 398, 398, 40, 441, 449, 453, 453, 453–454, 456, 538–539, 539, 539–540, 546, 546, 546–548, 568, 570, 570, 570–571, 571, 571, 574, 574, 574–575, 578, 587–588, 590–591, 593, 593–594, 596–597, 609–610, 610, 610–611, 613–618, 624, 631, 668, 668, 668, 670, 672–677, 683, 689, 689, 689–690, 692, 695, 700, 713, 741, 741, 799–800, 800, 800–801, 803, 806–807, 807, 807–808, 810–811, 814, 814–816, 819–820, 890, 902, 909, 930, 962, 983–984
   SelectableCanvas.ts94.43%92.24%94.23%96.17%1115, 1115–1116, 1119, 1139, 1139, 1188, 1196, 1315, 1317, 1319–1320, 519, 694–695, 697–698, 698, 698, 747–748, 809–810, 863–865, 897, 902–903, 930–931
   StaticCanvas.ts96.78%93.09%100%98.53%1036–1037, 1037, 1037–1038, 1158, 1168, 1224–1225, 1228, 1263–1264, 1340, 1349, 1349, 1353, 1353, 1400–1401, 308–309, 325, 693, 705–706
   TextEditingManager.ts84.31%71.43%91.67%88%17–18, 18, 18–19, 19, 19
src/canvas/DOMManagers
   CanvasDOMManager.ts95.52%70%100%100%21–22, 29
   StaticCanvasDOMManager.ts100%100%100%100%
   util.ts86.67%80.56%83.33%93.94%14, 26, 63–64, 67, 67, 74, 93–94
src/color
   Color.ts94.96%91.67%96.30%96.05%233, 258–259, 267–268, 48
   color_map.ts100%100%100%100%
   constants.ts100%100%100%100%
   util.ts85.71%76.92%100%89.74%55–56, 56, 58, 58, 58–59, 61–62, 89
src/controls
   Control.ts93.33%87.88%91.67%97.78%175, 240, 327, 327, 362
   changeWidth.ts100%100%100%100%
   commonControls.ts100%100%100%100%
   controlRendering.ts81.63%78%100%84.78%106, 111, 121, 121, 45, 50, 61, 61, 65–72, 81–82
   drag.ts100%100%100%100%
   fireEvent.ts88.89%75%100%100%13
   polyControl.ts5.97%0%0%11.11%100, 105, 119, 119, 119, 119, 119, 121–124, 124, 127, 134, 17, 25–29, 29, 29, 29, 29, 29, 29, 29, 50–56, 56, 56, 56, 56, 58, 63–64, 66, 76, 82–83, 83, 83–84, 88–90, 90, 90, 90, 90, 92
   rotate.ts19.57%12.50%50%21.43%41, 45, 51, 51, 51–52, 55–57, 59, 59, 59, 59, 59–61, 61, 61–63, 65, 65, 65–67, 67, 67–68, 73, 73, 73–74, 76, 78, 80–81
   scale.ts93.57%92.94%100%93.67%129–130, 132–134, 148–149, 181–183, 42
   scaleSkew.ts78.79%64.29%100%85.71%27, 29, 29, 29, 31, 33, 35
   skew.ts91.03%79.31%100%97.67%131–132, 163–164, 171, 177, 179
   util.ts100%100%100%100%
   wrapWithFireEvent.ts100%100%100%100%
   wrapWithFixedAnchor.ts100%100%100%100%
src/env
   browser.ts84.21%77.78%50%100%14, 17
   index.ts100%100%100%100%

@github-actions
Copy link
Contributor

github-actions bot commented Jul 8, 2023

Build Stats

file / KB (diff) bundled minified
fabric 915.851 (+2.299) 306.383 (+0.659)

@@ -42,7 +42,7 @@ jobs:
size: { fabric: { minified: fs.statSync('${{ env.minified }}').size, bundled: fs.statSync('${{ env.bundled }}').size } }
});
- name: checkout src files
run: git checkout origin/master -- src fabric.ts index.ts index.node.ts .browserslistrc rollup.config.mjs
run: git checkout ${{ github.event.pull_request.base.sha }}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed - didn't checkout added files for some reason

@ShaMan123
Copy link
Contributor Author

I also want animateColor to be removed and intergrated into animate - for that to happen we can make start/endValue a Color instance so we can check that in the condition in animate

@stale
Copy link

stale bot commented Sep 17, 2023

This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs 😔. Thank you for your contributions.

@stale stale bot added the stale Issue marked as stale by the stale bot label Sep 17, 2023
@ShaMan123 ShaMan123 added feature and removed stale Issue marked as stale by the stale bot labels Sep 18, 2023
const distance = pathProgress * context.length;
return callback(
{
...context.getPointOnPath(distance),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we need to respect a transform matrix line under group?

@asturur
Copy link
Member

asturur commented Oct 7, 2023

let me see if i can unblock this today

@asturur
Copy link
Member

asturur commented Oct 7, 2023

ok for me here there is a real divergent idea on how features needs to be exposed.
We will need to sync on this.

The smartest part of this pr is the callback wrapper that changes a value animation to a point on path context.
All the rest to me seems unnecessary construct to hide everything as an option of a generic utility.

This is exactly what i see as feature creep.

To explain myself at best, something like this:

  fabric.util.animate({
    duration: 3000,
    endValue: '85%',
    easing: fabric.util.ease.easeInOutBack,
    // path or pathContext as first argument depending if we think parsing the path is a cost needs to be avoided
    onChange: positionOnPathCallbackWrapper(path, ({ x, y }, v, t) {
      circle.setXY(new fabric.Point(x, y), 'center', 'center');
      canvas.renderAll();
    }),
  });

would be equally clean and require far less code to maintain and doesn't need to be part of the bundle either.

On top of that a Path animation for me would be taking path A, path B and move smoothly between path A and path B and not animating a point over a path.

All the other animation types do exactly that, change a color from a to b, a number from a to b and so on.

@ShaMan123
Copy link
Contributor Author

ShaMan123 commented Oct 16, 2023

I tried to align to existing patterns.
Your point is valid but makes color and array animations as unwanted as the point on path animation.
I can rename it to PointOnPathAnimation
Animating a path is not something I used, I did not experience this UX and do not understand the math of an abstract solution

The real problem lies in the code
The path iterator has many weird stuff in it, the worst is it pushes as the last entry a sum or whatever which is junk if you want to iterate over it but you do epect to need to read the entire method to use it.
Also the angle values of the first and therefore the last entries are wrong.
Fixing that and making the path iterator more freindly will remove this need entirely I believe.
But it is breaking so if you agree I will break and rename

@asturur
Copy link
Member

asturur commented Oct 17, 2023

I was not advocating for a path animation utility indeed.
I also don't like array animation, correct. Instead color is a bit of different because color is a first level property on many objects, so if you support object animation by properties, at some point someone needed the color and added it. It could be espressed as a callback wrapper too, but would make not possible to animate objects with group of properties.
Color is also a value and is not exactly clear how to animate it. Was also added many years ago when i had way less comprehensive view of the library.

The path iterator was born only for supporting text on a path, so is probable that may not support other use cases.
The initial angle of the first point is probably broken because text is never on the initial point since no glyph as a zero width, and if it has is invisible, so no one see the wrong angle, and so no one checked for it, but the solution is probably very simple for that.

I m not sure what you mean by weird stuff and junk. I really don't like to discuss code on that terms because i have to guess what are you referring to instead of you telling me what is that you don't like.

Here there is a callback wrapper that imho can be useful and light weight if couple with an example on how to use it.

If the path iterator can be done better, as long as it supports text on a path as it is today, we have no issue changing it, is not part of the public api per se, it was there because there was no distinction between public and private in fabric 5 because of how the code was written.

Then apart the callback wrapper, and the improvement on the path iterator, path iteration is not a fabricJS feature.
If it can be, point out the part that in your opinion need fixing before going head down fixing it.

@ShaMan123
Copy link
Contributor Author

I don't think the color animation is useful because it allow only rgb animation, where as hsl animation is what I am used to do.
I can open another PR targeting the path iterator.
It is a very good piece of code I would never have access to anywhere else and allows outstanding UX/UI but is extremely hard to use.
I will show you how I use it in my projects so you understand my intention.
I want it to become a class with methods exposing 1-2 methods and hiding all the iterator context as protected properties

export const parsePathForIteration = (
  path: string | fabric.TSimplePathData
) => {
  const pathData = fabric.util.makePathSimpler(
    Array.isArray(path) ? path : fabric.util.parsePath(path)
  );
  const data = fabric.util.getPathSegmentsInfo(pathData);
  return {
    path: pathData,
    data,
    // this is because in the code the last entry is a sum 
    length: data[data.length - 1]!.length,
    // unintuitve to understand this
    getPointOnPath: (distance: number) =>
      fabric.util.getPointOnPath(pathData, distance, data)!,
  };
};

@ShaMan123
Copy link
Contributor Author

so fixing the angles and removing the last entry + refactoring it to expose getPointOnPath etc.

@asturur
Copy link
Member

asturur commented Oct 17, 2023

Please i don't want to add any more classes.

Can we just add an iterate over to fabric.Path so that the intuitive part of:

    getPointOnPath: (distance: number) =>
      fabric.util.getPointOnPath(pathData, distance, data)!,

can be saved from the state of the path?

getPointOnPath: (distance: number) => {
  return fabric.util.getPointOnPath(this.path, distance, this.pathSegmentInfo)!,
}

so instead of creating a path iterator new class you just create a fabric path.

This could have the added benefit that you can either transform the path with the current transformation or not do that.

If you want to use it on a path as string, you can do that by not assigning any properties to the path, otherwise you can add transformation properties to the fabric.Path and get a transformed point.

@ShaMan123
Copy link
Contributor Author

I like that

@asturur
Copy link
Member

asturur commented Oct 17, 2023

the lenght on the last item was a lazy shortcut to get text on a path working.
that util, getPathSegmentsInfo, is not meaningful as public api and can be moved away into internals and change to return something that has an object with correct key/props that make sense.
length is a useful information.

For the transformation part i have no idea how that apply to lenght, i don't think is straight forward at all, maybe path.length is not something we want to expose or we can expose with getTotalLenght that will need to do a full parse of a transformed version of the path before calculating the length ad hoc.

there is some thinkering to do for transformations, but definely a method on fabric.Path seems to be easy to discover, easy to document

@ShaMan123
Copy link
Contributor Author

I don't understand the transform part

@asturur
Copy link
Member

asturur commented Oct 17, 2023

we can talk through it, the transform part is optional.

Like i have a path long 10, how long it is when my path has scaleX by 2? it could be 20, unless the path is a vertical line so is still 10.

this is nice for a relaxing chat maybe

@ShaMan123
Copy link
Contributor Author

I see, I would transform the points and that is it

@ShaMan123
Copy link
Contributor Author

anyone interested in this I suggest using bezier-js for the task

@ShaMan123 ShaMan123 closed this May 6, 2024
@ShaMan123 ShaMan123 deleted the feat-path-animation branch May 6, 2024 15:55
@asturur asturur restored the feat-path-animation branch May 7, 2024 19:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants