feat: notescraft-lottie player crate, egui lottie_icon painter, icon_sheet review example, first icon animation set
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Generated
+1
@@ -2254,6 +2254,7 @@ dependencies = [
|
||||
"notescraft-graph",
|
||||
"notescraft-i18n",
|
||||
"notescraft-index",
|
||||
"notescraft-lottie",
|
||||
"notescraft-markdown",
|
||||
"notescraft-math",
|
||||
"notescraft-templates",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"arrow-down","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"arrow-down","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[4.0,4.0],[20.0,4.0],[20.0,20.0],[4.0,20.0]],"c":true}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"arrow-up","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"arrow-up","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[5.0,12.0],[12.0,5.0],[19.0,12.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[12.0,19.0],[12.0,5.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"bold","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"bold","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[6.0,4.0],[13.0,4.0],[13.0,12.0],[6.0,12.0]],"c":true}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[6.0,12.0],[14.0,12.0],[14.0,20.0],[6.0,20.0]],"c":true}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":12,"w":24,"h":24,"nm":"bookmark","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"bookmark","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":6.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":12,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":6.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":12,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[17.0,3.0],[19.0,5.0],[19.0,20.0],[17.504,20.868],[12.992,18.29],[11.008,18.29],[6.496,20.868],[5.0,20.0],[5.0,5.0],[7.0,3.0]],"c":true}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":12,"st":0,"bm":0}],"markers":[{"cm":"off","tm":0,"dr":0},{"cm":"on","tm":12,"dr":0}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"bug","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"bug","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[8],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[8],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[12.0,20.0],[12.0,11.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[14.0,7.0],[18.0,11.0],[18.0,14.0],[6.0,14.0],[6.0,11.0],[10.0,7.0]],"c":true}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[14.12,3.88],[16.0,2.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[21.0,21.0],[17.19,17.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[21.0,5.0],[17.45,8.97]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[22.0,13.0],[18.0,13.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[3.0,21.0],[6.81,17.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[3.0,5.0],[6.55,8.97]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[6.0,13.0],[2.0,13.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[8.0,2.0],[9.88,3.88]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[9.0,7.13],[9.0,6.0],[15.0,6.0],[15.0,7.13]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"calendar","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"calendar","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[8.0,2.0],[8.0,5.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[16.0,2.0],[16.0,5.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[3.0,9.0],[21.0,9.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"check","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"check","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[20.0,6.0],[9.0,17.0],[4.0,12.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"chevron-down","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"chevron-down","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[6.0,9.0],[12.0,15.0],[18.0,9.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"chevron-left","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"chevron-left","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[15.0,18.0],[9.0,12.0],[15.0,6.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":12,"w":24,"h":24,"nm":"chevron-right","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"chevron-right","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":6.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":12,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":6.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":12,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[9.0,18.0],[15.0,12.0],[9.0,6.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":12,"st":0,"bm":0}],"markers":[{"cm":"off","tm":0,"dr":0},{"cm":"on","tm":12,"dr":0}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"chevron-up","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"chevron-up","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[18.0,15.0],[12.0,9.0],[6.0,15.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"circle-check","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"circle-check","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[16.0,9.0],[10.5,14.5],[8.0,12.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"circle-question-mark","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"circle-question-mark","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[9.09,9.0],[14.92,10.0],[11.92,13.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0]],"o":[[0,0]],"v":[[12.0,17.0]],"c":true}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"circle","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"circle","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[4.0,4.0],[20.0,4.0],[20.0,20.0],[4.0,20.0]],"c":true}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"clipboard","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"clipboard","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[16.0,4.0],[18.0,4.0],[20.0,6.0],[20.0,20.0],[18.0,22.0],[6.0,22.0],[4.0,20.0],[4.0,6.0],[6.0,4.0],[8.0,4.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"copy","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"copy","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[8.0,8.0],[20.0,8.0],[20.0,21.0],[8.0,21.0]],"c":true}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[16.0,8.0],[16.0,3.0],[4.0,3.0],[4.0,16.0],[8.0,16.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"diamond","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"diamond","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[2.7,10.3],[2.7,13.71],[10.29,21.3],[13.7,21.3],[21.29,13.71],[21.29,10.3],[13.7,2.71],[10.29,2.71]],"c":true}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"download","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"download","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[12.0,15.0],[12.0,3.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[21.0,15.0],[21.0,19.0],[19.0,21.0],[5.0,21.0],[3.0,19.0],[3.0,15.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[7.0,10.0],[12.0,15.0],[17.0,10.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"eraser","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"eraser","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[21.0,21.0],[8.0,21.0],[6.58,20.413],[2.586,16.414],[2.586,13.586],[12.586,3.586],[15.415,3.586],[21.414,9.586],[21.414,12.414],[12.834,21.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[5.082,11.09],[13.91,19.918]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"file-text","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"file-text","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[6.0,22.0],[4.0,20.0],[4.0,4.0],[6.0,2.0],[14.0,2.0],[15.704,2.706],[19.292,6.294],[20.0,8.0],[20.0,20.0],[18.0,22.0]],"c":true}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[14.0,2.0],[14.0,7.0],[15.0,8.0],[20.0,8.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[10.0,9.0],[8.0,9.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[16.0,13.0],[8.0,13.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[16.0,17.0],[8.0,17.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"flame","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"flame","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[12.0,3.0],[16.0,9.5],[19.0,15.0],[5.0,15.0],[6.0,12.0],[11.0,12.0],[9.5,7.0]],"c":true}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":12,"w":24,"h":24,"nm":"folder","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"folder","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":6.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":12,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":6.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":12,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[20.0,20.0],[22.0,18.0],[22.0,8.0],[20.0,6.0],[12.1,6.0],[10.41,5.1],[9.6,3.9],[7.93,3.0],[4.0,3.0],[2.0,5.0],[2.0,18.0],[4.0,20.0]],"c":true}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":12,"st":0,"bm":0}],"markers":[{"cm":"off","tm":0,"dr":0},{"cm":"on","tm":12,"dr":0}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"git-branch","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"git-branch","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[8],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[8],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[15.0,6.0],[6.0,15.0],[6.0,3.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"git-commit-horizontal","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"git-commit-horizontal","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[4.0,4.0],[20.0,4.0],[20.0,20.0],[4.0,20.0]],"c":true}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"git-fork","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"git-fork","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[6.0,3.0],[6.0,15.0],[11.0,20.0],[12.0,20.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[18.0,6.0],[18.0,6.1]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[6.0,3.0],[6.0,3.1]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[18.0,9.0],[18.0,11.0],[13.0,16.0],[12.0,16.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"grid-3x3","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"grid-3x3","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[3.0,9.0],[21.0,9.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[3.0,15.0],[21.0,15.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[9.0,3.0],[9.0,21.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[15.0,3.0],[15.0,21.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"hand","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"hand","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[18.0,11.0],[18.0,6.0],[16.0,4.0],[14.0,6.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[14.0,10.0],[14.0,4.0],[12.0,2.0],[10.0,4.0],[10.0,6.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[10.0,10.5],[10.0,6.0],[8.0,4.0],[6.0,6.0],[6.0,14.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[18.0,8.0],[22.0,8.0],[22.0,14.0],[14.0,22.0],[12.0,22.0],[6.01,19.66],[2.41,16.06],[5.24,13.24],[7.0,15.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"highlighter","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"highlighter","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[9.0,11.0],[13.0,15.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[5.0,20.0],[14.0,11.0],[19.0,16.0],[10.0,25.0],[5.0,25.0]],"c":true}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[14.0,7.0],[17.0,4.0],[23.0,10.0],[20.0,13.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"image","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"image","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[21.0,15.0],[17.914,11.914],[15.086,11.914],[6.0,21.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"toggle":["folder","chevron-right","panel-left","panel-right","star","bookmark","lock","sparkles","maximize"],"oneshot":["search","settings","plus","x","check","copy","trash-2","file-text","tag","layers","git-branch","link","list","download","upload","pencil","pen","eraser","hand","mouse-pointer-2","type","square","circle","diamond","move-up-right","slash","image","calendar","clipboard","quote","info","bug","triangle-alert","flame","zap","git-fork","git-commit-horizontal","undo-2","refresh-cw","notebook-pen","layout-dashboard","circle-check","circle-question-mark","bold","italic","strikethrough","highlighter","scissors","message-square-quote","arrow-up","arrow-down","chevron-up","chevron-down","chevron-left","grid-3x3"]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"info","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"info","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[12.0,16.0],[12.0,12.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0]],"o":[[0,0]],"v":[[12.0,8.0]],"c":true}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"italic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"italic","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[10.0,4.0],[20.0,4.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[4.0,20.0],[14.0,20.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[14.0,4.0],[10.0,20.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"layers","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"layers","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[12.83,2.18],[11.17,2.18],[2.6,6.08],[2.6,7.91],[11.18,11.82],[12.84,11.82],[21.42,7.92],[21.42,6.09]],"c":true}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[2.0,12.0],[2.58,12.91],[11.18,16.82],[12.83,16.82],[21.41,12.92],[22.0,12.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[2.0,17.0],[2.58,17.91],[11.18,21.82],[12.83,21.82],[21.41,17.92],[22.0,17.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"layout-dashboard","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"layout-dashboard","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[4.0,4.0],[20.0,4.0],[20.0,20.0],[4.0,20.0]],"c":true}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"link","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"link","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[10.0,13.0],[17.54,13.54],[20.54,10.54],[13.47,3.47],[11.75,5.18]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[14.0,11.0],[6.46,10.46],[3.46,13.46],[10.53,20.53],[12.24,18.82]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"list","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"list","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0]],"o":[[0,0]],"v":[[3.0,5.0]],"c":true}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0]],"o":[[0,0]],"v":[[3.0,12.0]],"c":true}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0]],"o":[[0,0]],"v":[[3.0,19.0]],"c":true}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[8.0,5.0],[21.0,5.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[8.0,12.0],[21.0,12.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[8.0,19.0],[21.0,19.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":12,"w":24,"h":24,"nm":"lock","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"lock","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":6.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":12,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":6.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":12,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[7.0,11.0],[7.0,7.0],[17.0,7.0],[17.0,11.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":12,"st":0,"bm":0}],"markers":[{"cm":"off","tm":0,"dr":0},{"cm":"on","tm":12,"dr":0}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":12,"w":24,"h":24,"nm":"maximize","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"maximize","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":6.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":12,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":6.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":12,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[8.0,3.0],[5.0,3.0],[3.0,5.0],[3.0,8.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[21.0,8.0],[21.0,5.0],[19.0,3.0],[16.0,3.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[3.0,16.0],[3.0,19.0],[5.0,21.0],[8.0,21.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16.0,21.0],[19.0,21.0],[21.0,19.0],[21.0,16.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":12,"st":0,"bm":0}],"markers":[{"cm":"off","tm":0,"dr":0},{"cm":"on","tm":12,"dr":0}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"message-square-quote","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"message-square-quote","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[4.0,4.0],[20.0,4.0],[20.0,17.0],[9.0,17.0],[4.0,21.0]],"c":true}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[8.0,9.0],[11.0,9.0],[11.0,12.0],[9.0,12.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[14.0,9.0],[17.0,9.0],[17.0,12.0],[15.0,12.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"mouse-pointer-2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"mouse-pointer-2","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[4.037,4.688],[4.688,4.037],[20.688,10.537],[20.625,11.484],[14.501,13.064],[13.063,14.499],[11.484,20.625],[10.537,20.688]],"c":true}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"move-up-right","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"move-up-right","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[13.0,5.0],[19.0,5.0],[19.0,11.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[19.0,5.0],[5.0,19.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"notebook-pen","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"notebook-pen","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[13.4,2.0],[6.0,2.0],[4.0,4.0],[4.0,20.0],[6.0,22.0],[18.0,22.0],[20.0,20.0],[20.0,12.6]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[2.0,6.0],[6.0,6.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[2.0,10.0],[6.0,10.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[2.0,14.0],[6.0,14.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[2.0,18.0],[6.0,18.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[21.378,5.626],[18.374,2.622],[13.364,7.634],[12.858,8.488],[12.021,11.358],[12.641,11.978],[15.511,11.141],[16.365,10.635]],"c":true}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":12,"w":24,"h":24,"nm":"panel-left","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"panel-left","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":6.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":12,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":6.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":12,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[9.0,3.0],[9.0,21.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":12,"st":0,"bm":0}],"markers":[{"cm":"off","tm":0,"dr":0},{"cm":"on","tm":12,"dr":0}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":12,"w":24,"h":24,"nm":"panel-right","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"panel-right","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":6.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":12,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":6.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":12,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[15.0,3.0],[15.0,21.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":12,"st":0,"bm":0}],"markers":[{"cm":"off","tm":0,"dr":0},{"cm":"on","tm":12,"dr":0}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"pen","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"pen","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[8],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[8],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[21.174,6.812],[17.188,2.825],[3.842,16.174],[3.342,17.004],[2.021,21.356],[2.644,21.978],[6.997,20.658],[7.827,20.161]],"c":true}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"pencil","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"pencil","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[8],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[8],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[21.174,6.812],[17.188,2.825],[3.842,16.174],[3.342,17.004],[2.021,21.356],[2.644,21.978],[6.997,20.658],[7.827,20.161]],"c":true}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[15.0,5.0],[19.0,9.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"plus","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"plus","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[8],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[8],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[5.0,12.0],[19.0,12.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[12.0,5.0],[12.0,19.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"quote","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"quote","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[16.0,3.0],[14.0,5.0],[14.0,11.0],[16.0,13.0],[17.0,14.0],[17.0,15.0],[15.0,17.0],[14.0,18.0],[14.0,20.0],[15.0,21.0],[21.0,15.0],[21.0,5.0],[19.0,3.0]],"c":true}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[5.0,3.0],[3.0,5.0],[3.0,11.0],[5.0,13.0],[6.0,14.0],[6.0,15.0],[4.0,17.0],[3.0,18.0],[3.0,20.0],[4.0,21.0],[10.0,15.0],[10.0,5.0],[8.0,3.0]],"c":true}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"refresh-cw","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"refresh-cw","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[360],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[360]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[3.0,12.0],[12.0,3.0],[18.74,5.74],[21.0,8.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[21.0,3.0],[21.0,8.0],[16.0,8.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[21.0,12.0],[12.0,21.0],[5.26,18.26],[3.0,16.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[8.0,16.0],[3.0,16.0],[3.0,21.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"scissors","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"scissors","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[6.0,6.0],[6.0,6.1]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[6.0,18.0],[6.0,18.1]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[8.0,7.0],[20.0,19.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[8.0,17.0],[13.0,12.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[14.0,10.0],[20.0,4.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"search","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"search","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[21.0,21.0],[16.66,16.66]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"settings","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"settings","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[8],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[8],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[9.671,4.136],[14.33,4.136],[17.649,6.051],[19.979,10.084],[19.979,13.915],[17.649,17.948],[14.33,19.863],[9.671,19.863],[6.351,17.948],[4.021,13.915],[4.021,10.084],[6.35,6.051]],"c":true}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"slash","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"slash","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[22.0,2.0],[2.0,22.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":12,"w":24,"h":24,"nm":"sparkles","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"sparkles","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":6.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":12,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":6.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":12,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[11.017,2.814],[12.983,2.814],[14.034,8.372],[15.628,9.966],[21.186,11.017],[21.186,12.983],[15.628,14.034],[14.034,15.628],[12.983,21.186],[11.017,21.186],[9.966,15.628],[8.372,14.034],[2.814,12.983],[2.814,11.017],[8.372,9.966],[9.966,8.372]],"c":true}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[20.0,2.0],[20.0,6.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[22.0,4.0],[18.0,4.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":12,"st":0,"bm":0}],"markers":[{"cm":"off","tm":0,"dr":0},{"cm":"on","tm":12,"dr":0}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"square","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"square","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[4.0,4.0],[20.0,4.0],[20.0,20.0],[4.0,20.0]],"c":true}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":12,"w":24,"h":24,"nm":"star","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"star","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":6.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":12,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":6.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":12,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[11.525,2.295],[12.475,2.295],[14.785,6.974],[16.38,8.134],[21.546,8.89],[21.84,9.794],[18.104,13.432],[17.493,15.31],[18.375,20.45],[17.604,21.01],[12.986,18.582],[11.013,18.582],[6.396,21.01],[5.626,20.45],[6.507,15.311],[5.896,13.432],[2.16,9.795],[2.454,8.889],[7.619,8.134],[9.216,6.974]],"c":true}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":12,"st":0,"bm":0}],"markers":[{"cm":"off","tm":0,"dr":0},{"cm":"on","tm":12,"dr":0}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"strikethrough","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"strikethrough","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[4.0,12.0],[20.0,12.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[17.0,7.0],[13.0,4.0],[9.0,4.0],[8.0,11.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[7.0,17.0],[11.0,20.0],[15.0,20.0],[18.0,18.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"tag","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"tag","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[8],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[8],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[12.586,2.586],[11.172,2.0],[4.0,2.0],[2.0,4.0],[2.0,11.172],[2.586,12.586],[11.29,21.29],[14.71,21.29],[21.29,14.71],[21.29,11.29]],"c":true}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"trash-2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"trash-2","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[3.0,6.0],[21.0,6.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[19.0,6.0],[19.0,20.0],[17.0,22.0],[7.0,22.0],[5.0,20.0],[5.0,6.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[8.0,6.0],[8.0,4.0],[10.0,2.0],[14.0,2.0],[16.0,4.0],[16.0,6.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"triangle-alert","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"triangle-alert","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[8],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[8],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[21.73,18.0],[13.73,4.0],[10.25,4.0],[2.25,18.0],[4.0,21.0],[20.0,21.0]],"c":true}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[12.0,9.0],[12.0,13.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0]],"o":[[0,0]],"v":[[12.0,17.0]],"c":true}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"type","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"type","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[12.0,4.0],[12.0,20.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[4.0,7.0],[4.0,5.0],[5.0,4.0],[19.0,4.0],[20.0,5.0],[20.0,7.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[9.0,20.0],[15.0,20.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"undo-2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"undo-2","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[9.0,14.0],[4.0,9.0],[9.0,4.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[4.0,9.0],[14.5,9.0],[20.0,14.5],[14.5,20.0],[11.0,20.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"upload","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"upload","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[12.0,3.0],[12.0,15.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[17.0,8.0],[12.0,3.0],[7.0,8.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[21.0,15.0],[21.0,19.0],[19.0,21.0],[5.0,21.0],[3.0,19.0],[3.0,15.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"x","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"x","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[8],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[8],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[18.0,6.0],[6.0,18.0]],"c":false}},"nm":"Path"},{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[6.0,6.0],[18.0,18.0]],"c":false}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"v":"5.7.4","fr":60,"ip":0,"op":18,"w":24,"h":24,"nm":"zap","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"zap","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[0],"e":[0],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[0]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"e":[102,102,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":9.0,"s":[102,102,100],"e":[100,100,100],"i":{"x":[0.42],"y":[0]},"o":{"x":[0.58],"y":[1]}},{"t":18,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[15.914,4.0],[13.44,2.439],[4.44,11.439],[5.5,14.0],[9.502,14.0],[9.973,14.666],[8.086,20.0],[10.561,21.56],[19.561,12.56],[18.5,10.0],[14.503,10.0],[14.031,9.333]],"c":true}},"nm":"Path"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.75},"lc":2,"lj":2,"nm":"Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}],"nm":"Icon"}],"ip":0,"op":18,"st":0,"bm":0}],"markers":[{"cm":"play","tm":0,"dr":18}]}
|
||||
@@ -0,0 +1,77 @@
|
||||
//! Renderer-independent draw list, in animation pixel coordinates.
|
||||
|
||||
/// Stroke endpoint decoration.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Cap {
|
||||
/// Flat endpoint.
|
||||
Butt,
|
||||
/// Semicircular endpoint.
|
||||
Round,
|
||||
/// Square projecting endpoint.
|
||||
Square,
|
||||
}
|
||||
|
||||
/// Stroke corner decoration.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Join {
|
||||
/// Extended corner.
|
||||
Miter,
|
||||
/// Circular corner.
|
||||
Round,
|
||||
/// Clipped corner.
|
||||
Bevel,
|
||||
}
|
||||
|
||||
/// Flattened path in animation pixels.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Polyline {
|
||||
/// Vertices in traversal order; a closed path has an implicit closing edge.
|
||||
pub points: Vec<[f32; 2]>,
|
||||
/// Whether the final vertex connects to the first.
|
||||
pub closed: bool,
|
||||
}
|
||||
|
||||
/// Host-tinted stroke geometry.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct StrokePath {
|
||||
/// Paths sharing this stroke.
|
||||
pub polylines: Vec<Polyline>,
|
||||
/// Stroke width in animation pixels, scaled by the composed transform's
|
||||
/// geometric mean scale (including reflections and nonuniform scales).
|
||||
pub width: f32,
|
||||
/// Endpoint decoration.
|
||||
pub cap: Cap,
|
||||
/// Corner decoration.
|
||||
pub join: Join,
|
||||
/// Combined layer, group and stroke opacity in `0..=1`.
|
||||
pub opacity: f32,
|
||||
}
|
||||
|
||||
/// Host-tinted fill geometry.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct FillPath {
|
||||
/// Paths sharing this fill.
|
||||
pub polylines: Vec<Polyline>,
|
||||
/// Combined layer, group and fill opacity in `0..=1`.
|
||||
pub opacity: f32,
|
||||
}
|
||||
|
||||
/// One painter operation.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Item {
|
||||
/// Stroke the paths.
|
||||
Stroke(StrokePath),
|
||||
/// Fill the paths using the nonzero winding rule.
|
||||
Fill(FillPath),
|
||||
}
|
||||
|
||||
/// Evaluated draw list, in painter order (bottom first).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Frame {
|
||||
/// Animation width in pixels.
|
||||
pub width: f32,
|
||||
/// Animation height in pixels.
|
||||
pub height: f32,
|
||||
/// Painter operations.
|
||||
pub items: Vec<Item>,
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
//! Structured failures at the untrusted JSON boundary.
|
||||
|
||||
/// A malformed, unsupported, or over-budget animation.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// The source exceeds the 512 KiB limit.
|
||||
#[error("lottie: source exceeds 512 KiB")]
|
||||
TooLarge,
|
||||
/// The document is not valid JSON.
|
||||
#[error("lottie: invalid JSON: {message}")]
|
||||
Json {
|
||||
/// JSON decoder diagnostic.
|
||||
message: String,
|
||||
},
|
||||
/// A required value has the wrong type or range.
|
||||
#[error("lottie: invalid {what} at {at}")]
|
||||
Invalid {
|
||||
/// Property or violated invariant.
|
||||
what: String,
|
||||
/// Layer name/index and property path.
|
||||
at: String,
|
||||
},
|
||||
/// A feature or resource budget lies outside the supported subset.
|
||||
#[error("lottie: unsupported {what} at {at}")]
|
||||
Unsupported {
|
||||
/// Unsupported feature or exceeded resource budget.
|
||||
what: String,
|
||||
/// Layer name/index and property path.
|
||||
at: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) fn invalid(what: impl Into<String>, at: &str) -> Error {
|
||||
Error::Invalid { what: what.into(), at: at.to_owned() }
|
||||
}
|
||||
|
||||
pub(crate) fn unsupported(what: impl Into<String>, at: &str) -> Error {
|
||||
Error::Unsupported { what: what.into(), at: at.to_owned() }
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
//! Evaluates validated shape layers into renderer-independent paths.
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::model::{Layer, Matrix, Position, Shape, Transform};
|
||||
use crate::{Animation, FillPath, Frame, Item, Polyline, StrokePath, flatten, trim};
|
||||
|
||||
const PERCENT: f32 = 100.0;
|
||||
const MAX_PARENT_DEPTH: usize = 64;
|
||||
|
||||
struct Geometry {
|
||||
paths: Vec<Polyline>,
|
||||
opacity: f32,
|
||||
scale: f32,
|
||||
}
|
||||
|
||||
struct Paint<'a> {
|
||||
shape: &'a Shape,
|
||||
geometry: Range<usize>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Scene<'a> {
|
||||
geometry: Vec<Geometry>,
|
||||
paints: Vec<Paint<'a>>,
|
||||
}
|
||||
|
||||
pub(crate) fn frame(animation: &Animation, requested: f32) -> Frame {
|
||||
let time = if requested.is_finite() { requested.clamp(animation.first, animation.last) } else { animation.first };
|
||||
let mut output = Frame { width: animation.width, height: animation.height, items: Vec::new() };
|
||||
for layer in animation.layers.iter().rev() {
|
||||
// `op` is exclusive, but the animation's own last frame must still show the layers that run to the end.
|
||||
let ends_here = time >= animation.last && layer.last >= animation.last;
|
||||
if layer.hidden || time < layer.first || (time >= layer.last && !ends_here) {
|
||||
continue;
|
||||
}
|
||||
let local_time = time - layer.start;
|
||||
let (local, opacity) = transform(&layer.transform, local_time);
|
||||
let matrix = parents(animation, layer, time).then(local);
|
||||
let mut scene = Scene::default();
|
||||
scene.collect(&layer.shapes, local_time, matrix, opacity);
|
||||
scene.paint(local_time, &mut output.items);
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn parents(animation: &Animation, layer: &Layer, time: f32) -> Matrix {
|
||||
let mut chain = Vec::new();
|
||||
let mut parent_id = layer.parent;
|
||||
for _ in 0..MAX_PARENT_DEPTH {
|
||||
let Some(parent) = parent_id.and_then(|id| animation.layers.iter().find(|candidate| candidate.id == id)) else {
|
||||
break;
|
||||
};
|
||||
chain.push(parent);
|
||||
parent_id = parent.parent;
|
||||
}
|
||||
chain.into_iter().rev().fold(Matrix::IDENTITY, |matrix, parent| matrix.then(transform(&parent.transform, time - parent.start).0))
|
||||
}
|
||||
|
||||
fn transform(input: &Transform, time: f32) -> (Matrix, f32) {
|
||||
let [ax, ay] = pair(&input.anchor.at(time));
|
||||
let [px, py] = match &input.position {
|
||||
Position::Combined(position) => pair(&position.at(time)),
|
||||
Position::Split(x, y) => [x.scalar(time), y.scalar(time)],
|
||||
};
|
||||
let [sx, sy] = pair(&input.scale.at(time)).map(|value| value / PERCENT);
|
||||
let (sin, cos) = input.rotation.scalar(time).to_radians().sin_cos();
|
||||
let [a, b, c, d] = [cos * sx, sin * sx, -sin * sy, cos * sy];
|
||||
(Matrix([a, b, c, d, px - a * ax - c * ay, py - b * ax - d * ay]), input.opacity.scalar(time) / PERCENT)
|
||||
}
|
||||
|
||||
fn pair(values: &[f32]) -> [f32; 2] {
|
||||
let mut values = values.iter().copied();
|
||||
[values.next().unwrap_or_default(), values.next().unwrap_or_default()]
|
||||
}
|
||||
|
||||
impl<'a> Scene<'a> {
|
||||
fn collect(&mut self, shapes: &'a [Shape], time: f32, matrix: Matrix, opacity: f32) {
|
||||
let first = self.geometry.len();
|
||||
for shape in shapes {
|
||||
let bezier = match shape {
|
||||
Shape::Path(path) => path.at(time),
|
||||
Shape::Rectangle { position, size, radius, reversed } => {
|
||||
let mut path = flatten::rectangle(pair(&position.at(time)), pair(&size.at(time)), radius.scalar(time));
|
||||
if *reversed {
|
||||
reverse(&mut path);
|
||||
}
|
||||
path
|
||||
}
|
||||
Shape::Ellipse { position, size, reversed } => {
|
||||
let mut path = flatten::ellipse(pair(&position.at(time)), pair(&size.at(time)));
|
||||
if *reversed {
|
||||
reverse(&mut path);
|
||||
}
|
||||
path
|
||||
}
|
||||
Shape::Group(children, input) => {
|
||||
let (local, alpha) = transform(input, time);
|
||||
self.collect(children, time, matrix.then(local), opacity * alpha);
|
||||
continue;
|
||||
}
|
||||
Shape::Stroke { .. } | Shape::Fill { .. } => {
|
||||
self.paints.push(Paint { shape, geometry: first..self.geometry.len() });
|
||||
continue;
|
||||
}
|
||||
Shape::Trim { start, end, offset } => {
|
||||
self.trim(first, start.scalar(time), end.scalar(time), offset.scalar(time));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let [a, b, c, d, _, _] = matrix.0;
|
||||
self.geometry.push(Geometry { paths: vec![flatten::path(&bezier, matrix)], opacity, scale: (a * d - b * c).abs().sqrt() });
|
||||
}
|
||||
}
|
||||
|
||||
fn trim(&mut self, first: usize, start: f32, end: f32, offset: f32) {
|
||||
for geometry in self.geometry.iter_mut().skip(first) {
|
||||
let paths = std::mem::take(&mut geometry.paths).into_iter().enumerate().collect();
|
||||
geometry.paths = trim::apply_tagged(paths, start, end, offset).into_iter().map(|(_, path)| path).collect();
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&self, time: f32, items: &mut Vec<Item>) {
|
||||
for paint in self.paints.iter().rev() {
|
||||
let is_fill = matches!(paint.shape, Shape::Fill { .. });
|
||||
for batch in self
|
||||
.geometry
|
||||
.get(paint.geometry.clone())
|
||||
.unwrap_or_default()
|
||||
.chunk_by(|a, b| a.opacity.to_bits() == b.opacity.to_bits() && (is_fill || a.scale.to_bits() == b.scale.to_bits()))
|
||||
{
|
||||
let Some(geometry) = batch.first() else {
|
||||
continue;
|
||||
};
|
||||
if geometry.opacity <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let polylines: Vec<_> = batch.iter().flat_map(|geometry| geometry.paths.iter().cloned()).collect();
|
||||
if polylines.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let item = match paint.shape {
|
||||
Shape::Stroke { width, opacity, cap, join } => {
|
||||
let width = width.scalar(time) * geometry.scale;
|
||||
let opacity = opacity.scalar(time) / PERCENT * geometry.opacity;
|
||||
if width <= 0.0 || opacity <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
Item::Stroke(StrokePath { polylines, width, opacity, cap: *cap, join: *join })
|
||||
}
|
||||
Shape::Fill { opacity } => {
|
||||
let opacity = opacity.scalar(time) / PERCENT * geometry.opacity;
|
||||
if opacity <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
Item::Fill(FillPath { polylines, opacity })
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reverse(path: &mut crate::model::Bezier) {
|
||||
path.vertices.reverse();
|
||||
path.incoming.reverse();
|
||||
path.outgoing.reverse();
|
||||
std::mem::swap(&mut path.incoming, &mut path.outgoing);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "eval_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,137 @@
|
||||
//! Evaluation regression tests.
|
||||
|
||||
use super::*;
|
||||
use crate::model::{Key, Position, Track};
|
||||
|
||||
fn track(values: &[f32]) -> Track {
|
||||
Track { keys: vec![Key { time: 0.0, value: values.to_vec(), incoming: Vec::new(), outgoing: Vec::new(), hold: false }] }
|
||||
}
|
||||
|
||||
fn identity() -> Transform {
|
||||
Transform {
|
||||
anchor: track(&[0.0, 0.0]),
|
||||
position: Position::Combined(track(&[0.0, 0.0])),
|
||||
scale: track(&[100.0, 100.0]),
|
||||
rotation: track(&[0.0]),
|
||||
opacity: track(&[100.0]),
|
||||
}
|
||||
}
|
||||
|
||||
fn rectangle() -> Shape {
|
||||
Shape::Rectangle { position: track(&[0.0, 0.0]), size: track(&[20.0, 10.0]), radius: track(&[0.0]), reversed: false }
|
||||
}
|
||||
|
||||
fn layer(id: i64, shapes: Vec<Shape>) -> Layer {
|
||||
Layer { id, parent: None, first: 0.0, last: 20.0, start: 0.0, hidden: false, transform: identity(), shapes }
|
||||
}
|
||||
|
||||
fn animation(layers: Vec<Layer>) -> Animation {
|
||||
Animation { width: 100.0, height: 100.0, rate: 30.0, first: 0.0, last: 20.0, layers, markers: Vec::new() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_applies_anchor_scale_rotation_then_position() {
|
||||
let mut input = identity();
|
||||
input.anchor = track(&[1.0, 2.0]);
|
||||
input.scale = track(&[200.0, 300.0]);
|
||||
input.rotation = track(&[90.0]);
|
||||
input.position = Position::Combined(track(&[10.0, 20.0]));
|
||||
input.opacity = track(&[50.0]);
|
||||
let (matrix, opacity) = transform(&input, 0.0);
|
||||
let [x, y] = matrix.apply([2.0, 3.0]);
|
||||
assert!((x - 7.0).abs() < 0.0001);
|
||||
assert!((y - 22.0).abs() < 0.0001);
|
||||
assert!((opacity - 0.5).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_and_style_opacities_multiply() {
|
||||
let mut group = identity();
|
||||
group.opacity = track(&[50.0]);
|
||||
let mut input = layer(1, vec![Shape::Group(vec![rectangle()], group), Shape::Fill { opacity: track(&[50.0]) }]);
|
||||
input.transform.opacity = track(&[50.0]);
|
||||
let output = frame(&animation(vec![input]), 0.0);
|
||||
let Some(Item::Fill(fill)) = output.items.first() else {
|
||||
panic!("expected fill");
|
||||
};
|
||||
assert!((fill.opacity - 0.125).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn later_trim_also_affects_earlier_style() {
|
||||
let input =
|
||||
layer(1, vec![rectangle(), Shape::Fill { opacity: track(&[100.0]) }, Shape::Trim { start: track(&[0.0]), end: track(&[0.0]), offset: track(&[0.0]) }]);
|
||||
assert_eq!(frame(&animation(vec![input]), 0.0).items, Vec::<Item>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layer_interval_excludes_out_point_and_hidden_layers() {
|
||||
let mut input = layer(1, vec![rectangle(), Shape::Fill { opacity: track(&[100.0]) }]);
|
||||
input.last = 10.0;
|
||||
let mut hidden = input.clone();
|
||||
hidden.hidden = true;
|
||||
assert_eq!(frame(&animation(vec![input, hidden]), 10.0).items, Vec::<Item>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parent_transform_composes_without_parent_opacity() {
|
||||
let mut parent = layer(1, Vec::new());
|
||||
parent.transform.position = Position::Combined(track(&[30.0, 40.0]));
|
||||
parent.transform.opacity = track(&[0.0]);
|
||||
let mut child = layer(2, vec![rectangle(), Shape::Fill { opacity: track(&[100.0]) }]);
|
||||
child.parent = Some(1);
|
||||
let output = frame(&animation(vec![parent, child]), 0.0);
|
||||
let Some(Item::Fill(fill)) = output.items.first() else {
|
||||
panic!("expected child fill");
|
||||
};
|
||||
assert!(fill.polylines.iter().flat_map(|line| &line.points).all(|point| point.first().is_some_and(|x| *x >= 20.0)));
|
||||
assert!((fill.opacity - 1.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parent_chain_composes_in_validated_order_without_intermediate_overflow() {
|
||||
let mut outer = layer(0, Vec::new());
|
||||
outer.transform.scale = track(&[1e-42, 1e-42]);
|
||||
let mut layers = vec![outer];
|
||||
for id in 1..=11 {
|
||||
let shapes = if id == 11 { vec![rectangle(), Shape::Fill { opacity: track(&[100.0]) }] } else { Vec::new() };
|
||||
let mut child = layer(id, shapes);
|
||||
child.parent = Some(id - 1);
|
||||
child.transform.scale = track(&[1_000_000.0, 1_000_000.0]);
|
||||
layers.push(child);
|
||||
}
|
||||
let animation = animation(layers);
|
||||
assert!(crate::validate::animation(&animation).is_ok());
|
||||
let output = frame(&animation, 0.0);
|
||||
assert!(output.items.iter().all(|item| match item {
|
||||
Item::Fill(fill) => fill.polylines.iter().flat_map(|line| line.points.iter().flatten()).all(|value| value.is_finite()),
|
||||
Item::Stroke(stroke) => stroke.polylines.iter().flat_map(|line| line.points.iter().flatten()).all(|value| value.is_finite()),
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn later_styles_paint_below_earlier_styles() {
|
||||
let input = layer(
|
||||
1,
|
||||
vec![
|
||||
rectangle(),
|
||||
Shape::Stroke { width: track(&[2.0]), opacity: track(&[100.0]), cap: crate::Cap::Butt, join: crate::Join::Miter },
|
||||
Shape::Fill { opacity: track(&[100.0]) },
|
||||
],
|
||||
);
|
||||
let output = frame(&animation(vec![input]), 0.0);
|
||||
assert!(matches!(output.items.as_slice(), [Item::Fill(_), Item::Stroke(_)]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simultaneous_trim_applies_percentage_to_each_original_shape() {
|
||||
let wider = Shape::Rectangle { position: track(&[0.0, 0.0]), size: track(&[40.0, 10.0]), radius: track(&[0.0]), reversed: false };
|
||||
let input = layer(
|
||||
1,
|
||||
vec![rectangle(), wider, Shape::Trim { start: track(&[0.0]), end: track(&[50.0]), offset: track(&[0.0]) }, Shape::Fill { opacity: track(&[100.0]) }],
|
||||
);
|
||||
let output = frame(&animation(vec![input]), 0.0);
|
||||
let Some(Item::Fill(fill)) = output.items.first() else { panic!("expected fill") };
|
||||
assert_eq!(fill.polylines.len(), 2);
|
||||
assert!(fill.polylines.iter().all(|line| line.points.last().is_some_and(|[_, y]| (*y - 5.0).abs() < 0.001)));
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
//! Cubic path flattening and primitive shape construction.
|
||||
|
||||
use crate::Polyline;
|
||||
use crate::model::{Bezier, Matrix};
|
||||
|
||||
const TOLERANCE: f64 = 0.05;
|
||||
const ELLIPSE_TANGENT: f32 = 0.552_284_8;
|
||||
const MAX_SUBDIVISION_DEPTH: u32 = 256;
|
||||
type Cubic = [[f32; 2]; 4];
|
||||
|
||||
/// Flattens a validated path to at most 0.05 transformed pixels of cubic error.
|
||||
pub(crate) fn path(shape: &Bezier, matrix: Matrix) -> Polyline {
|
||||
let mut points = Vec::new();
|
||||
let scale = matrix.scale();
|
||||
if let Some(first) = shape.vertices.first() {
|
||||
points.push(matrix.apply(*first));
|
||||
}
|
||||
for curve in curves(shape) {
|
||||
let remaining = depth(curve, scale);
|
||||
let mut pending = vec![(curve.map(|point| matrix.apply(point)), remaining)];
|
||||
while let Some((curve, remaining)) = pending.pop() {
|
||||
let [a, b, c, d] = curve;
|
||||
if remaining == 0 || distance(b, a, d).max(distance(c, a, d)) <= TOLERANCE {
|
||||
points.push(d);
|
||||
continue;
|
||||
}
|
||||
let ab = midpoint(a, b);
|
||||
let bc = midpoint(b, c);
|
||||
let cd = midpoint(c, d);
|
||||
let abc = midpoint(ab, bc);
|
||||
let bcd = midpoint(bc, cd);
|
||||
let center = midpoint(abc, bcd);
|
||||
pending.push(([center, bcd, cd, d], remaining - 1));
|
||||
pending.push(([a, ab, abc, center], remaining - 1));
|
||||
}
|
||||
}
|
||||
Polyline { points, closed: shape.closed }
|
||||
}
|
||||
|
||||
/// Bounds output points for a transform whose operator norm is at most `scale`.
|
||||
pub(crate) fn estimate(shape: &Bezier, scale: f32) -> usize {
|
||||
curves(shape)
|
||||
.fold(usize::from(!shape.vertices.is_empty()), |total, curve| total.saturating_add(1_usize.checked_shl(depth(curve, scale)).unwrap_or(usize::MAX)))
|
||||
}
|
||||
|
||||
fn curves(shape: &Bezier) -> impl Iterator<Item = Cubic> + '_ {
|
||||
let nodes = shape.vertices.iter().zip(&shape.incoming).zip(&shape.outgoing).map(|((vertex, incoming), outgoing)| (*vertex, *incoming, *outgoing));
|
||||
let final_node = nodes.clone().next().filter(|_| shape.closed);
|
||||
nodes.clone().zip(nodes.skip(1).chain(final_node)).map(|((a, _, outgoing), (d, incoming, _))| [a, add(a, outgoing), add(d, incoming), d])
|
||||
}
|
||||
|
||||
fn depth([a, b, c, d]: Cubic, scale: f32) -> u32 {
|
||||
let second = |[ax, ay]: [f32; 2], [bx, by]: [f32; 2], [cx, cy]: [f32; 2]| {
|
||||
(f64::from(ax) - 2.0 * f64::from(bx) + f64::from(cx)).hypot(f64::from(ay) - 2.0 * f64::from(by) + f64::from(cy))
|
||||
};
|
||||
// Linear interpolation error is bounded by max |B''| / 8.
|
||||
let mut error = 0.75 * second(a, b, c).max(second(b, c, d)) * f64::from(scale.abs());
|
||||
let mut depth = 0;
|
||||
while error > TOLERANCE && depth < MAX_SUBDIVISION_DEPTH {
|
||||
error *= 0.25;
|
||||
depth += 1;
|
||||
}
|
||||
depth
|
||||
}
|
||||
|
||||
fn add([ax, ay]: [f32; 2], [bx, by]: [f32; 2]) -> [f32; 2] {
|
||||
[ax + bx, ay + by]
|
||||
}
|
||||
|
||||
fn midpoint([ax, ay]: [f32; 2], [bx, by]: [f32; 2]) -> [f32; 2] {
|
||||
[ax * 0.5 + bx * 0.5, ay * 0.5 + by * 0.5]
|
||||
}
|
||||
|
||||
fn distance([px, py]: [f32; 2], [ax, ay]: [f32; 2], [bx, by]: [f32; 2]) -> f64 {
|
||||
let dx = f64::from(bx) - f64::from(ax);
|
||||
let dy = f64::from(by) - f64::from(ay);
|
||||
let x = f64::from(px) - f64::from(ax);
|
||||
let y = f64::from(py) - f64::from(ay);
|
||||
let length = dx * dx + dy * dy;
|
||||
let t = if length == 0.0 { 0.0 } else { ((x * dx + y * dy) / length).clamp(0.0, 1.0) };
|
||||
(x - dx * t).hypot(y - dy * t)
|
||||
}
|
||||
|
||||
/// Creates a clockwise rectangle, limiting corner radius to its half dimensions.
|
||||
pub(crate) fn rectangle([x, y]: [f32; 2], [width, height]: [f32; 2], radius: f32) -> Bezier {
|
||||
let left = x - width * 0.5;
|
||||
let right = x + width * 0.5;
|
||||
let top = y - height * 0.5;
|
||||
let bottom = y + height * 0.5;
|
||||
let r = radius.min(width.abs() * 0.5).min(height.abs() * 0.5);
|
||||
if r == 0.0 {
|
||||
return Bezier {
|
||||
closed: true,
|
||||
vertices: vec![[left, top], [right, top], [right, bottom], [left, bottom]],
|
||||
incoming: vec![[0.0; 2]; 4],
|
||||
outgoing: vec![[0.0; 2]; 4],
|
||||
};
|
||||
}
|
||||
let tangent = r * ELLIPSE_TANGENT;
|
||||
Bezier {
|
||||
closed: true,
|
||||
vertices: vec![
|
||||
[left + r, top],
|
||||
[right - r, top],
|
||||
[right, top + r],
|
||||
[right, bottom - r],
|
||||
[right - r, bottom],
|
||||
[left + r, bottom],
|
||||
[left, bottom - r],
|
||||
[left, top + r],
|
||||
],
|
||||
incoming: vec![[-tangent, 0.0], [0.0; 2], [0.0, -tangent], [0.0; 2], [tangent, 0.0], [0.0; 2], [0.0, tangent], [0.0; 2]],
|
||||
outgoing: vec![[0.0; 2], [tangent, 0.0], [0.0; 2], [0.0, tangent], [0.0; 2], [-tangent, 0.0], [0.0; 2], [0.0, -tangent]],
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the standard four-cubic clockwise Lottie ellipse.
|
||||
pub(crate) fn ellipse([x, y]: [f32; 2], [width, height]: [f32; 2]) -> Bezier {
|
||||
let rx = width * 0.5;
|
||||
let ry = height * 0.5;
|
||||
let tx = rx * ELLIPSE_TANGENT;
|
||||
let ty = ry * ELLIPSE_TANGENT;
|
||||
Bezier {
|
||||
closed: true,
|
||||
vertices: vec![[x, y - ry], [x + rx, y], [x, y + ry], [x - rx, y]],
|
||||
incoming: vec![[-tx, 0.0], [0.0, -ty], [tx, 0.0], [0.0, ty]],
|
||||
outgoing: vec![[tx, 0.0], [0.0, ty], [-tx, 0.0], [0.0, -ty]],
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ellipse, estimate, path, rectangle};
|
||||
use crate::model::{Bezier, Matrix};
|
||||
|
||||
fn identity() -> Matrix {
|
||||
Matrix([1.0, 0.0, 0.0, 1.0, 0.0, 0.0])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn straight_path_keeps_endpoints_and_transform() {
|
||||
let shape = Bezier { closed: false, vertices: vec![[0.0, 0.0], [10.0, 0.0]], incoming: vec![[0.0; 2]; 2], outgoing: vec![[0.0; 2]; 2] };
|
||||
let result = path(&shape, Matrix([2.0, 0.0, 0.0, 2.0, 3.0, 4.0]));
|
||||
assert_eq!(result.points, vec![[3.0, 4.0], [23.0, 4.0]]);
|
||||
assert!(!result.closed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curved_path_deviates_less_than_tolerance() {
|
||||
let shape = Bezier {
|
||||
closed: false,
|
||||
vertices: vec![[0.0, 0.0], [100.0, 0.0]],
|
||||
incoming: vec![[0.0, 0.0], [0.0, 100.0]],
|
||||
outgoing: vec![[0.0, 100.0], [0.0, 0.0]],
|
||||
};
|
||||
let result = path(&shape, identity());
|
||||
assert!(result.points.len() > 10);
|
||||
assert!(estimate(&shape, 1.0) >= result.points.len());
|
||||
for step in 0..=1000 {
|
||||
let parameter = f64::from(step) / 1000.0;
|
||||
let x = 100.0 * parameter * parameter * (3.0 - 2.0 * parameter);
|
||||
let y = 300.0 * parameter * (1.0 - parameter);
|
||||
let distance = result
|
||||
.points
|
||||
.windows(2)
|
||||
.map(|pair| {
|
||||
let [a, b] = pair else {
|
||||
return f64::INFINITY;
|
||||
};
|
||||
let dx = f64::from(b[0] - a[0]);
|
||||
let dy = f64::from(b[1] - a[1]);
|
||||
let fraction = (((x - f64::from(a[0])) * dx + (y - f64::from(a[1])) * dy) / (dx * dx + dy * dy)).clamp(0.0, 1.0);
|
||||
(x - f64::from(a[0]) - dx * fraction).hypot(y - f64::from(a[1]) - dy * fraction)
|
||||
})
|
||||
.fold(f64::INFINITY, f64::min);
|
||||
assert!(distance <= 0.05, "deviation {distance}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collinear_cubic_preserves_reversal() {
|
||||
let shape =
|
||||
Bezier { closed: false, vertices: vec![[0.0; 2], [1.0, 0.0]], incoming: vec![[0.0; 2], [-101.0, 0.0]], outgoing: vec![[100.0, 0.0], [0.0; 2]] };
|
||||
let result = path(&shape, identity());
|
||||
assert!(result.points.iter().any(|[x, _]| *x > 20.0));
|
||||
assert!(result.points.iter().any(|[x, _]| *x < -20.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancelling_translation_retains_subpixel_curve_precision() {
|
||||
let mut shape = Bezier {
|
||||
closed: false,
|
||||
vertices: vec![[0.0, 0.0], [100.0, 0.0]],
|
||||
incoming: vec![[0.0, 0.0], [0.0, 100.0]],
|
||||
outgoing: vec![[0.0, 100.0], [0.0, 0.0]],
|
||||
};
|
||||
let expected = path(&shape, identity());
|
||||
for vertex in &mut shape.vertices {
|
||||
*vertex = vertex.map(|coordinate| coordinate + 10_000_000.0);
|
||||
}
|
||||
let translated = path(&shape, Matrix([1.0, 0.0, 0.0, 1.0, -10_000_000.0, -10_000_000.0]));
|
||||
assert_eq!(translated.points, expected.points);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shapes_have_expected_extents_and_closed_edges() {
|
||||
for shape in [rectangle([10.0, 20.0], [40.0, 20.0], 4.0), ellipse([10.0, 20.0], [40.0, 20.0])] {
|
||||
let result = path(&shape, identity());
|
||||
assert!(result.closed);
|
||||
assert!(result.points.iter().all(|[x, y]| (-10.0..=30.0).contains(x) && (10.0..=30.0).contains(y)));
|
||||
assert_eq!(result.points.first(), result.points.last());
|
||||
assert!(estimate(&shape, 1.0) >= result.points.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
//! Bounded temporal easing and property interpolation.
|
||||
|
||||
use crate::model::{Bezier, PathTrack, Track};
|
||||
|
||||
impl Track {
|
||||
pub(crate) fn constant(values: &[f32]) -> Self {
|
||||
Self { keys: vec![crate::model::Key { time: 0.0, value: values.to_vec(), incoming: vec![[1.0, 1.0]], outgoing: vec![[0.0, 0.0]], hold: false }] }
|
||||
}
|
||||
|
||||
pub(crate) fn at(&self, frame: f32) -> Vec<f32> {
|
||||
let Some(first) = self.keys.first() else { return Vec::new() };
|
||||
let mut previous = first;
|
||||
for next in self.keys.iter().skip(1) {
|
||||
if frame < next.time {
|
||||
if previous.hold || frame <= previous.time {
|
||||
return previous.value.clone();
|
||||
}
|
||||
let progress = (frame - previous.time) / (next.time - previous.time);
|
||||
return previous
|
||||
.value
|
||||
.iter()
|
||||
.zip(&next.value)
|
||||
.enumerate()
|
||||
.map(|(dimension, (&start, &end))| {
|
||||
let outgoing = previous.outgoing.get(dimension).or_else(|| previous.outgoing.first()).copied().unwrap_or([0.0, 0.0]);
|
||||
let incoming = next.incoming.get(dimension).or_else(|| next.incoming.first()).copied().unwrap_or([1.0, 1.0]);
|
||||
start + (end - start) * ease(progress, outgoing, incoming)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
previous = next;
|
||||
}
|
||||
previous.value.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn scalar(&self, frame: f32) -> f32 {
|
||||
self.at(frame).first().copied().unwrap_or(0.0)
|
||||
}
|
||||
|
||||
pub(crate) fn bounds(&self) -> Vec<[f32; 2]> {
|
||||
let Some(first) = self.keys.first() else { return Vec::new() };
|
||||
let mut bounds: Vec<_> = first.value.iter().map(|&value| [value, value]).collect();
|
||||
for key in &self.keys {
|
||||
for (bound, &value) in bounds.iter_mut().zip(&key.value) {
|
||||
bound[0] = bound[0].min(value);
|
||||
bound[1] = bound[1].max(value);
|
||||
}
|
||||
}
|
||||
for pair in self.keys.windows(2) {
|
||||
let Some((previous, tail)) = pair.split_first() else { continue };
|
||||
let Some(next) = tail.first() else { continue };
|
||||
if previous.hold {
|
||||
continue;
|
||||
}
|
||||
for (dimension, ((bound, &start), &end)) in bounds.iter_mut().zip(&previous.value).zip(&next.value).enumerate() {
|
||||
let outgoing = previous.outgoing.get(dimension).or_else(|| previous.outgoing.first()).copied().unwrap_or([0.0, 0.0]);
|
||||
let incoming = next.incoming.get(dimension).or_else(|| next.incoming.first()).copied().unwrap_or([1.0, 1.0]);
|
||||
for weight in [0.0_f32.min(outgoing[1]).min(incoming[1]), 1.0_f32.max(outgoing[1]).max(incoming[1])] {
|
||||
let value = start + (end - start) * weight;
|
||||
bound[0] = bound[0].min(value);
|
||||
bound[1] = bound[1].max(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
bounds
|
||||
}
|
||||
|
||||
pub(crate) fn magnitude(&self) -> f32 {
|
||||
self.bounds().iter().flatten().fold(0.0_f32, |maximum, value| maximum.max(value.abs()))
|
||||
}
|
||||
}
|
||||
|
||||
impl PathTrack {
|
||||
pub(crate) fn at(&self, frame: f32) -> Bezier {
|
||||
let values = self.track.at(frame);
|
||||
let mut path = Bezier { closed: self.closed, vertices: Vec::new(), incoming: Vec::new(), outgoing: Vec::new() };
|
||||
for vertex in values.as_chunks::<6>().0 {
|
||||
let [x, y, ix, iy, ox, oy] = *vertex;
|
||||
path.vertices.push([x, y]);
|
||||
path.incoming.push([ix, iy]);
|
||||
path.outgoing.push([ox, oy]);
|
||||
}
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
fn cubic(parameter: f32, first: f32, second: f32) -> f32 {
|
||||
let inverse = 1.0 - parameter;
|
||||
3.0 * inverse * inverse * parameter * first + 3.0 * inverse * parameter * parameter * second + parameter * parameter * parameter
|
||||
}
|
||||
|
||||
fn ease(progress: f32, outgoing: [f32; 2], incoming: [f32; 2]) -> f32 {
|
||||
let mut lower = 0.0;
|
||||
let mut upper = 1.0;
|
||||
// Bisection remains stable for flat derivatives and all monotone timing curves.
|
||||
for _ in 0..24 {
|
||||
let middle = f32::midpoint(lower, upper);
|
||||
if cubic(middle, outgoing[0], incoming[0]) < progress {
|
||||
lower = middle;
|
||||
} else {
|
||||
upper = middle;
|
||||
}
|
||||
}
|
||||
cubic(f32::midpoint(lower, upper), outgoing[1], incoming[1])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::model::{Key, Track};
|
||||
|
||||
#[test]
|
||||
fn easing_and_hold_use_adjacent_keys() {
|
||||
let first = Key { time: 0.0, value: vec![0.0], incoming: vec![[0.0, 0.0]], outgoing: vec![[0.5, 0.0]], hold: false };
|
||||
let last = Key { time: 10.0, value: vec![10.0], incoming: vec![[0.5, 1.0]], outgoing: vec![[1.0, 1.0]], hold: false };
|
||||
let mut track = Track { keys: vec![first, last] };
|
||||
assert!((track.scalar(5.0) - 5.0).abs() < 0.001);
|
||||
if let Some(first) = track.keys.first_mut() {
|
||||
first.hold = true;
|
||||
}
|
||||
assert_eq!(track.scalar(9.0), 0.0);
|
||||
assert_eq!(track.scalar(10.0), 10.0);
|
||||
}
|
||||
}
|
||||
+106
-6
@@ -1,11 +1,111 @@
|
||||
//! Lottie subset player (TODO: filled in by the lottie-crate task).
|
||||
//! Bounded, pure-data Lottie shape animation player for host-tinted UI icons.
|
||||
//!
|
||||
//! Spatial position tangents (`ti`/`to`) are ignored: positions interpolate
|
||||
//! linearly in space, with the specified temporal Bezier easing. Colours are
|
||||
//! validated but discarded so the host can tint every stroke and fill.
|
||||
//! Parent layers contribute transforms, while opacity comes from the drawn
|
||||
//! layer, its shape groups, and each paint item. Stroke widths use the geometric
|
||||
//! mean of the composed horizontal and vertical scales.
|
||||
//!
|
||||
//! Input numbers have magnitude at most one million; composed geometry and
|
||||
//! transforms have a further numeric budget. Point estimates are conservative
|
||||
//! over all keyframes and may reject animations whose sampled frames are smaller.
|
||||
//! The 0.05 pixel cubic tessellation tolerance is subject to `f32` rounding.
|
||||
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn package_name_is_correct() {
|
||||
assert_eq!(env!("CARGO_PKG_NAME"), "notescraft-lottie");
|
||||
mod draw;
|
||||
mod error;
|
||||
mod eval;
|
||||
mod flatten;
|
||||
mod keyframes;
|
||||
mod model;
|
||||
mod parse;
|
||||
mod properties;
|
||||
mod shapes;
|
||||
mod trim;
|
||||
mod validate;
|
||||
|
||||
pub use draw::{Cap, FillPath, Frame, Item, Join, Polyline, StrokePath};
|
||||
pub use error::Error;
|
||||
|
||||
/// Crate result type.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Named timeline interval, measured in absolute composition frames.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Marker {
|
||||
/// Marker name.
|
||||
pub name: String,
|
||||
/// Absolute starting frame.
|
||||
pub frame: f32,
|
||||
/// Duration in frames.
|
||||
pub duration: f32,
|
||||
}
|
||||
|
||||
/// Parsed and validated animation, with a conservative per-frame geometry budget.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Animation {
|
||||
pub(crate) width: f32,
|
||||
pub(crate) height: f32,
|
||||
pub(crate) rate: f32,
|
||||
pub(crate) first: f32,
|
||||
pub(crate) last: f32,
|
||||
pub(crate) layers: Vec<model::Layer>,
|
||||
pub(crate) markers: Vec<Marker>,
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
/// Parse Bodymovin v5 JSON, rejecting unsupported features and invalid input.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`Error::TooLarge`] above 512 KiB, [`Error::Json`] for malformed
|
||||
/// JSON, [`Error::Invalid`] for invalid values, and [`Error::Unsupported`]
|
||||
/// for features outside this subset or resource budgets.
|
||||
pub fn parse(json: &str) -> Result<Self> {
|
||||
parse::animation(json)
|
||||
}
|
||||
|
||||
/// Canvas width and height in pixels.
|
||||
#[must_use]
|
||||
pub fn size(&self) -> (f32, f32) {
|
||||
(self.width, self.height)
|
||||
}
|
||||
|
||||
/// Timeline frames per second.
|
||||
#[must_use]
|
||||
pub fn frame_rate(&self) -> f32 {
|
||||
self.rate
|
||||
}
|
||||
|
||||
/// First absolute timeline frame.
|
||||
#[must_use]
|
||||
pub fn first_frame(&self) -> f32 {
|
||||
self.first
|
||||
}
|
||||
|
||||
/// Last absolute timeline frame (the Lottie out point).
|
||||
#[must_use]
|
||||
pub fn last_frame(&self) -> f32 {
|
||||
self.last
|
||||
}
|
||||
|
||||
/// Timeline duration in seconds.
|
||||
#[must_use]
|
||||
pub fn duration_secs(&self) -> f32 {
|
||||
(self.last - self.first) / self.rate
|
||||
}
|
||||
|
||||
/// Look up a named marker.
|
||||
#[must_use]
|
||||
pub fn marker(&self, name: &str) -> Option<Marker> {
|
||||
self.markers.iter().find(|marker| marker.name == name).cloned()
|
||||
}
|
||||
|
||||
/// Evaluate an absolute frame, clamped to the timeline bounds.
|
||||
/// Non-finite frame arguments evaluate the first frame.
|
||||
#[must_use]
|
||||
pub fn frame_at(&self, frame: f32) -> Frame {
|
||||
eval::frame(self, frame)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
//! Validated animation data and affine transforms.
|
||||
|
||||
use crate::{Cap, Join};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Track {
|
||||
pub keys: Vec<Key>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Key {
|
||||
pub time: f32,
|
||||
pub value: Vec<f32>,
|
||||
pub incoming: Vec<[f32; 2]>,
|
||||
pub outgoing: Vec<[f32; 2]>,
|
||||
pub hold: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Bezier {
|
||||
pub closed: bool,
|
||||
pub vertices: Vec<[f32; 2]>,
|
||||
pub incoming: Vec<[f32; 2]>,
|
||||
pub outgoing: Vec<[f32; 2]>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct PathTrack {
|
||||
pub track: Track,
|
||||
pub closed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum Position {
|
||||
Combined(Track),
|
||||
Split(Track, Track),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Transform {
|
||||
pub anchor: Track,
|
||||
pub position: Position,
|
||||
pub scale: Track,
|
||||
pub rotation: Track,
|
||||
pub opacity: Track,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum Shape {
|
||||
Group(Vec<Self>, Transform),
|
||||
Path(PathTrack),
|
||||
Rectangle { position: Track, size: Track, radius: Track, reversed: bool },
|
||||
Ellipse { position: Track, size: Track, reversed: bool },
|
||||
Stroke { width: Track, opacity: Track, cap: Cap, join: Join },
|
||||
Fill { opacity: Track },
|
||||
Trim { start: Track, end: Track, offset: Track },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Layer {
|
||||
pub id: i64,
|
||||
pub parent: Option<i64>,
|
||||
pub first: f32,
|
||||
pub last: f32,
|
||||
pub start: f32,
|
||||
pub hidden: bool,
|
||||
pub transform: Transform,
|
||||
pub shapes: Vec<Shape>,
|
||||
}
|
||||
|
||||
/// Affine matrix `[a, b, c, d, tx, ty]`.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct Matrix(pub [f32; 6]);
|
||||
|
||||
impl Matrix {
|
||||
pub const IDENTITY: Self = Self([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
|
||||
|
||||
pub fn apply(self, [x, y]: [f32; 2]) -> [f32; 2] {
|
||||
let [xx, yx, xy, yy, tx, ty] = self.0;
|
||||
[xx * x + xy * y + tx, yx * x + yy * y + ty]
|
||||
}
|
||||
|
||||
pub fn then(self, child: Self) -> Self {
|
||||
let [xx, yx, xy, yy, tx, ty] = self.0;
|
||||
let [cxx, cyx, cxy, cyy, ux, uy] = child.0;
|
||||
Self([xx * cxx + xy * cyx, yx * cxx + yy * cyx, xx * cxy + xy * cyy, yx * cxy + yy * cyy, xx * ux + xy * uy + tx, yx * ux + yy * uy + ty])
|
||||
}
|
||||
|
||||
pub fn scale(self) -> f32 {
|
||||
let [a, b, c, d, _, _] = self.0;
|
||||
// Frobenius norm bounds the largest singular value.
|
||||
(a * a + b * b + c * c + d * d).sqrt()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
//! Document and layer parsing with precise feature diagnostics.
|
||||
|
||||
use crate::error::{invalid, unsupported};
|
||||
use crate::model::Layer;
|
||||
use crate::properties::{array, fields, flag, integer, num, number, required, transform};
|
||||
use crate::{Animation, Error, Marker, Result, shapes, validate};
|
||||
use serde_json::Value;
|
||||
|
||||
const MAX_BYTES: usize = 512 * 1024;
|
||||
const MAX_LAYERS: usize = 64;
|
||||
|
||||
pub(crate) fn animation(json: &str) -> Result<Animation> {
|
||||
if json.len() > MAX_BYTES {
|
||||
return Err(Error::TooLarge);
|
||||
}
|
||||
let document: Value = serde_json::from_str(json).map_err(|error| Error::Json { message: error.to_string() })?;
|
||||
fields(&document, &["v", "fr", "ip", "op", "w", "h", "layers", "markers", "nm", "ddd", "assets"], "document")?;
|
||||
if num(&document, "ddd", 0.0, "document")? != 0.0 {
|
||||
return Err(unsupported("3D (ddd)", "document"));
|
||||
}
|
||||
if let Some(assets) = document.get("assets")
|
||||
&& !array(assets, "document.assets")?.is_empty()
|
||||
{
|
||||
return Err(unsupported("assets", "document"));
|
||||
}
|
||||
let version = required(&document, "v", "document")?.as_str().ok_or_else(|| invalid("version string", "document.v"))?;
|
||||
if !version.starts_with("5.") {
|
||||
return Err(unsupported("version other than Bodymovin v5", "document.v"));
|
||||
}
|
||||
let width = number(required(&document, "w", "document")?, "document.w")?;
|
||||
let height = number(required(&document, "h", "document")?, "document.h")?;
|
||||
let rate = number(required(&document, "fr", "document")?, "document.fr")?;
|
||||
let first = number(required(&document, "ip", "document")?, "document.ip")?;
|
||||
let last = number(required(&document, "op", "document")?, "document.op")?;
|
||||
if width <= 0.0 || height <= 0.0 || rate <= 0.0 || last <= first || !((last - first) / rate).is_finite() {
|
||||
return Err(invalid("positive dimensions/rate and ordered timeline", "document"));
|
||||
}
|
||||
let source_layers = array(required(&document, "layers", "document")?, "document.layers")?;
|
||||
if source_layers.len() > MAX_LAYERS {
|
||||
return Err(unsupported("more than 64 layers", "document.layers"));
|
||||
}
|
||||
let mut vertices = 0;
|
||||
let mut layers = Vec::with_capacity(source_layers.len());
|
||||
for (index, value) in source_layers.iter().enumerate() {
|
||||
let name = value.get("nm").and_then(Value::as_str).unwrap_or("unnamed");
|
||||
let at = format!("layer[{index}] ({name})");
|
||||
layers.push(layer(value, index, first, last, &mut vertices, &at)?);
|
||||
}
|
||||
let mut markers = Vec::new();
|
||||
if let Some(source) = document.get("markers") {
|
||||
for (index, value) in array(source, "document.markers")?.iter().enumerate() {
|
||||
let at = format!("marker[{index}]");
|
||||
fields(value, &["cm", "tm", "dr"], &at)?;
|
||||
let name = required(value, "cm", &at)?.as_str().ok_or_else(|| invalid("marker name", &at))?.to_owned();
|
||||
if markers.iter().any(|marker: &Marker| marker.name == name) {
|
||||
return Err(invalid("duplicate marker name", &at));
|
||||
}
|
||||
let frame = number(required(value, "tm", &at)?, &at)?;
|
||||
let duration = number(required(value, "dr", &at)?, &at)?;
|
||||
if duration < 0.0 {
|
||||
return Err(invalid("negative marker duration", &at));
|
||||
}
|
||||
markers.push(Marker { name, frame, duration });
|
||||
}
|
||||
}
|
||||
let animation = Animation { width, height, rate, first, last, layers, markers };
|
||||
validate::animation(&animation)?;
|
||||
Ok(animation)
|
||||
}
|
||||
|
||||
fn layer(value: &Value, index: usize, first: f32, last: f32, vertices: &mut usize, at: &str) -> Result<Layer> {
|
||||
for key in ["tt", "td", "ef", "masksProperties"] {
|
||||
if value.get(key).is_some() {
|
||||
return Err(unsupported(key, at));
|
||||
}
|
||||
}
|
||||
if let Some(has_mask) = value.get("hasMask")
|
||||
&& flag(has_mask, at)?
|
||||
{
|
||||
return Err(unsupported("hasMask masks", at));
|
||||
}
|
||||
fields(value, &["ty", "ks", "ip", "op", "st", "ind", "parent", "hd", "nm", "mn", "ddd", "sr", "shapes", "ao", "bm", "hasMask"], at)?;
|
||||
if num(value, "ddd", 0.0, at)? != 0.0 {
|
||||
return Err(unsupported("3D (ddd)", at));
|
||||
}
|
||||
if !matches!(num(value, "sr", 1.0, at)?, 1.0) {
|
||||
return Err(unsupported("time stretch other than 1", at));
|
||||
}
|
||||
if num(value, "ao", 0.0, at)? != 0.0 {
|
||||
return Err(unsupported("auto orientation", at));
|
||||
}
|
||||
if num(value, "bm", 0.0, at)? != 0.0 {
|
||||
return Err(unsupported("blend mode", at));
|
||||
}
|
||||
let kind = integer(required(value, "ty", at)?, at)?;
|
||||
if ![3, 4].contains(&kind) {
|
||||
return Err(unsupported(format!("layer type {kind}"), at));
|
||||
}
|
||||
let id = value.get("ind").map_or_else(|| i64::try_from(index).map_err(|_| invalid("layer index", at)), |value| integer(value, at))?;
|
||||
let parent = value.get("parent").map(|value| integer(value, at)).transpose()?;
|
||||
let layer_first = num(value, "ip", first, at)?;
|
||||
let layer_last = num(value, "op", last, at)?;
|
||||
if layer_last <= layer_first {
|
||||
return Err(invalid("layer timing", at));
|
||||
}
|
||||
let transform = transform(required(value, "ks", at)?, &format!("{at}.ks"), false)?;
|
||||
let hidden = value.get("hd").map_or(Ok(false), |value| flag(value, at))?;
|
||||
let shapes = match (kind, value.get("shapes")) {
|
||||
(4, Some(source)) => shapes::parse(source, 0, vertices, at)?,
|
||||
(4, None) => return Err(invalid("missing shapes", at)),
|
||||
(_, Some(source)) if !array(source, at)?.is_empty() => return Err(unsupported("shapes on null layer", at)),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
Ok(Layer { id, parent, first: layer_first, last: layer_last, start: num(value, "st", 0.0, at)?, hidden, transform, shapes })
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
//! Strict property decoding and numeric boundary validation.
|
||||
|
||||
use crate::Result;
|
||||
use crate::error::{invalid, unsupported};
|
||||
use crate::model::{Key, Position, Track, Transform};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
const NUMBER_LIMIT: f32 = 1_000_000.0;
|
||||
|
||||
pub(crate) fn object<'a>(value: &'a Value, at: &str) -> Result<&'a Map<String, Value>> {
|
||||
value.as_object().ok_or_else(|| invalid("object", at))
|
||||
}
|
||||
|
||||
pub(crate) fn array<'a>(value: &'a Value, at: &str) -> Result<&'a Vec<Value>> {
|
||||
value.as_array().ok_or_else(|| invalid("array", at))
|
||||
}
|
||||
|
||||
pub(crate) fn required<'a>(value: &'a Value, key: &str, at: &str) -> Result<&'a Value> {
|
||||
value.get(key).ok_or_else(|| invalid(format!("missing {key}"), at))
|
||||
}
|
||||
|
||||
pub(crate) fn number(value: &Value, at: &str) -> Result<f32> {
|
||||
if !value.is_number() {
|
||||
return Err(invalid("number", at));
|
||||
}
|
||||
let number = value.to_string().parse::<f32>().map_err(|_| invalid("finite number", at))?;
|
||||
if !number.is_finite() || number.abs() > NUMBER_LIMIT {
|
||||
return Err(invalid("number out of range", at));
|
||||
}
|
||||
Ok(number)
|
||||
}
|
||||
|
||||
pub(crate) fn num(value: &Value, key: &str, default: f32, at: &str) -> Result<f32> {
|
||||
value.get(key).map_or(Ok(default), |value| number(value, &format!("{at}.{key}")))
|
||||
}
|
||||
|
||||
pub(crate) fn integer(value: &Value, at: &str) -> Result<i64> {
|
||||
value.as_i64().ok_or_else(|| invalid("integer", at))
|
||||
}
|
||||
|
||||
pub(crate) fn flag(value: &Value, at: &str) -> Result<bool> {
|
||||
value.as_bool().ok_or_else(|| invalid("boolean", at))
|
||||
}
|
||||
|
||||
pub(crate) fn fields(value: &Value, allowed: &[&str], at: &str) -> Result<()> {
|
||||
for key in object(value, at)?.keys() {
|
||||
if !allowed.contains(&key.as_str()) {
|
||||
return Err(unsupported(key, at));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn values(value: &Value, dimensions: usize, at: &str) -> Result<Vec<f32>> {
|
||||
if dimensions == 1 && value.is_number() {
|
||||
return Ok(vec![number(value, at)?]);
|
||||
}
|
||||
let components = array(value, at)?;
|
||||
if components.len() != dimensions {
|
||||
return Err(invalid("component count", at));
|
||||
}
|
||||
components.iter().map(|component| number(component, at)).collect()
|
||||
}
|
||||
|
||||
fn plane(value: &Value, neutral: f32, at: &str) -> Result<Vec<f32>> {
|
||||
let components = array(value, at)?;
|
||||
if ![2, 3].contains(&components.len()) {
|
||||
return Err(invalid("2D vector component count", at));
|
||||
}
|
||||
if components.len() == 3 {
|
||||
let z = number(components.last().ok_or_else(|| invalid("z component", at))?, at)?;
|
||||
if (z - neutral).abs() > 0.0 {
|
||||
return Err(unsupported("3D vector component", at));
|
||||
}
|
||||
}
|
||||
components.iter().take(2).map(|component| number(component, at)).collect()
|
||||
}
|
||||
|
||||
pub(crate) fn track(value: &Value, dimensions: usize, at: &str, spatial: bool) -> Result<Track> {
|
||||
read_track(value, at, spatial, |value, at| values(value, dimensions, at))
|
||||
}
|
||||
|
||||
pub(crate) fn read_track(value: &Value, at: &str, spatial: bool, mut read: impl FnMut(&Value, &str) -> Result<Vec<f32>>) -> Result<Track> {
|
||||
fields(value, &["a", "k", "ix", "l"], at)?;
|
||||
let animated = value.get("a").map_or(Ok(0), |value| integer(value, at))?;
|
||||
let data = required(value, "k", at)?;
|
||||
if animated == 0 {
|
||||
return Ok(Track::constant(&read(data, at)?));
|
||||
}
|
||||
if animated != 1 {
|
||||
return Err(invalid("animation flag", at));
|
||||
}
|
||||
let frames = array(data, at)?;
|
||||
if frames.is_empty() {
|
||||
return Err(invalid("empty keyframes", at));
|
||||
}
|
||||
let mut keys: Vec<Key> = Vec::with_capacity(frames.len());
|
||||
let mut endpoint: Option<Vec<f32>> = None;
|
||||
for (index, frame) in frames.iter().enumerate() {
|
||||
let location = format!("{at}.key[{index}]");
|
||||
fields(frame, &["t", "s", "e", "i", "o", "h", "ti", "to"], &location)?;
|
||||
let time = number(required(frame, "t", &location)?, &location)?;
|
||||
let components = match frame.get("s") {
|
||||
Some(value) => read(value, &location)?,
|
||||
None if index.saturating_add(1) == frames.len() => endpoint.clone().ok_or_else(|| invalid("missing terminal value", &location))?,
|
||||
None => return Err(invalid("missing s", &location)),
|
||||
};
|
||||
if let Some(expected) = endpoint.take()
|
||||
&& expected.iter().map(|value| value.to_bits()).ne(components.iter().map(|value| value.to_bits()))
|
||||
{
|
||||
return Err(unsupported("discontinuous keyframe endpoint", &location));
|
||||
}
|
||||
endpoint = frame.get("e").map(|value| read(value, &location)).transpose()?;
|
||||
if let Some(previous) = keys.last()
|
||||
&& (time <= previous.time || components.len() != previous.value.len())
|
||||
{
|
||||
return Err(invalid("keyframe order or component count", &location));
|
||||
}
|
||||
for tangent in ["ti", "to"] {
|
||||
if let Some(value) = frame.get(tangent) {
|
||||
if !spatial {
|
||||
return Err(unsupported(format!("spatial tangent {tangent}"), &location));
|
||||
}
|
||||
if components.len() == 2 {
|
||||
plane(value, 0.0, &location)?;
|
||||
} else {
|
||||
values(value, components.len(), &location)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
let hold = match num(frame, "h", 0.0, &location)? {
|
||||
0.0 => false,
|
||||
1.0 => true,
|
||||
_ => return Err(invalid("hold flag", &location)),
|
||||
};
|
||||
let incoming = control(frame.get("i"), [1.0, 1.0], components.len(), &location)?;
|
||||
let outgoing = control(frame.get("o"), [0.0, 0.0], components.len(), &location)?;
|
||||
keys.push(Key { time, value: components, incoming, outgoing, hold });
|
||||
}
|
||||
if endpoint.is_some() {
|
||||
return Err(unsupported("terminal endpoint without a following keyframe", at));
|
||||
}
|
||||
Ok(Track { keys })
|
||||
}
|
||||
|
||||
fn control(value: Option<&Value>, default: [f32; 2], dimensions: usize, at: &str) -> Result<Vec<[f32; 2]>> {
|
||||
let Some(value) = value else {
|
||||
return Ok(vec![default]);
|
||||
};
|
||||
fields(value, &["x", "y"], at)?;
|
||||
let axis = |key: &str| -> Result<Vec<f32>> {
|
||||
let value = required(value, key, at)?;
|
||||
if value.is_number() {
|
||||
return Ok(vec![number(value, at)?]);
|
||||
}
|
||||
let values = array(value, at)?;
|
||||
if values.len() != 1 && values.len() != dimensions {
|
||||
return Err(invalid("easing component count", at));
|
||||
}
|
||||
values.iter().map(|value| number(value, at)).collect()
|
||||
};
|
||||
let xs = axis("x")?;
|
||||
let ys = axis("y")?;
|
||||
if xs.is_empty() || ys.is_empty() {
|
||||
return Err(invalid("empty easing", at));
|
||||
}
|
||||
let mut points = Vec::new();
|
||||
for dimension in 0..xs.len().max(ys.len()) {
|
||||
let x = xs.get(dimension).or_else(|| xs.first()).copied().ok_or_else(|| invalid("easing x", at))?;
|
||||
let y = ys.get(dimension).or_else(|| ys.first()).copied().ok_or_else(|| invalid("easing y", at))?;
|
||||
if !(0.0..=1.0).contains(&x) {
|
||||
return Err(invalid("easing x outside 0..1", at));
|
||||
}
|
||||
points.push([x, y]);
|
||||
}
|
||||
Ok(points)
|
||||
}
|
||||
|
||||
pub(crate) fn property(value: &Value, key: &str, default: &[f32], at: &str, spatial: bool) -> Result<Track> {
|
||||
value.get(key).map_or_else(|| Ok(Track::constant(default)), |value| track(value, default.len(), &format!("{at}.{key}"), spatial))
|
||||
}
|
||||
|
||||
fn vector(value: &Value, key: &str, default: &[f32], neutral: f32, at: &str, spatial: bool) -> Result<Track> {
|
||||
value
|
||||
.get(key)
|
||||
.map_or_else(|| Ok(Track::constant(default)), |value| read_track(value, &format!("{at}.{key}"), spatial, |value, at| plane(value, neutral, at)))
|
||||
}
|
||||
|
||||
pub(crate) fn range(track: &Track, lower: f32, upper: f32, at: &str) -> Result<()> {
|
||||
if track.bounds().iter().any(|&[minimum, maximum]| minimum < lower || maximum > upper || !minimum.is_finite() || !maximum.is_finite()) {
|
||||
return Err(invalid(format!("property outside {lower}..{upper}"), at));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn transform(value: &Value, at: &str, group: bool) -> Result<Transform> {
|
||||
let mut allowed = vec!["a", "p", "s", "r", "o", "ix"];
|
||||
if group {
|
||||
allowed.extend(["ty", "nm", "mn", "hd", "np", "cix"]);
|
||||
}
|
||||
fields(value, &allowed, at)?;
|
||||
let position = match value.get("p") {
|
||||
Some(position) if position.get("s").and_then(Value::as_bool) == Some(true) => {
|
||||
fields(position, &["s", "x", "y", "ix"], at)?;
|
||||
Position::Split(track(required(position, "x", at)?, 1, at, true)?, track(required(position, "y", at)?, 1, at, true)?)
|
||||
}
|
||||
_ => Position::Combined(vector(value, "p", &[0.0, 0.0], 0.0, at, true)?),
|
||||
};
|
||||
let opacity = property(value, "o", &[100.0], at, false)?;
|
||||
range(&opacity, 0.0, 100.0, at)?;
|
||||
Ok(Transform {
|
||||
anchor: vector(value, "a", &[0.0, 0.0], 0.0, at, false)?,
|
||||
position,
|
||||
scale: vector(value, "s", &[100.0, 100.0], 100.0, at, false)?,
|
||||
rotation: property(value, "r", &[0.0], at, false)?,
|
||||
opacity,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
//! Shape decoding, topology checks, and nesting limits.
|
||||
|
||||
use crate::error::{invalid, unsupported};
|
||||
use crate::model::{PathTrack, Shape};
|
||||
use crate::properties::{array, fields, flag, num, number, property, range, read_track, required, transform, values};
|
||||
use crate::{Cap, Join, Result};
|
||||
use serde_json::Value;
|
||||
|
||||
const MAX_DEPTH: usize = 8;
|
||||
const MAX_VERTICES: usize = 4096;
|
||||
|
||||
pub(crate) fn parse(value: &Value, depth: usize, vertices: &mut usize, at: &str) -> Result<Vec<Shape>> {
|
||||
if depth > MAX_DEPTH {
|
||||
return Err(unsupported("shape nesting deeper than 8", at));
|
||||
}
|
||||
let source = array(value, at)?;
|
||||
if let Some((last, children)) = source.split_last()
|
||||
&& last.get("ty").and_then(Value::as_str) == Some("tr")
|
||||
{
|
||||
let transform = transform(last, at, true)?;
|
||||
let children = parse(&Value::Array(children.to_vec()), depth.saturating_add(1), vertices, at)?;
|
||||
return Ok(vec![Shape::Group(children, transform)]);
|
||||
}
|
||||
let mut shapes = Vec::new();
|
||||
for (index, value) in source.iter().enumerate() {
|
||||
let location = format!("{at}.shape[{index}]");
|
||||
let kind = required(value, "ty", &location)?.as_str().ok_or_else(|| invalid("shape type", &location))?;
|
||||
let shape = match kind {
|
||||
"gr" => group(value, depth, vertices, &location)?,
|
||||
"sh" => {
|
||||
check_fields(value, &["ks", "ind"], &location)?;
|
||||
let path = path(required(value, "ks", &location)?, &location)?;
|
||||
let count = path.track.keys.first().map_or(0, |key| key.value.len() / 6);
|
||||
add_vertices(vertices, count, &location)?;
|
||||
Shape::Path(path)
|
||||
}
|
||||
"rc" => {
|
||||
check_fields(value, &["p", "s", "r", "d"], &location)?;
|
||||
add_vertices(vertices, 8, &location)?;
|
||||
let size = property(value, "s", &[0.0, 0.0], &location, false)?;
|
||||
let radius = property(value, "r", &[0.0], &location, false)?;
|
||||
range(&size, 0.0, 1_000_000.0, &location)?;
|
||||
range(&radius, 0.0, 1_000_000.0, &location)?;
|
||||
Shape::Rectangle { position: property(value, "p", &[0.0, 0.0], &location, true)?, size, radius, reversed: direction(value, &location)? }
|
||||
}
|
||||
"el" => {
|
||||
check_fields(value, &["p", "s", "d"], &location)?;
|
||||
add_vertices(vertices, 4, &location)?;
|
||||
let size = property(value, "s", &[0.0, 0.0], &location, false)?;
|
||||
range(&size, 0.0, 1_000_000.0, &location)?;
|
||||
Shape::Ellipse { position: property(value, "p", &[0.0, 0.0], &location, true)?, size, reversed: direction(value, &location)? }
|
||||
}
|
||||
"st" => {
|
||||
check_fields(value, &["w", "lc", "lj", "o", "c"], &location)?;
|
||||
colour(required(value, "c", &location)?, &location)?;
|
||||
let cap = match num(value, "lc", 1.0, &location)? {
|
||||
1.0 => Cap::Butt,
|
||||
2.0 => Cap::Round,
|
||||
3.0 => Cap::Square,
|
||||
_ => return Err(invalid("stroke cap", &location)),
|
||||
};
|
||||
let join = match num(value, "lj", 1.0, &location)? {
|
||||
1.0 => Join::Miter,
|
||||
2.0 => Join::Round,
|
||||
3.0 => Join::Bevel,
|
||||
_ => return Err(invalid("stroke join", &location)),
|
||||
};
|
||||
let width = property(value, "w", &[1.0], &location, false)?;
|
||||
range(&width, 0.0, 1_000_000.0, &location)?;
|
||||
Shape::Stroke { width, opacity: opacity(value, &location)?, cap, join }
|
||||
}
|
||||
"fl" => {
|
||||
check_fields(value, &["o", "c", "r"], &location)?;
|
||||
if !matches!(num(value, "r", 1.0, &location)?, 1.0) {
|
||||
return Err(unsupported("even-odd fill rule", &location));
|
||||
}
|
||||
colour(required(value, "c", &location)?, &location)?;
|
||||
Shape::Fill { opacity: opacity(value, &location)? }
|
||||
}
|
||||
"tm" => {
|
||||
check_fields(value, &["s", "e", "o", "m"], &location)?;
|
||||
if !matches!(num(value, "m", 1.0, &location)?, 1.0) {
|
||||
return Err(unsupported("trim mode other than simultaneous (m=1)", &location));
|
||||
}
|
||||
let start = property(value, "s", &[0.0], &location, false)?;
|
||||
let end = property(value, "e", &[100.0], &location, false)?;
|
||||
range(&start, 0.0, 100.0, &location)?;
|
||||
range(&end, 0.0, 100.0, &location)?;
|
||||
Shape::Trim { start, end, offset: property(value, "o", &[0.0], &location, false)? }
|
||||
}
|
||||
"tr" => return Err(unsupported("transform outside final group position", &location)),
|
||||
other => return Err(unsupported(format!("shape {other}"), &location)),
|
||||
};
|
||||
if !value.get("hd").map_or(Ok(false), |value| flag(value, &location))? {
|
||||
shapes.push(shape);
|
||||
}
|
||||
}
|
||||
Ok(shapes)
|
||||
}
|
||||
|
||||
fn group(value: &Value, depth: usize, vertices: &mut usize, at: &str) -> Result<Shape> {
|
||||
check_fields(value, &["it", "np", "cix", "bm"], at)?;
|
||||
if num(value, "bm", 0.0, at)? != 0.0 {
|
||||
return Err(unsupported("blend mode", at));
|
||||
}
|
||||
let items = array(required(value, "it", at)?, at)?;
|
||||
let Some((last, children)) = items.split_last() else {
|
||||
return Err(invalid("group requires final transform", at));
|
||||
};
|
||||
if last.get("ty").and_then(Value::as_str) != Some("tr") {
|
||||
return Err(invalid("group requires final transform", at));
|
||||
}
|
||||
let transform = transform(last, at, true)?;
|
||||
let children = parse(&Value::Array(children.to_vec()), depth.saturating_add(1), vertices, at)?;
|
||||
Ok(Shape::Group(children, transform))
|
||||
}
|
||||
|
||||
fn check_fields(value: &Value, extra: &[&str], at: &str) -> Result<()> {
|
||||
let mut allowed = vec!["ty", "nm", "mn", "ix", "hd"];
|
||||
allowed.extend(extra);
|
||||
fields(value, &allowed, at)
|
||||
}
|
||||
|
||||
fn direction(value: &Value, at: &str) -> Result<bool> {
|
||||
match num(value, "d", 1.0, at)? {
|
||||
1.0 | 2.0 => Ok(false),
|
||||
3.0 => Ok(true),
|
||||
_ => Err(invalid("path direction", at)),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_vertices(vertices: &mut usize, count: usize, at: &str) -> Result<()> {
|
||||
*vertices = vertices.saturating_add(count);
|
||||
if *vertices > MAX_VERTICES {
|
||||
return Err(unsupported("more than 4096 vertices", at));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn opacity(value: &Value, at: &str) -> Result<crate::model::Track> {
|
||||
let opacity = property(value, "o", &[100.0], at, false)?;
|
||||
range(&opacity, 0.0, 100.0, at)?;
|
||||
Ok(opacity)
|
||||
}
|
||||
|
||||
fn colour(value: &Value, at: &str) -> Result<()> {
|
||||
// Lottie exports either RGB or RGBA. Both are validated, then host-tinted.
|
||||
let colour = read_track(value, at, false, |value, at| {
|
||||
let channels = array(value, at)?;
|
||||
if ![3, 4].contains(&channels.len()) {
|
||||
return Err(invalid("RGB/RGBA colour", at));
|
||||
}
|
||||
channels.iter().map(|value| number(value, at)).collect()
|
||||
})?;
|
||||
range(&colour, 0.0, 1.0, at)
|
||||
}
|
||||
|
||||
fn path(value: &Value, at: &str) -> Result<PathTrack> {
|
||||
let mut closed = None;
|
||||
let mut count = None;
|
||||
let track = read_track(value, at, false, |value, at| {
|
||||
let data = if value.is_array() {
|
||||
let values = array(value, at)?;
|
||||
if values.len() != 1 {
|
||||
return Err(invalid("path keyframe", at));
|
||||
}
|
||||
values.first().ok_or_else(|| invalid("path keyframe", at))?
|
||||
} else {
|
||||
value
|
||||
};
|
||||
fields(data, &["c", "v", "i", "o"], at)?;
|
||||
let is_closed = flag(required(data, "c", at)?, at)?;
|
||||
let vertices = array(required(data, "v", at)?, at)?;
|
||||
let incoming = array(required(data, "i", at)?, at)?;
|
||||
let outgoing = array(required(data, "o", at)?, at)?;
|
||||
if vertices.len() > MAX_VERTICES {
|
||||
return Err(unsupported("more than 4096 vertices", at));
|
||||
}
|
||||
if vertices.is_empty() || vertices.len() != incoming.len() || vertices.len() != outgoing.len() {
|
||||
return Err(invalid("path vertex/tangent count", at));
|
||||
}
|
||||
if closed.is_some_and(|previous| previous != is_closed) || count.is_some_and(|previous| previous != vertices.len()) {
|
||||
return Err(invalid("path keyframe topology", at));
|
||||
}
|
||||
closed = Some(is_closed);
|
||||
count = Some(vertices.len());
|
||||
let mut coordinates = Vec::with_capacity(vertices.len().saturating_mul(6));
|
||||
for ((vertex, incoming), outgoing) in vertices.iter().zip(incoming).zip(outgoing) {
|
||||
coordinates.extend(values(vertex, 2, at)?);
|
||||
coordinates.extend(values(incoming, 2, at)?);
|
||||
coordinates.extend(values(outgoing, 2, at)?);
|
||||
}
|
||||
Ok(coordinates)
|
||||
})?;
|
||||
Ok(PathTrack { track, closed: closed.unwrap_or(false) })
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
//! Arc-length trim paths applied simultaneously across shape contours.
|
||||
|
||||
use crate::Polyline;
|
||||
|
||||
/// Trims normalized percentages over the combined length of every contour.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn apply(paths: Vec<Polyline>, start: f32, end: f32, offset: f32) -> Vec<Polyline> {
|
||||
apply_tagged(paths.into_iter().enumerate().collect(), start, end, offset).into_iter().map(|(_, path)| path).collect()
|
||||
}
|
||||
|
||||
/// Preserves caller tags when trimming splits contours into visible fragments.
|
||||
pub(crate) fn apply_tagged(paths: Vec<(usize, Polyline)>, start: f32, end: f32, offset: f32) -> Vec<(usize, Polyline)> {
|
||||
let span = (end - start).abs() / 100.0;
|
||||
if span >= 1.0 {
|
||||
return paths;
|
||||
}
|
||||
if span == 0.0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let total = paths.iter().map(|(_, path)| length(path)).sum::<f32>();
|
||||
if total == 0.0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let start = (start.min(end) / 100.0 + offset / 360.0).rem_euclid(1.0) * total;
|
||||
let end = start + span * total;
|
||||
let intervals = [(start, end.min(total)), (0.0, (end - total).max(0.0))];
|
||||
let mut result = Vec::new();
|
||||
let mut position = 0.0;
|
||||
for (tag, path) in paths {
|
||||
let path_length = length(&path);
|
||||
let mut pieces = Vec::new();
|
||||
for (from, to) in intervals {
|
||||
let from = from.max(position);
|
||||
let to = to.min(position + path_length);
|
||||
if from < to {
|
||||
pieces.push(piece(&path, from - position, to - position));
|
||||
}
|
||||
}
|
||||
if path.closed && pieces.len() == 2 {
|
||||
let head = pieces.pop();
|
||||
if let (Some(tail), Some(head)) = (pieces.last_mut(), head) {
|
||||
tail.points.extend(head.points.into_iter().skip(1));
|
||||
}
|
||||
}
|
||||
result.extend(pieces.into_iter().map(|piece| (tag, piece)));
|
||||
position += path_length;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn edges(path: &Polyline) -> impl Iterator<Item = ([f32; 2], [f32; 2])> + '_ {
|
||||
let closure = path.points.first().filter(|first| path.closed && Some(*first) != path.points.last());
|
||||
path.points.iter().zip(path.points.iter().skip(1).chain(closure)).map(|(a, b)| (*a, *b))
|
||||
}
|
||||
|
||||
fn distance([ax, ay]: [f32; 2], [bx, by]: [f32; 2]) -> f32 {
|
||||
(bx - ax).hypot(by - ay)
|
||||
}
|
||||
|
||||
fn length(path: &Polyline) -> f32 {
|
||||
edges(path).map(|(a, b)| distance(a, b)).sum()
|
||||
}
|
||||
|
||||
fn piece(path: &Polyline, start: f32, end: f32) -> Polyline {
|
||||
let mut points = Vec::new();
|
||||
let mut position = 0.0;
|
||||
for (a, b) in edges(path) {
|
||||
let length = distance(a, b);
|
||||
let from = start.max(position);
|
||||
let to = end.min(position + length);
|
||||
if from < to {
|
||||
let first = interpolate(a, b, (from - position) / length);
|
||||
if points.last() != Some(&first) {
|
||||
points.push(first);
|
||||
}
|
||||
points.push(interpolate(a, b, (to - position) / length));
|
||||
}
|
||||
position += length;
|
||||
if position >= end {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Polyline { points, closed: false }
|
||||
}
|
||||
|
||||
fn interpolate([ax, ay]: [f32; 2], [bx, by]: [f32; 2], t: f32) -> [f32; 2] {
|
||||
[ax + (bx - ax) * t, ay + (by - ay) * t]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::apply;
|
||||
use crate::Polyline;
|
||||
|
||||
fn line(start: f32, end: f32) -> Polyline {
|
||||
Polyline { points: vec![[start, 0.0], [end, 0.0]], closed: false }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trims_exactly_by_length_across_paths() {
|
||||
let result = apply(vec![line(0.0, 10.0), line(20.0, 50.0)], 12.5, 75.0, 0.0);
|
||||
assert_eq!(result.len(), 2);
|
||||
assert_eq!(result.first().map(|p| &p.points), Some(&vec![[5.0, 0.0], [10.0, 0.0]]));
|
||||
assert_eq!(result.last().map(|p| &p.points), Some(&vec![[20.0, 0.0], [40.0, 0.0]]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_wraps_around_closed_contour() {
|
||||
let square = Polyline { points: vec![[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0], [0.0, 0.0]], closed: true };
|
||||
let result = apply(vec![square], 0.0, 50.0, 270.0);
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result.first().map(|p| &p.points), Some(&vec![[0.0, 10.0], [0.0, 0.0], [10.0, 0.0]]));
|
||||
assert!(result.iter().all(|p| !p.closed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_trim_retains_closed_path_and_zero_trim_removes_it() {
|
||||
let mut full = line(0.0, 10.0);
|
||||
full.closed = true;
|
||||
let result = apply(vec![full], 0.0, 100.0, 23.0);
|
||||
assert!(result.first().is_some_and(|p| p.closed));
|
||||
assert_eq!(apply(vec![line(0.0, 10.0)], 50.0, 50.0, 0.0), Vec::<Polyline>::new());
|
||||
assert_eq!(apply(Vec::new(), 0.0, 50.0, 0.0), Vec::<Polyline>::new());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
//! Conservative animation-wide bounds, parent validation, and output budgets.
|
||||
|
||||
use crate::error::{invalid, unsupported};
|
||||
use crate::model::{PathTrack, Position, Shape, Transform};
|
||||
use crate::{Animation, Result};
|
||||
|
||||
const MAX_POINTS: usize = 20_000;
|
||||
const MAX_MAGNITUDE: f32 = 1_000_000_000.0;
|
||||
const FLATNESS: f64 = 0.05;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Bounds {
|
||||
scale: f32,
|
||||
extent: f32,
|
||||
}
|
||||
|
||||
impl Bounds {
|
||||
fn transform(self, transform: &Transform, at: &str) -> Result<Self> {
|
||||
let local_scale = transform.scale.magnitude() / 100.0;
|
||||
let position = match &transform.position {
|
||||
Position::Combined(position) => position.magnitude(),
|
||||
Position::Split(x, y) => x.magnitude().max(y.magnitude()),
|
||||
};
|
||||
let scale = self.scale * local_scale;
|
||||
let extent = self.extent + self.scale * (position + local_scale * transform.anchor.magnitude()) * 2.0;
|
||||
if !scale.is_finite() || !extent.is_finite() || scale > MAX_MAGNITUDE || extent > MAX_MAGNITUDE {
|
||||
return Err(unsupported("transform numeric budget", at));
|
||||
}
|
||||
Ok(Self { scale, extent })
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn animation(animation: &Animation) -> Result<()> {
|
||||
let mut total = 0_usize;
|
||||
for (index, layer) in animation.layers.iter().enumerate() {
|
||||
let at = format!("layer[{index}] (ind={})", layer.id);
|
||||
if animation.layers.iter().filter(|candidate| candidate.id == layer.id).count() != 1 {
|
||||
return Err(invalid("duplicate layer ind", &at));
|
||||
}
|
||||
let mut visited = Vec::new();
|
||||
let mut chain = Vec::new();
|
||||
let mut current = Some(layer);
|
||||
while let Some(layer) = current {
|
||||
if visited.contains(&layer.id) {
|
||||
return Err(invalid("parent cycle", &at));
|
||||
}
|
||||
visited.push(layer.id);
|
||||
chain.push(&layer.transform);
|
||||
current = match layer.parent {
|
||||
Some(id) => Some(animation.layers.iter().find(|candidate| candidate.id == id).ok_or_else(|| invalid("unknown parent ind", &at))?),
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
let mut bounds = Bounds { scale: 1.0, extent: 0.0 };
|
||||
for transform in chain.into_iter().rev() {
|
||||
bounds = bounds.transform(transform, &at)?;
|
||||
}
|
||||
let (points, paths, styles, trims) = shapes(&layer.shapes, bounds, &at)?;
|
||||
let point_count = points.saturating_add(paths.saturating_mul(trims).saturating_mul(4));
|
||||
total = total.saturating_add(point_count.saturating_mul(styles.max(1)));
|
||||
if total > MAX_POINTS {
|
||||
return Err(unsupported("more than 20000 points per frame", &at));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn shapes(input: &[Shape], bounds: Bounds, at: &str) -> Result<(usize, usize, usize, usize)> {
|
||||
let (mut points, mut paths, mut styles, mut trims) = (0_usize, 0_usize, 0_usize, 0_usize);
|
||||
for shape in input {
|
||||
let count = match shape {
|
||||
Shape::Path(path) => {
|
||||
check_extent(path.track.magnitude(), bounds, at)?;
|
||||
path_points(path, bounds.scale)
|
||||
}
|
||||
Shape::Rectangle { position, size, radius, .. } => {
|
||||
let extent = position.magnitude() + size.magnitude() + radius.magnitude();
|
||||
check_extent(extent, bounds, at)?;
|
||||
// Eight edges, each with a control polygon bounded by the size.
|
||||
primitive_points(8, size.magnitude(), bounds.scale)
|
||||
}
|
||||
Shape::Ellipse { position, size, .. } => {
|
||||
check_extent(position.magnitude() + size.magnitude(), bounds, at)?;
|
||||
primitive_points(4, size.magnitude(), bounds.scale)
|
||||
}
|
||||
Shape::Group(children, transform) => {
|
||||
let (child_points, child_paths, child_styles, child_trims) = shapes(children, bounds.transform(transform, at)?, at)?;
|
||||
points = points.saturating_add(child_points);
|
||||
paths = paths.saturating_add(child_paths);
|
||||
styles = styles.saturating_add(child_styles);
|
||||
trims = trims.saturating_add(child_trims);
|
||||
continue;
|
||||
}
|
||||
Shape::Stroke { width, .. } => {
|
||||
check_extent(width.magnitude(), bounds, at)?;
|
||||
styles = styles.saturating_add(1);
|
||||
continue;
|
||||
}
|
||||
Shape::Fill { .. } => {
|
||||
styles = styles.saturating_add(1);
|
||||
continue;
|
||||
}
|
||||
Shape::Trim { .. } => {
|
||||
trims = trims.saturating_add(1);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
points = points.saturating_add(count);
|
||||
paths = paths.saturating_add(1);
|
||||
}
|
||||
Ok((points, paths, styles, trims))
|
||||
}
|
||||
|
||||
fn check_extent(extent: f32, bounds: Bounds, at: &str) -> Result<()> {
|
||||
let maximum = bounds.extent + extent * bounds.scale * 4.0;
|
||||
if !maximum.is_finite() || maximum > MAX_MAGNITUDE {
|
||||
return Err(unsupported("geometry numeric budget", at));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn primitive_points(edges: usize, size: f32, scale: f32) -> usize {
|
||||
// Every primitive control coordinate lies within a size-wide box. This
|
||||
// includes rounded rectangles whose radius saturates at half the size.
|
||||
let second = f64::from(size) * 8.0 * std::f64::consts::SQRT_2;
|
||||
edges.saturating_mul(segments(second, scale)).saturating_add(1)
|
||||
}
|
||||
|
||||
fn path_points(path: &PathTrack, scale: f32) -> usize {
|
||||
if path.track.keys.len() == 1 {
|
||||
return crate::flatten::estimate(&path.at(0.0), scale * std::f32::consts::SQRT_2);
|
||||
}
|
||||
let values = path.track.bounds();
|
||||
let vertices: Vec<_> = values.as_chunks::<6>().0.iter().collect();
|
||||
let first = vertices.first().copied().filter(|_| path.closed);
|
||||
let mut count = 1_usize;
|
||||
for (start, end) in vertices.iter().copied().zip(vertices.iter().copied().skip(1).chain(first)) {
|
||||
let ([vx, vy, _, _, ox, oy], [ex, ey, ix, iy, _, _]) = (start, end);
|
||||
let x = second_bound(*vx, *ex, *ox, *ix);
|
||||
let y = second_bound(*vy, *ey, *oy, *iy);
|
||||
count = count.saturating_add(segments(x.hypot(y), scale));
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
fn second_bound(start: [f32; 2], end: [f32; 2], outgoing: [f32; 2], incoming: [f32; 2]) -> f64 {
|
||||
let [start_min, start_max] = start.map(f64::from);
|
||||
let [end_min, end_max] = end.map(f64::from);
|
||||
let [out_min, out_max] = outgoing.map(f64::from);
|
||||
let [in_min, in_max] = incoming.map(f64::from);
|
||||
[
|
||||
end_min - start_max + in_min - 2.0 * out_max,
|
||||
end_max - start_min + in_max - 2.0 * out_min,
|
||||
start_min - end_max + out_min - 2.0 * in_max,
|
||||
start_max - end_min + out_max - 2.0 * in_min,
|
||||
]
|
||||
.into_iter()
|
||||
.map(f64::abs)
|
||||
.fold(0.0, f64::max)
|
||||
}
|
||||
|
||||
fn segments(second: f64, scale: f32) -> usize {
|
||||
// Rotation can align either axis: sqrt(2) bounds Euclidean scale from the
|
||||
// maximum component scale, including nonuniform parent compositions.
|
||||
let mut error = second * 0.75 * f64::from(scale) * std::f64::consts::SQRT_2;
|
||||
let mut points = 1_usize;
|
||||
for _ in 0..64 {
|
||||
if error <= FLATNESS {
|
||||
return points;
|
||||
}
|
||||
error *= 0.25;
|
||||
points = points.saturating_mul(2);
|
||||
}
|
||||
usize::MAX
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//! Every shipped icon animation parses and evaluates at every frame.
|
||||
|
||||
use notescraft_lottie::Animation;
|
||||
|
||||
#[test]
|
||||
fn every_shipped_icon_parses_and_plays() {
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets/icons-anim");
|
||||
let mut seen = 0;
|
||||
let mut failures = Vec::new();
|
||||
for entry in std::fs::read_dir(&dir).map(|entries| entries.flatten().collect::<Vec<_>>()).unwrap_or_default() {
|
||||
let path = entry.path();
|
||||
if path.extension().is_none_or(|ext| ext != "json") || path.file_name().is_some_and(|name| name == "index.json") {
|
||||
continue;
|
||||
}
|
||||
let text = std::fs::read_to_string(&path).unwrap_or_default();
|
||||
let animation = match Animation::parse(&text) {
|
||||
Ok(animation) => animation,
|
||||
Err(error) => {
|
||||
failures.push(format!("{}: {error}", path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default()));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let (first, last) = (animation.first_frame(), animation.last_frame());
|
||||
assert!(last > first, "{}: no duration", path.display());
|
||||
let mut frame = first;
|
||||
while frame <= last {
|
||||
assert!(!animation.frame_at(frame).items.is_empty(), "{}: empty at frame {frame}", path.display());
|
||||
frame += 1.0;
|
||||
}
|
||||
seen += 1;
|
||||
}
|
||||
assert!(failures.is_empty(), "icons that do not parse:\n{}", failures.join("\n"));
|
||||
assert!(seen >= 60, "icons found: {seen}");
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
//! Canonical Bodymovin vector forms and explicit endpoint validation.
|
||||
|
||||
use notescraft_lottie::{Animation, Error, Item};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn fixture() -> Value {
|
||||
serde_json::from_str(include_str!("fixtures/stroke.json")).expect("valid fixture")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_dimensional_layers_accept_neutral_z_components() {
|
||||
let mut input = fixture();
|
||||
input["layers"][0]["ks"]["a"]["k"] = json!([0, 0, 0]);
|
||||
input["layers"][0]["ks"]["p"]["k"] = json!([10, 20, 0]);
|
||||
input["layers"][0]["ks"]["s"]["k"] = json!([200, 200, 100]);
|
||||
let animation = Animation::parse(&input.to_string()).expect("2D transform with neutral z");
|
||||
let frame = animation.frame_at(0.0);
|
||||
let Some(Item::Stroke(stroke)) = frame.items.first() else { panic!("stroke") };
|
||||
assert_eq!(stroke.polylines.first().and_then(|line| line.points.first()), Some(&[14.0, 28.0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonneutral_z_is_rejected_as_three_dimensional() {
|
||||
let mut input = fixture();
|
||||
input["layers"][0]["ks"]["p"]["k"] = json!([0, 0, 1]);
|
||||
assert!(matches!(Animation::parse(&input.to_string()), Err(Error::Unsupported { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn final_shape_transform_applies_to_root_geometry() {
|
||||
let mut input = fixture();
|
||||
input["layers"][0]["shapes"].as_array_mut().expect("shapes").push(json!({"ty":"tr", "p":{"a":0,"k":[3,4]}}));
|
||||
let animation = Animation::parse(&input.to_string()).expect("root transform");
|
||||
let frame = animation.frame_at(0.0);
|
||||
let Some(Item::Stroke(stroke)) = frame.items.first() else { panic!("stroke") };
|
||||
assert_eq!(stroke.polylines.first().and_then(|line| line.points.first()), Some(&[5.0, 8.0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_keyframe_endpoints_are_validated_and_terminal_start_can_be_omitted() {
|
||||
let mut input = fixture();
|
||||
input["layers"][0]["ks"]["p"] = json!({"a":1,"k":[{"t":0,"s":[0,0],"e":[10,0]},{"t":10}]});
|
||||
assert!(Animation::parse(&input.to_string()).is_ok());
|
||||
input["layers"][0]["ks"]["p"]["k"][1]["s"] = json!([20, 0]);
|
||||
assert!(Animation::parse(&input.to_string()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_shackle_vertices_and_tangents_animate_with_fixed_topology() {
|
||||
let animation = Animation::parse(include_str!("fixtures/animated-shackle.json")).expect("animated shackle");
|
||||
for (time, endpoint) in [(0.0, [16.0, 12.0]), (5.0, [18.0, 10.0]), (10.0, [20.0, 8.0])] {
|
||||
let frame = animation.frame_at(time);
|
||||
let Some(Item::Stroke(stroke)) = frame.items.first() else { panic!("stroke") };
|
||||
let line = stroke.polylines.first().expect("shackle contour");
|
||||
assert_eq!(line.points.first(), Some(&[8.0, 12.0]));
|
||||
for (&actual, expected) in line.points.last().expect("endpoint").iter().zip(endpoint) {
|
||||
assert!((actual - expected).abs() < 0.001);
|
||||
}
|
||||
assert!(line.points.len() > 4);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn animated_path_vertex_count_changes_return_error() {
|
||||
let mut value: Value = serde_json::from_str(include_str!("fixtures/animated-shackle.json")).expect("fixture");
|
||||
for key in ["v", "i", "o"] {
|
||||
value["layers"][0]["shapes"][0]["ks"]["k"][1]["s"][0][key].as_array_mut().expect("vertices").pop();
|
||||
}
|
||||
assert!(Animation::parse(&value.to_string()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_fixture_byte_can_be_mutated_without_panicking() {
|
||||
for source in [include_str!("fixtures/stroke.json"), include_str!("fixtures/animated-shackle.json")] {
|
||||
for index in 0..source.len() {
|
||||
let mut bytes = source.as_bytes().to_vec();
|
||||
bytes[index] ^= 0x5a;
|
||||
let input = String::from_utf8_lossy(&bytes);
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
if let Ok(animation) = Animation::parse(&input) {
|
||||
let _ = animation.frame_at(5.0);
|
||||
}
|
||||
});
|
||||
assert!(result.is_ok(), "byte {index}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"v":"5.7.4", "fr":10, "ip":0, "op":11, "w":24, "h":24,
|
||||
"layers":[{
|
||||
"ty":4,"ind":1,"nm":"deforming lock shackle","ip":0,"op":11,
|
||||
"ks":{},
|
||||
"shapes":[
|
||||
{"ty":"sh","ks":{"a":1,"k":[
|
||||
{"t":0,"s":[{
|
||||
"c":false,
|
||||
"v":[[8,12],[8,8],[16,8],[16,12]],
|
||||
"i":[[0,0],[0,0],[0,-5],[0,0]],
|
||||
"o":[[0,0],[0,-5],[0,0],[0,0]]
|
||||
}],"o":{"x":0,"y":0}},
|
||||
{"t":10,"s":[{
|
||||
"c":false,
|
||||
"v":[[8,12],[10,6],[18,4],[20,8]],
|
||||
"i":[[0,0],[0,0],[-2,-5],[0,0]],
|
||||
"o":[[0,0],[0,-5],[0,0],[0,0]]
|
||||
}],"i":{"x":1,"y":1}}
|
||||
]}},
|
||||
{"ty":"st","c":{"a":0,"k":[0,0,0,1]},"w":{"a":0,"k":2},"lc":2,"lj":2}
|
||||
]
|
||||
}]
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"v": "5.7.4", "fr": 10, "ip": 0, "op": 11, "w": 32, "h": 32,
|
||||
"layers": [
|
||||
{"ty": 3, "ind": 1, "ip": 0, "op": 11, "ks": {
|
||||
"p": {"a": 0, "k": [10, 0]}, "s": {"a": 0, "k": [200, 200]},
|
||||
"r": {"a": 0, "k": 90}, "o": {"a": 0, "k": 50}
|
||||
}},
|
||||
{"ty": 4, "ind": 2, "parent": 1, "ip": 0, "op": 11,
|
||||
"ks": {"p": {"a": 0, "k": [1, 0]}, "o": {"a": 0, "k": 80}},
|
||||
"shapes": [{"ty": "gr", "it": [
|
||||
{"ty": "sh", "ks": {"a": 0, "k": {"v": [[1, 0], [2, 0]], "i": [[0, 0], [0, 0]], "o": [[0, 0], [0, 0]], "c": false}}},
|
||||
{"ty": "st", "c": {"a": 0, "k": [0, 0, 0, 1]}, "w": {"a": 0, "k": 1}, "o": {"a": 0, "k": 50}, "lc": 1, "lj": 1},
|
||||
{"ty": "tr", "p": {"a": 0, "k": [2, 0]}, "a": {"a": 0, "k": [1, 0]},
|
||||
"s": {"a": 0, "k": [100, 100]}, "r": {"a": 0, "k": 0}, "o": {"a": 0, "k": 50}}
|
||||
]}]
|
||||
}
|
||||
]
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"v": "5.7.4", "fr": 10, "ip": 0, "op": 11, "w": 32, "h": 32,
|
||||
"layers": [{"ty": 4, "ind": 1, "ip": 0, "op": 11, "st": 0,
|
||||
"ks": {
|
||||
"a": {"a": 0, "k": [0, 0]},
|
||||
"p": {"a": 1, "k": [
|
||||
{"t": 0, "s": [0, 0], "e": [10, 0], "o": {"x": [0.42], "y": [0]}},
|
||||
{"t": 10, "s": [10, 0], "i": {"x": [1], "y": [1]}}
|
||||
]},
|
||||
"s": {"a": 1, "k": [
|
||||
{"t": 0, "s": [100, 100], "e": [200, 200], "o": {"x": [0], "y": [0]}},
|
||||
{"t": 10, "s": [200, 200], "i": {"x": [1], "y": [1]}}
|
||||
]},
|
||||
"r": {"a": 1, "k": [{"t": 0, "s": [0], "e": [90], "h": 1}, {"t": 10, "s": [90]}]},
|
||||
"o": {"a": 0, "k": 100}
|
||||
},
|
||||
"shapes": [
|
||||
{"ty": "sh", "ks": {"a": 0, "k": {"v": [[2, 0], [4, 0]], "i": [[0, 0], [0, 0]], "o": [[0, 0], [0, 0]], "c": false}}},
|
||||
{"ty": "st", "c": {"a": 0, "k": [0, 0, 0, 1]}, "w": {"a": 0, "k": 1}, "o": {"a": 0, "k": 100}, "lc": 3, "lj": 2}
|
||||
]
|
||||
}]
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"v": "5.7.4", "fr": 10, "ip": 10, "op": 30, "w": 24, "h": 24,
|
||||
"layers": [],
|
||||
"markers": [{"cm": "unlock", "tm": 12, "dr": 5}, {"cm": "locked", "tm": 25, "dr": 0}]
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"v": "5.7.4", "fr": 10, "ip": 0, "op": 11, "w": 24, "h": 24,
|
||||
"layers": [{"ty": 4, "ind": 1, "ip": 0, "op": 11,
|
||||
"ks": {"a": {"a": 0, "k": [8, 12]}, "p": {"a": 0, "k": [8, 12]},
|
||||
"r": {"a": 1, "k": [
|
||||
{"t": 0, "s": [0], "e": [-90], "o": {"x": [0], "y": [0]}},
|
||||
{"t": 10, "s": [-90], "i": {"x": [1], "y": [1]}}
|
||||
]}, "o": {"a": 0, "k": 100}},
|
||||
"shapes": [
|
||||
{"ty": "sh", "ks": {"a": 0, "k": {
|
||||
"v": [[8, 12], [8, 8], [16, 8], [16, 12]],
|
||||
"i": [[0, 0], [0, 0], [0, -5.333333], [0, 0]],
|
||||
"o": [[0, 0], [0, -5.333333], [0, 0], [0, 0]], "c": false
|
||||
}}},
|
||||
{"ty": "st", "c": {"a": 0, "k": [0, 0, 0, 1]}, "w": {"a": 0, "k": 2}, "o": {"a": 0, "k": 100}, "lc": 2, "lj": 2}
|
||||
]
|
||||
}]
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"v": "5.7.4", "fr": 10, "ip": 0, "op": 10, "w": 24, "h": 24,
|
||||
"layers": [{
|
||||
"ty": 4, "ind": 1, "ip": 0, "op": 10, "st": 0, "sr": 1,
|
||||
"ks": {"a": {"a": 0, "k": [0, 0]}, "p": {"a": 0, "k": [0, 0]},
|
||||
"s": {"a": 0, "k": [100, 100]}, "r": {"a": 0, "k": 0}, "o": {"a": 0, "k": 80}},
|
||||
"shapes": [
|
||||
{"ty": "sh", "ks": {"a": 0, "k": {"v": [[2, 4], [22, 4]], "i": [[0, 0], [0, 0]], "o": [[0, 0], [0, 0]], "c": false}}},
|
||||
{"ty": "st", "c": {"a": 0, "k": [0.9, 0.2, 0.4, 1]}, "w": {"a": 0, "k": 2}, "o": {"a": 0, "k": 50}, "lc": 2, "lj": 3}
|
||||
]
|
||||
}]
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"v": "5.7.4", "fr": 10, "ip": 0, "op": 11, "w": 24, "h": 24,
|
||||
"layers": [{"ty": 4, "ind": 1, "ip": 0, "op": 11,
|
||||
"ks": {"p": {"a": 0, "k": [0, 0]}, "o": {"a": 0, "k": 100}},
|
||||
"shapes": [
|
||||
{"ty": "sh", "ks": {"a": 0, "k": {"v": [[0, 0], [10, 0]], "i": [[0, 0], [0, 0]], "o": [[0, 0], [0, 0]], "c": false}}},
|
||||
{"ty": "st", "c": {"a": 0, "k": [1, 1, 1, 1]}, "w": {"a": 0, "k": 2}, "o": {"a": 0, "k": 100}, "lc": 1, "lj": 1},
|
||||
{"ty": "tm", "s": {"a": 0, "k": 0}, "e": {"a": 1, "k": [
|
||||
{"t": 0, "s": [0], "e": [100], "o": {"x": [0], "y": [0]}},
|
||||
{"t": 10, "s": [100], "i": {"x": [1], "y": [1]}}
|
||||
]}, "o": {"a": 0, "k": 0}, "m": 1}
|
||||
]
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
//! Unsupported features, bounded input, and deterministic parser mutations.
|
||||
|
||||
use std::panic::catch_unwind;
|
||||
|
||||
use notescraft_lottie::{Animation, Error, Frame, Item};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
const STROKE: &str = include_str!("fixtures/stroke.json");
|
||||
|
||||
fn fixture() -> Value {
|
||||
serde_json::from_str(STROKE).expect("fixture")
|
||||
}
|
||||
|
||||
fn group(mut shapes: Value) -> Value {
|
||||
shapes.as_array_mut().expect("shape array").push(json!({"ty": "tr"}));
|
||||
json!([{"ty": "gr", "it": shapes}])
|
||||
}
|
||||
|
||||
fn rejected(value: &Value, reason: &str) {
|
||||
assert!(Animation::parse(&value.to_string()).is_err(), "accepted {reason}");
|
||||
}
|
||||
|
||||
fn unsupported(value: &Value, reason: &str) {
|
||||
assert!(
|
||||
matches!(Animation::parse(&value.to_string()),
|
||||
Err(Error::Unsupported { what, at }) if !what.is_empty() && !at.is_empty()),
|
||||
"expected contextual Unsupported for {reason}"
|
||||
);
|
||||
}
|
||||
|
||||
fn finite_bounded(frame: &Frame) {
|
||||
assert!(frame.width.is_finite());
|
||||
assert!(frame.height.is_finite());
|
||||
let mut count = 0;
|
||||
for item in &frame.items {
|
||||
let (lines, opacity) = match item {
|
||||
Item::Stroke(path) => {
|
||||
assert!(path.width.is_finite());
|
||||
(&path.polylines, path.opacity)
|
||||
}
|
||||
Item::Fill(path) => (&path.polylines, path.opacity),
|
||||
};
|
||||
assert!(opacity.is_finite());
|
||||
for line in lines {
|
||||
count += line.points.len();
|
||||
for point in &line.points {
|
||||
assert!(point.iter().all(|value| value.is_finite()));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(count <= 20_000, "sample budget exceeded: {count}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_precomp_solid_image_and_text_layers() {
|
||||
for layer_type in [0, 1, 2, 5] {
|
||||
let mut value = fixture();
|
||||
value["layers"][0]["ty"] = json!(layer_type);
|
||||
unsupported(&value, "unsupported layer type");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_masks_mattes_effects_three_d_and_time_stretch() {
|
||||
for (key, content) in
|
||||
[("masksProperties", json!([{"mode": "a"}])), ("tt", json!(1)), ("td", json!(1)), ("ef", json!([{"ty": 0}])), ("ddd", json!(1)), ("sr", json!(2))]
|
||||
{
|
||||
let mut value = fixture();
|
||||
value["layers"][0][key] = content;
|
||||
unsupported(&value, key);
|
||||
}
|
||||
let mut value = fixture();
|
||||
value["ddd"] = json!(1);
|
||||
unsupported(&value, "3D composition");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_expressions_at_property_boundaries() {
|
||||
for expression in [json!("time * 10"), json!(""), json!(true)] {
|
||||
let mut value = fixture();
|
||||
value["layers"][0]["ks"]["p"]["x"] = expression;
|
||||
unsupported(&value, "expression property");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unsupported_shape_operators_including_nested_groups() {
|
||||
for shape_type in ["gf", "gs", "rp", "mm", "rd", "pb", "zz"] {
|
||||
let mut value = fixture();
|
||||
value["layers"][0]["shapes"].as_array_mut().expect("shapes").push(json!({"ty": shape_type}));
|
||||
unsupported(&value, shape_type);
|
||||
let shapes = value["layers"][0]["shapes"].take();
|
||||
value["layers"][0]["shapes"] = group(shapes);
|
||||
unsupported(&value, "nested unsupported shape");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_composition_numbers_without_silent_repair() {
|
||||
for (key, number) in
|
||||
[("fr", json!(0)), ("fr", json!(-1)), ("w", json!(0)), ("w", json!(-1)), ("h", json!(0)), ("op", json!(-1)), ("fr", json!(1e100)), ("w", json!(1e100))]
|
||||
{
|
||||
let mut value = fixture();
|
||||
value[key] = number;
|
||||
rejected(&value, key);
|
||||
}
|
||||
for invalid in ["NaN", "Infinity", "-Infinity", "1e999"] {
|
||||
let source = STROKE.replacen("\"fr\": 10", &format!("\"fr\": {invalid}"), 1);
|
||||
assert!(Animation::parse(&source).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_nonfinite_or_overflowing_path_and_transform_values() {
|
||||
for pointer in ["/layers/0/ks/p/k/0", "/layers/0/ks/s/k/0", "/layers/0/ks/r/k", "/layers/0/shapes/1/w/k", "/layers/0/shapes/0/ks/k/v/0/0"] {
|
||||
let mut value = fixture();
|
||||
*value.pointer_mut(pointer).expect("fixture field") = json!(1e100);
|
||||
rejected(&value, pointer);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_mismatched_vertex_and_tangent_counts() {
|
||||
for field in ["v", "i", "o"] {
|
||||
let mut value = fixture();
|
||||
value["layers"][0]["shapes"][0]["ks"]["k"][field] = json!([[0, 0]]);
|
||||
rejected(&value, "mismatched path arrays");
|
||||
}
|
||||
for point in [json!([]), json!([0]), json!([0, 0, 0]), json!(["x", 0])] {
|
||||
let mut value = fixture();
|
||||
value["layers"][0]["shapes"][0]["ks"]["k"]["v"][0] = point;
|
||||
rejected(&value, "invalid vertex dimensions");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_parent_cycles_and_unknown_parent_ids() {
|
||||
let mut value = fixture();
|
||||
value["layers"][0]["parent"] = json!(1);
|
||||
rejected(&value, "self parent");
|
||||
value["layers"][0]["parent"] = json!(999);
|
||||
rejected(&value, "unknown parent");
|
||||
let mut parent = value["layers"][0].clone();
|
||||
value["layers"][0]["parent"] = json!(2);
|
||||
parent["ind"] = json!(2);
|
||||
parent["parent"] = json!(1);
|
||||
value["layers"].as_array_mut().expect("layers").push(parent);
|
||||
rejected(&value, "parent cycle");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_layer_budget_and_ten_thousand_layer_attack() {
|
||||
for count in [65, 10_000] {
|
||||
let mut value = fixture();
|
||||
value["layers"] = json!(
|
||||
(0..count)
|
||||
.map(|index| {
|
||||
let mut layer = value["layers"][0].clone();
|
||||
layer["ind"] = json!(index + 1);
|
||||
layer
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
if count == 65 {
|
||||
unsupported(&value, "layer cap");
|
||||
} else {
|
||||
rejected(&value, "ten thousand layer attack");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_group_nesting_beyond_eight() {
|
||||
let mut value = fixture();
|
||||
let mut shapes = value["layers"][0]["shapes"].take();
|
||||
for _ in 0..9 {
|
||||
shapes = group(shapes);
|
||||
}
|
||||
value["layers"][0]["shapes"] = shapes;
|
||||
unsupported(&value, "group depth cap");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_more_than_four_thousand_ninety_six_vertices() {
|
||||
let mut value = fixture();
|
||||
let points = vec![[0, 0]; 4097];
|
||||
value["layers"][0]["shapes"][0]["ks"]["k"] = json!({
|
||||
"v": points, "i": points, "o": points, "c": false
|
||||
});
|
||||
unsupported(&value, "vertex cap");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_cubic_subdivision_requiring_more_than_twenty_thousand_points() {
|
||||
let mut value = fixture();
|
||||
value["layers"][0]["shapes"][0]["ks"]["k"] = json!({
|
||||
"v": [[0, 0], [10, 0]], "i": [[0, 0], [0, 1_000_000]],
|
||||
"o": [[0, 1_000_000], [0, 0]], "c": false
|
||||
});
|
||||
value["layers"][0]["ks"]["s"]["k"] = json!([10_000, 10_000]);
|
||||
unsupported(&value, "flattened point budget");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_source_is_rejected_before_json_parsing() {
|
||||
let source = " ".repeat(512 * 1024 + 1);
|
||||
assert!(matches!(Animation::parse(&source), Err(Error::TooLarge)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncation_and_wrong_json_types_return_errors() {
|
||||
for end in [0, 1, 10, STROKE.len() / 2, STROKE.len() - 3] {
|
||||
assert!(Animation::parse(&STROKE[..end]).is_err(), "prefix {end}");
|
||||
}
|
||||
for source in ["null", "[]", "{}", "true", "42", "\"lottie\""] {
|
||||
assert!(Animation::parse(source).is_err(), "accepted {source}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonfinite_and_extreme_sample_frames_never_emit_nonfinite_geometry() {
|
||||
let animation = Animation::parse(STROKE).expect("fixture");
|
||||
for frame in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX, -f32::MAX] {
|
||||
finite_bounded(&animation.frame_at(frame));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_byte_mutations_never_panic_or_emit_invalid_geometry() {
|
||||
let sources = [STROKE, include_str!("fixtures/shackle.json"), include_str!("fixtures/keyframes.json")];
|
||||
let mut state = 0x9e37_79b9_u32;
|
||||
for iteration in 0..2000 {
|
||||
let source = sources[iteration % sources.len()];
|
||||
let mut bytes = source.as_bytes().to_vec();
|
||||
state ^= state << 13;
|
||||
state ^= state >> 17;
|
||||
state ^= state << 5;
|
||||
let index = usize::try_from(state).expect("u32 fits usize") % bytes.len();
|
||||
bytes[index] ^= state.to_le_bytes()[1];
|
||||
let input = String::from_utf8_lossy(&bytes);
|
||||
let outcome = catch_unwind(|| {
|
||||
if let Ok(animation) = Animation::parse(&input) {
|
||||
for frame in [0.0, 2.5, 5.0, 10.0] {
|
||||
finite_bounded(&animation.frame_at(frame));
|
||||
}
|
||||
}
|
||||
});
|
||||
assert!(outcome.is_ok(), "mutation {iteration} at byte {index} panicked");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//! Public metadata and input contract.
|
||||
|
||||
use notescraft_lottie::Animation;
|
||||
|
||||
const STROKE: &str = include_str!("fixtures/stroke.json");
|
||||
|
||||
fn near(actual: f32, expected: f32) {
|
||||
assert!((actual - expected).abs() < 0.001, "{actual} != {expected}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composition_metadata_is_available_without_sampling() {
|
||||
let animation = Animation::parse(STROKE).expect("valid handwritten fixture");
|
||||
near(animation.size().0, 24.0);
|
||||
near(animation.size().1, 24.0);
|
||||
near(animation.frame_rate(), 10.0);
|
||||
near(animation.first_frame(), 0.0);
|
||||
near(animation.last_frame(), 10.0);
|
||||
near(animation.duration_secs(), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markers_preserve_names_absolute_frames_and_durations() {
|
||||
let animation = Animation::parse(include_str!("fixtures/markers.json")).expect("fixture");
|
||||
near(animation.first_frame(), 10.0);
|
||||
near(animation.last_frame(), 30.0);
|
||||
near(animation.duration_secs(), 2.0);
|
||||
let marker = animation.marker("unlock").expect("named marker");
|
||||
assert_eq!(marker.name, "unlock");
|
||||
near(marker.frame, 12.0);
|
||||
near(marker.duration, 5.0);
|
||||
near(animation.marker("locked").expect("zero duration marker").duration, 0.0);
|
||||
assert!(animation.marker("missing").is_none());
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
//! Primitive evaluation and combined resource boundaries.
|
||||
|
||||
use notescraft_lottie::{Animation, Error, Item};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn fixture() -> Value {
|
||||
serde_json::from_str(include_str!("fixtures/stroke.json")).expect("valid fixture")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rounded_rectangle_and_ellipse_emit_closed_pixel_contours() {
|
||||
for primitive in [
|
||||
json!({"ty":"rc","p":{"a":0,"k":[12,12]},"s":{"a":0,"k":[20,10]},"r":{"a":0,"k":2}}),
|
||||
json!({"ty":"el","p":{"a":0,"k":[12,12]},"s":{"a":0,"k":[20,10]}}),
|
||||
] {
|
||||
let mut input = fixture();
|
||||
input["layers"][0]["shapes"][0] = primitive;
|
||||
let animation = Animation::parse(&input.to_string()).expect("primitive");
|
||||
let frame = animation.frame_at(0.0);
|
||||
let Some(Item::Stroke(stroke)) = frame.items.first() else { panic!("stroke") };
|
||||
let line = stroke.polylines.first().expect("contour");
|
||||
assert!(line.closed);
|
||||
assert!(line.points.len() > 8);
|
||||
assert!(line.points.iter().all(|[x, y]| (2.0..=22.0).contains(x) && (7.0..=17.0).contains(y)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn animated_ellipse_size_and_position_interpolate() {
|
||||
let mut input = fixture();
|
||||
input["layers"][0]["shapes"][0] = json!({
|
||||
"ty":"el",
|
||||
"p":{"a":1,"k":[{"t":0,"s":[10,10]},{"t":10,"s":[20,20]}]},
|
||||
"s":{"a":1,"k":[{"t":0,"s":[10,10]},{"t":10,"s":[20,20]}]}
|
||||
});
|
||||
let animation = Animation::parse(&input.to_string()).expect("ellipse");
|
||||
let frame = animation.frame_at(5.0);
|
||||
let Some(Item::Stroke(stroke)) = frame.items.first() else { panic!("stroke") };
|
||||
let [x, y] = stroke.polylines.first().and_then(|line| line.points.first()).expect("top vertex");
|
||||
assert!((*x - 15.0).abs() < 0.001);
|
||||
assert!((*y - 7.5).abs() < 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertex_budget_counts_all_paths_not_only_each_path() {
|
||||
let mut input = fixture();
|
||||
let mut path = input["layers"][0]["shapes"][0].clone();
|
||||
path["ks"]["k"] = json!({"v":vec![[0,0];2050],"i":vec![[0,0];2050],"o":vec![[0,0];2050],"c":false});
|
||||
input["layers"][0]["shapes"] = json!([path, path]);
|
||||
assert!(matches!(Animation::parse(&input.to_string()), Err(Error::Unsupported {what,..}) if what.contains("4096")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exactly_eight_group_levels_are_accepted() {
|
||||
let mut input = fixture();
|
||||
let mut shapes = input["layers"][0]["shapes"].take();
|
||||
for _ in 0..8 {
|
||||
shapes.as_array_mut().expect("shapes").push(json!({"ty":"tr"}));
|
||||
shapes = json!([{"ty":"gr","it":shapes}]);
|
||||
}
|
||||
input["layers"][0]["shapes"] = shapes;
|
||||
assert!(Animation::parse(&input.to_string()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn masks_and_unimplemented_trim_modes_are_explicitly_unsupported() {
|
||||
let mut input = fixture();
|
||||
input["layers"][0]["hasMask"] = json!(true);
|
||||
assert!(matches!(Animation::parse(&input.to_string()),Err(Error::Unsupported{what,..}) if what.contains("Mask")));
|
||||
input["layers"][0]["hasMask"] = json!(false);
|
||||
input["layers"][0]["shapes"].as_array_mut().expect("shapes").push(json!({"ty":"tm","m":2}));
|
||||
assert!(matches!(Animation::parse(&input.to_string()),Err(Error::Unsupported{what,..}) if what.contains("trim")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_budget_includes_every_stroke_and_fill_copy() {
|
||||
let mut input = fixture();
|
||||
let path = json!({"ty":"sh","ks":{"a":0,"k":{
|
||||
"v":[[0,0],[100,0]],"i":[[0,0],[0,100]],"o":[[0,100],[0,0]],"c":false
|
||||
}}});
|
||||
let stroke = input["layers"][0]["shapes"][1].clone();
|
||||
let mut shapes = vec![path];
|
||||
shapes.extend(std::iter::repeat_n(stroke, 1000));
|
||||
input["layers"][0]["shapes"] = json!(shapes);
|
||||
assert!(matches!(Animation::parse(&input.to_string()),Err(Error::Unsupported{what,..}) if what.contains("20000")));
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
//! Hand-calculated geometry checks for the renderer-independent draw list.
|
||||
|
||||
use notescraft_lottie::{Animation, Cap, Frame, Item, Join, StrokePath};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
const STROKE: &str = include_str!("fixtures/stroke.json");
|
||||
const TRIM: &str = include_str!("fixtures/trim.json");
|
||||
const KEYFRAMES: &str = include_str!("fixtures/keyframes.json");
|
||||
const HIERARCHY: &str = include_str!("fixtures/hierarchy.json");
|
||||
const SHACKLE: &str = include_str!("fixtures/shackle.json");
|
||||
|
||||
fn near(actual: f32, expected: f32) {
|
||||
assert!((actual - expected).abs() < 0.015, "{actual} != {expected}");
|
||||
}
|
||||
|
||||
fn stroke(frame: &Frame) -> &StrokePath {
|
||||
match frame.items.first().expect("one paint item") {
|
||||
Item::Stroke(path) => path,
|
||||
Item::Fill(_) => panic!("expected stroke"),
|
||||
}
|
||||
}
|
||||
|
||||
fn first(frame: &Frame) -> [f32; 2] {
|
||||
*stroke(frame).polylines[0].points.first().expect("point")
|
||||
}
|
||||
|
||||
fn last(frame: &Frame) -> [f32; 2] {
|
||||
*stroke(frame).polylines[0].points.last().expect("point")
|
||||
}
|
||||
|
||||
fn parse_value(value: &Value) -> Animation {
|
||||
Animation::parse(&value.to_string()).expect("valid fixture variation")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stroke_preserves_geometry_style_and_multiplies_opacity() {
|
||||
let animation = Animation::parse(STROKE).expect("fixture");
|
||||
let frame = animation.frame_at(5.0);
|
||||
near(frame.width, 24.0);
|
||||
near(frame.height, 24.0);
|
||||
assert_eq!(frame.items.len(), 1);
|
||||
let path = stroke(&frame);
|
||||
near(path.width, 2.0);
|
||||
near(path.opacity, 0.4);
|
||||
assert!(matches!(path.cap, Cap::Round));
|
||||
assert!(matches!(path.join, Join::Bevel));
|
||||
assert!(!path.polylines[0].closed);
|
||||
near(first(&frame)[0], 2.0);
|
||||
near(first(&frame)[1], 4.0);
|
||||
near(last(&frame)[0], 22.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cap_and_join_numeric_codes_follow_lottie() {
|
||||
let mut value: Value = serde_json::from_str(STROKE).expect("fixture");
|
||||
for code in 1..=3 {
|
||||
value["layers"][0]["shapes"][1]["lc"] = json!(code);
|
||||
value["layers"][0]["shapes"][1]["lj"] = json!(code);
|
||||
let frame = parse_value(&value).frame_at(0.0);
|
||||
let path = stroke(&frame);
|
||||
assert!(matches!((code, &path.cap, &path.join), (1, Cap::Butt, Join::Miter) | (2, Cap::Round, Join::Round) | (3, Cap::Square, Join::Bevel)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closed_fill_produces_fill_item_and_layer_opacity() {
|
||||
let mut value: Value = serde_json::from_str(STROKE).expect("fixture");
|
||||
value["layers"][0]["shapes"][0]["ks"]["k"] = json!({
|
||||
"v": [[2, 2], [12, 2], [12, 12]],
|
||||
"i": [[0, 0], [0, 0], [0, 0]],
|
||||
"o": [[0, 0], [0, 0], [0, 0]], "c": true
|
||||
});
|
||||
value["layers"][0]["shapes"][1] = json!({
|
||||
"ty": "fl", "c": {"a": 0, "k": [1, 0, 0, 1]},
|
||||
"o": {"a": 0, "k": 50}, "r": 1
|
||||
});
|
||||
let frame = parse_value(&value).frame_at(0.0);
|
||||
let Item::Fill(path) = &frame.items[0] else { panic!("fill item") };
|
||||
near(path.opacity, 0.4);
|
||||
assert!(path.polylines[0].closed);
|
||||
assert!(path.polylines[0].points.len() >= 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_draw_on_uses_arc_length_at_start_middle_and_end() {
|
||||
let animation = Animation::parse(TRIM).expect("fixture");
|
||||
let start = animation.frame_at(0.0);
|
||||
let visible_points: usize = start
|
||||
.items
|
||||
.iter()
|
||||
.map(|item| match item {
|
||||
Item::Stroke(path) => path.polylines.iter().map(|line| line.points.len()).sum::<usize>(),
|
||||
Item::Fill(path) => path.polylines.iter().map(|line| line.points.len()).sum::<usize>(),
|
||||
})
|
||||
.sum();
|
||||
assert!(visible_points <= 1, "zero trim must not draw a segment");
|
||||
near(last(&animation.frame_at(5.0))[0], 5.0);
|
||||
near(last(&animation.frame_at(10.0))[0], 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn temporal_bezier_easing_changes_position_from_linear_midpoint() {
|
||||
let animation = Animation::parse(KEYFRAMES).expect("fixture");
|
||||
let point = first(&animation.frame_at(5.0));
|
||||
// Cubic-bezier(.42, 0, 1, 1) at x=.5 is about .31536.
|
||||
// The local x=2 also scales to 3; rotation holds at zero.
|
||||
near(point[0], 6.1536);
|
||||
near(point[1], 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hold_rotation_jumps_at_keyframe_and_linear_scale_reaches_endpoint() {
|
||||
let animation = Animation::parse(KEYFRAMES).expect("fixture");
|
||||
let before = first(&animation.frame_at(9.0));
|
||||
near(before[1], 0.0);
|
||||
let endpoint = first(&animation.frame_at(10.0));
|
||||
near(endpoint[0], 10.0);
|
||||
near(endpoint[1], 4.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layer_start_time_offsets_animation_keyframes() {
|
||||
let mut value: Value = serde_json::from_str(KEYFRAMES).expect("fixture");
|
||||
value["layers"][0]["st"] = json!(2);
|
||||
let point = first(&parse_value(&value).frame_at(7.0));
|
||||
near(point[0], 6.1536);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parent_and_group_transforms_compose_with_anchor_and_opacity() {
|
||||
let frame = Animation::parse(HIERARCHY).expect("fixture").frame_at(0.0);
|
||||
assert_eq!(frame.items.len(), 1, "null parent never paints");
|
||||
let point = first(&frame);
|
||||
near(point[0], 10.0);
|
||||
near(point[1], 6.0);
|
||||
near(last(&frame)[1], 8.0);
|
||||
near(stroke(&frame).opacity, 0.2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curved_shackle_flattens_cubics_and_rotates_about_hinge() {
|
||||
let animation = Animation::parse(SHACKLE).expect("fixture");
|
||||
let closed = animation.frame_at(0.0);
|
||||
let open = animation.frame_at(10.0);
|
||||
let line = &stroke(&closed).polylines[0];
|
||||
assert!(line.points.len() > 4, "cubic must contain interior samples");
|
||||
assert!(line.points.iter().any(|point| point[1] < 5.0));
|
||||
near(first(&open)[0], 8.0);
|
||||
near(first(&open)[1], 12.0);
|
||||
near(last(&open)[0], 8.0);
|
||||
near(last(&open)[1], 4.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layer_visibility_uses_inclusive_in_and_exclusive_out_frame() {
|
||||
let mut value: Value = serde_json::from_str(STROKE).expect("fixture");
|
||||
value["layers"][0]["ip"] = json!(2);
|
||||
value["layers"][0]["op"] = json!(8);
|
||||
let animation = parse_value(&value);
|
||||
assert_eq!(animation.frame_at(1.0).items, Vec::new());
|
||||
assert_eq!(animation.frame_at(2.0).items.len(), 1);
|
||||
assert_eq!(animation.frame_at(8.0).items, Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn separated_position_components_animate_independently() {
|
||||
let mut value: Value = serde_json::from_str(STROKE).expect("fixture");
|
||||
value["layers"][0]["ks"]["p"] = json!({
|
||||
"s": true,
|
||||
"x": {"a": 1, "k": [
|
||||
{"t": 0, "s": [0], "e": [10], "o": {"x": [0], "y": [0]}},
|
||||
{"t": 10, "s": [10], "i": {"x": [1], "y": [1]}}
|
||||
]},
|
||||
"y": {"a": 0, "k": 6}
|
||||
});
|
||||
let frame = parse_value(&value).frame_at(5.0);
|
||||
near(first(&frame)[0], 7.0);
|
||||
near(first(&frame)[1], 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn topmost_lottie_layer_is_painted_last() {
|
||||
let mut value: Value = serde_json::from_str(STROKE).expect("fixture");
|
||||
let mut back = value["layers"][0].clone();
|
||||
back["ind"] = json!(2);
|
||||
back["ks"]["p"]["k"] = json!([10, 0]);
|
||||
value["layers"].as_array_mut().expect("layers").push(back);
|
||||
let frame = parse_value(&value).frame_at(0.0);
|
||||
let starts: Vec<_> = frame
|
||||
.items
|
||||
.iter()
|
||||
.flat_map(|item| match item {
|
||||
Item::Stroke(path) => &path.polylines,
|
||||
Item::Fill(path) => &path.polylines,
|
||||
})
|
||||
.filter_map(|line| line.points.first())
|
||||
.collect();
|
||||
assert_eq!(starts.len(), 2);
|
||||
near(starts[0][0], 12.0);
|
||||
near(starts[1][0], 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_requests_clamp_to_composition_and_nonfinite_uses_first_frame() {
|
||||
let mut value: Value = serde_json::from_str(KEYFRAMES).expect("fixture");
|
||||
value["op"] = json!(10);
|
||||
value["layers"][0]["op"] = json!(20);
|
||||
let animation = parse_value(&value);
|
||||
for requested in [-10.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
|
||||
let point = first(&animation.frame_at(requested));
|
||||
near(point[0], 2.0);
|
||||
near(point[1], 0.0);
|
||||
}
|
||||
let point = first(&animation.frame_at(100.0));
|
||||
near(point[0], 10.0);
|
||||
near(point[1], 4.0);
|
||||
}
|
||||
@@ -24,6 +24,7 @@ thiserror = { workspace = true }
|
||||
notescraft-engine = { workspace = true }
|
||||
notescraft-acp = { workspace = true }
|
||||
notescraft-git = { workspace = true }
|
||||
notescraft-lottie = { workspace = true }
|
||||
notescraft-i18n = { workspace = true }
|
||||
notescraft-math = { workspace = true }
|
||||
notescraft-themes = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
//! Renders animated icons as contact sheets for visual review:
|
||||
//! `icon_sheet out.png [--frames 9] [--cell 72] name...` draws one row of frames per icon (toggles run
|
||||
//! `off` -> `on`, one-shots run their whole length).
|
||||
|
||||
use egui::{Color32, Rect, Vec2};
|
||||
|
||||
fn main() {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let output = args.next().unwrap_or_else(|| "/tmp/icon-sheet.png".to_owned());
|
||||
let (mut frames, mut cell) = (9_u16, 72.0_f32);
|
||||
let mut names = Vec::new();
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--frames" => frames = args.next().and_then(|v| v.parse().ok()).unwrap_or(frames),
|
||||
"--cell" => cell = args.next().and_then(|v| v.parse().ok()).unwrap_or(cell),
|
||||
name => names.push(name.to_owned()),
|
||||
}
|
||||
}
|
||||
let rows = u16::try_from(names.len()).unwrap_or(u16::MAX);
|
||||
let size = Vec2::new(cell * f32::from(frames) + 100.0, cell * f32::from(rows) + 8.0);
|
||||
let mut harness = egui_kittest::Harness::builder().with_size(size).with_pixels_per_point(2.0).wgpu().build_ui_state(
|
||||
|ui, state: &mut (Vec<String>, u16)| {
|
||||
egui_extras::install_image_loaders(ui.ctx());
|
||||
ui.painter().rect_filled(ui.max_rect(), 0.0, Color32::from_rgb(24, 24, 37));
|
||||
for (row, name) in state.0.iter().enumerate() {
|
||||
let top = 4.0 + cell * f32::from(u16::try_from(row).unwrap_or(0));
|
||||
ui.painter().text(egui::pos2(6.0, top + cell / 2.0), egui::Align2::LEFT_CENTER, name, egui::FontId::proportional(11.0), Color32::GRAY);
|
||||
let Some((first, last)) = notescraft_ui_egui::lottie_icon::range(name) else { continue };
|
||||
for column in 0..state.1 {
|
||||
let t = if state.1 > 1 { f32::from(column) / f32::from(state.1 - 1) } else { 0.0 };
|
||||
let rect = Rect::from_min_size(egui::pos2(100.0 + cell * f32::from(column), top), Vec2::splat(cell)).shrink(cell * 0.22);
|
||||
notescraft_ui_egui::lottie_icon::paint_frame(ui, name, rect, Color32::from_rgb(205, 214, 244), first + (last - first) * t);
|
||||
}
|
||||
}
|
||||
},
|
||||
(names, frames),
|
||||
);
|
||||
harness.run();
|
||||
match harness.render() {
|
||||
Ok(image) => match image.save(&output) {
|
||||
Ok(()) => println!("wrote {output}"),
|
||||
Err(error) => eprintln!("could not save {output}: {error}"),
|
||||
},
|
||||
Err(error) => eprintln!("render failed: {error}"),
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ pub mod graph_view;
|
||||
pub mod icon_motion;
|
||||
pub mod icons;
|
||||
pub mod layout;
|
||||
pub mod lottie_icon;
|
||||
pub mod math_face;
|
||||
pub mod math_view;
|
||||
pub mod motion;
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
//! Animated icons: Lottie shape animations (`assets/icons-anim`) played with the egui painter, tinted with the
|
||||
//! caller's colour. An icon with no animation falls back to its static Lucide drawing.
|
||||
//!
|
||||
//! Two kinds exist. A *toggle* has an `off` and an `on` marker and runs forward to open and backward to close
|
||||
//! ([`toggle`]); a *one-shot* has a `play` marker and runs once when triggered ([`play`]).
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use egui::{Color32, Id, Pos2, Rect, Shape, Stroke, Ui};
|
||||
use notescraft_lottie::{Animation, Cap, Item, Polyline};
|
||||
|
||||
mod files;
|
||||
|
||||
/// Marker name of a toggle's resting frame.
|
||||
const OFF: &str = "off";
|
||||
/// Marker name of a toggle's final frame.
|
||||
const ON: &str = "on";
|
||||
|
||||
fn library() -> &'static HashMap<&'static str, Animation> {
|
||||
static LIBRARY: OnceLock<HashMap<&'static str, Animation>> = OnceLock::new();
|
||||
LIBRARY.get_or_init(|| files::FILES.iter().filter_map(|(name, text)| Animation::parse(text).ok().map(|animation| (*name, animation))).collect())
|
||||
}
|
||||
|
||||
/// Whether `name` has an animation.
|
||||
#[must_use]
|
||||
pub fn has(name: &str) -> bool {
|
||||
library().contains_key(name)
|
||||
}
|
||||
|
||||
/// The first and last frame to review an icon over: a toggle's `off`..`on`, a one-shot's whole length.
|
||||
#[must_use]
|
||||
pub fn range(name: &str) -> Option<(f32, f32)> {
|
||||
let animation = library().get(name)?;
|
||||
Some(if animation.marker(OFF).is_some() { toggle_range(animation) } else { (animation.first_frame(), animation.last_frame()) })
|
||||
}
|
||||
|
||||
/// Paints icon `name` at the animation's `frame` into `rect` (square), tinted `tint`. Falls back to the static
|
||||
/// icon when there is no animation.
|
||||
pub fn paint_frame(ui: &Ui, name: &str, rect: Rect, tint: Color32, frame: f32) {
|
||||
let Some(animation) = library().get(name) else {
|
||||
crate::icons::paint(ui, name, rect, tint);
|
||||
return;
|
||||
};
|
||||
let side = rect.width().min(rect.height());
|
||||
let drawn = Rect::from_center_size(rect.center(), egui::Vec2::splat(side));
|
||||
let (width, height) = animation.size();
|
||||
let scale = side / width.max(f32::EPSILON).max(height.max(f32::EPSILON)).max(1.0);
|
||||
let to_screen = |point: [f32; 2]| Pos2::new(drawn.left() + point[0] * scale, drawn.top() + point[1] * scale);
|
||||
for item in animation.frame_at(frame).items {
|
||||
match item {
|
||||
Item::Stroke(stroke) => {
|
||||
let colour = tint.gamma_multiply(stroke.opacity.clamp(0.0, 1.0));
|
||||
let line = Stroke::new((stroke.width * scale).max(0.5), colour);
|
||||
for polyline in &stroke.polylines {
|
||||
stroke_polyline(ui, polyline, line, stroke.cap == Cap::Round, &to_screen);
|
||||
}
|
||||
}
|
||||
Item::Fill(fill) => {
|
||||
let colour = tint.gamma_multiply(fill.opacity.clamp(0.0, 1.0));
|
||||
for polyline in &fill.polylines {
|
||||
let points: Vec<Pos2> = polyline.points.iter().map(|point| to_screen(*point)).collect();
|
||||
if points.len() >= 3 {
|
||||
ui.painter().add(Shape::convex_polygon(points, colour, Stroke::NONE));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stroke_polyline(ui: &Ui, polyline: &Polyline, stroke: Stroke, round: bool, to_screen: &dyn Fn([f32; 2]) -> Pos2) {
|
||||
let points: Vec<Pos2> = polyline.points.iter().map(|point| to_screen(*point)).collect();
|
||||
if points.len() < 2 {
|
||||
return;
|
||||
}
|
||||
let painter = ui.painter();
|
||||
if polyline.closed {
|
||||
painter.add(Shape::closed_line(points, stroke));
|
||||
return;
|
||||
}
|
||||
if round {
|
||||
// egui lines have butt ends: a dot at each end makes the round cap.
|
||||
for end in [points.first(), points.last()].into_iter().flatten() {
|
||||
painter.circle_filled(*end, stroke.width / 2.0, stroke.color);
|
||||
}
|
||||
}
|
||||
painter.add(Shape::line(points, stroke));
|
||||
}
|
||||
|
||||
/// The frames a toggle runs between.
|
||||
fn toggle_range(animation: &Animation) -> (f32, f32) {
|
||||
let off = animation.marker(OFF).map_or_else(|| animation.first_frame(), |marker| marker.frame);
|
||||
let on = animation.marker(ON).map_or_else(|| animation.last_frame(), |marker| marker.frame);
|
||||
(off, on)
|
||||
}
|
||||
|
||||
/// Paints a toggle icon that plays forward when `on` turns true and backward when it turns false, so opening and
|
||||
/// closing both animate. Without an animation it draws the static icon.
|
||||
pub fn toggle(ui: &Ui, id: impl std::hash::Hash + std::fmt::Debug, name: &str, rect: Rect, tint: Color32, on: bool) {
|
||||
let Some(animation) = library().get(name) else {
|
||||
crate::icons::paint(ui, name, rect, tint);
|
||||
return;
|
||||
};
|
||||
let (off, end) = toggle_range(animation);
|
||||
let seconds = ((end - off).abs() / animation.frame_rate().max(1.0)).max(0.05);
|
||||
let progress = crate::motion::progress(ui.ctx(), (id, "lottie-toggle"), on, seconds, linear);
|
||||
paint_frame(ui, name, rect, tint, off + (end - off) * progress);
|
||||
}
|
||||
|
||||
fn linear(t: f32) -> f32 {
|
||||
t
|
||||
}
|
||||
|
||||
/// Paints a one-shot icon: it runs once each time `trigger` turns true (for example on hover) and rests on its
|
||||
/// first frame otherwise.
|
||||
pub fn play(ui: &Ui, id: impl std::hash::Hash + std::fmt::Debug, name: &str, rect: Rect, tint: Color32, trigger: bool) {
|
||||
let Some(animation) = library().get(name) else {
|
||||
crate::icons::paint(ui, name, rect, tint);
|
||||
return;
|
||||
};
|
||||
let ctx = ui.ctx();
|
||||
let key = Id::new((id, "lottie-play"));
|
||||
let now = ctx.input(|input| input.time);
|
||||
let was = ctx.data(|data| data.get_temp::<bool>(key.with("was"))).unwrap_or(false);
|
||||
ctx.data_mut(|data| data.insert_temp(key.with("was"), trigger));
|
||||
if trigger && !was {
|
||||
ctx.data_mut(|data| data.insert_temp(key, now));
|
||||
}
|
||||
let started = ctx.data(|data| data.get_temp::<f64>(key));
|
||||
let length = animation.last_frame() - animation.first_frame();
|
||||
let elapsed = started.map_or(f64::INFINITY, |start| now - start);
|
||||
let frame = if crate::motion::enabled(ctx) && elapsed < f64::from(animation.duration_secs()) {
|
||||
ctx.request_repaint();
|
||||
#[allow(clippy::cast_possible_truncation)] // a frame offset inside one animation
|
||||
let offset = (elapsed as f32 * animation.frame_rate()).clamp(0.0, length);
|
||||
animation.first_frame() + offset
|
||||
} else {
|
||||
animation.first_frame()
|
||||
};
|
||||
paint_frame(ui, name, rect, tint, frame);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Every file in `assets/icons-anim` is embedded (so a new animation cannot be forgotten) and parses.
|
||||
#[test]
|
||||
fn every_animation_file_is_embedded_and_valid() {
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets/icons-anim");
|
||||
let mut on_disk: Vec<String> = std::fs::read_dir(dir)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flatten()
|
||||
.filter_map(|entry| entry.file_name().to_str().and_then(|name| name.strip_suffix(".json")).map(str::to_owned))
|
||||
.filter(|name| name != "index")
|
||||
.collect();
|
||||
on_disk.sort();
|
||||
let embedded: Vec<&str> = files::FILES.iter().map(|(name, _)| *name).collect();
|
||||
assert_eq!(on_disk.iter().map(String::as_str).collect::<Vec<_>>(), embedded);
|
||||
assert_eq!(library().len(), embedded.len(), "every embedded file parses");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggles_run_between_their_markers() {
|
||||
let Some(animation) = library().get("folder") else { return };
|
||||
let (off, on) = toggle_range(animation);
|
||||
assert!(on > off);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
//! The embedded animated icon files (`assets/icons-anim`). A test checks the list covers the folder.
|
||||
|
||||
/// `(name, Lottie JSON)` of every shipped animation.
|
||||
pub(super) static FILES: &[(&str, &str)] = &[
|
||||
("arrow-down", include_str!("../../../../assets/icons-anim/arrow-down.json")),
|
||||
("arrow-up", include_str!("../../../../assets/icons-anim/arrow-up.json")),
|
||||
("bold", include_str!("../../../../assets/icons-anim/bold.json")),
|
||||
("bookmark", include_str!("../../../../assets/icons-anim/bookmark.json")),
|
||||
("bug", include_str!("../../../../assets/icons-anim/bug.json")),
|
||||
("calendar", include_str!("../../../../assets/icons-anim/calendar.json")),
|
||||
("check", include_str!("../../../../assets/icons-anim/check.json")),
|
||||
("chevron-down", include_str!("../../../../assets/icons-anim/chevron-down.json")),
|
||||
("chevron-left", include_str!("../../../../assets/icons-anim/chevron-left.json")),
|
||||
("chevron-right", include_str!("../../../../assets/icons-anim/chevron-right.json")),
|
||||
("chevron-up", include_str!("../../../../assets/icons-anim/chevron-up.json")),
|
||||
("circle", include_str!("../../../../assets/icons-anim/circle.json")),
|
||||
("circle-check", include_str!("../../../../assets/icons-anim/circle-check.json")),
|
||||
("circle-question-mark", include_str!("../../../../assets/icons-anim/circle-question-mark.json")),
|
||||
("clipboard", include_str!("../../../../assets/icons-anim/clipboard.json")),
|
||||
("copy", include_str!("../../../../assets/icons-anim/copy.json")),
|
||||
("diamond", include_str!("../../../../assets/icons-anim/diamond.json")),
|
||||
("download", include_str!("../../../../assets/icons-anim/download.json")),
|
||||
("eraser", include_str!("../../../../assets/icons-anim/eraser.json")),
|
||||
("file-text", include_str!("../../../../assets/icons-anim/file-text.json")),
|
||||
("flame", include_str!("../../../../assets/icons-anim/flame.json")),
|
||||
("folder", include_str!("../../../../assets/icons-anim/folder.json")),
|
||||
("git-branch", include_str!("../../../../assets/icons-anim/git-branch.json")),
|
||||
("git-commit-horizontal", include_str!("../../../../assets/icons-anim/git-commit-horizontal.json")),
|
||||
("git-fork", include_str!("../../../../assets/icons-anim/git-fork.json")),
|
||||
("grid-3x3", include_str!("../../../../assets/icons-anim/grid-3x3.json")),
|
||||
("hand", include_str!("../../../../assets/icons-anim/hand.json")),
|
||||
("highlighter", include_str!("../../../../assets/icons-anim/highlighter.json")),
|
||||
("image", include_str!("../../../../assets/icons-anim/image.json")),
|
||||
("info", include_str!("../../../../assets/icons-anim/info.json")),
|
||||
("italic", include_str!("../../../../assets/icons-anim/italic.json")),
|
||||
("layers", include_str!("../../../../assets/icons-anim/layers.json")),
|
||||
("layout-dashboard", include_str!("../../../../assets/icons-anim/layout-dashboard.json")),
|
||||
("link", include_str!("../../../../assets/icons-anim/link.json")),
|
||||
("list", include_str!("../../../../assets/icons-anim/list.json")),
|
||||
("lock", include_str!("../../../../assets/icons-anim/lock.json")),
|
||||
("maximize", include_str!("../../../../assets/icons-anim/maximize.json")),
|
||||
("message-square-quote", include_str!("../../../../assets/icons-anim/message-square-quote.json")),
|
||||
("mouse-pointer-2", include_str!("../../../../assets/icons-anim/mouse-pointer-2.json")),
|
||||
("move-up-right", include_str!("../../../../assets/icons-anim/move-up-right.json")),
|
||||
("notebook-pen", include_str!("../../../../assets/icons-anim/notebook-pen.json")),
|
||||
("panel-left", include_str!("../../../../assets/icons-anim/panel-left.json")),
|
||||
("panel-right", include_str!("../../../../assets/icons-anim/panel-right.json")),
|
||||
("pen", include_str!("../../../../assets/icons-anim/pen.json")),
|
||||
("pencil", include_str!("../../../../assets/icons-anim/pencil.json")),
|
||||
("plus", include_str!("../../../../assets/icons-anim/plus.json")),
|
||||
("quote", include_str!("../../../../assets/icons-anim/quote.json")),
|
||||
("refresh-cw", include_str!("../../../../assets/icons-anim/refresh-cw.json")),
|
||||
("scissors", include_str!("../../../../assets/icons-anim/scissors.json")),
|
||||
("search", include_str!("../../../../assets/icons-anim/search.json")),
|
||||
("settings", include_str!("../../../../assets/icons-anim/settings.json")),
|
||||
("slash", include_str!("../../../../assets/icons-anim/slash.json")),
|
||||
("sparkles", include_str!("../../../../assets/icons-anim/sparkles.json")),
|
||||
("square", include_str!("../../../../assets/icons-anim/square.json")),
|
||||
("star", include_str!("../../../../assets/icons-anim/star.json")),
|
||||
("strikethrough", include_str!("../../../../assets/icons-anim/strikethrough.json")),
|
||||
("tag", include_str!("../../../../assets/icons-anim/tag.json")),
|
||||
("trash-2", include_str!("../../../../assets/icons-anim/trash-2.json")),
|
||||
("triangle-alert", include_str!("../../../../assets/icons-anim/triangle-alert.json")),
|
||||
("type", include_str!("../../../../assets/icons-anim/type.json")),
|
||||
("undo-2", include_str!("../../../../assets/icons-anim/undo-2.json")),
|
||||
("upload", include_str!("../../../../assets/icons-anim/upload.json")),
|
||||
("x", include_str!("../../../../assets/icons-anim/x.json")),
|
||||
("zap", include_str!("../../../../assets/icons-anim/zap.json")),
|
||||
];
|
||||
Reference in New Issue
Block a user