diff --git a/.bazelrc b/.bazelrc index 7e6c513edb..5325dfd50d 100644 --- a/.bazelrc +++ b/.bazelrc @@ -13,6 +13,8 @@ build --per_file_copt="third-party/webrtc/.*\.mm$","@-std=c++17" build --per_file_copt="submodules/LottieMeshSwift/LottieMeshBinding/Sources/.*\.mm$","@-std=c++17" build --per_file_copt="submodules/TelegramUI/Components/LottieCpp/Sources/.*\.mm$","@-std=c++17" build --per_file_copt="submodules/TelegramUI/Components/LottieCpp/Sources/.*\.cpp$","@-std=c++17" +build --per_file_copt="Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/.*\.cpp$","@-std=c++17" +build --per_file_copt="Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/.*\.mm$","@-std=c++17" build --swiftcopt=-whole-module-optimization diff --git a/Tests/LottieMesh/Resources/Cat.json b/Tests/LottieMesh/Resources/Cat.json deleted file mode 100644 index 9ed36bd9be..0000000000 --- a/Tests/LottieMesh/Resources/Cat.json +++ /dev/null @@ -1 +0,0 @@ -{"tgs":1,"v":"5.5.2","fr":60,"ip":0,"op":120,"w":512,"h":512,"nm":"Cat Kissing heart","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 1","parent":28,"sr":1,"ks":{"o":{"a":0,"k":0},"p":{"a":0,"k":[144.75,81.342,0]},"a":{"a":0,"k":[50,50,0]}},"ao":0,"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"f1","parent":4,"sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[16.902,56.403,0],"to":[-10.812,-9.286,0],"ti":[-4.15,9.939,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[7.578,18.211,0],"to":[-3.682,8.922,0],"ti":[-11.474,-9.33,0]},{"t":100,"s":[16.902,56.403,0]}]},"a":{"a":0,"k":[41.268,20.785,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0.651,-0.673],[8.733,-1.767],[-19.861,3.406],[-2.625,-8.662],[-1,0.306],[12.5,-2.186],[-32.67,3.532],[-9.545,2.662]],"o":[[-7.894,8.108],[-18.845,2.227],[9.22,-1.644],[0.979,3.228],[-9.903,3.028],[-27.269,4.551],[11.584,-0.098],[1.044,-0.292]],"v":[[22.717,-18.677],[-4.571,-15.821],[6.137,12.114],[33.53,0.445],[31.529,4.186],[7.089,18.038],[-1.84,-22.491],[21.727,-20.236]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[{"i":[[0.793,-0.267],[8.733,-1.767],[-21.574,0.524],[-8.505,-5.453],[1.254,0.321],[7.891,-1.169],[-32.67,3.532],[-9.454,0.339]],"o":[[-10.484,3.106],[-19.463,2.918],[10.711,-0.207],[2.701,1.732],[-6.741,1.29],[-27.325,3.017],[11.584,-0.098],[1.054,-0.034]],"v":[[26.863,-13.654],[-3.731,-17.014],[2.892,8.287],[24.973,7.108],[20.51,8.322],[3.334,13.632],[-1.853,-22.754],[27.297,-16.351]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":45,"s":[{"i":[[0.929,0.119],[8.733,-1.767],[-23.207,-2.223],[-1.817,-2.005],[3.403,0.335],[3.497,-0.198],[-32.67,3.532],[-9.368,-1.877]],"o":[[-12.954,-1.662],[-20.051,3.577],[12.132,1.162],[2.265,2.499],[-3.727,-0.367],[-27.379,1.554],[11.584,-0.098],[1.063,0.213]],"v":[[30.815,-8.865],[-2.93,-18.152],[-0.201,4.64],[15.987,10.187],[9.335,9.402],[-0.247,9.432],[-1.866,-23.005],[32.608,-12.648]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":54,"s":[{"i":[[0.871,-0.045],[8.733,-1.767],[-22.513,-1.055],[-4.661,-3.471],[2.489,0.329],[5.366,-0.611],[-32.67,3.532],[-9.405,-0.935]],"o":[[-11.903,0.366],[-19.801,3.296],[11.528,0.58],[2.451,2.173],[-5.009,0.337],[-27.356,2.176],[11.584,-0.098],[1.059,0.108]],"v":[[29.134,-10.902],[-3.271,-17.668],[1.115,6.191],[19.808,8.877],[14.087,8.943],[1.276,11.218],[-1.861,-22.898],[36.162,-12.839]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":65,"s":[{"i":[[0.793,-0.267],[8.733,-1.767],[-21.574,0.524],[-8.505,-5.453],[1.254,0.321],[7.891,-1.169],[-32.67,3.532],[-9.454,0.339]],"o":[[-10.484,3.106],[-19.463,2.918],[10.711,-0.207],[2.701,1.732],[-6.741,1.29],[-27.325,3.017],[11.584,-0.098],[1.054,-0.034]],"v":[[26.863,-13.654],[-3.731,-17.014],[2.892,8.287],[24.973,7.108],[20.51,8.322],[3.334,13.632],[-1.853,-22.754],[27.297,-16.351]],"c":true}]},{"t":100,"s":[{"i":[[0.651,-0.673],[8.733,-1.767],[-19.861,3.406],[-2.625,-8.662],[-1,0.306],[12.5,-2.186],[-32.67,3.532],[-9.545,2.662]],"o":[[-7.894,8.108],[-18.845,2.227],[9.22,-1.644],[0.979,3.228],[-9.903,3.028],[-27.269,4.551],[11.584,-0.098],[1.044,-0.292]],"v":[[22.717,-18.677],[-4.571,-15.821],[6.137,12.114],[33.53,0.445],[31.529,4.186],[7.089,18.038],[-1.84,-22.491],[21.727,-20.236]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149000010771,0.090000002992,0.176000004189,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[34.759,22.839]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[-9.375,0.938],[6.562,4.312]],"o":[[-12.188,3.938],[9.375,-0.937],[-6.562,-4.313]],"v":[[-6.437,-5],[-0.687,8],[12.063,-4.625]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[{"i":[[0,0],[-9.375,0.938],[6.562,4.312]],"o":[[-12.188,3.938],[9.375,-0.937],[-6.562,-4.313]],"v":[[-7.605,-7.323],[-3.029,-8.211],[10.895,-6.948]],"c":true}]},{"t":100,"s":[{"i":[[0,0],[-9.375,0.938],[6.562,4.312]],"o":[[-12.188,3.938],[9.375,-0.937],[-6.562,-4.313]],"v":[[-6.437,-5],[-0.687,8],[12.063,-4.625]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.847000002394,0.39199999641,0.611999990426,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[35.293,11.008]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[5,-3],[-7,-9],[-8,2],[-7.143,4.495]],"o":[[-4,-5],[-5,3],[7,9],[8,-2],[7.417,-4.666]],"v":[[3.042,-13],[-18.958,-15],[-20.958,8],[2.042,16],[20.542,0.916]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":25,"s":[{"i":[[1.874,0.695],[6.475,-3.732],[-7,-9],[-9.105,4.499],[-7.143,4.495]],"o":[[-4.562,-4.055],[-5.035,2.939],[7,9],[8.308,-3.769],[2.475,-4.597]],"v":[[3.873,-14.069],[-19.066,-15.353],[-22.312,6.098],[3.572,11.589],[22.755,-0.962]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[{"i":[[5.712,2.12],[9.496,-5.232],[-7,-9],[-9.131,-6.24],[-7.143,4.495]],"o":[[-5.712,-2.12],[-5.107,2.814],[7,9],[8,-2],[-7.646,-4.456]],"v":[[5.575,-16.258],[-19.287,-16.076],[-25.084,2.203],[8.536,12.446],[27.288,-4.807]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":75,"s":[{"i":[[1.874,0.695],[6.475,-3.732],[-7,-9],[-9.105,4.499],[-7.143,4.495]],"o":[[-4.562,-4.055],[-5.035,2.939],[7,9],[8.308,-3.769],[2.475,-4.597]],"v":[[3.873,-14.069],[-19.066,-15.353],[-22.312,6.098],[3.572,11.589],[22.755,-0.962]],"c":true}]},{"t":100,"s":[{"i":[[0,0],[5,-3],[-7,-9],[-8,2],[-7.143,4.495]],"o":[[-4,-5],[-5,3],[7,9],[8,-2],[7.417,-4.666]],"v":[[3.042,-13],[-18.958,-15],[-20.958,8],[2.042,16],[20.542,0.916]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.317999985639,0.250999989229,0.340999977261,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[41.94,21.383]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 3","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"f2","parent":4,"sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[-2.767,39.504,0],"to":[-1.188,-2.175,0],"ti":[-1.923,2.047,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[-2.018,31.694,0],"to":[-1.855,1.914,0],"ti":[-1.109,-2.178,0]},{"t":100,"s":[-2.767,39.504,0]}]},"a":{"a":0,"k":[26.122,23.263,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0.756,-0.2],[10.97,-6.857],[-1.658,-5.764],[-8.699,0.471],[3.793,-0.425],[4.467,6.13],[-13.35,2.744],[-1.28,-0.875]],"o":[[-7.171,1.887],[-4.457,2.666],[2.673,8.425],[4.046,-0.22],[-12.093,1.336],[-8.359,-11.275],[12.733,-2.744],[1.179,0.806]],"v":[[22.538,-7.906],[-5.818,-11.696],[-11.643,3.163],[11.24,15.109],[11.836,21.079],[-15.357,10.918],[-3.292,-19.671],[22.538,-9.906]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[{"i":[[0.756,-0.2],[9.396,-1.276],[-0.159,-8.296],[-6.871,-2.834],[3.795,-0.409],[4.224,10.934],[-13.35,2.744],[-1.28,-0.875]],"o":[[-7.171,1.887],[-5.267,0.715],[0.25,13.056],[3.746,1.545],[-13.734,1.479],[-3.273,-8.473],[12.733,-2.744],[1.179,0.806]],"v":[[22.538,-7.906],[-2.349,-14.456],[-15.161,-3.212],[14.168,11.861],[8.431,13.362],[-20.118,0.478],[-3.292,-19.671],[22.538,-9.906]],"c":true}]},{"t":100,"s":[{"i":[[0.756,-0.2],[10.97,-6.857],[-1.658,-5.764],[-8.699,0.471],[3.793,-0.425],[4.467,6.13],[-13.35,2.744],[-1.28,-0.875]],"o":[[-7.171,1.887],[-4.457,2.666],[2.673,8.425],[4.046,-0.22],[-12.093,1.336],[-8.359,-11.275],[12.733,-2.744],[1.179,0.806]],"v":[[22.538,-7.906],[-5.818,-11.696],[-11.643,3.163],[11.24,15.109],[11.836,21.079],[-15.357,10.918],[-3.292,-19.671],[22.538,-9.906]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149000010771,0.090000002992,0.176000004189,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[23.966,22.665]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[-8.188,-1.187],[4.5,6.375]],"o":[[-7.125,0.75],[8.187,1.188],[-4.5,-6.375]],"v":[[-5.969,-7.063],[-7.469,5.188],[11.156,0.688]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[{"i":[[0,0],[-8.231,-0.842],[4.5,6.375]],"o":[[-7.125,0.75],[14.536,1.487],[-4.5,-6.375]],"v":[[-5.969,-7.063],[-7.165,-5.362],[11.157,0.688]],"c":true}]},{"t":100,"s":[{"i":[[0,0],[-8.188,-1.187],[4.5,6.375]],"o":[[-7.125,0.75],[8.187,1.188],[-4.5,-6.375]],"v":[[-5.969,-7.063],[-7.469,5.188],[11.156,0.688]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.847000002394,0.39199999641,0.611999990426,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[30.347,13.072]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[4.666,-6.333],[-3,-6],[-10,1]],"o":[[-7,-8],[-4.667,6.334],[3,6],[10,-1]],"v":[[15,-8.667],[-12.333,-13.001],[-16,10.333],[9,18.333]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[{"i":[[0,0],[6.92,-3.741],[-1.692,-6.098],[-17.478,-1.491]],"o":[[-7,-8],[-4.634,2.506],[3.335,12.017],[10.014,0.854]],"v":[[15,-8.667],[-12.625,-13.957],[-19.628,-1.106],[11.685,12.153]],"c":true}]},{"t":100,"s":[{"i":[[0,0],[4.666,-6.333],[-3,-6],[-10,1]],"o":[[-7,-8],[-4.667,6.334],[3,6],[10,-1]],"v":[[15,-8.667],[-12.333,-13.001],[-16,10.333],[9,18.333]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.317999985639,0.250999989229,0.340999977261,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[26.504,22.426]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 3","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"paw","sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":45,"s":[107]},{"t":100,"s":[0]}]},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[111.441,291.275,0],"to":[15.458,-25.875,0],"ti":[-18.418,-3.424,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[183.941,262.775,0],"to":[-17.832,-3.201,0],"ti":[15.224,-27.332,0]},{"t":100,"s":[111.441,291.275,0]}]},"a":{"a":0,"k":[58.556,54.608,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[6.209,-5.835],[-7.703,-6.298],[-12.093,-4.587],[0.908,-0.236],[0,0],[13.005,13.809],[-5.857,11.364],[-0.037,-3.497]],"o":[[-11.16,10.306],[7.74,6.78],[0.881,0.334],[0,0],[-14.998,3.791],[-8.334,-8.919],[6.873,-13.117],[0.031,2.937]],"v":[[-12.669,-16.371],[-7.044,17.83],[26.777,14.898],[26.678,16.739],[26.635,16.75],[-13.581,19.769],[-21.801,-14.878],[10.484,-30.081]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":25,"s":[{"i":[[17.681,-13.462],[-5.195,-3.752],[-4.316,-1.138],[0.908,-0.236],[0,0],[7.616,6.279],[-5.857,11.364],[-1.898,-5.319]],"o":[[-11.16,10.306],[5.195,3.752],[4.245,-0.016],[0,0],[-4.356,0.822],[-8.14,-7.079],[12.606,-17.343],[0.987,2.766]],"v":[[-13.1,-16.985],[-9.293,9.312],[11.728,15.879],[14.154,19.355],[11.429,19.77],[-13.505,12.501],[-21.801,-14.878],[21.891,-30.155]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[{"i":[[6.209,-5.835],[-8.027,-3.588],[-2.125,-0.389],[-12.491,-10.035],[1.162,1.384],[8.488,4.463],[-5.857,11.364],[-0.037,-3.497]],"o":[[-11.16,10.306],[1.656,0.74],[13.201,2.417],[1.388,1.115],[-2.312,-2.754],[-7.453,-3.919],[6.873,-13.117],[0.031,2.937]],"v":[[-12.669,-16.371],[-7.853,13.045],[1.242,15.385],[32.684,23.987],[27.093,25.302],[-11.742,16.592],[-21.801,-14.878],[10.484,-30.081]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":75,"s":[{"i":[[17.681,-13.462],[-5.195,-3.752],[-4.316,-1.138],[0.908,-0.236],[0,0],[7.616,6.279],[-5.857,11.364],[-1.898,-5.319]],"o":[[-11.16,10.306],[5.195,3.752],[4.245,-0.016],[0,0],[-4.356,0.822],[-8.14,-7.079],[12.606,-17.343],[0.987,2.766]],"v":[[-13.1,-16.985],[-9.293,9.312],[11.728,15.879],[14.154,19.355],[11.429,19.77],[-13.505,12.501],[-21.801,-14.878],[21.891,-30.155]],"c":true}]},{"t":100,"s":[{"i":[[6.209,-5.835],[-7.703,-6.298],[-12.093,-4.587],[0.908,-0.236],[0,0],[13.005,13.809],[-5.857,11.364],[-0.037,-3.497]],"o":[[-11.16,10.306],[7.74,6.78],[0.881,0.334],[0,0],[-14.998,3.791],[-8.334,-8.919],[6.873,-13.117],[0.031,2.937]],"v":[[-12.669,-16.371],[-7.044,17.83],[26.777,14.898],[26.678,16.739],[26.635,16.75],[-13.581,19.769],[-21.801,-14.878],[10.484,-30.081]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149000010771,0.090000002992,0.176000004189,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[27.909,41.177]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":10,"s":[{"i":[[7.705,8.101],[-4.791,-0.109],[1.39,2.233],[6.232,1.487]],"o":[[7.036,12.193],[10.912,0.249],[-1.07,-1.718],[-8.815,-2.103]],"v":[[7.312,41.849],[27.461,58.027],[36.406,57.558],[28.732,54.731]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":25,"s":[{"i":[[7.705,8.101],[-4.791,-0.109],[1.39,2.233],[6.232,1.487]],"o":[[7.036,12.193],[10.912,0.249],[-1.07,-1.718],[-8.815,-2.103]],"v":[[7.312,41.849],[30.461,57.652],[39.05,58.965],[27.521,56.399]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[{"i":[[7.705,8.101],[-10.915,0.064],[4.829,11.063],[8.986,1.174]],"o":[[7.909,17.319],[19.18,-0.113],[-1.309,-2.998],[-8.986,-1.174]],"v":[[7.312,41.849],[37.533,60.321],[68.597,59.319],[49.423,51.326]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":75,"s":[{"i":[[7.705,8.101],[-4.791,-0.109],[1.39,2.233],[6.232,1.487]],"o":[[7.036,12.193],[10.912,0.249],[-1.07,-1.718],[-8.815,-2.103]],"v":[[7.312,41.849],[30.461,57.652],[39.05,58.965],[27.521,56.399]],"c":true}]},{"t":100,"s":[{"i":[[7.705,8.101],[-4.791,-0.109],[1.39,2.233],[6.232,1.487]],"o":[[7.036,12.193],[10.912,0.249],[-1.07,-1.718],[-8.815,-2.103]],"v":[[7.312,41.849],[27.461,58.027],[36.406,57.558],[28.732,54.731]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.439215686275,0.380392156863,0.462745098039,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[5.988,3.7]],"o":[[-0.133,-8.013],[0,0]],"v":[[4.471,10.997],[-5.334,-11.612]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":25,"s":[{"i":[[0,0],[16.114,0.14]],"o":[[-0.133,-8.013],[0,0]],"v":[[4.471,10.996],[-23.015,-14.798]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[{"i":[[0,0],[29.161,-11.697]],"o":[[-0.133,-8.013],[0,0]],"v":[[4.471,10.997],[-38.222,-13.226]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":75,"s":[{"i":[[0,0],[16.114,0.14]],"o":[[-0.133,-8.013],[0,0]],"v":[[4.471,10.996],[-23.015,-14.798]],"c":false}]},{"t":100,"s":[{"i":[[0,0],[5.988,3.7]],"o":[[-0.133,-8.013],[0,0]],"v":[[4.471,10.997],[-5.334,-11.612]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.149000010771,0.090000002992,0.176000004189,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[68.085,25.996]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[-7.778,-1.46],[-3.058,0.611],[-8,9],[3.43,2.535],[-0.883,1.684],[1.073,1.314],[0,0]],"o":[[2.301,0.432],[0,0],[5.054,-5.686],[-1.529,-1.13],[0.528,-1.008],[-7.042,-8.625],[-7.215,3.25]],"v":[[-12.089,9.48],[-4.046,9.312],[15.954,10.313],[13.852,-2.398],[12.546,-7.28],[11.954,-10.688],[-13.794,-5.954]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[{"i":[[-7.778,-1.46],[-3.058,0.611],[-6.645,-4.976],[3.43,2.535],[1.831,0.512],[1.303,0.154],[0,0]],"o":[[2.301,0.432],[0,0],[6.089,4.56],[-1.529,-1.13],[-2.469,-0.69],[-3.42,-0.405],[-6.52,-2.658]],"v":[[-8.944,-11.872],[-0.715,-13.142],[19.452,-7.749],[15.245,-10.666],[10.002,-13.037],[5.412,-13.916],[-9.252,-12.049]],"c":true}]},{"t":100,"s":[{"i":[[-7.778,-1.46],[-3.058,0.611],[-8,9],[3.43,2.535],[-0.883,1.684],[1.073,1.314],[0,0]],"o":[[2.301,0.432],[0,0],[5.054,-5.686],[-1.529,-1.13],[0.528,-1.008],[-7.042,-8.625],[-7.215,3.25]],"v":[[-12.089,9.48],[-4.046,9.312],[15.954,10.313],[13.852,-2.398],[12.546,-7.28],[11.954,-10.688],[-13.794,-5.954]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.847000002394,0.39199999641,0.611999990426,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[41.661,23.688]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 3","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[-1,3],[6.99,7.542],[13.008,-12.252],[-10,-9],[-13,-1]],"o":[[2.846,-8.538],[-6.99,-7.542],[-13.008,12.252],[10,9],[13,1]],"v":[[34.077,3.5],[25.077,-23.5],[-23.923,-18.5],[-18.923,20.5],[17.077,16.5]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":25,"s":[{"i":[[-1,3],[6.99,7.542],[13.008,-12.252],[-10.087,-8.9],[-13,-1]],"o":[[2.846,-8.538],[-6.99,-7.542],[-13.008,12.252],[9.4,8.317],[13,1]],"v":[[34.077,3.5],[25.077,-23.5],[-23.923,-18.5],[-19.657,12.64],[17.1,17.136]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[{"i":[[-1,3],[6.99,7.542],[13.008,-12.252],[-10.266,-8.695],[-11.542,-6.348]],"o":[[2.846,-8.538],[-6.99,-7.542],[-13.008,12.252],[8.17,6.919],[11.425,6.283]],"v":[[34.077,3.5],[25.077,-23.5],[-23.923,-18.5],[-22.185,14.962],[21.634,22.427]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":75,"s":[{"i":[[-1,3],[6.99,7.542],[13.008,-12.252],[-10.087,-8.9],[-13,-1]],"o":[[2.846,-8.538],[-6.99,-7.542],[-13.008,12.252],[9.4,8.317],[13,1]],"v":[[34.077,3.5],[25.077,-23.5],[-23.923,-18.5],[-19.657,12.64],[17.1,17.136]],"c":true}]},{"t":100,"s":[{"i":[[-1,3],[6.99,7.542],[13.008,-12.252],[-10,-9],[-13,-1]],"o":[[2.846,-8.538],[-6.99,-7.542],[-13.008,12.252],[10,9],[13,1]],"v":[[34.077,3.5],[25.077,-23.5],[-23.923,-18.5],[-18.923,20.5],[17.077,16.5]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.317999985639,0.250999989229,0.340999977261,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[37.538,40.501]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 4","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"f3","parent":4,"sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[4.619,25.343,0],"to":[-3.608,6.724,0],"ti":[-3.478,-6.387,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[5.5,47.034,0],"to":[-3.684,-6.408,0],"ti":[-3.401,6.708,0]},{"t":100,"s":[4.619,25.343,0]}]},"a":{"a":0,"k":[37.004,30.342,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[8,-2],[-3,-8],[-2,6]],"o":[[-12,-9],[-8,2],[3,8],[2,-6]],"v":[[19.5,-3.5],[-8.5,-13.5],[-18.5,7.5],[19.5,9.5]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[{"i":[[0,0],[8,-2],[-7.921,-9.107],[-13.277,-5.057]],"o":[[-12,-9],[-8,2],[8.122,9.339],[5.91,2.251]],"v":[[19.5,-3.5],[-8.5,-13.5],[-20.347,2.313],[19.5,9.5]],"c":true}]},{"t":100,"s":[{"i":[[0,0],[8,-2],[-3,-8],[-2,6]],"o":[[-12,-9],[-8,2],[3,8],[2,-6]],"v":[[19.5,-3.5],[-8.5,-13.5],[-18.5,7.5],[19.5,9.5]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.149000010771,0.090000002992,0.176000004189,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[36.5,30.5]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[-6.438,-2.687],[-1.687,3.188],[4.437,3]],"o":[[-9.688,2.5],[6.438,2.688],[1.688,-3.187],[-4.438,-3]],"v":[[-0.874,-6.469],[-8.749,4.156],[10.876,5.031],[10.751,-5.219]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[{"i":[[0,0],[-6.572,2.341],[-4.068,-1.048],[4.437,3]],"o":[[-9.688,2.5],[7.594,-2.706],[3.492,0.9],[-4.438,-3]],"v":[[-0.874,-6.469],[-8.495,-4.418],[9.323,-5.605],[10.751,-5.219]],"c":true}]},{"t":100,"s":[{"i":[[0,0],[-6.438,-2.687],[-1.687,3.188],[4.437,3]],"o":[[-9.688,2.5],[6.438,2.688],[1.688,-3.187],[-4.438,-3]],"v":[[-0.874,-6.469],[-8.749,4.156],[10.876,5.031],[10.751,-5.219]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.847000002394,0.39199999641,0.611999990426,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[30.624,24.343]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[8,-2],[-3,-8],[-2,6]],"o":[[-12,-9],[-8,2],[3,8],[2,-6]],"v":[[19.5,-3.5],[-8.5,-13.5],[-18.5,7.5],[19.5,9.5]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[{"i":[[0,0],[8,-2],[-7.921,-9.107],[-13.277,-5.057]],"o":[[-12,-9],[-8,2],[8.122,9.339],[5.91,2.251]],"v":[[19.5,-3.5],[-8.5,-13.5],[-20.347,2.313],[19.5,9.5]],"c":true}]},{"t":100,"s":[{"i":[[0,0],[8,-2],[-3,-8],[-2,6]],"o":[[-12,-9],[-8,2],[3,8],[2,-6]],"v":[[19.5,-3.5],[-8.5,-13.5],[-18.5,7.5],[19.5,9.5]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.439215689898,0.380392163992,0.46274510026,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[36.5,30.5]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 3","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":3,"nm":"Null heart","sr":1,"ks":{"o":{"a":0,"k":0},"r":{"a":0,"k":-10},"p":{"a":0,"k":[245.089,223.749,0]},"a":{"a":0,"k":[80.714,97.833,0]}},"ao":0,"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"heart","parent":6,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":102,"s":[100]},{"t":103,"s":[0]}]},"r":{"a":1,"k":[{"i":{"x":[0.189],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":80,"s":[10]},{"t":105,"s":[30]}]},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[80.714,97.833,0],"to":[-4.609,-6.736,0],"ti":[-3.364,4.442,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":60,"s":[81.349,78.398,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":80,"s":[81.349,78.398,0],"to":[-14.819,-10.736,0],"ti":[-14.105,157.019,0]},{"t":105,"s":[-52.15,-44.145,0]}]},"a":{"a":0,"k":[80.714,97.833,0]},"s":{"a":1,"k":[{"i":{"x":[0.189,0.189,0.189],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":45,"s":[0,0,100]},{"t":105,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[1.558,-4.781],[2.924,-0.08],[0.219,0.653]],"o":[[-2.251,6.795],[-0.723,0.02],[-6.051,-18.128]],"v":[[4.379,-0.723],[1.682,9.603],[0.114,8.506]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.620000023935,0.008000000785,0.340999977261,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[52.977,37.118]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-4.472,1.491],[-3.206,18.21],[14.833,3.5],[3.417,-14.708],[9.453,-8.569],[-20.125,-12.875]],"o":[[4.5,-1.5],[3.42,-19.427],[-14.483,-3.417],[0,0],[-13.375,12.125],[16.471,10.537]],"v":[[19.978,40.609],[41.103,-0.516],[24.603,-38.683],[-5.648,-19.266],[-31.148,-20.266],[-19.398,27.859]],"c":true}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.620000023935,0.008000000785,0.340999977261,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[59.522,57.1]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0.125,2.25],[-2.437,-0.375],[1.875,-2.813]],"o":[[-0.33,-5.932],[2.438,0.375],[-1.875,2.812]],"v":[[-3.867,3.625],[1.759,-5.5],[0.509,2.625]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.501999978458,0.776000019148,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[64.033,37.542]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 3","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[4.597,-0.418],[0.438,-5.75],[-5.187,-4.187],[-0.687,6.875]],"o":[[-5.166,0.469],[-0.437,5.75],[3.581,2.89],[1.03,-10.293]],"v":[[2.878,-15.096],[-7.038,-4.063],[-1.038,12.624],[0.837,-3.063]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.501999978458,0.776000019148,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[34.225,55.648]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 4","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[1.5,3.313],[-7.843,6.138],[-0.171,14.548],[6.125,6.625],[-0.713,1.695],[-6.653,-12.938],[5.232,-3.113]],"o":[[0,0],[7.666,-6],[0.136,-11.629],[-4.735,-5.122],[0.713,-1.695],[10.914,21.229],[-5.232,3.113]],"v":[[-26.625,31.541],[-8.458,30.333],[3.261,-8.902],[-4.25,-34.834],[-10.125,-39.084],[15.711,-27.646],[-1,37.666]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.823999980852,0.016000001571,0.501999978458,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[82,59.168]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 5","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-1.475,18.432],[9.07,3.416],[4.25,-15.875],[12.237,-7.231],[-13.886,-10.392],[-9.621,-0.165]],"o":[[1.916,-23.942],[-9.071,-3.415],[0,0],[-21.676,12.81],[14.476,10.835],[5.881,0.1]],"v":[[44.099,-1.657],[28.092,-35.997],[-1.265,-18.217],[-24.339,-21.799],[-14.867,27.484],[27.909,39.312]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.905999995213,0.156999999402,0.528999956916,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[55.39,56.301]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 6","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"heart 9","parent":7,"sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.189],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":77,"s":[-19.681]},{"t":102,"s":[0.319]}]},"p":{"a":1,"k":[{"i":{"x":0.189,"y":1},"o":{"x":0.167,"y":0},"t":97,"s":[63.394,65.09,0],"to":[51.309,11.913,0],"ti":[0,0,0]},{"t":117,"s":[133.266,127.688,0]}]},"a":{"a":0,"k":[66.22,63.693,0]},"s":{"a":1,"k":[{"i":{"x":[0.189,0.189,0.189],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":42,"s":[0,0,100]},{"i":{"x":[0.576,0.576,0.576],"y":[0.599,0.599,23.85]},"o":{"x":[0.17,0.17,0.17],"y":[0,0,0]},"t":97,"s":[100.318,100.318,100]},{"i":{"x":[0.836,0.836,0.836],"y":[1,1,1]},"o":{"x":[0.408,0.408,0.408],"y":[0.623,0.623,-26.829]},"t":106,"s":[43.173,43.173,100]},{"t":117,"s":[0,0,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[14.788,14.788]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.823529411765,0.01568627451,0.501960784314,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[66.22,63.693]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":-3,"op":177,"st":-3,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"heart 8","parent":7,"sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.189,"y":1},"o":{"x":0.167,"y":0},"t":99,"s":[63.394,65.09,0],"to":[-12.835,49.669,0],"ti":[0,0,0]},{"t":119,"s":[85.951,183.108,0]}]},"a":{"a":0,"k":[66.22,63.693,0]},"s":{"a":1,"k":[{"i":{"x":[0.189,0.189,0.189],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":44,"s":[0,0,100]},{"i":{"x":[0.565,0.565,0.565],"y":[0.605,0.605,18.812]},"o":{"x":[0.18,0.18,0.18],"y":[0,0,0]},"t":99,"s":[100.318,100.318,100]},{"i":{"x":[0.841,0.841,0.841],"y":[1,1,1]},"o":{"x":[0.384,0.384,0.384],"y":[0.533,0.533,-29.239]},"t":106,"s":[55.072,55.072,100]},{"t":119,"s":[0,0,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[14.788,14.788]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.823529411765,0.01568627451,0.501960784314,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[66.22,63.693]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":-1,"op":179,"st":-1,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"heart 7","parent":7,"sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.189,"y":1},"o":{"x":0.167,"y":0},"t":98,"s":[63.394,65.09,0],"to":[37.549,-61.843,0],"ti":[0,0,0]},{"t":118,"s":[187.317,21.394,0]}]},"a":{"a":0,"k":[66.22,63.693,0]},"s":{"a":1,"k":[{"i":{"x":[0.189,0.189,0.189],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":43,"s":[0,0,100]},{"i":{"x":[0.57,0.57,0.57],"y":[0.599,0.599,21.437]},"o":{"x":[0.175,0.175,0.175],"y":[0,0,0]},"t":98,"s":[100.318,100.318,100]},{"i":{"x":[0.839,0.839,0.839],"y":[1,1,1]},"o":{"x":[0.397,0.397,0.397],"y":[0.577,0.577,-28.27]},"t":106,"s":[49.155,49.155,100]},{"t":118,"s":[0,0,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[14.788,14.788]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.823529411765,0.01568627451,0.501960784314,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[66.22,63.693]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":-2,"op":178,"st":-2,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"heart 6","parent":7,"sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.189],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":76,"s":[-19.681]},{"t":101,"s":[0.319]}]},"p":{"a":1,"k":[{"i":{"x":0.189,"y":1},"o":{"x":0.167,"y":0},"t":96,"s":[63.394,65.09,0],"to":[-62.756,-13.289,0],"ti":[0,0,0]},{"t":116,"s":[-12.34,101.557,0]}]},"a":{"a":0,"k":[66.22,63.693,0]},"s":{"a":1,"k":[{"i":{"x":[0.189,0.189,0.189],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":41,"s":[0,0,100]},{"i":{"x":[0.583,0.583,0.583],"y":[0.603,0.603,26]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":96,"s":[100.318,100.318,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.417,0.417,0.417],"y":[0.675,0.675,-25]},"t":106,"s":[37.159,37.159,100]},{"t":116,"s":[0,0,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[14.788,14.788]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.823529411765,0.01568627451,0.501960784314,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[66.22,63.693]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":-4,"op":176,"st":-4,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"heart 5","parent":7,"sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.189],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":78,"s":[-19.681]},{"t":103,"s":[0.319]}]},"p":{"a":1,"k":[{"i":{"x":0.189,"y":1},"o":{"x":0.167,"y":0},"t":98,"s":[63.394,65.09,0],"to":[67.385,-10.433,0],"ti":[0,0,0]},{"t":118,"s":[174.241,84.573,0]}]},"a":{"a":0,"k":[66.22,63.693,0]},"s":{"a":1,"k":[{"i":{"x":[0.189,0.189,0.189],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":43,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":98,"s":[100.318,100.318,100]},{"t":118,"s":[0,0,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[14.788,14.788]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.823529411765,0.01568627451,0.501960784314,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[66.22,63.693]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":-2,"op":178,"st":-2,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"heart 4","parent":7,"sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.189,"y":1},"o":{"x":0.167,"y":0},"t":100,"s":[63.394,65.09,0],"to":[-53.484,38.126,0],"ti":[0,0,0]},{"t":120,"s":[24.859,164.373,0]}]},"a":{"a":0,"k":[66.22,63.693,0]},"s":{"a":1,"k":[{"i":{"x":[0.189,0.189,0.189],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":45,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":100,"s":[100.318,100.318,100]},{"t":120,"s":[0,0,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[14.788,14.788]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.823529411765,0.01568627451,0.501960784314,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[66.22,63.693]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"heart 3","parent":7,"sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.189,"y":1},"o":{"x":0.167,"y":0},"t":99,"s":[63.394,65.09,0],"to":[-3.869,-69.648,0],"ti":[0,0,0]},{"t":119,"s":[130.36,-25.451,0]}]},"a":{"a":0,"k":[66.22,63.693,0]},"s":{"a":1,"k":[{"i":{"x":[0.189,0.189,0.189],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":44,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":99,"s":[100.318,100.318,100]},{"t":119,"s":[0,0,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[14.788,14.788]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.823529411765,0.01568627451,0.501960784314,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[66.22,63.693]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":-1,"op":179,"st":-1,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"heart 2","parent":7,"sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.189],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":77,"s":[-19.681]},{"t":102,"s":[0.319]}]},"p":{"a":1,"k":[{"i":{"x":0.189,"y":1},"o":{"x":0.167,"y":0},"t":97,"s":[63.394,65.09,0],"to":[-34.617,-57.318,0],"ti":[0,0,0]},{"t":117,"s":[-31.348,31.057,0]}]},"a":{"a":0,"k":[66.22,63.693,0]},"s":{"a":1,"k":[{"i":{"x":[0.189,0.189,0.189],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":42,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":97,"s":[100.318,100.318,100]},{"t":117,"s":[0,0,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[14.788,14.788]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.823529411765,0.01568627451,0.501960784314,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[66.22,63.693]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":-3,"op":177,"st":-3,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"Shape Layer 2","parent":25,"sr":1,"ks":{"p":{"a":0,"k":[2.759,85.47,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[36.962,-2.672],[18.863,70.063]],"o":[[-55.916,4.042],[-19.074,-70.847]],"v":[[-0.674,-58.611],[-133.389,-154.947]],"c":false}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":90,"s":[0]},{"t":109,"s":[100]}]},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":85,"s":[0]},{"t":103,"s":[100]}]},"o":{"a":0,"k":0},"m":1,"nm":"Trim Paths 1","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"Shape Layer 1","parent":25,"sr":1,"ks":{"p":{"a":0,"k":[2.759,85.47,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[35.36,-11.092],[18.863,70.063]],"o":[[-55.87,17.525],[-19.074,-70.847]],"v":[[-0.674,-58.611],[-141.474,-121.937]],"c":false}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":94,"s":[0]},{"t":113,"s":[100]}]},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":89,"s":[0]},{"t":107,"s":[100]}]},"o":{"a":0,"k":0},"m":1,"nm":"Trim Paths 1","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"f4","parent":4,"sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[37.738,13.01,0],"to":[-0.376,4.472,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[35.485,39.841,0],"to":[0,0,0],"ti":[-0.376,4.472,0]},{"t":100,"s":[37.738,13.01,0]}]},"a":{"a":0,"k":[44.123,32.01,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[1.894,4.735],[8,0],[10,-6],[-4,-5],[0,0]],"o":[[-2,-5],[-8,0],[-10,6],[4,5],[0,0]],"v":[[27.053,4.5],[10.053,-6.5],[-18.947,-9.5],[-23.947,10.5],[5.053,14.5]],"c":true}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.149000010771,0.090000002992,0.176000004189,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[43.947,30.5]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[1,3],[6,-1],[-1.125,-1.625],[-6,2]],"o":[[-1.14,-3.42],[-6,1],[1.125,1.625],[6,-2]],"v":[[10.125,-2.5],[-1.875,-7.5],[-10,1.875],[0.125,6.5]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.847000002394,0.39199999641,0.611999990426,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[29.875,32.5]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0.838,5.03],[8,0],[10,-6],[-4,-5],[0,0]],"o":[[-1,-6],[-8,0],[-10,6],[4,5],[0,0]],"v":[[27.581,4.5],[10.581,-6.5],[-18.419,-9.5],[-23.419,10.5],[5.581,14.5]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.317999985639,0.250999989229,0.340999977261,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[43.419,30.5]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 3","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":"arm","parent":4,"sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":10,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":45,"s":[-46]},{"t":100,"s":[0]}]},"p":{"a":0,"k":[49.516,39.146,0]},"a":{"a":0,"k":[35.9,36.357,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[17.013,22.683],[-7,-13],[-4,-33],[-22.75,-8.167]],"o":[[-8.159,-24.976],[-9.159,-12.212],[7,13],[4,33],[0,0]],"v":[[45.768,-1.733],[-5.768,-85.769],[-38.768,-51.769],[-23.768,16.231],[26.44,97.981]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[13.927,25.823],[-7,-13],[-4,-33],[-24.518,-5.842]],"o":[[-8.159,-24.976],[-7.254,-13.262],[7,13],[4,33],[0,0]],"v":[[45.768,-1.733],[-6.04,-84.749],[-43.223,-65.617],[-23.768,16.231],[30.074,89.313]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":45,"s":[{"i":[[0,0],[10.985,28.818],[-7,-13],[-4,-33],[-26.203,-3.625]],"o":[[-8.159,-24.976],[-5.437,-14.264],[7,13],[4,33],[0,0]],"v":[[45.768,-1.733],[-6.3,-83.777],[-40.255,-58.882],[-23.768,16.231],[33.539,81.05]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":70,"s":[{"i":[[0,0],[13.927,25.823],[-7,-13],[-4,-33],[-24.518,-5.842]],"o":[[-8.159,-24.976],[-7.254,-13.262],[7,13],[4,33],[0,0]],"v":[[45.768,-1.733],[-6.04,-84.749],[-43.223,-65.617],[-23.768,16.231],[30.074,89.313]],"c":false}]},{"t":100,"s":[{"i":[[0,0],[17.013,22.683],[-7,-13],[-4,-33],[-22.75,-8.167]],"o":[[-8.159,-24.976],[-9.159,-12.212],[7,13],[4,33],[0,0]],"v":[[45.768,-1.733],[-5.768,-85.769],[-38.768,-51.769],[-23.768,16.231],[26.44,97.981]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.149000010771,0.090000002992,0.176000004189,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[60.768,112.981]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[-1.359,-0.09],[-5.833,-15.399],[11.666,7.333],[6.833,32.667]],"o":[[0,0],[3.171,0.21],[8.334,22],[-11.667,-7.334],[-6.834,-32.666]],"v":[[-32.667,-70],[-30.497,-70.27],[-15.333,-52],[21.001,66.667],[-20.999,-0.667]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[0,0],[-2.003,-2.449],[-5.423,-14.196],[-6.727,-9.744],[6.555,30.332]],"o":[[0,0],[6.396,5.722],[8.409,21.971],[-17.397,-6.976],[-7.09,-32.609]],"v":[[-40.093,-90.028],[-34.95,-89.325],[-15.72,-51.98],[24.971,61.365],[-20.999,-0.667]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":45,"s":[{"i":[[0,0],[-3.32,-7.281],[-4.585,-11.731],[-44.394,-44.716],[5.986,25.549]],"o":[[0,0],[3.297,7.231],[8.563,21.912],[-29.133,-6.243],[-7.613,-32.493]],"v":[[-38.219,-86.798],[-29.558,-88.913],[-16.511,-51.938],[33.102,50.508],[-20.999,-0.667]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":70,"s":[{"i":[[0,0],[-2.003,-2.449],[-5.423,-14.196],[-6.727,-9.744],[6.555,30.332]],"o":[[0,0],[6.396,5.722],[8.409,21.971],[-17.397,-6.976],[-7.09,-32.609]],"v":[[-40.093,-90.028],[-34.95,-89.325],[-15.72,-51.98],[24.971,61.365],[-20.999,-0.667]],"c":true}]},{"t":100,"s":[{"i":[[0,0],[-1.359,-0.09],[-5.833,-15.399],[11.666,7.333],[6.833,32.667]],"o":[[0,0],[3.171,0.21],[8.334,22],[-11.667,-7.334],[-6.834,-32.666]],"v":[[-32.667,-70],[-30.497,-70.27],[-15.333,-52],[21.001,66.667],[-20.999,-0.667]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.438999998803,0.380000005984,0.463000009574,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[61,143.545]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[1,22],[17,19],[-7,-13],[-4,-33],[-21.248,-14.628]],"o":[[-1.079,-23.745],[-10.178,-11.376],[7,13],[4,33],[11.5,7.916]],"v":[[47,7.563],[-8,-89.77],[-41,-55.77],[-26,12.23],[19,93.23]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":45,"s":[{"i":[[19.898,36.371],[7.089,17.797],[-7,-13],[-4,-33],[-28.358,-8.672]],"o":[[-14.292,-26.124],[-5.649,-14.181],[7,13],[4,33],[13.351,4.083]],"v":[[50.035,7.244],[-9.309,-87.923],[-41,-55.77],[-26,12.23],[30.422,77.266]],"c":true}]},{"t":100,"s":[{"i":[[1,22],[17,19],[-7,-13],[-4,-33],[-21.248,-14.628]],"o":[[-1.079,-23.745],[-10.178,-11.376],[7,13],[4,33],[11.5,7.916]],"v":[[47,7.563],[-8,-89.77],[-41,-55.77],[-26,12.23],[19,93.23]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.317999985639,0.250999989229,0.340999977261,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[63,116.982]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 3","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":20,"ty":4,"nm":"browe","parent":1,"sr":1,"ks":{"p":{"a":0,"k":[91.297,12.085,0]},"a":{"a":0,"k":[63.694,23.139,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":19,"s":[{"i":[[3.904,0.985],[-4.273,-0.119],[-3.051,-3.041],[-0.671,-3.881],[1.679,-3.651],[1.806,2.552],[1.843,1.804],[2.47,1.309]],"o":[[2.677,-3.02],[4.285,0.094],[3.092,3.036],[0.656,3.898],[-2.497,-3.193],[-1.778,-2.548],[-1.848,-1.812],[-2.48,-1.325]],"v":[[-15.956,-2.474],[-4.937,-6.893],[6.735,-1.755],[12.368,9.059],[11.044,20.526],[5.037,11.902],[-0.292,5.357],[-6.545,0.776]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":30,"s":[{"i":[[3.904,0.985],[-4.273,-0.119],[-3.051,-3.041],[-0.671,-3.881],[1.679,-3.651],[1.806,2.552],[1.843,1.804],[2.47,1.309]],"o":[[2.677,-3.02],[4.285,0.094],[3.092,3.036],[0.656,3.898],[-2.497,-3.193],[-1.778,-2.548],[-1.848,-1.812],[-2.48,-1.325]],"v":[[-11.733,-16.371],[-0.714,-20.79],[10.958,-15.652],[16.591,-4.838],[15.267,6.629],[9.26,-1.995],[3.931,-8.54],[-2.322,-13.121]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":75,"s":[{"i":[[3.904,0.985],[-4.273,-0.119],[-3.051,-3.041],[-0.671,-3.881],[1.679,-3.651],[1.806,2.552],[1.843,1.804],[2.47,1.309]],"o":[[2.677,-3.02],[4.285,0.094],[3.092,3.036],[0.656,3.898],[-2.497,-3.193],[-1.778,-2.548],[-1.848,-1.812],[-2.48,-1.325]],"v":[[-11.733,-16.371],[-0.714,-20.79],[10.958,-15.652],[16.591,-4.838],[15.267,6.629],[9.26,-1.995],[3.931,-8.54],[-2.322,-13.121]],"c":true}]},{"t":80,"s":[{"i":[[3.904,0.985],[-4.273,-0.119],[-3.051,-3.041],[-0.671,-3.881],[1.679,-3.651],[1.806,2.552],[1.843,1.804],[2.47,1.309]],"o":[[2.677,-3.02],[4.285,0.094],[3.092,3.036],[0.656,3.898],[-2.497,-3.193],[-1.778,-2.548],[-1.848,-1.812],[-2.48,-1.325]],"v":[[-15.956,-2.474],[-4.937,-6.893],[6.735,-1.755],[12.368,9.059],[11.044,20.526],[5.037,11.902],[-0.292,5.357],[-6.545,0.776]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.067000003889,0.027000000898,0.086000001197,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[112.887,31.951]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0.806,-0.356],[-0.272,0.808],[-0.429,0.72],[-1.226,1.085],[-3.332,0.582],[-3.14,-1.565],[-1.131,-1.275],[-0.389,-1.719],[1.365,-0.006],[1.152,-0.115],[2.18,-0.355],[2.471,-0.449],[0,0],[0.729,-0.147]],"o":[[-0.147,-0.878],[0.27,-0.826],[0.863,-1.45],[2.472,-2.141],[3.311,-0.574],[1.557,0.786],[1.109,1.306],[-1.782,-0.038],[-1.364,0.019],[-2.287,0.193],[-2.175,0.362],[0,0],[-0.69,0.116],[-0.719,0.171]],"v":[[-14.926,6.735],[-14.62,4.111],[-13.545,1.785],[-10.353,-1.986],[-1.491,-6.161],[8.609,-4.855],[12.714,-1.765],[15.074,2.735],[10.469,2.607],[6.739,2.793],[0.214,3.691],[-6.698,4.955],[-10.57,5.665],[-12.662,6.074]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.067000003889,0.027000000898,0.086000001197,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[15.324,6.985]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":21,"ty":4,"nm":"eye R closed","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":24,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":25,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":77,"s":[0]},{"t":78,"s":[100]}]},"p":{"a":0,"k":[114.362,76.994,0]},"a":{"a":0,"k":[50.774,38.871,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":19,"s":[{"i":[[-20.555,-27.688],[0.654,0.277],[36.721,-5.441],[0.259,1.49]],"o":[[0.426,0.573],[-8.064,-3.405],[-1.731,0.286],[-1.047,-6.006]],"v":[[36.722,13.173],[35.885,14.238],[-32.455,-1.642],[-36.101,-2.091]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-21.797,1.882],[0.486,-0.167],[10.773,18.711],[-0.274,-0.172]],"o":[[0.839,0.403],[-7.115,4.407],[-2.216,-3.849],[10.576,22.542]],"v":[[33.082,15.738],[32.009,18.317],[-35.783,4.352],[-34.917,-0.614]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":39,"s":[{"i":[[-1.986,-0.344],[0.306,-0.641],[14.716,47.168],[-0.844,-1.948]],"o":[[1.28,0.222],[-6.1,12.756],[-0.694,-2.225],[22.999,53.054]],"v":[[29.193,18.479],[27.865,22.677],[-36.796,1.496],[-34.86,-2.052]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":70,"s":[{"i":[[-1.986,-0.344],[0.306,-0.641],[14.716,47.168],[-0.844,-1.948]],"o":[[1.28,0.222],[-6.1,12.756],[-0.694,-2.225],[22.999,53.054]],"v":[[29.193,18.479],[27.865,22.677],[-36.796,1.496],[-34.86,-2.052]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":75,"s":[{"i":[[-21.797,1.882],[0.486,-0.167],[10.773,18.711],[-0.274,-0.172]],"o":[[0.839,0.403],[-7.115,4.407],[-2.216,-3.849],[10.576,22.542]],"v":[[33.082,15.738],[32.009,18.317],[-35.783,4.352],[-34.917,-0.614]],"c":true}]},{"t":80,"s":[{"i":[[-20.555,-27.688],[0.654,0.277],[36.721,-5.441],[0.259,1.49]],"o":[[0.426,0.573],[-8.064,-3.405],[-1.731,0.286],[-1.047,-6.006]],"v":[[36.722,13.173],[35.885,14.238],[-32.455,-1.642],[-36.101,-2.091]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458999992819,0.322000002394,0.361000001197,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[52.147,27.854]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":19,"s":[{"i":[[25,0],[-17.787,5.711],[-8.761,-9.663]],"o":[[-30.266,0],[25.543,-8.201],[9.605,10.594]],"v":[[7.029,22.538],[-27.346,-14.337],[35.529,-3.42]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[25,0],[-12.093,-5.6],[-17.585,11.825]],"o":[[-30.266,0],[19.383,13.979],[5.767,4.477]],"v":[[7.029,22.538],[-28.403,-8.57],[36.594,0.145]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":39,"s":[{"i":[[25,0],[-6.008,-17.689],[-20.726,25.677]],"o":[[-30.266,0],[12.799,37.684],[1.664,-2.062]],"v":[[7.029,22.538],[-28.396,-12.858],[37.732,3.956]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":70,"s":[{"i":[[25,0],[-6.008,-17.689],[-20.726,25.677]],"o":[[-30.266,0],[12.799,37.684],[1.664,-2.062]],"v":[[7.029,22.538],[-28.396,-12.858],[37.732,3.956]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":75,"s":[{"i":[[25,0],[-12.093,-5.6],[-17.585,11.825]],"o":[[-30.266,0],[19.383,13.979],[5.767,4.477]],"v":[[7.029,22.538],[-28.403,-8.57],[36.594,0.145]],"c":true}]},{"t":80,"s":[{"i":[[25,0],[-17.787,5.711],[-8.761,-9.663]],"o":[[-30.266,0],[25.543,-8.201],[9.605,10.594]],"v":[[7.029,22.538],[-27.346,-14.337],[35.529,-3.42]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.948999980852,0.913999968884,0.925,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[45.383,39.464]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[50.329,41.267]},"a":{"a":0,"k":[50.329,41.267]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 5","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":22,"ty":4,"nm":"eye R open","parent":1,"sr":1,"ks":{"p":{"a":0,"k":[115.656,65.054,0]},"a":{"a":0,"k":[44.404,41.45,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.762},"o":{"x":0.333,"y":0},"t":19,"s":[{"i":[[1.25,-1.938],[4.625,-0.063],[-11.5,3.438]],"o":[[-1.25,1.938],[-4.625,0.063],[11.5,-3.438]],"v":[[81.561,40.083],[69.748,42.896],[70.436,52.396]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.238},"t":23.199,"s":[{"i":[[0.692,-0.789],[2.948,0.007],[-5.577,0.064]],"o":[[-0.708,0.877],[-2.829,0.045],[7.701,-2.862]],"v":[[77.631,41.586],[70.57,42.943],[71.053,48.706]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":26.699,"s":[{"i":[[0.082,0.468],[1.112,0.084],[0.908,-3.629]],"o":[[-0.115,-0.284],[-0.862,0.025],[3.541,-2.232]],"v":[[73.329,43.23],[71.47,42.995],[69.374,52.487]],"c":true}]},{"i":{"x":0.833,"y":0.633},"o":{"x":0.167,"y":0},"t":79,"s":[{"i":[[0.082,0.468],[1.112,0.084],[0.908,-3.629]],"o":[[-0.115,-0.284],[-0.862,0.025],[3.541,-2.232]],"v":[[73.329,43.23],[71.47,42.995],[69.374,52.487]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.367},"t":81,"s":[{"i":[[0.692,-0.789],[2.948,0.007],[-5.577,0.064]],"o":[[-0.708,0.877],[-2.829,0.045],[7.701,-2.862]],"v":[[77.631,41.586],[70.57,42.943],[71.053,48.706]],"c":true}]},{"t":84,"s":[{"i":[[1.25,-1.938],[4.625,-0.063],[-11.5,3.438]],"o":[[-1.25,1.938],[-4.625,0.063],[11.5,-3.438]],"v":[[81.561,40.083],[69.748,42.896],[70.436,52.396]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149019607843,0.090196078431,0.176470588235,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":19,"s":[{"i":[[-6.094,5.6],[-17.909,-0.943],[-3.25,-5.875],[16.623,0.761]],"o":[[4.625,-4.25],[13.745,0.724],[2.961,5.352],[-22.114,-1.013]],"v":[[-28.276,0.524],[5.599,-10.079],[28.849,3.649],[3.812,-5.34]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":33,"s":[{"i":[[-1.248,8.182],[-21.537,-3.172],[0.675,-6.079],[16.623,0.761]],"o":[[1.125,-7.375],[19.921,2.934],[-0.675,6.079],[-22.114,-1.013]],"v":[[-29.026,-2.351],[7.729,-24.95],[29.599,3.649],[4.389,27.361]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":74,"s":[{"i":[[-1.248,8.182],[-21.537,-3.172],[0.675,-6.079],[16.623,0.761]],"o":[[1.125,-7.375],[19.921,2.934],[-0.675,6.079],[-22.114,-1.013]],"v":[[-29.026,-2.351],[7.729,-24.95],[29.599,3.649],[4.389,27.361]],"c":true}]},{"t":84,"s":[{"i":[[-6.094,5.6],[-17.909,-0.943],[-3.25,-5.875],[16.623,0.761]],"o":[[4.625,-4.25],[13.745,0.724],[2.961,5.352],[-22.114,-1.013]],"v":[[-28.276,0.524],[5.599,-10.079],[28.849,3.649],[3.812,-5.34]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.149000010771,0.090000002992,0.176000004189,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[42.774,40.622]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.762},"o":{"x":0.333,"y":0},"t":19,"s":[{"i":[[41.875,4.125],[-5.625,5.25],[-13.75,-0.5],[-5.25,-8.375],[8,0.125],[-0.5,-6.5],[-31.5,6.875]],"o":[[-41.875,-4.125],[5.625,-5.25],[13.75,0.5],[-6.875,-13],[-8,-0.125],[0.5,6.5],[31.5,-6.875]],"v":[[51.373,11.896],[10.248,44.646],[45.491,30.825],[69.123,47.896],[41.784,34.116],[8.248,46.646],[57.873,73.646]],"c":true}]},{"i":{"x":0.833,"y":0.762},"o":{"x":0.167,"y":0.238},"t":23.199,"s":[{"i":[[41.875,4.125],[-5.625,5.25],[-24.89,-0.067],[-3.768,-8.65],[12.124,0.214],[0.475,-3.189],[-31.5,6.875]],"o":[[-41.875,-4.125],[5.625,-5.25],[21.279,1.376],[-5.609,-1.715],[-14.124,-1.368],[-0.961,6.448],[31.5,-6.875]],"v":[[51.373,11.896],[10.248,44.646],[46.529,25.979],[73.579,48.269],[42.759,45.329],[8.248,46.646],[57.873,73.646]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.238},"t":26.699,"s":[{"i":[[41.875,4.125],[-4.175,6.463],[-22.366,-1.384],[2.66,-9.833],[17.895,0.338],[-0.5,-6.5],[-31.5,6.875]],"o":[[-41.875,-4.125],[5.701,-8.825],[19.013,1.136],[-2.056,3.375],[-22.692,-3.108],[0.5,6.5],[31.5,-6.875]],"v":[[51.373,11.896],[10.248,44.646],[47.459,21.34],[70.49,48.244],[44.191,57.316],[8.248,46.646],[57.873,73.646]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":33,"s":[{"i":[[41.875,4.125],[-5.625,5.25],[-31.574,-2.328],[2.108,-9.741],[15.633,-1.377],[-0.5,-6.5],[-31.5,6.875]],"o":[[-41.875,-4.125],[5.625,-5.25],[24.637,1.817],[-2.406,7.753],[-20.224,1.781],[0.5,6.5],[31.5,-6.875]],"v":[[51.373,11.896],[10.248,44.646],[48.499,15.613],[71.952,48.615],[46.789,68.732],[8.248,46.646],[57.873,73.646]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":74,"s":[{"i":[[41.875,4.125],[-5.625,5.25],[-31.574,-2.328],[2.108,-9.741],[15.633,-1.377],[-0.5,-6.5],[-31.5,6.875]],"o":[[-41.875,-4.125],[5.625,-5.25],[24.637,1.817],[-2.406,7.753],[-20.224,1.781],[0.5,6.5],[31.5,-6.875]],"v":[[51.373,11.896],[10.248,44.646],[48.499,15.613],[71.952,48.615],[46.789,68.732],[8.248,46.646],[57.873,73.646]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":79,"s":[{"i":[[41.875,4.125],[-4.425,6.295],[-22.366,-1.384],[3.223,-5.158],[17.895,0.338],[-0.5,-6.5],[-31.5,6.875]],"o":[[-41.875,-4.125],[4.794,-6.82],[22.57,1.897],[-2.056,3.375],[-22.692,-3.108],[0.5,6.5],[31.5,-6.875]],"v":[[51.373,11.896],[10.248,44.646],[47.238,21.797],[70.49,48.244],[44.156,54.518],[8.248,46.646],[57.873,73.646]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":82,"s":[{"i":[[41.875,4.125],[-5.625,5.25],[-21.281,-1.086],[-4.352,-8.542],[10.498,0.179],[-0.5,-6.5],[-31.5,6.875]],"o":[[-41.875,-4.125],[5.625,-5.25],[21.05,1.074],[-3.375,-4.354],[-20.928,1.053],[0.5,6.5],[31.5,-6.875]],"v":[[51.373,11.896],[10.248,44.646],[45.756,27.766],[74.451,49.491],[45.494,40.579],[9.077,48.743],[57.873,73.646]],"c":true}]},{"t":84,"s":[{"i":[[41.875,4.125],[-5.625,5.25],[-13.75,-0.5],[-5.25,-8.375],[8,0.125],[-0.5,-6.5],[-31.5,6.875]],"o":[[-41.875,-4.125],[5.625,-5.25],[13.75,0.5],[-6.875,-13],[-8,-0.125],[0.5,6.5],[31.5,-6.875]],"v":[[51.373,11.896],[10.248,44.646],[45.491,30.825],[69.123,47.896],[41.784,34.116],[8.248,46.646],[57.873,73.646]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.949019607843,0.913725490196,0.925490196078,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[1.604,-3.044],[2.992,1.578],[-1.605,3.044],[-2.992,-1.578]],"o":[[-1.604,3.044],[-2.993,-1.577],[1.605,-3.044],[2.993,1.577]],"v":[[5.419,2.856],[-2.905,5.512],[-5.418,-2.856],[2.906,-5.511]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.987999949736,0.987999949736,0.987999949736,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[55.961,41.093]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[3.15,-5.978],[5.875,3.098],[-3.151,5.978],[-5.875,-3.097]],"o":[[-3.151,5.977],[-5.876,-3.098],[3.151,-5.977],[5.876,3.098]],"v":[[10.64,5.608],[-5.705,10.823],[-10.639,-5.609],[5.705,-10.823]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149000010771,0.090000002992,0.176000004189,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[42.998,45.646]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 3","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[4.638,-8.796],[8.648,4.559],[-4.638,8.797],[-8.647,-4.559]],"o":[[-4.638,8.796],[-8.647,-4.558],[4.637,-8.796],[8.648,4.558]],"v":[[15.657,8.254],[-8.397,15.927],[-15.657,-8.254],[8.396,-15.927]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.224000010771,0.741000007181,0.969000004787,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[42.998,45.646]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 4","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-1.736,8.093],[-21.434,-3.774],[0.675,-6.078],[19.841,1.319]],"o":[[1.066,-4.972],[21.993,3.874],[-0.675,6.08],[-19.556,-1.301]],"v":[[-30.552,-1.841],[8.287,-25.121],[31.614,2.62],[3.278,27.576]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.987999949736,0.987999949736,0.987999949736,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[43.885,40.407]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 5","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":23,"ty":4,"nm":"eye L","parent":1,"sr":1,"ks":{"p":{"a":0,"k":[12.887,32.795,0]},"a":{"a":0,"k":[20.137,19.359,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[10.468,4.469],[-0.565,3.893],[-2.429,-23.587],[0.491,0.77]],"o":[[-8.042,-3.442],[1.037,-7.151],[0.094,0.908],[-5.974,-9.366]],"v":[[-5.84,-5.944],[-17.423,-10.526],[17.894,16.352],[16.298,16.908]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458999992819,0.322000002394,0.361000001197,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[21.507,17.966]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[3.417,7.333],[10,-0.334],[0,-3]],"o":[[8,3],[-4.568,-9.805],[-10,0.333],[0,3]],"v":[[10.292,12.333],[16.292,3.333],[-7.708,-14.999],[-19.708,-9.667]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.948999980852,0.913999968884,0.925,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[19.958,24.897]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[16.031,0],[1.005,2.494],[0.257,-1.046],[-4.474,-1.491]],"o":[[3,-10],[-5.985,0],[-0.402,-0.999],[-1.169,4.769],[12,4]],"v":[[12.383,15.782],[-0.909,-11.801],[-11.402,-14.783],[-13.531,-14.606],[-10.909,-1.801]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149000010771,0.090000002992,0.176000004189,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[26.159,16.032]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 3","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":24,"ty":4,"nm":"nose","parent":1,"sr":1,"ks":{"p":{"a":0,"k":[44.266,62.819,0]},"a":{"a":0,"k":[31.895,25.428,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[1.98,-5.569],[-3.148,-1.474],[-4.21,4.321],[5.95,1.986]],"o":[[-5.326,-10.492],[-2.699,7.588],[3.15,1.475],[4.124,-4.234],[-6.688,-2.232]],"v":[[-0.87,-3.81],[-16.367,-6.586],[-6.793,12.827],[14.942,9.459],[12.32,-3.394]],"c":true}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.517999985639,0.097999999102,0.317999985639,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.847000002394,0.39199999641,0.611999990426,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[31.566,26.802]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-2.305,1.963],[-13.254,-8.275],[0.668,0.234],[7.118,-4.211]],"o":[[6.574,-5.61],[0.603,0.376],[-14.536,-5.112],[-2.707,1.602]],"v":[[-14.745,0.825],[16.448,1.811],[15.935,2.881],[-11.797,4.862]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458999992819,0.322000002394,0.361000001197,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[30.726,14.376]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":25,"ty":4,"nm":"mouth","parent":1,"sr":1,"ks":{"p":{"a":0,"k":[35.082,113.25,0]},"a":{"a":0,"k":[38.091,44.721,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":35,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":45,"s":[94,106,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":80,"s":[94,106,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":90,"s":[110,90,100]},{"t":105,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[10.203,6.005],[-1.149,-0.299],[-3.818,2.432],[-2.016,-2.478]],"o":[[-1.032,-0.607],[4.568,1.192],[2.753,-1.665],[3.343,4.11]],"v":[[-11.311,1.249],[-10.527,-0.643],[2.089,-2.645],[9,-4.776]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":7,"s":[{"i":[[3.837,-13.554],[-2.5,7.537],[-3.548,3.386],[-0.991,-2.915]],"o":[[-0.484,0.526],[1.291,-3.892],[2.753,-1.665],[1.401,2.886]],"v":[[-6.592,18.515],[-8.145,11.669],[1.698,-3.279],[9,-4.776]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[3.484,-13.441],[-0.69,4.518],[-3.499,3.559],[-0.805,-2.995]],"o":[[-0.385,0.731],[1.561,-4.328],[2.753,-1.665],[1.05,2.665]],"v":[[-4.552,17.288],[-6.529,10.799],[1.627,-3.394],[9,-4.776]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[2.416,-17.375],[-0.534,6.157],[-3.39,3.943],[-0.393,-3.17]],"o":[[-0.165,1.186],[0.538,-6.206],[2.753,-1.665],[0.269,2.173]],"v":[[-1.405,18.556],[-4.664,13.52],[1.47,-3.649],[9,-4.776]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[2.416,-17.375],[-0.534,6.157],[-3.39,3.943],[-0.393,-3.17]],"o":[[-0.165,1.186],[0.538,-6.206],[2.753,-1.665],[0.269,2.173]],"v":[[-1.405,18.556],[-4.664,13.52],[1.47,-3.649],[9,-4.776]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":27,"s":[{"i":[[3.484,-13.441],[-0.69,4.518],[-3.499,3.559],[-0.805,-2.995]],"o":[[-0.385,0.731],[1.561,-4.328],[2.753,-1.665],[1.05,2.665]],"v":[[-4.552,17.288],[-6.529,10.799],[1.627,-3.394],[9,-4.776]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[3.837,-13.554],[-2.5,7.537],[-3.548,3.386],[-0.991,-2.915]],"o":[[-0.484,0.526],[1.291,-3.892],[2.753,-1.665],[1.401,2.886]],"v":[[-6.592,18.515],[-8.145,11.669],[1.698,-3.279],[9,-4.776]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":35,"s":[{"i":[[10.203,6.005],[-1.149,-0.299],[-3.818,2.432],[-2.016,-2.478]],"o":[[-1.032,-0.607],[4.568,1.192],[2.753,-1.665],[3.343,4.11]],"v":[[-11.311,1.249],[-10.527,-0.643],[2.089,-2.645],[9,-4.776]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[10.203,6.005],[-1.149,-0.299],[-3.818,2.432],[-2.016,-2.478]],"o":[[-1.032,-0.607],[4.568,1.192],[2.753,-1.665],[3.343,4.11]],"v":[[-7.877,1.746],[-7.092,-0.146],[2.089,-2.645],[9,-4.776]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":60,"s":[{"i":[[10.203,6.005],[-1.149,-0.299],[-3.818,2.432],[-2.016,-2.478]],"o":[[-1.032,-0.607],[4.568,1.192],[2.753,-1.665],[3.343,4.11]],"v":[[-11.311,1.249],[-10.527,-0.643],[2.089,-2.645],[9,-4.776]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":90,"s":[{"i":[[10.203,6.005],[-1.149,-0.299],[-3.818,2.432],[-2.016,-2.478]],"o":[[-1.032,-0.607],[4.568,1.192],[2.753,-1.665],[3.343,4.11]],"v":[[-11.311,1.249],[-10.527,-0.643],[2.089,-2.645],[9,-4.776]],"c":true}]},{"t":105,"s":[{"i":[[10.203,6.005],[-1.149,-0.299],[-3.818,2.432],[-2.016,-2.478]],"o":[[-1.032,-0.607],[4.568,1.192],[2.753,-1.665],[3.343,4.11]],"v":[[-11.311,1.249],[-10.527,-0.643],[2.089,-2.645],[9,-4.776]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458999992819,0.322000002394,0.361000001197,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[35.864,8.423]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-15.394,-15.632],[2.347,0.88],[5.217,0.4],[-0.036,0],[-7.125,-0.125],[1.765,3.851],[-0.748,-1]],"o":[[1.838,1.866],[-2.957,-0.263],[-1.83,-0.101],[-3.682,0.04],[-4.514,-2.527],[-2.247,-4.915],[1.624,2.171]],"v":[[9.884,7.541],[7.338,11.536],[-4.069,10.146],[-8.04,10.086],[3.402,6.903],[-8.83,-7.224],[-8.257,-11.416]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[-16.24,-13.855],[1.025,-0.204],[2.047,0.45],[1.151,0.735],[1.171,1.235],[1.765,3.851],[-0.748,-1]],"o":[[5.159,2.307],[-1.025,0.204],[-1.85,-0.406],[-0.778,-0.497],[-3.141,-1.765],[-2.247,-4.915],[1.624,2.171]],"v":[[14.294,8.435],[18.033,11.47],[12.523,11.037],[6.857,8.314],[2.836,5.002],[-6.558,-6.601],[-5.665,-10.675]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":7,"s":[{"i":[[-18.86,-8.351],[-1.325,-0.349],[5.852,-0.709],[4.242,1.826],[1.256,1.441],[1.829,3.821],[0.032,-0.225]],"o":[[15.448,3.672],[1.695,0.404],[-5.839,0.741],[-3.983,-1.684],[-2.935,-2.249],[-1.645,-3.436],[1.624,2.171]],"v":[[27.955,11.204],[51.167,11.266],[37.552,15.631],[19.983,12.914],[10.717,7.368],[0.481,-4.673],[2.803,-7.567]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[-20.859,-4.056],[-3.088,-0.458],[8.57,-2.196],[8.307,3.026],[1.32,1.597],[1.765,3.851],[-0.748,-1]],"o":[[21.544,4.19],[3.735,0.554],[-8.209,2.104],[-6.47,-2.357],[-2.78,-2.611],[-2.247,-4.915],[1.624,2.171]],"v":[[38.201,13.282],[74.898,10.233],[56.324,19.076],[30.196,17.492],[15.629,9.015],[4.642,-4.356],[8.275,-8.787]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[-20.859,-4.056],[-3.088,-0.458],[8.57,-2.196],[8.307,3.026],[1.32,1.597],[1.765,3.851],[-0.748,-1]],"o":[[21.544,4.19],[3.735,0.554],[-8.209,2.104],[-6.47,-2.357],[-2.78,-2.611],[-2.247,-4.915],[1.624,2.171]],"v":[[38.201,13.282],[74.898,10.233],[56.324,19.076],[30.196,17.492],[15.629,9.015],[4.642,-4.356],[8.275,-8.787]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[-18.86,-8.351],[-1.325,-0.349],[5.852,-0.709],[4.242,1.826],[1.256,1.441],[1.829,3.821],[0.032,-0.225]],"o":[[15.448,3.672],[1.695,0.404],[-5.839,0.741],[-3.983,-1.684],[-2.935,-2.249],[-1.645,-3.436],[1.624,2.171]],"v":[[27.955,11.204],[51.167,11.266],[37.552,15.631],[19.983,12.914],[10.717,7.368],[0.481,-4.673],[2.803,-7.567]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":32,"s":[{"i":[[-16.24,-13.855],[1.025,-0.204],[2.047,0.45],[1.151,0.735],[1.171,1.235],[1.765,3.851],[-0.748,-1]],"o":[[5.159,2.307],[-1.025,0.204],[-1.85,-0.406],[-0.778,-0.497],[-3.141,-1.765],[-2.247,-4.915],[1.624,2.171]],"v":[[14.294,8.435],[18.033,11.47],[12.523,11.037],[6.857,8.314],[2.836,5.002],[-6.558,-6.601],[-5.665,-10.675]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":35,"s":[{"i":[[-15.394,-15.632],[2.347,0.88],[5.217,0.4],[-0.036,0],[-7.125,-0.125],[1.765,3.851],[-0.748,-1]],"o":[[1.838,1.866],[-2.957,-0.263],[-1.83,-0.101],[-3.682,0.04],[-4.514,-2.527],[-2.247,-4.915],[1.624,2.171]],"v":[[9.884,7.541],[7.338,11.536],[-4.069,10.146],[-8.04,10.086],[3.402,6.903],[-8.83,-7.224],[-8.257,-11.416]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":45,"s":[{"i":[[-5.096,-8.374],[2.347,0.88],[1.609,-0.119],[-0.036,0],[-7.125,-0.125],[0.611,4.46],[-0.419,-1.176]],"o":[[1.362,2.237],[-2.957,-0.263],[-1.83,-0.101],[-3.682,0.04],[-2.743,-2.686],[-0.734,-5.354],[1.376,3.858]],"v":[[-0.002,4.819],[-2.548,8.814],[-8.021,9.11],[-11.992,9.05],[-7.704,3.855],[-12.193,-7.774],[-8.257,-11.416]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":60,"s":[{"i":[[-15.394,-15.632],[2.347,0.88],[5.217,0.4],[-0.036,0],[-7.125,-0.125],[1.765,3.851],[-0.748,-1]],"o":[[1.838,1.866],[-2.957,-0.263],[-1.83,-0.101],[-3.682,0.04],[-4.514,-2.527],[-2.247,-4.915],[1.624,2.171]],"v":[[9.884,7.541],[7.338,11.536],[-4.069,10.146],[-8.04,10.086],[3.402,6.903],[-8.83,-7.224],[-8.257,-11.416]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":80,"s":[{"i":[[2.831,-14.008],[4.18,1.205],[2.903,3.906],[-1.337,-1.27],[6.062,14.467],[6.231,4.347],[-1.228,-0.224]],"o":[[-1.105,5.467],[-4.18,-1.205],[-1.125,-1.513],[12.63,12.003],[-1.746,-4.167],[-4.432,-3.092],[10.781,1.969]],"v":[[16.69,17.355],[4.53,23.304],[-5.988,15.963],[-8.04,10.086],[10.483,7.764],[-6.757,-5.861],[-9.015,-10.391]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":85,"s":[{"i":[[-0.909,-6.876],[2.955,-0.693],[2.518,2.319],[-0.686,-0.635],[2.939,7.703],[3.998,4.099],[-0.988,-0.612]],"o":[[0.357,2.702],[-2.955,0.693],[-2.518,-2.319],[5.253,4.075],[-3.749,-9.825],[-3.34,-4.004],[10.418,3.126]],"v":[[11.518,11.956],[5.934,17.42],[-5.311,14.46],[-8.04,10.086],[7.161,9.489],[-7.516,-4.502],[-5.538,-6.981]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":90,"s":[{"i":[[-15.394,-15.632],[2.347,0.88],[5.217,0.4],[-0.036,0],[-7.125,-0.125],[1.765,3.851],[-0.748,-1]],"o":[[1.838,1.866],[-2.957,-0.263],[-1.83,-0.101],[-3.682,0.04],[-4.514,-2.527],[-2.247,-4.915],[1.624,2.171]],"v":[[9.884,7.541],[7.338,11.536],[-4.069,10.146],[-8.04,10.086],[3.402,6.903],[-8.83,-7.224],[-8.257,-11.416]],"c":true}]},{"t":105,"s":[{"i":[[-15.394,-15.632],[2.347,0.88],[5.217,0.4],[-0.036,0],[-7.125,-0.125],[1.765,3.851],[-0.748,-1]],"o":[[1.838,1.866],[-2.957,-0.263],[-1.83,-0.101],[-3.682,0.04],[-4.514,-2.527],[-2.247,-4.915],[1.624,2.171]],"v":[[9.884,7.541],[7.338,11.536],[-4.069,10.146],[-8.04,10.086],[3.402,6.903],[-8.83,-7.224],[-8.257,-11.416]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458999992819,0.322000002394,0.361000001197,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[25.326,31.486]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 4","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":0,"s":[{"i":[[10.258,-3.375],[-9.607,-1.68],[2.975,0.536],[-6.896,2.144],[0.601,-1.029]],"o":[[0.521,-0.026],[1.134,0.198],[-14.084,-2.525],[5.564,-1.249],[2.187,-3.745]],"v":[[-2.798,0.26],[8.057,10.161],[6.826,14.91],[-4.012,-4.59],[4.286,-11.701]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[-0.116,-6.751],[-10.685,-1.372],[2.996,0.317],[-2.222,4.723],[0.22,-2.367]],"o":[[1.846,2.009],[1.14,0.152],[-14.178,-1.494],[0.832,-5.579],[1.4,0.476]],"v":[[-1.429,-0.328],[13.527,8.814],[12.322,13.535],[-5.494,-3.009],[0.533,-15]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[-4.282,-5.58],[-12.071,-0.976],[3.023,0.036],[3.787,8.039],[-0.269,-4.088]],"o":[[3.55,4.626],[1.147,0.093],[-14.299,-0.169],[-5.251,-11.146],[0.388,5.903]],"v":[[-1.669,-0.799],[20.559,7.081],[19.39,11.767],[-7.4,-0.975],[-4.293,-19.242]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[-4.282,-5.58],[-12.071,-0.976],[3.023,0.036],[3.787,8.039],[-0.269,-4.088]],"o":[[3.55,4.626],[1.147,0.093],[-14.299,-0.169],[-5.251,-11.146],[0.388,5.903]],"v":[[-1.669,-0.799],[20.559,7.081],[19.39,11.767],[-7.4,-0.975],[-4.293,-19.242]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-0.116,-6.751],[-10.685,-1.372],[2.996,0.317],[-2.222,4.723],[0.22,-2.367]],"o":[[1.846,2.009],[1.14,0.152],[-14.178,-1.494],[0.832,-5.579],[1.4,0.476]],"v":[[-1.429,-0.328],[13.527,8.814],[12.322,13.535],[-5.494,-3.009],[0.533,-15]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":35,"s":[{"i":[[10.258,-3.375],[-9.607,-1.68],[2.975,0.536],[-6.896,2.144],[0.601,-1.029]],"o":[[0.521,-0.026],[1.134,0.198],[-14.084,-2.525],[5.564,-1.249],[2.187,-3.745]],"v":[[-2.798,0.26],[8.057,10.161],[6.826,14.91],[-4.012,-4.59],[4.286,-11.701]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":45,"s":[{"i":[[10.258,-3.375],[-9.607,-1.68],[2.975,0.536],[-6.896,2.144],[0.601,-1.029]],"o":[[0.521,-0.026],[1.134,0.198],[-14.084,-2.525],[5.564,-1.249],[2.187,-3.745]],"v":[[-6.155,3.293],[8.057,10.161],[6.826,14.91],[-7.369,-1.558],[3.837,-7.347]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":60,"s":[{"i":[[10.258,-3.375],[-9.607,-1.68],[2.975,0.536],[-6.896,2.144],[0.601,-1.029]],"o":[[0.521,-0.026],[1.134,0.198],[-14.084,-2.525],[5.564,-1.249],[2.187,-3.745]],"v":[[-2.798,0.26],[8.057,10.161],[6.826,14.91],[-4.012,-4.59],[4.286,-11.701]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":80,"s":[{"i":[[1.393,-7.271],[-4.657,-2.941],[3.574,2.066],[-2.086,2.974],[-0.436,-1.356]],"o":[[-0.312,1.629],[0.973,0.615],[-12.387,-7.162],[2.086,-2.974],[0.436,1.356]],"v":[[1.159,-1.943],[12.854,8.333],[10.121,11.238],[-4.012,-4.59],[4.286,-11.701]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":90,"s":[{"i":[[10.258,-3.375],[-9.607,-1.68],[2.956,0.631],[-5.926,-1.672],[0.601,-1.029]],"o":[[0.521,-0.026],[1.134,0.198],[-18.736,-4.002],[5.532,1.561],[2.187,-3.745]],"v":[[-5.256,-0.876],[8.057,10.161],[6.826,14.91],[-7.343,-6.875],[4.286,-11.701]],"c":true}]},{"t":105,"s":[{"i":[[10.258,-3.375],[-9.607,-1.68],[2.975,0.536],[-6.896,2.144],[0.601,-1.029]],"o":[[0.521,-0.026],[1.134,0.198],[-14.084,-2.525],[5.564,-1.249],[2.187,-3.745]],"v":[[-2.798,0.26],[8.057,10.161],[6.826,14.91],[-4.012,-4.59],[4.286,-11.701]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458999992819,0.322000002394,0.361000001197,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[11.157,15.696]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 6","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-2.036,-20.327],[-8.366,-0.032],[-18.363,1.556],[10.924,13.307]],"o":[[2.383,23.785],[6.825,8.911],[18.363,-1.556],[-10.924,-13.307]],"v":[[3.259,1.966],[29.743,25.343],[73.71,48.538],[83.32,23.548]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.949019607843,0.913725490196,0.925490196078,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":0,"s":[{"i":[[0,0],[-8.194,0.491],[0,0]],"o":[[0,0],[3.86,-0.232],[0,0]],"v":[[-6.773,4.761],[0.981,-2.393],[6.728,3.902]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[2.201,1.759],[-4.768,0.77],[0,0]],"o":[[-2.201,-1.759],[3.818,-0.616],[0,0]],"v":[[-4.836,5.261],[-5.394,-1.83],[2.29,4.84]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[2.054,1.642],[-4.997,0.751],[0,0]],"o":[[-2.054,-1.642],[3.821,-0.591],[0,0]],"v":[[-3.153,2.04],[-4.967,-1.868],[2.587,4.777]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[2.054,1.642],[-4.997,0.751],[0,0]],"o":[[-2.054,-1.642],[3.821,-0.591],[0,0]],"v":[[-1.748,-0.634],[-4.967,-1.868],[2.587,4.777]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":32,"s":[{"i":[[2.201,1.759],[-4.768,0.77],[0,0]],"o":[[-2.201,-1.759],[3.818,-0.616],[0,0]],"v":[[-4.836,5.261],[-5.394,-1.83],[2.29,4.84]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":35,"s":[{"i":[[0,0],[-8.194,0.491],[0,0]],"o":[[0,0],[3.86,-0.232],[0,0]],"v":[[-6.773,4.761],[0.981,-2.393],[6.728,3.902]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":45,"s":[{"i":[[0,0],[-2.704,-0.654],[0,0]],"o":[[0,0],[3.759,0.909],[0,0]],"v":[[-4.962,3.552],[-6.343,-2.288],[-1.444,2.223]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":60,"s":[{"i":[[0,0],[-8.194,0.491],[0,0]],"o":[[0,0],[3.86,-0.232],[0,0]],"v":[[-6.773,4.761],[0.981,-2.393],[6.728,3.902]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":80,"s":[{"i":[[10.384,13.247],[3.772,12.379],[4.779,-13.11]],"o":[[-4.157,-5.303],[-1.127,-3.699],[-0.564,1.548]],"v":[[-3.763,9.055],[0.656,-7.872],[14.766,13.782]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":90,"s":[{"i":[[0,0],[-8.194,0.491],[0,0]],"o":[[0,0],[3.86,-0.232],[0,0]],"v":[[-6.773,4.761],[0.981,-2.393],[6.728,3.902]],"c":true}]},{"t":105,"s":[{"i":[[0,0],[-8.194,0.491],[0,0]],"o":[[0,0],[3.86,-0.232],[0,0]],"v":[[-6.773,4.761],[0.981,-2.393],[6.728,3.902]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.447000002394,0.102000000898,0.102000000898,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[23.782,36.03]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 5","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[4.772,4.418],[-6.346,6.521],[-0.083,-4.777],[5.417,-5.639],[-6.26,-2.111],[-2.375,-0.833],[1,0.057]],"o":[[-4.351,-4.234],[4.656,-4.71],[0.074,4.288],[-3.982,4.06],[2.39,0.799],[4.029,1.254],[-4.481,-0.255]],"v":[[-6.153,11.467],[-5.353,-8.396],[8.798,-11.109],[-1.774,-4.906],[-0.14,8.869],[7.67,10.739],[9.92,13.592]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[4.772,4.418],[-6.346,6.521],[-0.083,-4.777],[1.752,-1.585],[-6.26,-2.111],[-2.375,-0.833],[1,0.057]],"o":[[-4.351,-4.234],[2.143,-1.157],[0.074,4.288],[-3.982,4.06],[2.39,0.799],[4.029,1.254],[-4.481,-0.255]],"v":[[-6.153,11.467],[-5.353,-8.396],[2.611,-8.796],[-1.774,-4.906],[-0.14,8.869],[7.67,10.739],[9.92,13.592]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[4.772,4.418],[-6.346,6.521],[-0.083,-4.777],[2.003,-1.863],[-6.26,-2.111],[-2.375,-0.833],[1,0.057]],"o":[[-4.351,-4.234],[2.316,-1.401],[0.074,4.288],[-3.982,4.06],[2.39,0.799],[4.029,1.254],[-4.481,-0.255]],"v":[[-5.402,11.469],[-5.353,-8.396],[3.035,-8.955],[-1.774,-4.906],[-0.14,8.869],[7.67,10.739],[9.92,13.592]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[4.772,4.418],[-6.346,6.521],[-0.083,-4.777],[2.129,-2.002],[-6.26,-2.111],[-2.375,-0.833],[1,0.057]],"o":[[-4.351,-4.234],[2.402,-1.522],[0.074,4.288],[-3.982,4.06],[2.39,0.799],[4.029,1.254],[-4.481,-0.255]],"v":[[-4.026,9.844],[-5.353,-8.396],[3.247,-9.034],[-1.774,-4.906],[0.861,6.12],[7.67,10.739],[9.92,13.592]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":7,"s":[{"i":[[4.772,4.418],[-6.346,6.521],[-0.083,-4.777],[2.255,-2.141],[-6.26,-2.111],[-2.375,-0.833],[1,0.057]],"o":[[-4.351,-4.234],[2.488,-1.644],[0.074,4.288],[-3.982,4.06],[2.39,0.799],[4.029,1.254],[-4.481,-0.255]],"v":[[-2.65,8.22],[-5.353,-8.396],[3.459,-9.113],[-1.774,-4.906],[-0.14,8.869],[7.67,10.739],[9.92,13.592]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[4.772,4.418],[-6.346,6.521],[-0.083,-4.777],[2.255,-2.141],[-6.26,-2.111],[-2.375,-0.833],[1,0.057]],"o":[[-4.351,-4.234],[2.488,-1.644],[0.074,4.288],[-3.982,4.06],[2.39,0.799],[4.029,1.254],[-4.481,-0.255]],"v":[[-0.896,6.642],[-5.353,-8.396],[3.459,-9.113],[-1.774,-4.906],[1.582,6.971],[7.67,10.739],[9.92,13.592]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":29,"s":[{"i":[[2.9,2.721],[-6.346,6.521],[-0.083,-4.777],[2.129,-2.002],[-6.26,-2.111],[-2.375,-0.833],[1,0.057]],"o":[[-4.351,-4.234],[2.402,-1.522],[0.074,4.288],[-3.982,4.06],[2.39,0.799],[4.029,1.254],[-4.481,-0.255]],"v":[[-4.018,9.22],[-5.353,-8.396],[3.247,-9.034],[-1.774,-4.906],[1.151,7.446],[7.67,10.739],[9.92,13.592]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":32,"s":[{"i":[[4.772,4.418],[-6.346,6.521],[-0.083,-4.777],[1.752,-1.585],[-6.26,-2.111],[-2.375,-0.833],[1,0.057]],"o":[[-4.351,-4.234],[2.143,-1.157],[0.074,4.288],[-3.982,4.06],[2.39,0.799],[4.029,1.254],[-4.481,-0.255]],"v":[[-6.153,11.467],[-5.353,-8.396],[2.611,-8.796],[-1.774,-4.906],[-0.14,8.869],[7.67,10.739],[9.92,13.592]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":35,"s":[{"i":[[4.772,4.418],[-6.346,6.521],[-0.083,-4.777],[5.417,-5.639],[-6.26,-2.111],[-2.375,-0.833],[1,0.057]],"o":[[-4.351,-4.234],[4.656,-4.71],[0.074,4.288],[-3.982,4.06],[2.39,0.799],[4.029,1.254],[-4.481,-0.255]],"v":[[-6.153,11.467],[-5.353,-8.396],[8.798,-11.109],[-1.774,-4.906],[-0.14,8.869],[7.67,10.739],[9.92,13.592]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":45,"s":[{"i":[[7.665,6.41],[-6.346,6.521],[-0.083,-4.777],[5.417,-5.639],[-6.26,-2.111],[-2.375,-0.833],[1,0.057]],"o":[[-4.351,-4.234],[4.656,-4.71],[0.074,4.288],[-3.982,4.06],[2.39,0.799],[4.029,1.254],[-4.481,-0.255]],"v":[[-10.108,16.681],[-5.181,-5.699],[8.798,-11.109],[-1.602,-2.208],[-4.095,14.083],[3.369,9.254],[5.619,12.107]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":60,"s":[{"i":[[4.772,4.418],[-6.346,6.521],[-0.083,-4.777],[5.417,-5.639],[-6.26,-2.111],[-2.375,-0.833],[1,0.057]],"o":[[-4.351,-4.234],[4.656,-4.71],[0.074,4.288],[-3.982,4.06],[2.39,0.799],[4.029,1.254],[-4.481,-0.255]],"v":[[-6.153,11.467],[-5.353,-8.396],[8.798,-11.109],[-1.774,-4.906],[-0.14,8.869],[7.67,10.739],[9.92,13.592]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":80,"s":[{"i":[[4.772,4.418],[-6.346,6.521],[-3.128,-3.612],[3.79,-4.411],[-7.455,-5.647],[-2.375,-0.833],[1,0.057]],"o":[[-5.508,-4.522],[1.627,-2.692],[2.224,2.568],[-3.982,4.06],[2.009,1.522],[4.029,1.254],[-4.481,-0.255]],"v":[[-3.872,19.16],[-7.333,-2.337],[13.194,-2.767],[-3.754,1.154],[2.141,16.562],[9.951,18.431],[12.201,21.284]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":90,"s":[{"i":[[4.772,4.418],[-6.346,6.521],[-0.083,-4.777],[5.417,-5.639],[-6.26,-2.111],[-2.375,-0.833],[1,0.057]],"o":[[-4.351,-4.234],[4.656,-4.71],[0.074,4.288],[-3.982,4.06],[2.39,0.799],[4.029,1.254],[-4.481,-0.255]],"v":[[-8.475,11.923],[-7.675,-7.941],[8.798,-11.109],[-3.985,-4.431],[-2.351,9.344],[7.67,10.739],[9.92,13.592]],"c":true}]},{"t":105,"s":[{"i":[[4.772,4.418],[-6.346,6.521],[-0.083,-4.777],[5.417,-5.639],[-6.26,-2.111],[-2.375,-0.833],[1,0.057]],"o":[[-4.351,-4.234],[4.656,-4.71],[0.074,4.288],[-3.982,4.06],[2.39,0.799],[4.029,1.254],[-4.481,-0.255]],"v":[[-6.153,11.467],[-5.353,-8.396],[8.798,-11.109],[-1.774,-4.906],[-0.14,8.869],[7.67,10.739],[9.92,13.592]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458999992819,0.322000002394,0.361000001197,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[14.643,40.835]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 3","bm":0,"hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[17.694,45.337],"to":[3.346,-4.578],"ti":[-3.346,4.578]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":10,"s":[37.77,17.871],"to":[0,0],"ti":[0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":25,"s":[37.77,17.871],"to":[-3.346,4.578],"ti":[3.346,-4.578]},{"t":35,"s":[17.694,45.337]}]},"a":{"a":0,"k":[17.694,45.337]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 7","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-4.518,-5.656],[-1.641,-2.504],[0.211,0.215],[3.144,3.906]],"o":[[0,0],[0.673,1.12],[1.554,1.575],[-4.909,-5.93]],"v":[[0.919,-2.758],[2.987,4.551],[4.897,6.838],[-1.543,4.12]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[-4.518,-5.656],[-2.12,-1.954],[0.211,0.215],[3.144,3.906]],"o":[[0,0],[2.12,1.954],[1.554,1.575],[-4.909,-5.93]],"v":[[-1.566,-5.52],[3.366,3.802],[10.009,8.61],[-1.543,4.12]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[-4.518,-5.656],[-2.12,-1.954],[0.211,0.215],[3.144,3.906]],"o":[[0,0],[2.12,1.954],[1.554,1.575],[-4.909,-5.93]],"v":[[-1.566,-5.52],[3.366,3.802],[10.009,8.61],[-1.543,4.12]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":35,"s":[{"i":[[-4.518,-5.656],[-1.641,-2.504],[0.211,0.215],[3.144,3.906]],"o":[[0,0],[0.673,1.12],[1.554,1.575],[-4.909,-5.93]],"v":[[0.919,-2.758],[2.987,4.551],[4.897,6.838],[-1.543,4.12]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":45,"s":[{"i":[[-2.268,-2.926],[-1.579,-4.511],[0.211,0.215],[5.102,1.75]],"o":[[0,0],[0.673,1.12],[1.554,1.575],[-5.102,-1.75]],"v":[[-2.921,-1.106],[2.987,4.551],[2.519,5.994],[-4.294,2.955]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":60,"s":[{"i":[[-4.518,-5.656],[-1.641,-2.504],[0.211,0.215],[3.144,3.906]],"o":[[0,0],[0.673,1.12],[1.554,1.575],[-4.909,-5.93]],"v":[[0.919,-2.758],[2.987,4.551],[4.897,6.838],[-1.543,4.12]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":80,"s":[{"i":[[-4.518,-5.656],[-1.641,-2.504],[0.211,0.215],[3.144,3.906]],"o":[[0,0],[0.673,1.12],[1.554,1.575],[-4.909,-5.93]],"v":[[-0.536,4.221],[1.533,11.53],[3.443,13.817],[-2.997,11.099]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":90,"s":[{"i":[[-4.518,-5.656],[-1.641,-2.504],[0.211,0.215],[3.144,3.906]],"o":[[0,0],[0.673,1.12],[1.554,1.575],[-4.909,-5.93]],"v":[[0.919,-2.758],[2.987,4.551],[4.897,6.838],[-1.543,4.12]],"c":true}]},{"t":105,"s":[{"i":[[-4.518,-5.656],[-1.641,-2.504],[0.211,0.215],[3.144,3.906]],"o":[[0,0],[0.673,1.12],[1.554,1.575],[-4.909,-5.93]],"v":[[0.919,-2.758],[2.987,4.551],[4.897,6.838],[-1.543,4.12]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458999992819,0.322000002394,0.361000001197,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[17.84,55.727]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":26,"ty":4,"nm":"ear R","parent":28,"sr":1,"ks":{"p":{"a":0,"k":[326.561,103.226,0]},"a":{"a":0,"k":[73.794,102.718,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-8.384,-0.988],[-2.33,-63.067],[4,9]],"o":[[8.514,-14.316],[8.384,0.987],[0.612,16.55],[0,0]],"v":[[-29.691,-13.157],[3.189,-46.225],[29.079,30.663],[7.292,28.288]],"c":false}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.149000010771,0.090000002992,0.176000004189,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[44.691,62.213]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-14,-17]],"o":[[15,3],[0,0]],"v":[[-2.5,-38.5],[1.5,38.5]],"c":false}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.149000010771,0.090000002992,0.176000004189,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[52.483,55.001]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[4.144,-20.125],[-8.852,-5.401],[-1.856,11.197],[3.614,17.643]],"o":[[4.544,4.964],[-4.143,20.124],[8.85,5.4],[1.857,-11.197],[-3.613,-17.643]],"v":[[-10.756,-46.593],[-7.601,-10.445],[-5.412,33.083],[12.407,35.396],[6.71,-17.301]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.317999985639,0.250999989229,0.340999977261,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[60.825,61.569]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 3","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[5.431,8.375],[-8.383,-0.987],[-2.33,-63.067]],"o":[[8.528,-10.901],[8.383,0.988],[1.308,35.402]],"v":[[-30.039,-22.078],[5.029,-56.158],[28.732,21.742]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.541000007181,0.455000005984,0.573000021542,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[45.039,71.133]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 4","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":27,"ty":3,"nm":"Null 2","sr":1,"ks":{"o":{"a":0,"k":0},"r":{"a":0,"k":12},"p":{"a":0,"k":[287.811,339.011,0]},"a":{"a":0,"k":[50,50,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":5,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":45,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":75,"s":[97,103,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":95,"s":[103,97,100]},{"t":120,"s":[100,100,100]}]}},"ao":0,"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":28,"ty":4,"nm":"head","parent":27,"sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.569],"y":[0.038]},"t":5,"s":[-12]},{"i":{"x":[0.415],"y":[1.029]},"o":{"x":[0.333],"y":[0]},"t":45,"s":[-22]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":75,"s":[-12]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":95,"s":[-22]},{"t":120,"s":[-12]}]},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.569,"y":0},"t":2,"s":[73.83,-76.055,0],"to":[-8.534,0.03,0],"ti":[0.291,1.77,0]},{"i":{"x":0.415,"y":1},"o":{"x":0.333,"y":0},"t":42,"s":[59.761,-67.09,0],"to":[-0.291,-1.77,0],"ti":[-7.931,4.822,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":72,"s":[72.084,-86.674,0],"to":[1.59,-0.967,0],"ti":[-0.291,-1.77,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":92,"s":[59.761,-67.09,0],"to":[0.291,1.77,0],"ti":[1.861,-0.007,0]},{"t":117,"s":[73.83,-76.055,0]}]},"a":{"a":0,"k":[175.828,150.007,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":0,"s":[{"i":[[-7.757,37.124],[8.483,12.743],[33.421,-43.443],[21.766,-1.513],[7.1,-28.053],[-34.941,-37.761],[-11.174,88.362],[-37.879,-6.729],[-0.095,0.176],[-31.014,-58.505],[0,0],[14.241,-72.739],[-6.274,8.083]],"o":[[15.132,-80.993],[-34.437,-50.783],[-3.133,3.566],[-21.38,1.487],[-9.893,39.09],[10.577,12.163],[9.103,-71.845],[0.657,0.117],[26.393,-49.26],[0,0],[18.343,13.531],[-13.369,61.603],[16.807,-21.652]],"v":[[151.669,45.461],[120.64,-58.1],[-65.864,-77.467],[-100.43,-76.305],[-144.99,-26.303],[-112.398,104.376],[-154.982,-10.318],[-70.914,-81.08],[-70.335,-81.317],[127.425,-58.144],[126.56,-59.162],[158.173,46.77],[112.167,122.494]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":40,"s":[{"i":[[-7.757,37.124],[8.483,12.743],[33.421,-43.443],[21.766,-1.513],[4.256,-36.267],[-30.211,-32.649],[-10.213,88.478],[-27.783,-18.484],[-0.095,0.176],[-31.014,-58.505],[0,0],[14.241,-72.739],[-6.274,8.083]],"o":[[15.132,-80.993],[-34.437,-50.783],[-5.773,7.066],[-25.413,1.767],[-4.314,36.76],[10.577,12.163],[8.852,-76.684],[0.556,0.37],[26.393,-49.26],[0,0],[18.343,13.531],[-13.369,61.603],[16.807,-21.652]],"v":[[151.669,45.461],[120.64,-58.1],[-65.864,-77.467],[-100.728,-82.604],[-147.748,-17.722],[-112.398,104.376],[-154.126,-16.168],[-70.914,-81.08],[-70.335,-81.317],[127.425,-58.144],[126.56,-59.162],[158.173,46.77],[112.167,122.494]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":70,"s":[{"i":[[-7.757,37.124],[8.483,12.743],[33.421,-43.443],[22.815,-4.317],[7.097,-28.054],[-29.159,-28.84],[-4.11,84.085],[-38.304,1.543],[-0.095,0.176],[-31.014,-58.505],[0,0],[14.241,-72.739],[-6.274,8.083]],"o":[[15.132,-80.993],[-34.437,-50.783],[-3.133,3.566],[-21.058,3.984],[-11.568,45.726],[10.577,12.163],[3.536,-72.333],[0.667,-0.027],[26.393,-49.26],[0,0],[18.343,13.531],[-13.369,61.603],[16.807,-21.652]],"v":[[151.669,45.461],[120.64,-58.1],[-65.864,-77.467],[-100.626,-71.441],[-148.288,-23.678],[-113.806,103.623],[-158.728,-1.603],[-70.914,-81.08],[-70.335,-81.317],[127.425,-58.144],[126.56,-59.162],[158.173,46.77],[112.167,122.494]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":90,"s":[{"i":[[-7.757,37.124],[8.483,12.743],[33.421,-43.443],[21.766,-1.513],[4.256,-36.267],[-30.211,-32.649],[-10.213,88.478],[-27.783,-18.484],[-0.095,0.176],[-31.014,-58.505],[0,0],[14.241,-72.739],[-6.274,8.083]],"o":[[15.132,-80.993],[-34.437,-50.783],[-5.773,7.066],[-25.413,1.767],[-4.314,36.76],[10.577,12.163],[8.852,-76.684],[0.556,0.37],[26.393,-49.26],[0,0],[18.343,13.531],[-13.369,61.603],[16.807,-21.652]],"v":[[151.669,45.461],[120.64,-58.1],[-65.864,-77.467],[-100.728,-82.604],[-147.748,-17.722],[-112.398,104.376],[-154.126,-16.168],[-70.914,-81.08],[-70.335,-81.317],[127.425,-58.144],[126.56,-59.162],[158.173,46.77],[112.167,122.494]],"c":true}]},{"t":115,"s":[{"i":[[-7.757,37.124],[8.483,12.743],[33.421,-43.443],[21.766,-1.513],[7.1,-28.053],[-34.941,-37.761],[-11.174,88.362],[-37.879,-6.729],[-0.095,0.176],[-31.014,-58.505],[0,0],[14.241,-72.739],[-6.274,8.083]],"o":[[15.132,-80.993],[-34.437,-50.783],[-3.133,3.566],[-21.38,1.487],[-9.893,39.09],[10.577,12.163],[9.103,-71.845],[0.657,0.117],[26.393,-49.26],[0,0],[18.343,13.531],[-13.369,61.603],[16.807,-21.652]],"v":[[151.669,45.461],[120.64,-58.1],[-65.864,-77.467],[-100.43,-76.305],[-144.99,-26.303],[-112.398,104.376],[-154.982,-10.318],[-70.914,-81.08],[-70.335,-81.317],[127.425,-58.144],[126.56,-59.162],[158.173,46.77],[112.167,122.494]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458999992819,0.322000002394,0.361000001197,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[172.914,130.827]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[14,54],[4.444,4.955],[34.368,-32.605],[60,57],[5.808,10.261],[-20.215,-19.205],[-39,37]],"o":[[-1.285,-4.957],[6.329,47.928],[-39,37],[-9.629,-9.147],[5.076,22.27],[60,57],[39,-37]],"v":[[144.152,-85.831],[134.574,-101.169],[90.818,27.502],[-135.182,9.502],[-158.152,-19.802],[-121.848,44.169],[104.152,62.169]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.819999964097,0.74900004069,0.769000004787,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[181.598,191.84]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[4.833,-2.167],[-19.334,0.834],[17,1.667]],"o":[[-22.5,0.333],[-4.834,2.166],[19.333,-0.833],[-17,-1.666]],"v":[[-5.458,-5.521],[-39.916,0.021],[7.084,4.687],[27.75,-3.813]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.987999949736,0.987999949736,0.987999949736,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[183.333,35.655]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 3","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[17,-23],[-60,-57],[-39,37],[14,54],[0,0],[73,8]],"o":[[-26,-2],[-17,23],[60,57],[39,-37],[-3.654,-14.094],[0,0],[-78.17,-8.566]],"v":[[-66.25,-98.217],[-132.25,-81.217],[-109.75,77.783],[111.75,97.283],[155.75,-45.217],[127.75,-77.217],[33.75,-126.217]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":40,"s":[{"i":[[0,0],[17,-23],[-60,-57],[-39,37],[14,54],[0,0],[73,8]],"o":[[-21.525,-11.852],[-17,23],[60,57],[39,-37],[-3.654,-14.094],[0,0],[-78.17,-8.566]],"v":[[-66.25,-98.217],[-130.836,-87.743],[-109.75,77.783],[111.75,97.283],[155.75,-45.217],[127.75,-77.217],[33.75,-126.217]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":70,"s":[{"i":[[0,0],[17,-23],[-57.731,-43.104],[-39,37],[14,54],[0,0],[73,8]],"o":[[-24.615,-2.505],[-17,23],[66.314,49.512],[39,-37],[-3.654,-14.094],[0,0],[-78.17,-8.566]],"v":[[-66.25,-98.217],[-140.677,-66.566],[-109.75,77.783],[111.75,97.283],[155.75,-45.217],[127.75,-77.217],[33.75,-126.217]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":90,"s":[{"i":[[0,0],[17,-23],[-60,-57],[-39,37],[14,54],[0,0],[73,8]],"o":[[-21.525,-11.852],[-17,23],[60,57],[39,-37],[-3.654,-14.094],[0,0],[-78.17,-8.566]],"v":[[-66.25,-98.217],[-130.836,-87.743],[-109.75,77.783],[111.75,97.283],[155.75,-45.217],[127.75,-77.217],[33.75,-126.217]],"c":true}]},{"t":115,"s":[{"i":[[0,0],[17,-23],[-60,-57],[-39,37],[14,54],[0,0],[73,8]],"o":[[-26,-2],[-17,23],[60,57],[39,-37],[-3.654,-14.094],[0,0],[-78.17,-8.566]],"v":[[-66.25,-98.217],[-132.25,-81.217],[-109.75,77.783],[111.75,97.283],[155.75,-45.217],[127.75,-77.217],[33.75,-126.217]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.948999980852,0.913999968884,0.925,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[170,151.226]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 4","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":29,"ty":4,"nm":"ear L","parent":28,"sr":1,"ks":{"p":{"a":0,"k":[192.291,30.766,0]},"a":{"a":0,"k":[86.998,57.532,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-9.095,-14.043]],"o":[[9.581,30.312],[0,0]],"v":[[-3.436,-30.542],[2.95,30.542]],"c":false}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.149000010771,0.090000002992,0.176000004189,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[61.699,46.215]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[5.857,0.673],[-23.668,-30.971]],"o":[[-8.132,-22.162],[-5.857,-0.672],[23.669,30.97]],"v":[[35.349,-10.926],[7.914,-52.801],[-11.681,22.503]],"c":true}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.149000010771,0.090000002992,0.176000004189,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[50.348,68.473]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[4.383,9.068],[-0.078,-3.404]],"o":[[1.704,-11.734],[10.745,4.364],[0.078,3.404]],"v":[[-14.265,31.038],[-14.276,-31.038],[14.198,12.608]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.317999985639,0.250999989229,0.340999977261,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[72.8,49.412]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 3","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[5.857,0.673],[-23.668,-30.971]],"o":[[-8.132,-22.162],[-5.857,-0.672],[23.669,30.97]],"v":[[35.349,-10.926],[7.914,-52.801],[-11.681,22.503]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.541000007181,0.455000005984,0.573000021542,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[50.348,68.473]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 4","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":30,"ty":4,"nm":"neck","sr":1,"ks":{"p":{"a":0,"k":[338.345,409.653,0]},"a":{"a":0,"k":[161.669,147.644,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":2,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":42,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":72,"s":[97,103,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":92,"s":[103,97,100]},{"t":120,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-1.381,-0.905],[10.284,-27.252],[34.027,-15.556],[-0.905,0.653],[-10.166,27.433],[24.062,15.756],[-0.891,1.362]],"o":[[26.373,17.266],[-10.304,28.396],[-1.034,0.474],[27.897,-20.172],[9.347,-24.506],[-1.414,-0.925],[0.906,-1.382]],"v":[[3.203,-58.085],[26.866,13.038],[-35.302,58.516],[-36.245,56.915],[21.253,10.978],[-0.063,-53.077],[-0.936,-57.222]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458999992819,0.322000002394,0.361000001197,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[291.254,59.24]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0.682,0.395],[15.347,32.96],[-0.949,-1.273],[-11.486,-18.784],[0.895,1]],"o":[[-32.358,-18.752],[-0.689,-1.481],[15.381,20.602],[4.645,7.557],[0.523,0.584]],"v":[[18.695,39.303],[-19.765,-37.046],[-17.495,-38.425],[6.315,21.575],[19.559,38.242]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458999992819,0.322000002394,0.361000001197,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[20.704,82.895]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[23,15],[0,0],[0,0],[0,0],[24,-17.375],[-9.775,9.164],[-14,36]],"o":[[0,0],[0,0],[0,0],[44.333,7.666],[16.5,5.25],[48,-45],[11.581,-29.781]],"v":[[43.71,-73.582],[-52.547,-60.97],[-68.29,-52.582],[-77.29,0.418],[-62.665,65.543],[-26.29,64.418],[65.71,-4.582]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.819999964097,0.74900004069,0.769000004787,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[249.114,77.241]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 3","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-1.024,-17.807],[-36,-3.834],[6.167,6.5],[41.167,12.167]],"o":[[2.167,37.667],[36,3.833],[-6.166,-6.5],[-34.276,-10.13]],"v":[[-69.988,-22.18],[3.179,36.154],[64.845,10.82],[-16.488,-3.68]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":40,"s":[{"i":[[-2.605,-17.645],[-36,-3.834],[6.167,6.5],[38.152,9.528]],"o":[[3.491,23.649],[36,3.833],[-6.166,-6.5],[-34.677,-8.66]],"v":[[-64.888,-8.014],[-2.893,35.124],[55.859,12.623],[-16.245,4.305]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":70,"s":[{"i":[[-1.024,-17.807],[-36,-3.834],[6.167,6.5],[41.167,12.167]],"o":[[2.167,37.667],[36,3.833],[-6.166,-6.5],[-34.276,-10.13]],"v":[[-69.988,-22.18],[3.179,36.154],[64.845,10.82],[-16.488,-3.68]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":90,"s":[{"i":[[-2.605,-17.645],[-36,-3.834],[6.167,6.5],[38.152,9.528]],"o":[[3.491,23.649],[36,3.833],[-6.166,-6.5],[-34.677,-8.66]],"v":[[-64.888,-8.014],[-2.893,35.124],[55.859,12.623],[-16.245,4.305]],"c":true}]},{"t":115,"s":[{"i":[[-1.024,-17.807],[-36,-3.834],[6.167,6.5],[41.167,12.167]],"o":[[2.167,37.667],[36,3.833],[-6.166,-6.5],[-34.276,-10.13]],"v":[[-69.988,-22.18],[3.179,36.154],[64.845,10.82],[-16.488,-3.68]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.987999949736,0.987999949736,0.987999949736,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[109.312,94.505]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 4","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[11.581,-29.78],[48,-45],[33,26],[15,25]],"o":[[23,15],[-14,36],[-33.726,31.616],[-32.52,-25.622],[0,0]],"v":[[128.96,-73.308],[150.96,-4.308],[55.626,41.692],[-124.04,44.692],[-162.54,-34.975]],"c":false}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.948999980852,0.913999968884,0.925,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[163.864,76.967]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 5","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":31,"ty":4,"nm":"body","sr":1,"ks":{"p":{"a":0,"k":[336.91,498.263,0]},"a":{"a":0,"k":[160.66,196.596,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":40,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":70,"s":[97,103,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":90,"s":[103,97,100]},{"t":117,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[5,-9]],"o":[[25.13,-47.91],[0,0],[0,0]],"v":[[125.436,48.632],[133.435,-48.632],[-150.565,-28.632]],"c":false}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.458999992819,0.322000002394,0.361000001197,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[176.815,63.632]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0.165,-17.083],[6.74,-1.296],[0.425,1.477],[-6.682,21.544],[-0.518,-0.16]],"o":[[-0.455,46.656],[-1.538,0.296],[-6.116,-21.534],[0.161,-0.518],[1.376,0.427]],"v":[[-0.565,-18.478],[1.853,36.903],[-1.602,34.775],[-1.911,-36.391],[-0.682,-37.038]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458999992819,0.322000002394,0.361000001197,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[11.935,116.928]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[1.432,3.581],[-32.508,23.877],[0.782,-0.856],[-16.566,-47.916]],"o":[[-16.339,-41.075],[0.934,-0.687],[-20.147,22.104],[1.299,3.754]],"v":[[-6.777,42.277],[21.05,-45.171],[22.335,-43.744],[-1.17,40.141]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458999992819,0.322000002394,0.361000001197,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[212.224,141.791]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 3","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0.5,16],[0,0],[8,-22.5],[0,0]],"o":[[-8.167,-16],[-0.5,-16],[0,0],[-8,22.5],[0,0]],"v":[[20.084,33.083],[11.916,-5.917],[18.251,-43.25],[-12.084,-5.584],[-7.249,43.25]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.948999980852,0.913999968884,0.925,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[215.666,140.25]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 4","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-23.723,7.742],[0,0],[54.204,-2.708],[0,0],[29.5,-25],[5.5,-17],[1.542,-18.312]],"o":[[144,-47],[0,0],[0,0],[21,5.5],[-29.5,25],[-4.868,15.048],[19.215,-1.335]],"v":[[-31.569,77.47],[70.431,-90.531],[-28.808,-86.048],[-48.569,-66.531],[-46.694,-8.906],[-88.569,35.47],[-95.931,90.531]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.819999964097,0.74900004069,0.769000004787,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[239.819,105.531]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 5","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[5,-9],[-13,-54],[-133.963,43.724],[0,0]],"o":[[-9.713,17.482],[0,0],[144,-47],[0,0]],"v":[[-141.75,-77.667],[-154.75,38.334],[40.25,70.334],[142.25,-97.667]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.948999980852,0.913999968884,0.925,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[168,112.667]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 6","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":32,"ty":4,"nm":"hand","sr":1,"ks":{"p":{"a":0,"k":[180.099,310.667,0]},"a":{"a":0,"k":[63.599,15,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":10,"s":[{"i":[[0,0],[3,-15],[-25,2],[-7,3]],"o":[[-18,4],[-3,15],[25,-2],[7,-3]],"v":[[14,-71],[-27,-35],[-21,69],[39,2]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[5.862,-12.632],[-28.202,-0.477],[-11.611,10.258]],"o":[[-18,4],[-15.681,16.622],[40.721,0.854],[6.424,-3.757]],"v":[[14,-71],[-34.301,-39.355],[-24.221,56.979],[37.975,-0.818]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[9.542,-9.589],[-32.32,-3.661],[-17.54,19.591]],"o":[[-18,4],[-31.985,18.708],[28.853,2.952],[5.682,-4.729]],"v":[[14,-71],[-43.689,-44.955],[-33.506,44.951],[36.658,-4.441]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":45,"s":[{"i":[[0,0],[14.172,-5.759],[-37.5,-7.667],[-25,31.333]],"o":[[-18,4],[-52.5,21.333],[31.579,6.456],[4.75,-5.953]],"v":[[14,-71],[-55.5,-52],[-51.75,37.75],[35,-9]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":60,"s":[{"i":[[0,0],[11.574,-7.907],[-34.594,-5.419],[-20.815,24.745]],"o":[[-18,4],[-40.991,19.861],[30.05,4.49],[5.273,-5.266]],"v":[[14,-71],[-48.874,-48.047],[-38.35,43.516],[35.93,-6.442]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":80,"s":[{"i":[[0,0],[6.751,-11.897],[-29.197,-1.246],[-13.044,12.514]],"o":[[-18,4],[-19.621,17.126],[35.591,2.732],[6.244,-3.992]],"v":[[14,-71],[-36.57,-40.708],[-22.841,52.851],[37.657,-1.694]],"c":true}]},{"t":100,"s":[{"i":[[0,0],[3,-15],[-25,2],[-7,3]],"o":[[-18,4],[-3,15],[25,-2],[7,-3]],"v":[[14,-71],[-27,-35],[-21,69],[39,2]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.458999992819,0.322000002394,0.361000001197,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[61,86]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[1.833,12.083],[5.334,-3.167],[0,0]],"o":[[11.333,-2.584],[-1.834,-12.084],[-5.333,3.167],[0,0]],"v":[[-5.833,19.292],[16.833,-7.208],[2.999,-13.875],[-18.667,4.792]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.948999980852,0.913999968884,0.925,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[53.5,35.708]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":10,"s":[{"i":[[0,0],[3,-15],[-25,2],[-7,3]],"o":[[-18,4],[-3,15],[25,-2],[7,-3]],"v":[[14,-71],[-27,-35],[-21,69],[39,2]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[5.862,-12.632],[-28.202,-0.477],[-11.611,10.258]],"o":[[-18,4],[-15.681,16.622],[40.721,0.854],[6.424,-3.757]],"v":[[14,-71],[-34.301,-39.355],[-24.221,56.979],[37.975,-0.818]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[9.542,-9.589],[-32.32,-3.661],[-17.54,19.591]],"o":[[-18,4],[-31.985,18.708],[28.853,2.952],[5.682,-4.729]],"v":[[14,-71],[-43.689,-44.955],[-33.506,44.951],[36.658,-4.441]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":45,"s":[{"i":[[0,0],[14.172,-5.759],[-37.5,-7.667],[-25,31.333]],"o":[[-18,4],[-52.5,21.333],[31.579,6.456],[4.75,-5.953]],"v":[[14,-71],[-55.5,-52],[-51.75,37.75],[35,-9]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":60,"s":[{"i":[[0,0],[11.574,-7.907],[-34.594,-5.419],[-20.815,24.745]],"o":[[-18,4],[-40.991,19.861],[30.05,4.49],[5.273,-5.266]],"v":[[14,-71],[-48.874,-48.047],[-38.35,43.516],[35.93,-6.442]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":80,"s":[{"i":[[0,0],[6.751,-11.897],[-29.197,-1.246],[-13.044,12.514]],"o":[[-18,4],[-19.621,17.126],[35.591,2.732],[6.244,-3.992]],"v":[[14,-71],[-36.57,-40.708],[-22.841,52.851],[37.657,-1.694]],"c":true}]},{"t":100,"s":[{"i":[[0,0],[3,-15],[-25,2],[-7,3]],"o":[[-18,4],[-3,15],[25,-2],[7,-3]],"v":[[14,-71],[-27,-35],[-21,69],[39,2]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.819999964097,0.74900004069,0.769000004787,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[61,86]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 3","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":33,"ty":4,"nm":"neck 2","parent":30,"sr":1,"ks":{"p":{"a":0,"k":[31.165,93.769,0]},"a":{"a":0,"k":[55.37,109.11,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[16.958,6.358],[-1.029,-37.04],[-22,20]],"o":[[-32,-12],[1,36],[22,-20]],"v":[[12.514,-41],[-44.486,-2],[23.515,33]],"c":true}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.458999992819,0.322000002394,0.361000001197,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.948999980852,0.913999968884,0.925,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[60.514,68]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0}]} \ No newline at end of file diff --git a/Tests/LottieMesh/Resources/Fireworks.json b/Tests/LottieMesh/Resources/Fireworks.json deleted file mode 100644 index 998fbd775c..0000000000 --- a/Tests/LottieMesh/Resources/Fireworks.json +++ /dev/null @@ -1 +0,0 @@ -{"tgs":1,"v":"5.5.2","fr":60,"ip":0,"op":180,"w":512,"h":512,"nm":"512_fireworks dop","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":0,"nm":"fireworks !!yellow 4 vspish","refId":"comp_1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":76,"s":[100]},{"t":86,"s":[0]}]},"r":{"a":0,"k":-10.694},"p":{"a":0,"k":[977.5,379,0]},"a":{"a":0,"k":[243,243,0]},"s":{"a":0,"k":[80,80,100]}},"ao":0,"w":486,"h":486,"ip":36,"op":86,"st":6,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"fireworks pink 5","refId":"comp_3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":83,"s":[100]},{"t":94,"s":[0]}]},"r":{"a":0,"k":-0.087},"p":{"a":0,"k":[1279.5,464,0]},"a":{"a":0,"k":[243,243,0]}},"ao":0,"w":486,"h":486,"ip":48,"op":94,"st":18,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"fireworks !!yellow 4 vspish","refId":"comp_1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":105,"s":[100]},{"t":115,"s":[0]}]},"r":{"a":0,"k":23.994},"p":{"a":0,"k":[305.5,923,0]},"a":{"a":0,"k":[243,243,0]},"s":{"a":0,"k":[78.011,78.011,100]}},"ao":0,"w":486,"h":486,"ip":65,"op":115,"st":35,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"fireworks pink 5","refId":"comp_3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":71,"s":[100]},{"t":81,"s":[0]}]},"p":{"a":0,"k":[441.5,398,0]},"a":{"a":0,"k":[243,243,0]},"s":{"a":0,"k":[157.86,157.86,100]}},"ao":0,"w":486,"h":486,"ip":31,"op":81,"st":1,"bm":0},{"ddd":0,"ind":5,"ty":3,"nm":"Trace Shape Layer 1: Path 6 [1.1]","cl":"1","sr":1,"ks":{"o":{"a":0,"k":0},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[-120.256]},{"t":29,"s":[-176.947]}]},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[1286,792,0],"to":[-172.306,-276.404,0],"ti":[305.23,23.654,0]},{"t":29,"s":[468.343,405.178,0]}]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":0,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[1,1,1],"y":[0,0,0]},"t":10,"s":[40,40,100]},{"t":30,"s":[150,150,100]}]}},"ao":0,"ip":0,"op":30,"st":-60,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"!!!rocket pink 2","parent":5,"refId":"comp_4","sr":1,"ks":{"r":{"a":0,"k":90},"p":{"a":0,"k":[-167.855,0.94,0]},"a":{"a":0,"k":[256.94,395.855,0]}},"ao":0,"w":512,"h":512,"ip":1,"op":36,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"fireworks !!yellow 4 vspish","refId":"comp_1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":89,"s":[100]},{"t":99,"s":[0]}]},"r":{"a":0,"k":-10.694},"p":{"a":0,"k":[633.5,1163,0]},"a":{"a":0,"k":[243,243,0]},"s":{"a":0,"k":[128.178,128.178,100]}},"ao":0,"w":486,"h":486,"ip":49,"op":99,"st":19,"bm":0},{"ddd":0,"ind":8,"ty":3,"nm":"Trace Shape Layer 1: Path 5 [1.2]","cl":"2","sr":1,"ks":{"o":{"a":0,"k":0},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":16,"s":[98.009]},{"t":46,"s":[185.502]}]},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":16,"s":[1236,866,0],"to":[-48.547,286.872,0],"ti":[243.966,25.033,0]},{"t":46,"s":[667.754,1167.601,0]}]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":16,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,3.222]},"o":{"x":[1,1,1],"y":[0,0,0]},"t":26,"s":[40,40,100]},{"t":46,"s":[130,130,100]}]}},"ao":0,"ip":16,"op":47,"st":-44,"bm":0},{"ddd":0,"ind":9,"ty":0,"nm":"!!!rocket yellow 2","parent":8,"refId":"comp_5","sr":1,"ks":{"r":{"a":0,"k":90},"p":{"a":0,"k":[-167.855,0.94,0]},"a":{"a":0,"k":[256.94,395.855,0]}},"ao":0,"w":512,"h":512,"ip":16,"op":52,"st":16,"bm":0},{"ddd":0,"ind":10,"ty":0,"nm":"fireworks !!blue 4 vspish","refId":"comp_6","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":89,"s":[100]},{"t":100,"s":[0]}]},"r":{"a":0,"k":-0.087},"p":{"a":0,"k":[695.5,832,0]},"a":{"a":0,"k":[243,243,0]},"s":{"a":0,"k":[201.159,201.159,100]}},"ao":0,"w":486,"h":486,"ip":54,"op":100,"st":24,"bm":0},{"ddd":0,"ind":11,"ty":3,"nm":"Trace Shape Layer 1: Path 8 [1.8]","cl":"8","sr":1,"ks":{"o":{"a":0,"k":0},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":22,"s":[112.759]},{"t":51,"s":[219.154]}]},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.82,"y":0},"t":22,"s":[1352,810,0],"to":[-138.369,305.192,0],"ti":[207.549,143.516,0]},{"t":51,"s":[705.012,835.922,0]}]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":22,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,-2.333]},"o":{"x":[1,1,1],"y":[0,0,0]},"t":32,"s":[40,40,100]},{"t":52,"s":[180,180,100]}]}},"ao":0,"ip":22,"op":52,"st":-38,"bm":0},{"ddd":0,"ind":12,"ty":0,"nm":"!!!rocket blue 2","parent":11,"refId":"comp_7","sr":1,"ks":{"r":{"a":0,"k":90},"p":{"a":0,"k":[-167.855,0.94,0]},"a":{"a":0,"k":[256.94,395.855,0]}},"ao":0,"w":512,"h":512,"ip":22,"op":58,"st":22,"bm":0},{"ddd":0,"ind":13,"ty":0,"nm":"fireworks pink 5","refId":"comp_3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":115,"s":[100]},{"t":125,"s":[0]}]},"p":{"a":0,"k":[1025.5,1230,0]},"a":{"a":0,"k":[243,243,0]},"s":{"a":0,"k":[-119.753,119.753,100]}},"ao":0,"w":486,"h":486,"ip":76,"op":125,"st":45,"bm":0},{"ddd":0,"ind":14,"ty":3,"nm":"Trace Shape Layer 1: Path 2 [1.4]","cl":"4","sr":1,"ks":{"o":{"a":0,"k":0},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[-0.702]},"o":{"x":[0.333],"y":[0]},"t":43,"s":[56.31]},{"i":{"x":[0.667],"y":[0.756]},"o":{"x":[0.333],"y":[0.183]},"t":53,"s":[67.425]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[1.177]},"t":62,"s":[160.472]},{"t":73,"s":[184.041]}]},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":43,"s":[1266,806,0],"to":[170.946,262.206,0],"ti":[330.742,0.419,0]},{"t":73,"s":[1052.274,1229.28,0]}]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":43,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,4.333]},"o":{"x":[1,1,1],"y":[0,0,0]},"t":53,"s":[40,40,100]},{"t":73,"s":[120,120,100]}]}},"ao":0,"ip":43,"op":74,"st":-17,"bm":0},{"ddd":0,"ind":15,"ty":0,"nm":"!!!rocket pink 2","parent":14,"refId":"comp_4","sr":1,"ks":{"r":{"a":0,"k":90},"p":{"a":0,"k":[-167.855,0.94,0]},"a":{"a":0,"k":[256.94,395.855,0]}},"ao":0,"w":512,"h":512,"ip":43,"op":79,"st":43,"bm":0},{"ddd":0,"ind":16,"ty":0,"nm":"fireworks !!blue 4 vspish","refId":"comp_6","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":127,"s":[100]},{"t":137,"s":[0]}]},"r":{"a":0,"k":12.326},"p":{"a":0,"k":[560.5,539,0]},"a":{"a":0,"k":[243,243,0]},"s":{"a":0,"k":[-177.976,177.976,100]}},"ao":0,"w":486,"h":486,"ip":87,"op":137,"st":57,"bm":0},{"ddd":0,"ind":17,"ty":3,"nm":"Trace Shape Layer 1: Path 7 [1.5]","cl":"5","sr":1,"ks":{"o":{"a":0,"k":0},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":55,"s":[155.577]},{"t":84,"s":[228.764]}]},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":55,"s":[1194,772,0],"to":[-257.364,110.984,0],"ti":[153.116,221.504,0]},{"t":84,"s":[574.62,567.336,0]}]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":55,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,-1.222]},"o":{"x":[1,1,1],"y":[0,0,0]},"t":65,"s":[40,40,100]},{"t":85,"s":[170,170,100]}]}},"ao":0,"ip":55,"op":85,"st":-5,"bm":0},{"ddd":0,"ind":18,"ty":0,"nm":"!!!rocket blue 2","parent":17,"refId":"comp_7","sr":1,"ks":{"r":{"a":0,"k":90},"p":{"a":0,"k":[-167.855,0.94,0]},"a":{"a":0,"k":[256.94,395.855,0]}},"ao":0,"w":512,"h":512,"ip":55,"op":91,"st":55,"bm":0},{"ddd":0,"ind":19,"ty":0,"nm":"fireworks !!yellow 4 vspish","refId":"comp_1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":165,"s":[100]},{"t":175,"s":[0]}]},"r":{"a":0,"k":-0.478},"p":{"a":0,"k":[1038.964,328.288,0]},"a":{"a":0,"k":[243,243,0]},"s":{"a":0,"k":[132.042,132.042,100]}},"ao":0,"w":486,"h":486,"ip":125,"op":175,"st":95,"bm":0},{"ddd":0,"ind":20,"ty":3,"nm":"Trace Shape Layer 1: Path 3 [1.6]","cl":"6","sr":1,"ks":{"o":{"a":0,"k":0},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":93,"s":[-102.219]},{"t":122,"s":[-148.865]}]},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":93,"s":[1362,740,0],"to":[-42.181,-182.712,0],"ti":[156.249,102.291,0]},{"t":122,"s":[1054.382,305.32,0]}]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":93,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[1,1,1],"y":[0,0,0]},"t":103,"s":[40,40,100]},{"t":123,"s":[130,130,100]}]}},"ao":0,"ip":93,"op":150,"st":33,"bm":0},{"ddd":0,"ind":21,"ty":0,"nm":"!!!rocket yellow 2","parent":20,"refId":"comp_5","sr":1,"ks":{"r":{"a":0,"k":90},"p":{"a":0,"k":[-167.855,0.94,0]},"a":{"a":0,"k":[256.94,395.855,0]}},"ao":0,"w":512,"h":512,"ip":93,"op":129,"st":93,"bm":0},{"ddd":0,"ind":22,"ty":0,"nm":"fireworks !!blue 4 vspish","refId":"comp_6","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":170,"s":[100]},{"t":180,"s":[0]}]},"r":{"a":0,"k":11.633},"p":{"a":0,"k":[655.5,1077,0]},"a":{"a":0,"k":[243,243,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":486,"h":486,"ip":134,"op":180,"st":105,"bm":0},{"ddd":0,"ind":23,"ty":3,"nm":"Trace Shape Layer 1: Path 1 [1.3]","cl":"3","sr":1,"ks":{"o":{"a":0,"k":0},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[0.703]},"o":{"x":[0.333],"y":[0]},"t":102,"s":[59.135]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0.623]},"t":112,"s":[132.595]},{"t":131,"s":[199.135]}]},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":102,"s":[1156,836,0],"to":[208.414,207.152,0],"ti":[260.651,130.415,0]},{"t":131,"s":[655.186,1070.59,0]}]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":102,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,6.556]},"o":{"x":[1,1,1],"y":[0,0,0]},"t":112,"s":[40,40,100]},{"t":132,"s":[100,100,100]}]}},"ao":0,"ip":102,"op":132,"st":42,"bm":0},{"ddd":0,"ind":24,"ty":0,"nm":"!!!rocket blue 2","parent":23,"refId":"comp_7","sr":1,"ks":{"r":{"a":0,"k":90},"p":{"a":0,"k":[-167.855,0.94,0]},"a":{"a":0,"k":[256.94,395.855,0]}},"ao":0,"w":512,"h":512,"ip":102,"op":138,"st":102,"bm":0}]},{"id":"comp_1","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"pink","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[100]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[100]},{"t":100,"s":[0]}]},"p":{"a":0,"k":[245.75,225.75,0]},"a":{"a":0,"k":[2.75,-17.25,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.629,0.629,0.629],"y":[0,0,0]},"t":30,"s":[43.235,43.235,100]},{"t":40,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[1.312,8.525],[3.244,0.766],[0.278,-4.348]],"o":[[-1.312,-8.525],[-5.358,-1.265],[-0.278,4.348]],"v":[[6.13,-24.571],[8.951,-66.768],[5.014,-23.638]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[3.55,23.061],[3.244,0.766],[0.753,-11.762]],"o":[[-3.55,-23.061],[-5.358,-1.265],[-0.753,11.762]],"v":[[11.788,-37.733],[14.47,-92.154],[8.769,-35.211]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.113,0.736],[3.244,0.766],[0.024,-0.376]],"o":[[-0.113,-0.736],[-5.358,-1.265],[-0.024,0.376]],"v":[[1.251,-14.165],[19.643,-137.56],[1.155,-14.085]],"c":true}]},{"t":100,"s":[{"i":[[0.002,0.014],[3.244,0.766],[0,-0.007]],"o":[[-0.002,-0.014],[-5.358,-1.265],[0,0.007]],"v":[[29.568,-215.114],[32.47,-250.154],[29.566,-215.113]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-0.775,14.209],[6.351,4.603],[2.652,-16.185]],"o":[[0.775,-14.209],[-4.056,-2.94],[-2.652,16.185]],"v":[[7.231,-23.969],[18.873,-61.186],[6.13,-24.571]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-2.096,38.439],[6.351,4.603],[7.173,-43.783]],"o":[[2.096,-38.439],[-4.056,-2.94],[-7.173,43.783]],"v":[[14.767,-36.105],[37.637,-108.647],[11.788,-37.733]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.067,1.227],[6.351,4.603],[0.229,-1.398]],"o":[[0.067,-1.227],[-4.056,-2.94],[-0.229,1.398]],"v":[[1.932,-14.242],[49.85,-146.868],[1.837,-14.294]],"c":true}]},{"t":100,"s":[{"i":[[-0.001,0.023],[6.351,4.603],[0.004,-0.026]],"o":[[0.001,-0.023],[-4.056,-2.94],[-0.004,0.026]],"v":[[75.069,-225.113],[80.137,-241.647],[75.068,-225.114]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.749,6.829],[6.31,5.609],[1.359,-6.48]],"o":[[-0.749,-6.829],[-3.554,-3.159],[-1.359,6.48]],"v":[[8.596,-23.217],[31.24,-73.607],[7.231,-23.969]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[2.026,18.473],[6.31,5.609],[3.677,-17.531]],"o":[[-2.026,-18.473],[-3.554,-3.159],[-3.677,17.531]],"v":[[18.46,-34.07],[47.481,-104.355],[14.767,-36.105]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.065,0.59],[6.31,5.609],[0.117,-0.56]],"o":[[-0.065,-0.59],[-3.554,-3.159],[-0.117,0.56]],"v":[[2.166,-13.836],[63.43,-142.576],[2.048,-13.901]],"c":true}]},{"t":100,"s":[{"i":[[0.001,0.011],[6.31,5.609],[0.002,-0.011]],"o":[[-0.001,-0.011],[-3.554,-3.159],[-0.002,0.011]],"v":[[84.072,-198.612],[102.981,-237.355],[84.069,-198.613]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-6.199,15.089],[5.581,6.595],[12.758,-17.563]],"o":[[6.199,-15.089],[-2.72,-3.214],[-12.758,17.563]],"v":[[9.486,-22.203],[48.882,-61.987],[8.281,-23.532]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-16.769,40.819],[5.581,6.595],[34.513,-47.512]],"o":[[16.769,-40.819],[-2.72,-3.214],[-34.513,47.512]],"v":[[21.719,-30.475],[103.282,-109.764],[18.46,-34.07]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.535,1.303],[5.581,6.595],[1.102,-1.517]],"o":[[0.535,-1.303],[-2.72,-3.214],[-1.102,1.517]],"v":[[3.204,-13.341],[122.823,-131.604],[3.099,-13.456]],"c":true}]},{"t":100,"s":[{"i":[[-0.01,0.025],[5.581,6.595],[0.021,-0.029]],"o":[[0.01,-0.025],[-2.72,-3.214],[-0.021,0.029]],"v":[[156.574,-169.11],[171.282,-185.764],[156.572,-169.112]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-4.223,9.945],[2.258,6.104],[6.726,-10.205]],"o":[[4.223,-9.945],[-1.817,-4.912],[-6.726,10.205]],"v":[[11.177,-20.424],[48.778,-58.677],[9.801,-21.888]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-11.423,26.904],[2.258,6.104],[18.195,-27.606]],"o":[[11.423,-26.904],[-1.817,-4.912],[-18.195,27.606]],"v":[[25.439,-26.516],[74.48,-79.649],[21.719,-30.475]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.365,0.859],[2.258,6.104],[0.581,-0.882]],"o":[[0.365,-0.859],[-1.817,-4.912],[-0.581,0.882]],"v":[[3.264,-12.932],[105.66,-107.524],[3.146,-13.058]],"c":true}]},{"t":100,"s":[{"i":[[-0.007,0.016],[2.258,6.104],[0.011,-0.017]],"o":[[0.007,-0.016],[-1.817,-4.912],[-0.011,0.017]],"v":[[152.076,-147.108],[182.98,-176.649],[152.074,-147.11]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":5,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-3.334,6.648],[-1.799,5.518],[9.673,-9.931]],"o":[[3.334,-6.648],[1.878,-5.758],[-9.673,9.931]],"v":[[12.897,-19.193],[48.972,-44.204],[11.177,-20.424]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-9.018,17.983],[-1.799,5.518],[26.167,-26.865]],"o":[[9.018,-17.983],[1.878,-5.758],[-26.167,26.865]],"v":[[30.094,-23.186],[81.927,-62.022],[25.439,-26.516]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.288,0.574],[-1.799,5.518],[0.836,-0.858]],"o":[[0.288,-0.574],[1.878,-5.758],[-0.836,0.858]],"v":[[3.883,-12.491],[120.292,-83.862],[3.734,-12.597]],"c":true}]},{"t":100,"s":[{"i":[[-0.005,0.011],[-1.799,5.518],[0.016,-0.016]],"o":[[0.005,-0.011],[1.878,-5.758],[-0.016,0.016]],"v":[[188.579,-121.106],[215.427,-138.022],[188.576,-121.108]],"c":true}]}]},"nm":"Path 6","hd":false},{"ind":6,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-9.869,8.704],[-3.432,7.058],[12.009,-7.852]],"o":[[9.869,-8.704],[2.981,-6.13],[-12.009,7.852]],"v":[[12.832,-16.628],[74.374,-42.527],[12.897,-19.193]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-26.696,23.547],[-3.432,7.058],[32.485,-21.24]],"o":[[26.696,-23.547],[2.981,-6.13],[-32.485,21.24]],"v":[[29.916,-16.247],[117.42,-56.876],[30.094,-23.186]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.852,0.752],[-3.432,7.058],[1.037,-0.678]],"o":[[0.852,-0.752],[2.981,-6.13],[-1.037,0.678]],"v":[[3.897,-11.786],[151.618,-69.521],[3.902,-12.008]],"c":true}]},{"t":100,"s":[{"i":[[-0.016,0.014],[-3.432,7.058],[0.02,-0.013]],"o":[[0.016,-0.014],[2.981,-6.13],[-0.02,0.013]],"v":[[190.079,-83.601],[236.42,-100.876],[190.079,-83.606]],"c":true}]}]},"nm":"Path 7","hd":false},{"ind":7,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-14.199,3.553],[-2.249,4.962],[22.38,-4.873]],"o":[[22.287,-5.577],[4.049,-8.935],[-22.38,4.873]],"v":[[10.861,-15.141],[88.919,-15.854],[12.516,-16.944]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-38.412,9.611],[-2.249,4.962],[60.542,-13.183]],"o":[[60.291,-15.085],[4.049,-8.935],[-60.542,13.183]],"v":[[25.439,-11.372],[154.986,-15.381],[29.916,-16.247]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-1.227,0.307],[-2.249,4.962],[1.933,-0.421]],"o":[[1.925,-0.482],[4.049,-8.935],[-1.933,0.421]],"v":[[3.934,-10.704],[182.862,-13.944],[4.077,-10.859]],"c":true}]},{"t":100,"s":[{"i":[[-0.023,0.006],[-2.249,4.962],[0.036,-0.008]],"o":[[0.036,-0.009],[4.049,-8.935],[-0.036,0.008]],"v":[[204.076,-11.599],[251.986,-10.381],[204.079,-11.601]],"c":true}]}]},"nm":"Path 8","hd":false},{"ind":8,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-4.711,2.16],[-0.839,0.958],[12.251,-0.88]],"o":[[4.711,-2.16],[1.348,-1.539],[-12.251,0.88]],"v":[[9.079,-12.869],[39.383,-12.388],[10.861,-14.826]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-12.744,5.843],[-0.839,0.958],[33.14,-2.38]],"o":[[12.744,-5.843],[1.348,-1.539],[-33.14,2.38]],"v":[[20.619,-6.076],[78.015,-8.131],[25.439,-11.372]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.407,0.187],[-0.839,0.958],[1.058,-0.076]],"o":[[0.407,-0.187],[1.348,-1.539],[-1.058,0.076]],"v":[[4.224,-10.251],[128.305,-2.239],[4.378,-10.421]],"c":true}]},{"t":100,"s":[{"i":[[-0.008,0.004],[-0.839,0.958],[0.02,-0.001]],"o":[[0.008,-0.004],[1.348,-1.539],[-0.02,0.001]],"v":[[238.573,10.405],[253.015,12.369],[238.576,10.401]],"c":true}]}]},"nm":"Path 9","hd":false},{"ind":9,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-19.702,-2.401],[-2.482,3.154],[14.29,0.398]],"o":[[19.702,2.401],[2.595,-3.297],[-14.29,-0.398]],"v":[[9.698,-12.307],[64.597,6.088],[9.395,-13.184]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-53.298,-6.494],[-2.482,3.154],[38.657,1.077]],"o":[[53.298,6.494],[2.595,-3.297],[-38.657,-1.077]],"v":[[21.439,-3.705],[93.137,16.968],[20.619,-6.076]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-1.702,-0.207],[-2.482,3.154],[1.234,0.034]],"o":[[1.702,0.207],[2.595,-3.297],[-1.234,-0.034]],"v":[[3.71,-9.493],[135.812,35.216],[3.683,-9.569]],"c":true}]},{"t":100,"s":[{"i":[[-0.032,-0.004],[-2.482,3.154],[0.023,0.001]],"o":[[0.032,0.004],[2.595,-3.297],[-0.023,-0.001]],"v":[[196.573,63.406],[241.637,80.468],[196.573,63.405]],"c":true}]}]},"nm":"Path 10","hd":false},{"ind":10,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-9.253,-3.386],[-5.064,4.293],[16.792,8.51]],"o":[[9.253,3.386],[4.126,-3.498],[-16.792,-8.51]],"v":[[11.313,-9.614],[54.159,17.809],[9.698,-11.992]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-25.032,-9.16],[-5.064,4.293],[45.426,23.022]],"o":[[25.032,9.16],[4.126,-3.498],[-45.426,-23.022]],"v":[[25.808,2.729],[103.197,50.764],[21.439,-3.705]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.799,-0.292],[-5.064,4.293],[1.451,0.735]],"o":[[0.799,0.292],[4.126,-3.498],[-1.451,-0.735]],"v":[[3.881,-8.702],[137.251,71.886],[3.742,-8.908]],"c":true}]},{"t":100,"s":[{"i":[[-0.015,-0.006],[-5.064,4.293],[0.027,0.014]],"o":[[0.015,0.006],[4.126,-3.498],[-0.027,-0.014]],"v":[[199.076,108.91],[221.697,124.264],[199.073,108.906]],"c":true}]}]},"nm":"Path 11","hd":false},{"ind":11,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-6.526,-3.215],[-3.502,3.162],[9.671,6.481]],"o":[[6.526,3.215],[3.502,-3.162],[-9.671,-6.481]],"v":[[10.491,-8.994],[73.057,40.569],[11.313,-9.929]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-17.653,-8.697],[-3.502,3.162],[26.163,17.532]],"o":[[17.653,8.697],[3.502,-3.162],[-26.163,-17.532]],"v":[[23.585,5.258],[96.236,63.117],[25.808,2.729]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.564,-0.278],[-3.502,3.162],[0.835,0.56]],"o":[[0.564,0.278],[3.502,-3.162],[-0.835,-0.56]],"v":[[3.102,-8.57],[126.267,90.274],[3.173,-8.651]],"c":true}]},{"t":100,"s":[{"i":[[-0.011,-0.005],[-3.502,3.162],[0.016,0.011]],"o":[[0.011,0.005],[3.502,-3.162],[-0.016,-0.011]],"v":[[144.075,112.911],[200.736,157.617],[144.076,112.91]],"c":true}]}]},"nm":"Path 12","hd":false},{"ind":12,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-13.475,-10.482],[-4.458,3.463],[10.073,7.849]],"o":[[13.475,10.482],[4.458,-3.463],[-10.073,-7.849]],"v":[[9.502,-7.951],[52.87,45.892],[10.491,-8.678]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-36.453,-28.355],[-4.458,3.463],[27.249,21.233]],"o":[[36.453,28.355],[4.458,-3.463],[-27.249,-21.233]],"v":[[20.909,7.226],[94.655,93.669],[23.585,5.258]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-1.164,-0.905],[-4.458,3.463],[0.87,0.678]],"o":[[1.164,0.905],[4.458,-3.463],[-0.87,-0.678]],"v":[[3.049,-7.986],[116.926,120.825],[3.134,-8.049]],"c":true}]},{"t":100,"s":[{"i":[[-0.022,-0.017],[-4.458,3.463],[0.016,0.013]],"o":[[0.022,0.017],[4.458,-3.463],[-0.016,-0.013]],"v":[[146.573,153.413],[172.155,188.169],[146.575,153.411]],"c":true}]}]},"nm":"Path 13","hd":false},{"ind":13,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-4.775,-3.087],[-3.488,2.449],[10.229,14.193]],"o":[[4.775,3.087],[3.488,-2.449],[-10.229,-14.193]],"v":[[7.167,-8.135],[48.091,56.332],[9.187,-7.951]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-12.917,-8.352],[-3.488,2.449],[27.671,38.395]],"o":[[12.917,8.352],[3.488,-2.449],[-27.671,-38.395]],"v":[[15.445,6.729],[62.124,74.78],[20.909,7.226]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.412,-0.267],[-3.488,2.449],[0.884,1.226]],"o":[[0.412,0.267],[3.488,-2.449],[-0.884,-1.226]],"v":[[2.321,-8.079],[84.827,113.576],[2.495,-8.063]],"c":true}]},{"t":100,"s":[{"i":[[-0.008,-0.005],[-3.488,2.449],[0.017,0.023]],"o":[[0.008,0.005],[3.488,-2.449],[-0.017,-0.023]],"v":[[103.57,147.412],[141.124,209.78],[103.573,147.413]],"c":true}]}]},"nm":"Path 14","hd":false},{"ind":14,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-5.362,-6.334],[-2.659,0.857],[3.694,6.735]],"o":[[5.361,6.334],[6.495,-2.093],[-3.694,-6.735]],"v":[[5.102,-9.295],[26.477,35.494],[7.167,-8.45]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-14.504,-17.136],[-2.659,0.857],[9.994,18.219]],"o":[[14.504,17.136],[6.495,-2.093],[-9.994,-18.219]],"v":[[9.859,4.445],[41.772,64.349],[15.445,6.729]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.463,-0.547],[-2.659,0.857],[0.319,0.582]],"o":[[0.463,0.547],[6.495,-2.093],[-0.319,-0.582]],"v":[[2.027,-7.553],[61.313,111.91],[2.205,-7.48]],"c":true}]},{"t":100,"s":[{"i":[[-0.009,-0.01],[-2.659,0.857],[0.006,0.011]],"o":[[0.009,0.01],[6.495,-2.093],[-0.006,-0.011]],"v":[[94.566,193.911],[109.772,229.849],[94.57,193.912]],"c":true}]}]},"nm":"Path 15","hd":false},{"ind":15,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-0.43,-4.038],[-5.792,0.94],[0.557,8.126]],"o":[[0.43,4.038],[3.71,-0.602],[-0.557,-8.126]],"v":[[3.69,-10.876],[17.077,55.965],[5.102,-9.295]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-1.164,-10.924],[-5.792,0.94],[1.507,21.981]],"o":[[1.164,10.924],[3.71,-0.602],[-1.507,-21.981]],"v":[[6.039,0.167],[22.28,71.891],[9.859,4.445]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.037,-0.349],[-5.792,0.94],[0.048,0.702]],"o":[[0.037,0.349],[3.71,-0.602],[-0.048,-0.702]],"v":[[1.242,-7.799],[31.62,122.9],[1.364,-7.663]],"c":true}]},{"t":100,"s":[{"i":[[-0.001,-0.007],[-5.792,0.94],[0.001,0.013]],"o":[[0.001,0.007],[3.71,-0.602],[-0.001,-0.013]],"v":[[43.064,185.408],[54.78,249.391],[43.066,185.411]],"c":true}]}]},"nm":"Path 16","hd":false},{"ind":16,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.268,-1.072],[-8.766,2.77],[-1.453,20.282]],"o":[[-0.268,1.072],[8.956,-2.83],[1.453,-20.282]],"v":[[2.258,-8.649],[11.352,48.031],[4.005,-10.876]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[0.726,-2.901],[-8.766,2.77],[-3.93,54.866]],"o":[[-0.726,2.901],[8.956,-2.83],[3.93,-54.866]],"v":[[1.312,6.192],[20.655,115.675],[6.039,0.167]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.023,-0.093],[-8.766,2.77],[-0.125,1.752]],"o":[[-0.023,0.093],[8.956,-2.83],[0.125,-1.752]],"v":[[0.956,-7.06],[24.966,155.333],[1.106,-7.252]],"c":true}]},{"t":100,"s":[{"i":[[0,-0.002],[-8.766,2.77],[-0.002,0.033]],"o":[[0,0.002],[8.956,-2.83],[0.002,-0.033]],"v":[[32.561,227.912],[35.655,253.675],[32.564,227.908]],"c":true}]}]},"nm":"Path 17","hd":false},{"ind":17,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.674,-5.118],[-9.34,-10.304],[-5.656,20.469]],"o":[[-0.674,5.118],[5.335,5.886],[5.656,-20.469]],"v":[[0.319,-7.862],[-7.754,79.004],[2.258,-8.649]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[1.822,-13.845],[-9.34,-10.304],[-15.299,55.372]],"o":[[-1.822,13.845],[5.335,5.886],[15.299,-55.372]],"v":[[-3.933,8.322],[-10.119,142.549],[1.312,6.192]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.058,-0.442],[-9.34,-10.304],[-0.489,1.768]],"o":[[-0.058,0.442],[5.335,5.886],[0.489,-1.768]],"v":[[0.157,-7.436],[-14.573,174.16],[0.325,-7.504]],"c":true}]},{"t":100,"s":[{"i":[[0.001,-0.008],[-9.34,-10.304],[-0.009,0.033]],"o":[[-0.001,0.008],[5.335,5.886],[0.009,-0.033]],"v":[[-16.442,193.413],[-25.619,252.549],[-16.439,193.412]],"c":true}]}]},"nm":"Path 18","hd":false},{"ind":18,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[1.134,-4.491],[-8.529,-8.273],[-6.696,18.034]],"o":[[-1.134,4.491],[3.166,3.071],[6.696,-18.034]],"v":[[-1.761,-9.64],[-13.598,51.106],[0.003,-7.546]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[3.068,-12.148],[-8.529,-8.273],[-18.113,48.786]],"o":[[-3.068,12.148],[3.166,3.071],[18.113,-48.786]],"v":[[-8.707,2.658],[-24.162,110.866],[-3.933,8.322]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.098,-0.388],[-8.529,-8.273],[-0.578,1.558]],"o":[[-0.098,0.388],[3.166,3.071],[0.578,-1.558]],"v":[[-0.446,-7.378],[-34.795,149.374],[-0.293,-7.198]],"c":true}]},{"t":100,"s":[{"i":[[0.002,-0.007],[-8.529,-8.273],[-0.011,0.029]],"o":[[-0.002,0.007],[3.166,3.071],[0.011,-0.029]],"v":[[-51.445,211.91],[-61.162,244.866],[-51.442,211.913]],"c":true}]}]},"nm":"Path 19","hd":false},{"ind":19,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[9.217,-13.161],[-2.977,-4.918],[-5.725,7.174]],"o":[[-9.217,13.161],[1.721,2.843],[5.725,-7.174]],"v":[[-1.154,-12.499],[-42.53,56.34],[-1.761,-9.64]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[24.934,-35.602],[-2.977,-4.918],[-15.488,19.406]],"o":[[-24.934,35.602],[1.721,2.843],[15.488,-19.406]],"v":[[-7.064,-5.076],[-47.418,71.95],[-8.707,2.658]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.796,-1.137],[-2.977,-4.918],[-0.495,0.62]],"o":[[-0.796,1.137],[1.721,2.843],[0.495,-0.62]],"v":[[-0.876,-8.404],[-71.414,113.188],[-0.929,-8.157]],"c":true}]},{"t":100,"s":[{"i":[[0.015,-0.021],[-2.977,-4.918],[-0.009,0.012]],"o":[[-0.015,0.021],[1.721,2.843],[0.009,-0.012]],"v":[[-88.944,151.405],[-130.918,215.45],[-88.945,151.41]],"c":true}]}]},"nm":"Path 20","hd":false},{"ind":20,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[7.626,-7.899],[-8.084,-5.313],[-13.235,7.936]],"o":[[-2.855,2.957],[3.306,2.173],[10.979,-6.583]],"v":[[-4.169,-12.723],[-45.434,37.243],[-0.839,-12.499]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[20.63,-21.369],[-8.084,-5.313],[-35.802,21.467]],"o":[[-7.724,8],[3.306,2.173],[29.699,-17.808]],"v":[[-16.072,-5.684],[-88.796,85.335],[-7.064,-5.076]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.659,-0.682],[-8.084,-5.313],[-1.143,0.686]],"o":[[-0.247,0.255],[3.306,2.173],[0.948,-0.569]],"v":[[-1.962,-8.308],[-113.223,114.36],[-1.674,-8.288]],"c":true}]},{"t":100,"s":[{"i":[[0.012,-0.013],[-8.084,-5.313],[-0.022,0.013]],"o":[[-0.005,0.005],[3.306,2.173],[0.018,-0.011]],"v":[[-150.949,160.405],[-173.796,186.335],[-150.944,160.405]],"c":true}]}]},"nm":"Path 21","hd":false},{"ind":21,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[14.117,-10.21],[-5.628,-14.818],[-24.822,15.796]],"o":[[-14.086,10.188],[3.081,4.733],[21.924,-13.951]],"v":[[-3.765,-15.075],[-82.448,63.651],[-4.484,-12.723]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[38.189,-27.621],[0.797,-25.979],[-67.149,42.73]],"o":[[-38.106,27.561],[-0.163,5.311],[59.307,-37.74]],"v":[[-14.127,-12.046],[-117.611,102.283],[-16.072,-5.684]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[1.219,-0.882],[-2.13,-20.894],[-2.144,1.364]],"o":[[-1.217,0.88],[1.315,5.048],[1.894,-1.205]],"v":[[-1.552,-9.148],[-137.871,121.393],[-1.614,-8.945]],"c":true}]},{"t":100,"s":[{"i":[[0.023,-0.017],[-9.389,-8.283],[-0.04,0.026]],"o":[[-0.023,0.017],[4.981,4.394],[0.036,-0.023]],"v":[[-123.948,110.901],[-188.111,168.783],[-123.949,110.905]],"c":true}]}]},"nm":"Path 22","hd":false},{"ind":22,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[2.794,-1.349],[0.656,-8.742],[-9.144,3.299]],"o":[[-0.388,0.187],[-0.279,3.717],[9.144,-3.298]],"v":[[-4.357,-15.318],[-39.206,7.131],[-3.283,-15.062]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[7.56,-3.648],[1.507,-8.661],[-24.737,8.919]],"o":[[-1.051,0.507],[-0.641,3.682],[24.737,-8.919]],"v":[[-17.032,-12.737],[-65.957,17.111],[-14.127,-12.046]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.241,-0.117],[1.12,-8.698],[-0.79,0.285]],"o":[[-0.034,0.016],[-0.476,3.698],[0.79,-0.285]],"v":[[-2.581,-9.26],[-111.172,47.264],[-2.489,-9.238]],"c":true}]},{"t":100,"s":[{"i":[[0.004,-0.003],[0.158,-8.79],[-0.014,0.008]],"o":[[-0.001,0],[-0.067,3.737],[0.014,-0.008]],"v":[[-196.685,103.922],[-223.294,122.037],[-196.683,103.922]],"c":true}]}]},"nm":"Path 23","hd":false},{"ind":23,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.652,-0.002],[9.621,-23.195],[-30.797,11.034]],"o":[[-0.652,0.002],[1.201,4.343],[30.797,-11.034]],"v":[[-5.938,-16.534],[-91.485,36.162],[-4.524,-15.646]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[1.764,-0.005],[34.94,-49.93],[-83.311,29.848]],"o":[[-1.764,0.005],[-2.448,3.499],[83.311,-29.848]],"v":[[-20.857,-15.139],[-161.337,74.163],[-17.032,-12.737]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.056,0],[23.404,-37.749],[-2.66,0.953]],"o":[[-0.056,0],[-0.786,3.883],[2.66,-0.953]],"v":[[-2.437,-9.678],[-180.879,84.077],[-2.314,-9.602]],"c":true}]},{"t":100,"s":[{"i":[[0.001,0],[-5.203,-7.542],[-0.05,0.018]],"o":[[-0.001,0],[3.337,4.837],[0.05,-0.018]],"v":[[-175.952,77.399],[-229.337,108.663],[-175.95,77.401]],"c":true}]}]},"nm":"Path 24","hd":false},{"ind":24,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[3.079,-0.51],[0.18,-8.491],[-14.645,0.239]],"o":[[-3.079,0.51],[-0.099,4.7],[14.645,-0.239]],"v":[[-8.996,-17.288],[-60.714,-4.682],[-6.253,-16.534]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[8.33,-1.38],[0.18,-8.491],[-39.617,0.647]],"o":[[-8.33,1.38],[-0.099,4.7],[39.617,-0.647]],"v":[[-28.279,-17.178],[-119.213,6.829],[-20.857,-15.139]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.266,-0.044],[0.18,-8.491],[-1.265,0.021]],"o":[[-0.266,0.044],[-0.099,4.7],[1.265,-0.021]],"v":[[-3.288,-10.423],[-157.506,13.654],[-3.051,-10.357]],"c":true}]},{"t":100,"s":[{"i":[[0.005,-0.001],[0.18,-8.491],[-0.024,0]],"o":[[-0.005,0.001],[-0.099,4.7],[0.024,0]],"v":[[-223.707,24.648],[-252.463,30.579],[-223.702,24.649]],"c":true}]}]},"nm":"Path 25","hd":false},{"ind":25,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[5.002,-0.018],[4.376,-11.742],[-21.034,-0.505]],"o":[[-5.002,0.018],[-1,2.682],[21.034,0.505]],"v":[[-8.482,-18.09],[-80.041,-5.651],[-8.996,-16.972]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[13.533,-0.05],[4.376,-11.742],[-56.9,-1.366]],"o":[[-13.533,0.05],[-1,2.682],[56.9,1.366]],"v":[[-26.888,-20.202],[-133.179,-1.393],[-28.279,-17.178]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.432,-0.002],[4.376,-11.742],[-1.817,-0.044]],"o":[[-0.432,0.002],[-1,2.682],[1.817,0.044]],"v":[[-2.957,-10.802],[-167.52,2.271],[-3.002,-10.706]],"c":true}]},{"t":100,"s":[{"i":[[0.008,0],[4.376,-11.742],[-0.034,-0.001]],"o":[[-0.008,0],[-1,2.682],[0.034,0.001]],"v":[[-201.456,2.646],[-252.679,11.357],[-201.457,2.648]],"c":true}]}]},"nm":"Path 26","hd":false},{"ind":26,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[4.015,0.648],[9.285,-5.938],[-12.687,-1.577]],"o":[[-1.837,-0.296],[-5.457,3.49],[12.687,1.577]],"v":[[-7.193,-19.109],[-55.539,-18.566],[-8.167,-18.09]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[10.862,1.752],[9.285,-5.938],[-34.32,-4.266]],"o":[[-4.969,-0.802],[-5.457,3.49],[34.32,4.266]],"v":[[-24.253,-22.959],[-119.714,-21.877],[-26.888,-20.202]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.347,0.056],[9.285,-5.938],[-1.096,-0.136]],"o":[[-0.159,-0.026],[-5.457,3.49],[1.096,0.136]],"v":[[-3.256,-11.254],[-157.719,-22.883],[-3.34,-11.166]],"c":true}]},{"t":100,"s":[{"i":[[0.007,0.001],[9.285,-5.938],[-0.021,-0.003]],"o":[[-0.003,0],[-5.457,3.49],[0.021,0.003]],"v":[[-231.204,-25.606],[-251.964,-25.377],[-231.206,-25.604]],"c":true}]}]},"nm":"Path 27","hd":false},{"ind":27,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[4.393,1.499],[5.601,-5.319],[-6.035,-3.074]],"o":[[-4.393,-1.499],[-2.043,1.94],[7.22,3.678]],"v":[[-5.608,-19.397],[-48.68,-27.903],[-7.508,-19.109]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[11.885,4.055],[5.601,-5.319],[-16.326,-8.316]],"o":[[-11.885,-4.055],[-2.043,1.94],[19.531,9.948]],"v":[[-19.113,-23.736],[-77.851,-34.052],[-24.253,-22.959]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.38,0.13],[5.601,-5.319],[-0.521,-0.266]],"o":[[-0.38,-0.129],[-2.043,1.94],[0.624,0.318]],"v":[[-2.828,-11.723],[-125.77,-43.679],[-2.992,-11.698]],"c":true}]},{"t":100,"s":[{"i":[[0.007,0.002],[5.601,-5.319],[-0.01,-0.005]],"o":[[-0.007,-0.002],[-2.043,1.94],[0.012,0.006]],"v":[[-210.701,-60.106],[-244.601,-67.552],[-210.704,-60.106]],"c":true}]}]},"nm":"Path 28","hd":false},{"ind":28,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[5.221,2.81],[2.362,-11.3],[-20.382,-17.294]],"o":[[-5.221,-2.81],[-1.358,6.576],[20.382,17.294]],"v":[[-7.193,-23.937],[-71.1,-48.972],[-5.293,-19.397]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[14.123,7.602],[4.156,-15.533],[-55.138,-46.784]],"o":[[-14.123,-7.602],[-2.317,8.659],[55.138,46.784]],"v":[[-24.253,-36.018],[-168.703,-94.857],[-19.113,-23.736]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.451,0.243],[3.339,-13.604],[-1.761,-1.494]],"o":[[-0.451,-0.243],[-1.88,7.71],[1.761,1.494]],"v":[[-2.925,-12.804],[-184.078,-101.754],[-2.761,-12.412]],"c":true}]},{"t":100,"s":[{"i":[[0.008,0.005],[1.312,-8.822],[-0.033,-0.028]],"o":[[-0.008,-0.005],[-0.797,5.357],[0.033,0.028]],"v":[[-205.454,-113.613],[-222.203,-118.857],[-205.451,-113.606]],"c":true}]}]},"nm":"Path 29","hd":false},{"ind":29,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[7.26,4.445],[7.011,-4.124],[-10.385,-10.766]],"o":[[-0.326,-0.199],[-7.011,4.124],[16.897,17.517]],"v":[[-5.801,-24.959],[-56.419,-56.973],[-7.193,-23.937]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[19.64,12.023],[7.011,-4.124],[-28.094,-29.123]],"o":[[-0.881,-0.54],[-7.011,4.124],[45.711,47.385]],"v":[[-20.488,-38.784],[-98.519,-86.459],[-24.253,-36.018]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.627,0.384],[7.011,-4.124],[-0.897,-0.93]],"o":[[-0.028,-0.017],[-7.011,4.124],[1.46,1.513]],"v":[[-2.277,-13.13],[-127.401,-106.145],[-2.397,-13.042]],"c":true}]},{"t":100,"s":[{"i":[[0.012,0.007],[7.011,-4.124],[-0.017,-0.018]],"o":[[-0.001,0],[-7.011,4.124],[0.028,0.029]],"v":[[-164.452,-132.115],[-199.019,-154.959],[-164.454,-132.113]],"c":true}]}]},"nm":"Path 30","hd":false},{"ind":30,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[23.866,20.95],[-0.775,-4.182],[-8.843,-10.116]],"o":[[-23.866,-20.95],[0.799,4.308],[8.843,10.116]],"v":[[-4.127,-25.389],[-47.359,-57.266],[-6.116,-25.275]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[64.562,56.675],[-0.775,-4.182],[-23.921,-27.366]],"o":[[-64.562,-56.675],[0.799,4.308],[23.921,27.366]],"v":[[-15.108,-39.093],[-105.227,-105.674],[-20.488,-38.784]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[2.062,1.81],[-0.775,-4.182],[-0.764,-0.874]],"o":[[-2.062,-1.81],[0.799,4.308],[0.764,0.874]],"v":[[-2.169,-13.514],[-128.217,-124.928],[-2.341,-13.504]],"c":true}]},{"t":100,"s":[{"i":[[0.039,0.034],[-0.775,-4.182],[-0.014,-0.016]],"o":[[-0.039,-0.034],[0.799,4.308],[0.014,0.016]],"v":[[-169.449,-161.115],[-185.227,-172.674],[-169.452,-161.115]],"c":true}]}]},"nm":"Path 31","hd":false},{"ind":31,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.861,0.835],[6.082,-5.676],[-10.296,-14.684]],"o":[[-0.861,-0.835],[-2.893,2.699],[10.296,14.684]],"v":[[-0.366,-23.104],[-55.133,-75.787],[-4.127,-25.389]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[2.329,2.258],[6.082,-5.676],[-27.852,-39.724]],"o":[[-2.329,-2.258],[-2.893,2.699],[27.852,39.724]],"v":[[-4.933,-32.911],[-79.889,-105.589],[-15.108,-39.093]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.074,0.072],[6.082,-5.676],[-0.889,-1.268]],"o":[[-0.074,-0.072],[-2.893,2.699],[0.889,1.268]],"v":[[-1.214,-13.207],[-103.885,-130.878],[-1.539,-13.404]],"c":true}]},{"t":100,"s":[{"i":[[0.001,0.001],[6.082,-5.676],[-0.017,-0.024]],"o":[[-0.001,-0.001],[-2.893,2.699],[0.017,0.024]],"v":[[-120.443,-152.612],[-163.389,-193.589],[-120.449,-152.615]],"c":true}]}]},"nm":"Path 32","hd":false},{"ind":32,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[3.542,19.181],[3.073,-0.981],[-0.152,-9.722]],"o":[[-3.542,-19.181],[-2.453,0.783],[0.152,9.722]],"v":[[0.539,-22.899],[-14.028,-61.928],[-0.366,-22.788]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[9.581,51.887],[3.073,-0.981],[-0.412,-26.3]],"o":[[-9.581,-51.887],[-2.453,0.783],[0.412,26.3]],"v":[[-2.483,-33.211],[-31.215,-116.327],[-4.933,-32.911]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.306,1.657],[3.073,-0.981],[-0.013,-0.84]],"o":[[-0.306,-1.657],[-2.453,0.783],[0.013,0.84]],"v":[[-0.517,-14.182],[-44.865,-152.105],[-0.596,-14.172]],"c":true}]},{"t":100,"s":[{"i":[[0.006,0.031],[3.073,-0.981],[0,-0.016]],"o":[[-0.006,-0.031],[-2.453,0.783],[0,0.016]],"v":[[-72.441,-227.612],[-78.715,-240.827],[-72.443,-227.612]],"c":true}]}]},"nm":"Path 33","hd":false},{"ind":33,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.512,3.311],[7.408,-2.515],[0.879,-9.578]],"o":[[-0.512,-3.311],[-4.274,1.451],[-0.879,9.578]],"v":[[1.755,-24.526],[-15.758,-89.049],[0.539,-22.899]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[1.386,8.956],[7.408,-2.515],[2.377,-25.91]],"o":[[-1.386,-8.956],[-4.274,1.451],[-2.377,25.91]],"v":[[0.805,-37.612],[-23.169,-137.772],[-2.483,-33.211]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.044,0.286],[7.408,-2.515],[0.076,-0.827]],"o":[[-0.044,-0.286],[-4.274,1.451],[-0.076,0.827]],"v":[[0.051,-14.02],[-30.928,-169.383],[-0.054,-13.879]],"c":true}]},{"t":100,"s":[{"i":[[0.001,0.005],[7.408,-2.515],[0.001,-0.016]],"o":[[-0.001,-0.005],[-4.274,1.451],[-0.001,0.016]],"v":[[-36.439,-204.114],[-50.169,-247.772],[-36.441,-204.112]],"c":true}]}]},"nm":"Path 34","hd":false},{"ind":34,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[1.275,0.477],[4.481,1.632],[2.961,-6.55],[-1.425,0.478]],"o":[[-2.55,-0.954],[-6.936,-2.526],[-1.48,3.275],[1.425,-0.478]],"v":[[5.014,-23.954],[2.623,-65.932],[2.07,-24.842],[3.754,-13.991]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[3.449,1.291],[4.481,1.632],[8.009,-17.719],[-3.855,1.292]],"o":[[-6.899,-2.581],[-6.936,-2.526],[-4.004,8.859],[3.855,-1.292]],"v":[[8.769,-35.211],[4.357,-117.808],[0.805,-37.612],[5.361,-8.261]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.11,0.041],[4.481,1.632],[0.256,-0.566],[-0.123,0.041]],"o":[[-0.22,-0.082],[-6.936,-2.526],[-0.128,0.283],[0.123,-0.041]],"v":[[0.788,-14.342],[3.064,-156.748],[0.534,-14.419],[0.679,-13.482]],"c":true}]},{"t":100,"s":[{"i":[[0.002,0.001],[4.481,1.632],[0.005,-0.011],[-0.002,0.001]],"o":[[-0.004,-0.002],[-6.936,-2.526],[-0.002,0.005],[0.002,-0.001]],"v":[[1.066,-235.113],[-0.143,-253.308],[1.061,-235.114],[1.064,-235.097]],"c":true}]}]},"nm":"Path 35","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":7,"k":{"a":0,"k":[0.166,1,1,1,0.291,1,0.986,0.894,0.379,1,0.973,0.788,0.522,0.961,0.876,0.394,0.708,0.922,0.78,0,0.865,0.961,0.888,0.206,0.999,1,0.996,0.412,0.166,1,0.291,1,0.379,1,0.522,0.9,0.708,0.8,0.865,0.8,0.999,0.8]}},"s":{"a":0,"k":[-12,-18]},"e":{"a":0,"k":[-123.596,-129.596]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-6.793,2.506]},"a":{"a":0,"k":[-6.793,2.506]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":-4,"op":99,"st":-65,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"white splashes precomp","refId":"comp_2","sr":1,"ks":{"p":{"a":0,"k":[243,243,0]},"a":{"a":0,"k":[243,243,0]}},"ao":0,"w":486,"h":486,"ip":-4,"op":99,"st":-5,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"ogonek5","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[1],"y":[0]},"t":30,"s":[100]},{"t":45,"s":[0]}]},"p":{"a":0,"k":[241,232.25,0]},"a":{"a":0,"k":[-51,43,0]},"s":{"a":1,"k":[{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,15.828]},"t":30,"s":[0,0,100]},{"t":45,"s":[151,151,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[164,164]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":7,"k":{"a":0,"k":[0.166,1,1,1,0.291,1,0.986,0.894,0.379,1,0.973,0.788,0.522,0.961,0.876,0.394,0.708,0.922,0.78,0,0.865,0.961,0.888,0.206,0.999,1,0.996,0.412,0.166,1,0.291,1,0.379,1,0.522,0.9,0.708,0.8,0.865,0.8,0.999,0.8]}},"s":{"a":0,"k":[1,0.5]},"e":{"a":0,"k":[-61.596,-53.096]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 7","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,43]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":30,"op":45,"st":-45,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"rose splashes 80%","sr":1,"ks":{"o":{"a":0,"k":80},"p":{"a":0,"k":[283.287,244.6,0]},"a":{"a":0,"k":[40.287,1.6,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[8.458,2.584],[-1.178,1.927]],"o":[[0,0],[1.178,-1.927]],"v":[[47.396,-2.495],[66.132,8.305]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[17.915,5.472],[-2.495,4.082]],"o":[[0,0],[2.494,-4.082]],"v":[[127.416,14.655],[167.1,37.528]],"c":true}]},{"t":100,"s":[{"i":[[2.922,0.892],[-0.407,0.666]],"o":[[0,0],[0.407,-0.666]],"v":[[231.544,79.714],[238.016,83.445]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-7.371,11.75],[-2.605,-2.954]],"o":[[0,0],[2.605,2.954]],"v":[[-19.417,21.61],[-33.613,50.565]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-16.511,26.322],[-5.835,-6.617]],"o":[[0,0],[5.835,6.617]],"v":[[-54.289,78.774],[-86.09,143.637]],"c":true}]},{"t":100,"s":[{"i":[[-2.019,3.219],[-0.714,-0.809]],"o":[[0,0],[0.714,0.809]],"v":[[-120,208.391],[-123.889,216.322]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[4.272,18.752],[3.489,0]],"o":[[0,0],[-2.919,0]],"v":[[13.337,36.564],[18.266,63.997]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[9.5,41.701],[7.758,0]],"o":[[0,0],[-6.491,0]],"v":[[34.44,120.578],[45.401,181.583]],"c":true}]},{"t":100,"s":[{"i":[[1.211,5.316],[0.989,0]],"o":[[0,0],[-0.827,0]],"v":[[49.981,239.625],[51.378,247.402]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[5.357,10.649],[2.065,-1.671]],"o":[[0,0],[-2.092,1.693]],"v":[[27.785,38.344],[37.752,60.836]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[12.682,25.21],[4.889,-3.955]],"o":[[0,0],[-4.953,4.007]],"v":[[73.965,124.33],[97.561,177.576]],"c":true}]},{"t":100,"s":[{"i":[[1.068,2.124],[0.412,-0.333]],"o":[[0,0],[-0.417,0.338]],"v":[[112.746,219.25],[114.734,223.735]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[7.125,6.764],[2.818,-3.288]],"o":[[0,0],[-1.952,2.278]],"v":[[39.186,19.394],[56.431,42.171]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[16.508,15.671],[6.529,-7.617]],"o":[[0,0],[-4.523,5.277]],"v":[[103.804,73.868],[143.76,126.64]],"c":true}]},{"t":100,"s":[{"i":[[1.631,1.548],[0.645,-0.753]],"o":[[0,0],[-0.447,0.521]],"v":[[178.352,168.75],[182.3,173.964]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":5,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[14.003,-18.483],[-2.995,-2.598]],"o":[[0,0],[2.346,2.035]],"v":[[19.896,-54.149],[37.88,-77.508]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[31.33,-41.355],[-6.701,-5.813]],"o":[[0,0],[5.249,4.553]],"v":[[52.828,-125.71],[93.066,-177.974]],"c":true}]},{"t":100,"s":[{"i":[[3.858,-5.092],[-0.825,-0.716]],"o":[[0,0],[0.646,0.561]],"v":[[119.615,-214],[124.57,-220.435]],"c":true}]}]},"nm":"Path 6","hd":false},{"ind":6,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-6.092,-13.223],[3.225,-0.461]],"o":[[0,0],[-3.225,0.461]],"v":[[-11.7,-53.501],[-23.447,-74.576]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-13.694,-29.721],[7.248,-1.036]],"o":[[0,0],[-7.248,1.036]],"v":[[-33.744,-123.528],[-60.149,-170.902]],"c":true}]},{"t":100,"s":[{"i":[[-1.641,-3.562],[0.869,-0.124]],"o":[[0,0],[-0.869,0.124]],"v":[[-92.793,-229.25],[-95.958,-234.928]],"c":true}]}]},"nm":"Path 7","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.996078431373,0.403921568627,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 7","hd":false},{"ty":"tr","p":{"a":0,"k":[40.287,1.6]},"a":{"a":0,"k":[40.287,1.6]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":-4,"op":99,"st":-65,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"rose splashes 40%","sr":1,"ks":{"o":{"a":0,"k":40},"p":{"a":0,"k":[255.462,250.532,0]},"a":{"a":0,"k":[12.462,7.532,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[2.498,-1.717],[-2.802,-3.967]],"o":[[-1.32,0.907],[0,0]],"v":[[-27.909,-69.864],[-23.485,-62.918]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[5.583,-3.836],[-6.261,-8.864]],"o":[[-2.949,2.026],[0,0]],"v":[[-77.365,-168.395],[-67.479,-152.872]],"c":true}]},{"t":100,"s":[{"i":[[0.692,-0.476],[-0.776,-1.099]],"o":[[-0.366,0.251],[0,0]],"v":[[-104.452,-204.425],[-103.226,-202.5]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-4.758,-1.202],[3.089,-1.172]],"o":[[0,0],[-3.089,1.172]],"v":[[-40.584,-41.866],[-51.165,-45.778]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-10.775,-2.722],[6.996,-2.654]],"o":[[0,0],[-6.996,2.654]],"v":[[-111.836,-92.023],[-135.799,-100.881]],"c":true}]},{"t":100,"s":[{"i":[[-1.235,-0.312],[0.802,-0.304]],"o":[[0,0],[-0.802,0.304]],"v":[[-194.867,-116.25],[-197.612,-117.265]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-6.418,17.711],[-4.713,-1.091]],"o":[[0,0],[2.758,0.638]],"v":[[-22.297,35.041],[-30.452,64.628]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-14.835,40.938],[-10.895,-2.522]],"o":[[0,0],[6.375,1.475]],"v":[[-62.088,115.52],[-80.938,183.907]],"c":true}]},{"t":100,"s":[{"i":[[-1.49,4.112],[-1.094,-0.253]],"o":[[0,0],[0.64,0.148]],"v":[[-89,203.172],[-90.893,210.041]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[1.125,2.009],[-1.205,-2.41]],"o":[[-0.644,-1.15],[1.205,2.41]],"v":[[60.208,34.488],[58.28,36.577]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[2.266,4.046],[-2.428,-4.856]],"o":[[-1.297,-2.316],[2.428,4.856]],"v":[[161.46,114.372],[157.576,118.58]],"c":true}]},{"t":100,"s":[{"i":[[0.457,0.815],[-0.489,-0.978]],"o":[[-0.261,-0.467],[0.489,0.978]],"v":[[178.425,143.467],[177.643,144.315]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.378,1.888],[-0.054,-1.294]],"o":[[-0.244,-1.218],[0.054,1.294]],"v":[[57.201,-37.513],[53.318,-36.704]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[0.721,3.602],[-0.103,-2.47]],"o":[[-0.465,-2.323],[0.103,2.47]],"v":[[152.987,-80.688],[145.577,-79.144]],"c":true}]},{"t":100,"s":[{"i":[[0.177,0.884],[-0.025,-0.606]],"o":[[-0.114,-0.57],[0.025,0.606]],"v":[[207.62,-96.485],[205.801,-96.106]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":5,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-0.033,1.484],[-0.082,-1.894]],"o":[[0.031,-1.392],[0.082,1.894]],"v":[[53.595,-30.294],[48.408,-29.669]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-0.074,3.328],[-0.185,-4.247]],"o":[[0.069,-3.121],[0.185,4.247]],"v":[[143.792,-61.221],[132.159,-59.821]],"c":true}]},{"t":100,"s":[{"i":[[-0.009,0.404],[-0.022,-0.516]],"o":[[0.008,-0.379],[0.022,0.516]],"v":[[216.285,-76.436],[214.873,-76.266]],"c":true}]}]},"nm":"Path 6","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.996078431373,0.403921568627,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 6","hd":false},{"ty":"tr","p":{"a":0,"k":[12.462,7.532]},"a":{"a":0,"k":[12.462,7.532]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":-4,"op":99,"st":-65,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"violet","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[33.333]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[100]},{"t":70,"s":[0]}]},"p":{"a":0,"k":[250.172,221.61,0]},"a":{"a":0,"k":[7.172,-21.39,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.629,0.629,0.629],"y":[0,0,0]},"t":30,"s":[43.235,43.235,100]},{"t":40,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[8.424,-5.071],[2.653,-10.693],[8.493,-0.25],[-0.281,-21.575],[8.206,2.479],[-9.071,-18.319],[10.163,-0.826],[-8.504,-13.367],[7.925,-6.142],[-9.44,-13.344],[2.854,-8.942],[-21.641,-24.17],[6.753,-1.287],[-6.374,-4.022],[10.533,-2.098],[-13.32,-3.511],[-1.839,-6.483],[-21.075,-3.827],[-5.954,-6.816],[-11.932,-2.571],[3.257,-17.575],[-38.942,9.134],[-0.325,-7.706],[-8.742,1.73],[-8.801,-7.785],[-30.134,14.717],[-9.382,-4.129],[-16.279,6.661],[1.088,-8.515],[-7.687,7.097],[-6.012,-8.868],[-10.651,19.166],[-10.35,0.696],[-9.579,21.376],[-8.09,4.605],[-5.131,22.602],[-3.808,-4.758],[-0.748,8.961],[-7.254,-2.788],[2.909,7.968],[-7.635,-1.047],[9.08,17.754],[-7.507,3.193],[9.997,10.579],[-6.351,4.692],[9.781,8.968],[0.178,6.817],[17.454,12.482],[0.03,6.973],[12.265,1.257],[-5.162,4.439],[17.018,1.365],[-5.218,7.12],[24.328,-1.527],[-3.121,9.666],[14.883,-6.778],[10.864,5.993],[11.338,-8.563],[5.168,9.097],[9.336,-10.339],[5.422,9.555],[17.401,-17.591],[8.409,7.467],[2.648,-7.068],[8.104,5.903],[5.776,-17.779],[7.39,-3.513],[1.572,-7.178],[9.221,3.529],[4.343,-6.683]],"o":[[-7.125,5.075],[-0.627,-20.281],[-8.492,0.25],[-7.605,-11.419],[-6.68,-3.509],[-2.4,-2.857],[-8.37,0.053],[-2.455,-2.437],[-7.318,5.151],[-25.313,-18.266],[-0.866,7.957],[-15.111,-8.219],[-6.731,1.147],[0,0],[-8.775,1.322],[-36.623,-3.316],[2.35,6.725],[-4.284,0.119],[5.62,5.119],[0,0],[-0.325,7.953],[-14.317,6.526],[0.333,6.979],[-5.382,3.713],[7.32,4.607],[-4.424,3.372],[7.613,2.532],[-10.42,10.905],[-1.668,7.104],[-2.075,4.92],[4.11,6.36],[-1.424,4.903],[7.699,-1.121],[0,0],[8.176,-4.621],[-1.052,23.086],[4.14,4.652],[5.615,9.885],[7.25,2.786],[5.203,5.546],[7.441,1.093],[15.761,17.732],[7.507,-3.193],[5.904,3.732],[6.351,-4.692],[9.84,5.408],[-0.622,-6.552],[19.337,4.71],[0.329,-7.351],[6.418,-1.574],[5.14,-4.409],[27.522,-2.876],[5.215,-7.117],[4.426,-6.277],[2.875,-9.309],[1.399,-5.562],[-7.079,-5.34],[4.34,-9.001],[-5.002,-8.493],[13.638,-18.255],[-5.312,-9.225],[0.333,-7.822],[-7.414,-6.268],[3.497,-13.498],[-8.104,-5.903],[-0.973,-5.144],[-8.335,3.293],[-2.994,-1.674],[-9.108,-3.513],[-0.006,-3.457]],"v":[[5.228,-95.512],[7.462,-28.929],[-2.195,-87.293],[6.416,-28.961],[-31.261,-81.602],[2.547,-32.204],[-43.849,-81.375],[0.273,-32.419],[-44.643,-71.2],[-1.483,-31.501],[-68.757,-71.437],[-0.116,-26.024],[-42.303,-41.201],[-2.32,-26.028],[-60.656,-35.365],[-6.28,-25.891],[-67.569,-24.781],[-6.096,-24.791],[-61.84,-18.285],[-3.524,-23.253],[-83.167,12.168],[-3.472,-21.52],[-43.616,2.576],[-3.348,-19.306],[-64.141,31.565],[0.002,-18.262],[-50.895,27.819],[3.739,-17.431],[-29.038,27.111],[2.554,-14.294],[-16.457,46.001],[4.192,-11.614],[-10.643,61.853],[6.528,-12.171],[7.719,52.216],[8.895,-14.393],[13.275,34.112],[10.222,-12.355],[26.825,31.666],[12.417,-11.04],[35.999,36.579],[14.675,-10.481],[49.25,44.887],[15.923,-11.135],[54.41,30.884],[17.017,-12.053],[59.976,24.143],[15.598,-15.029],[60.134,6.365],[15.406,-16.076],[56.972,-8.479],[17.766,-17.986],[90.136,-9.191],[19.955,-19.743],[79.759,-35.208],[20.474,-22.64],[63.92,-44.118],[18.736,-24.334],[63.006,-55.052],[17.431,-26.232],[77.149,-67.057],[16.295,-27.947],[47.537,-73.383],[14.878,-29.037],[41.274,-77.714],[13.733,-29.91],[25.971,-72.901],[12.306,-29.047],[19.524,-85.462],[9.122,-30.564]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[8.849,0.01],[7.184,-28.957],[3.052,-1.273],[-0.761,-58.423],[7.839,-5.353],[-24.563,-49.607],[9.192,-3.975],[-23.029,-36.198],[4.861,-7.946],[-25.563,-36.135],[12.25,-6.125],[-58.602,-65.452],[0.423,-3.502],[-17.26,-10.89],[11.497,-5.096],[-36.071,-9.509],[-2.201,-1.041],[-57.07,-10.364],[-1.998,-10.013],[-32.312,-6.962],[9.36,-30.715],[-105.454,24.735],[0.047,-4.178],[-23.672,4.686],[-7.461,-15.968],[-81.601,39.854],[-8.157,-7.347],[-44.083,18.039],[-3.026,-7.306],[-20.815,19.217],[-8.186,-10.792],[-28.842,51.901],[-12.022,-1.924],[-25.94,57.885],[-10.365,1.613],[-13.894,61.205],[-1.581,0.481],[-2.025,24.266],[-5.413,1.006],[7.876,21.576],[-4.573,1.132],[24.589,48.078],[-6.41,1.365],[27.072,28.648],[-4.858,2.346],[26.485,24.286],[-6.451,3.968],[47.266,33.8],[-2.771,2.906],[33.213,3.403],[-1.167,1.667],[46.083,3.696],[-1.333,9],[65.879,-4.134],[-5.083,7.361],[40.303,-18.354],[15.513,2.68],[30.702,-23.187],[1.943,7.096],[25.282,-27.999],[2.5,7.5],[47.12,-47.637],[6.155,7.421],[7.17,-19.139],[5.169,3.176],[15.641,-48.146],[3.103,0.727],[4.257,-19.437],[6.927,0.949],[11.76,-18.098]],"o":[[-5.323,-0.006],[-1.697,-54.919],[-3.052,1.273],[-20.594,-30.923],[-3.728,2.546],[-6.498,-7.737],[-4.346,1.879],[-6.647,-6.6],[-3.22,5.264],[-68.548,-49.463],[-6.872,3.436],[-40.921,-22.258],[-0.377,3.121],[0,0],[-6.756,2.995],[-99.173,-8.979],[3.585,1.695],[-11.602,0.322],[1.08,5.412],[0,0],[-1.42,4.661],[-38.77,17.671],[-0.025,2.213],[-14.575,10.054],[3.439,7.359],[-11.98,9.131],[3.354,3.021],[-28.218,29.53],[1.451,3.503],[-5.619,13.324],[3.041,4.009],[-3.855,13.277],[4.839,0.774],[0,0],[10.589,-1.648],[-2.849,62.516],[2.488,-0.757],[15.206,26.767],[5.413,-1.006],[14.09,15.019],[4.055,-1.004],[42.68,48.017],[6.41,-1.365],[15.989,10.107],[4.858,-2.346],[26.647,14.644],[5.257,-3.234],[52.364,12.754],[3.745,-3.927],[17.38,-4.263],[1.115,-1.592],[74.53,-7.789],[1.333,-9],[11.985,-16.998],[4.419,-6.4],[3.79,-15.061],[-5.258,-0.908],[11.752,-24.375],[-1.495,-5.458],[36.932,-49.434],[-2.202,-6.607],[0.901,-21.183],[-3.464,-4.176],[9.469,-36.552],[-5.169,-3.176],[-2.634,-13.929],[-5.658,-1.325],[-8.107,-4.533],[-6.628,-0.908],[-0.017,-9.362]],"v":[[3.02,-162.999],[8.243,-41.492],[-9.448,-140.273],[5.428,-41.577],[-66.581,-136.474],[-4.982,-50.303],[-94.937,-141.294],[-11.103,-50.883],[-91.631,-117.558],[-15.827,-48.412],[-150.75,-126.625],[-12.148,-33.673],[-76.677,-54.762],[-18.079,-33.683],[-125.935,-48.926],[-28.737,-33.315],[-144.516,-27.935],[-28.241,-30.355],[-129.957,-16.393],[-21.32,-26.218],[-189.443,52.219],[-21.179,-21.553],[-84.613,22.128],[-20.845,-15.595],[-143.927,91.798],[-11.831,-12.788],[-109.552,77.33],[-1.776,-10.549],[-60.574,69.369],[-4.963,-2.109],[-41.055,117.272],[-0.555,5.102],[-30.196,155.83],[5.729,3.604],[8.665,130.425],[12.099,-2.376],[18.32,81.416],[15.669,3.108],[41.962,76.131],[21.577,6.648],[62.49,91.451],[27.654,8.15],[95.923,118.365],[31.011,6.393],[102.975,84.18],[33.954,3.923],[112.957,71.446],[30.136,-4.087],[107.438,31.594],[29.62,-6.903],[94.5,0.667],[35.97,-12.045],[183.167,5],[41.86,-16.772],[147.562,-47.507],[43.258,-24.567],[106.809,-61.148],[38.582,-29.125],[103.057,-80.596],[35.068,-34.233],[141.167,-109],[32.012,-38.849],[76.865,-113.118],[28.198,-41.782],[66.503,-119.657],[25.119,-44.132],[37.324,-105.068],[21.279,-41.807],[29.931,-135.604],[12.71,-45.889]],"c":true}]},{"t":100,"s":[{"i":[[6.52,-0.499],[0,0],[7.552,-1.773],[0,0],[7.419,-3.974],[0,0],[5.063,-2.794],[0,0],[3.869,-4.058],[0,0],[3.25,-5.125],[0,0],[2.323,-7.262],[0,0],[1.065,-7.426],[0,0],[-0.016,-6.935],[0,0],[0.043,-8.393],[0,0],[-1.943,-5.281],[0,0],[-4.613,-6.872],[0,0],[-4.427,-4.702],[0,0],[-5.552,-5.67],[0,0],[-7.074,-5.131],[0,0],[-7.555,-2.728],[0,0],[-8.696,0.33],[0,0],[-5.835,-0.075],[0,0],[-5.179,-0.584],[0,0],[-7.538,4.131],[0,0],[-4.51,1.951],[0,0],[-7.077,4.865],[0,0],[-5.025,3.68],[0,0],[-5.043,6.946],[0,0],[-2.062,4.094],[0,0],[-1.5,7.666],[0,0],[-1.333,7.5],[0,0],[-0.438,6.493],[0,0],[2.309,7.852],[0,0],[1.057,4.404],[0,0],[1.667,6.5],[0,0],[6.365,6.382],[0,0],[3.503,2.843],[0,0],[5.324,3.432],[0,0],[2.931,0.896],[0,0]],"o":[[-6.521,0.499],[0,0],[-7.552,1.773],[0,0],[-7.419,3.974],[0,0],[-5.063,2.794],[0,0],[-3.869,4.058],[0,0],[-3.25,5.125],[0,0],[-2.323,7.262],[0,0],[-1.065,7.426],[0,0],[0.016,6.935],[0,0],[-0.043,8.393],[0,0],[1.943,5.281],[0,0],[4.613,6.872],[0,0],[4.427,4.702],[0,0],[5.552,5.67],[0,0],[7.074,5.131],[0,0],[7.555,2.728],[0,0],[8.696,-0.33],[0,0],[5.835,0.075],[0,0],[5.18,0.584],[0,0],[7.538,-4.131],[0,0],[4.51,-1.951],[0,0],[7.077,-4.865],[0,0],[5.025,-3.68],[0,0],[5.043,-6.946],[0,0],[2.062,-4.094],[0,0],[1.5,-7.666],[0,0],[1.333,-7.5],[0,0],[0.438,-6.493],[0,0],[-2.309,-7.852],[0,0],[-1.057,-4.404],[0,0],[-1.667,-6.5],[0,0],[-6.365,-6.382],[0,0],[-3.503,-2.843],[0,0],[-5.324,-3.432],[0,0],[-2.931,-0.896],[0,0]],"v":[[0.021,-238.999],[6.522,-22.25],[-22.948,-238.773],[6.486,-22.251],[-111.081,-205.974],[6.353,-22.363],[-136.437,-188.794],[6.274,-22.37],[-151.131,-175.058],[6.214,-22.339],[-176.75,-144.125],[6.261,-22.15],[-199.177,-102.262],[6.185,-22.15],[-209.935,-65.926],[6.048,-22.145],[-215.016,-31.435],[6.055,-22.107],[-215.457,-11.893],[6.143,-22.054],[-201.443,57.219],[6.145,-21.994],[-194.113,74.128],[6.149,-21.918],[-171.427,112.298],[6.265,-21.882],[-163.052,122.83],[6.394,-21.853],[-126.074,156.869],[6.353,-21.745],[-65.055,189.772],[6.41,-21.653],[-38.196,196.33],[6.49,-21.672],[9.165,202.425],[6.572,-21.748],[31.821,199.416],[6.618,-21.678],[80.962,187.631],[6.693,-21.633],[105.49,178.451],[6.771,-21.613],[127.923,167.865],[6.814,-21.636],[157.975,144.18],[6.852,-21.668],[174.957,125.946],[6.803,-21.77],[204.438,84.094],[6.796,-21.806],[223.5,34.166],[6.878,-21.872],[227.667,12],[6.953,-21.933],[227.062,-61.507],[6.971,-22.033],[213.309,-104.148],[6.911,-22.091],[198.057,-137.596],[6.866,-22.157],[195.667,-144],[6.827,-22.216],[142.865,-200.118],[6.778,-22.254],[122.503,-211.157],[6.739,-22.284],[84.324,-230.568],[6.689,-22.254],[51.431,-240.604],[6.58,-22.306]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":80},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0.001,0.788,0.314,0,0.707,0.776,0.349,0,1,0.765,0.384,0]}},"s":{"a":0,"k":[4,-20]},"e":{"a":0,"k":[177.39,-20]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 3","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":-4,"op":99,"st":-65,"bm":0}]},{"id":"comp_2","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"white splashes null","sr":1,"ks":{"o":{"a":0,"k":0},"p":{"a":0,"k":[244.121,231.173,0]},"s":{"a":1,"k":[{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,15]},"t":0,"s":[10,10,100]},{"t":45,"s":[100,100,100]}]}},"ao":0,"ip":0,"op":105,"st":-60,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"white splashes 16","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[100]},{"t":105,"s":[0]}]},"r":{"a":0,"k":-26.39},"a":{"a":0,"k":[1.121,-11.827,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[3.692,-18.443],[0.554,28.851],[-4.71,-0.153],[0.91,-34.729]],"o":[[0,0],[-0.811,-42.252],[4.48,0.146],[-0.566,21.614]],"v":[[3.698,-34.289],[7.153,-97.417],[3.704,-163.044],[9.887,-97.551]],"c":true}]},{"t":105,"s":[{"i":[[1.129,1.062],[-0.345,3.296],[-2.657,-2.497],[0.563,-5.372]],"o":[[-2.415,-2.27],[0.38,-3.627],[3.936,3.699],[-0.161,1.542]],"v":[[2.569,-224.224],[-0.708,-233.61],[4.557,-243.199],[6.236,-227.083]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862747669,0.854901969433,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":105,"st":-60,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"white splashes 15","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[100]},{"t":105,"s":[0]}]},"r":{"a":0,"k":-37.24},"a":{"a":0,"k":[1.121,-11.827,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[2.919,0.028],[0.458,3.186],[-3.113,-0.928],[-0.552,-2.173]],"o":[[-2.457,-0.024],[-0.45,-3.129],[2.517,0.751],[0.826,3.251]],"v":[[7.197,-189.998],[2.396,-196.671],[5.985,-201.733],[10.531,-196.769]],"c":true}]},{"t":105,"s":[{"i":[[0.715,0.672],[-0.218,2.085],[-1.681,-1.58],[0.356,-3.399]],"o":[[-1.528,-1.436],[0.24,-2.295],[2.49,2.34],[-0.102,0.975]],"v":[[3.804,-282.639],[1.73,-288.578],[5.061,-294.644],[6.124,-284.448]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[3.537,-26.73],[0.322,38.379],[-8.051,-1.852],[1.133,-39.172]],"o":[[0,0],[-0.423,-50.388],[5.402,1.243],[-0.739,25.566]],"v":[[8.88,-30.231],[9.352,-111.051],[8.877,-182.906],[15.292,-111.636]],"c":true}]},{"t":105,"s":[{"i":[[1.129,1.062],[-0.345,3.296],[-2.657,-2.497],[0.563,-5.372]],"o":[[-2.415,-2.27],[0.38,-3.627],[3.936,3.699],[-0.161,1.542]],"v":[[2.911,-258.66],[-0.366,-268.047],[4.899,-277.636],[6.578,-261.52]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862747669,0.854901969433,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":105,"st":-60,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"white splashes 14","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[100]},{"t":105,"s":[0]}]},"r":{"a":0,"k":-76.9},"a":{"a":0,"k":[1.121,-11.827,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[1.889,-0.602],[1.33,4.021],[-2.184,1.073],[-0.71,-4.999]],"o":[[-2.155,0.687],[-1.33,-4.021],[3.935,-1.933],[0.554,3.902]],"v":[[8.293,-195.666],[2.395,-202.581],[3.004,-212.136],[10.191,-204.071]],"c":true}]},{"t":105,"s":[{"i":[[0.715,0.672],[-0.218,2.085],[-1.681,-1.58],[0.356,-3.399]],"o":[[-1.528,-1.436],[0.24,-2.295],[2.49,2.34],[-0.102,0.975]],"v":[[3.084,-248.643],[1.01,-254.582],[4.342,-260.648],[5.404,-250.452]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[0,0],[-1.319,42.909],[-4.42,3.366],[0.367,-39.751]],"o":[[0,0],[1.684,-54.774],[4.638,-3.532],[-0.457,49.575]],"v":[[9.093,-35.688],[13.821,-104.066],[9.104,-187.932],[19.987,-125.011]],"c":true}]},{"t":105,"s":[{"i":[[1.129,1.062],[-0.345,3.296],[-2.657,-2.497],[0.563,-5.372]],"o":[[-2.415,-2.27],[0.38,-3.627],[3.936,3.699],[-0.161,1.542]],"v":[[2.192,-224.664],[-1.086,-234.051],[4.179,-243.64],[5.858,-227.523]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862747669,0.854901969433,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":105,"st":-60,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"white splashes 13","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[100]},{"t":105,"s":[0]}]},"r":{"a":0,"k":247.61},"a":{"a":0,"k":[1.121,-11.827,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[1.106,-0.312],[0.854,3.094],[-1.739,0.347],[-0.484,-3.307]],"o":[[-1.879,0.53],[-0.854,-3.094],[2.606,-0.52],[0.423,2.893]],"v":[[16.044,-188.261],[11.553,-193.701],[12.489,-200.455],[17.091,-194.354]],"c":true}]},{"t":105,"s":[{"i":[[0.715,0.672],[-0.218,2.085],[-1.681,-1.58],[0.356,-3.399]],"o":[[-1.528,-1.436],[0.24,-2.295],[2.49,2.34],[-0.102,0.975]],"v":[[4.94,-258.315],[2.866,-264.254],[6.198,-270.321],[7.26,-260.124]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[0,0],[0.386,32.409],[-2.865,-1.201],[0.359,-8.729]],"o":[[0,0],[-0.486,-40.852],[8.005,3.357],[-1.553,37.802]],"v":[[16.903,-34.891],[18.1,-112.135],[16.912,-182.742],[22.479,-110.91]],"c":true}]},{"t":105,"s":[{"i":[[1.129,1.062],[-0.345,3.296],[-2.657,-2.497],[0.563,-5.372]],"o":[[-2.415,-2.27],[0.38,-3.627],[3.936,3.699],[-0.161,1.542]],"v":[[2.192,-224.664],[-1.086,-234.051],[4.179,-243.64],[5.858,-227.523]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862747669,0.854901969433,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":105,"st":-60,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"white splashes 12","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[100]},{"t":105,"s":[0]}]},"r":{"a":0,"k":233.03},"a":{"a":0,"k":[1.121,-11.827,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[1.981,-0.058],[0.74,5.37],[-3.113,-0.709],[-0.16,-4.987]],"o":[[-1.929,0.057],[-0.74,-5.37],[3.113,0.709],[0.16,4.987]],"v":[[12.221,-233.342],[7.552,-244.188],[10.447,-254.057],[15.155,-243.212]],"c":true}]},{"t":105,"s":[{"i":[[0.715,0.672],[-0.218,2.085],[-1.681,-1.58],[0.356,-3.399]],"o":[[-1.528,-1.436],[0.24,-2.295],[2.49,2.34],[-0.102,0.975]],"v":[[9.651,-292.065],[7.577,-298.004],[10.908,-304.071],[11.971,-293.874]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[0,0],[1.356,31.642],[-4.56,1.137],[-0.088,-53.09]],"o":[[4.684,-49.409],[-2.183,-50.944],[5.665,-1.413],[0.11,66.517]],"v":[[12.032,-26.322],[15.369,-148.176],[12.025,-227.456],[21.738,-151.886]],"c":true}]},{"t":105,"s":[{"i":[[1.129,1.062],[-0.345,3.296],[-2.657,-2.497],[0.563,-5.372]],"o":[[-2.415,-2.27],[0.38,-3.627],[3.936,3.699],[-0.161,1.542]],"v":[[6.902,-258.414],[3.625,-267.801],[8.89,-277.39],[10.569,-261.273]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862747669,0.854901969433,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":105,"st":-60,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"white splashes 11","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[100]},{"t":105,"s":[0]}]},"r":{"a":0,"k":214.81},"a":{"a":0,"k":[1.121,-11.827,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[1.507,0.148],[-0.026,1.131],[-1.431,-0.266],[-0.107,-1.096]],"o":[[-1.786,-0.176],[0.031,-1.343],[1.208,0.224],[0.127,1.295]],"v":[[4.516,-198.027],[2.064,-200.467],[4.571,-202.884],[6.56,-200.468]],"c":true}]},{"t":105,"s":[{"i":[[0.372,0.35],[-0.114,1.086],[-0.875,-0.823],[0.185,-1.769]],"o":[[-0.795,-0.747],[0.125,-1.194],[1.296,1.218],[-0.053,0.508]],"v":[[11.374,-296.43],[10.295,-299.521],[12.029,-302.679],[12.582,-297.371]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[1.573,-0.068],[-0.07,2.424],[-1.723,-0.616],[-0.046,-2.055]],"o":[[-2.138,0.092],[0.076,-2.642],[1.541,0.552],[0.052,2.348]],"v":[[4.364,-207.182],[1.458,-211.938],[4.41,-216.605],[6.732,-211.938]],"c":true}]},{"t":105,"s":[{"i":[[0.715,0.672],[-0.218,2.085],[-1.681,-1.58],[0.356,-3.399]],"o":[[-1.528,-1.436],[0.24,-2.295],[2.49,2.34],[-0.102,0.975]],"v":[[12.529,-308.36],[10.456,-314.299],[13.787,-320.366],[14.849,-310.169]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[0,0],[1,27.626],[-5.456,-2.345],[0.879,-39.91]],"o":[[1.509,-38.059],[-1.888,-52.164],[5.09,2.188],[-1.019,46.266]],"v":[[5.034,-13.727],[5.006,-113.095],[5.033,-193.482],[10.403,-114.654]],"c":true}]},{"t":105,"s":[{"i":[[1.129,1.062],[-0.345,3.296],[-2.657,-2.497],[0.563,-5.372]],"o":[[-2.415,-2.27],[0.38,-3.627],[3.936,3.699],[-0.161,1.542]],"v":[[9.781,-274.709],[6.503,-284.096],[11.769,-293.685],[13.448,-277.569]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862747669,0.854901969433,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":105,"st":-60,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"white splashes 10","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[100]},{"t":105,"s":[0]}]},"r":{"a":0,"k":192.69},"a":{"a":0,"k":[1.121,-11.827,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[0.501,-5.417],[0.307,16.252],[-1.853,0.524],[0.918,-24.972]],"o":[[1.34,-26.772],[-0.617,-32.602],[4.732,-1.338],[-1.024,27.851]],"v":[[9.159,-30.175],[10.331,-94.286],[9.15,-140.552],[13.436,-94.62]],"c":true}]},{"t":105,"s":[{"i":[[1.129,1.062],[-0.345,3.296],[-2.657,-2.497],[0.563,-5.372]],"o":[[-2.415,-2.27],[0.38,-3.627],[3.936,3.699],[-0.161,1.542]],"v":[[2.872,-210.009],[-0.406,-219.396],[4.859,-228.985],[6.538,-212.869]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862747669,0.854901969433,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":105,"st":-60,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"white splashes 9","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[100]},{"t":105,"s":[0]}]},"r":{"a":0,"k":178.18},"a":{"a":0,"k":[1.121,-11.827,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[2.914,0.483],[-0.75,3.266],[-2.159,-0.434],[0.278,-3.069]],"o":[[-2.731,-0.452],[0.75,-3.266],[2.014,0.405],[-0.319,3.512]],"v":[[5.887,-190.899],[3.433,-198.105],[8.313,-203.983],[10.844,-197.439]],"c":true}]},{"t":105,"s":[{"i":[[1.129,1.062],[-0.345,3.296],[-2.657,-2.497],[0.563,-5.372]],"o":[[-2.415,-2.27],[0.38,-3.627],[3.936,3.699],[-0.161,1.542]],"v":[[5.198,-239.911],[1.92,-249.298],[7.185,-258.887],[8.864,-242.771]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[0,0],[-0.178,30.147],[-3.098,-0.098],[2.065,-33.166]],"o":[[-3.379,-29.806],[0.157,-26.65],[6.602,0.21],[-3.247,52.138]],"v":[[4.103,-38.228],[-0.753,-137.332],[4.092,-183.497],[5.25,-137.283]],"c":true}]},{"t":105,"s":[{"i":[[1.129,1.062],[-0.345,3.296],[-2.657,-2.497],[0.563,-5.372]],"o":[[-2.415,-2.27],[0.38,-3.627],[3.936,3.699],[-0.161,1.542]],"v":[[2.872,-210.009],[-0.406,-219.396],[4.859,-228.985],[6.538,-212.869]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862747669,0.854901969433,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":105,"st":-60,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"white splashes 8","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[100]},{"t":105,"s":[0]}]},"r":{"a":0,"k":159.43},"a":{"a":0,"k":[1.121,-11.827,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[1.785,-0.166],[-0.606,2.256],[-1.258,-0.472],[0.237,-1.901]],"o":[[-2.305,0.214],[0.54,-2.01],[1.419,0.532],[-0.209,1.677]],"v":[[1.45,-209.993],[-0.568,-214.537],[2.505,-217.89],[4.374,-213.361]],"c":true}]},{"t":105,"s":[{"i":[[1.129,1.062],[-0.345,3.296],[-2.657,-2.497],[0.563,-5.372]],"o":[[-2.415,-2.27],[0.38,-3.627],[3.936,3.699],[-0.161,1.542]],"v":[[5.198,-239.911],[1.92,-249.298],[7.185,-258.887],[8.864,-242.771]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[0,0],[1.193,46.077],[-4.889,-1.15],[1.642,-42.494]],"o":[[0,0],[-1.052,-40.634],[7.955,1.872],[-2.299,59.509]],"v":[[0.57,-29.865],[-3.566,-125.498],[0.579,-202.91],[2.057,-125.567]],"c":true}]},{"t":105,"s":[{"i":[[1.129,1.062],[-0.345,3.296],[-2.657,-2.497],[0.563,-5.372]],"o":[[-2.415,-2.27],[0.38,-3.627],[3.936,3.699],[-0.161,1.542]],"v":[[2.872,-210.009],[-0.406,-219.396],[4.859,-228.985],[6.538,-212.869]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862747669,0.854901969433,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":105,"st":-60,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"white splashes 7","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[100]},{"t":105,"s":[0]}]},"r":{"a":0,"k":121.14},"a":{"a":0,"k":[1.121,-11.827,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[0,0],[-0.156,34.036],[-4.468,-1.124],[3.836,-53.869]],"o":[[-9.01,-37.907],[0.232,-50.528],[6.047,1.521],[-4.23,59.393]],"v":[[-1.995,-37.202],[-13.819,-149.054],[-1.999,-236.043],[-8.402,-150.485]],"c":true}]},{"t":105,"s":[{"i":[[0.894,1.266],[-0.999,3.16],[-2.102,-2.979],[1.628,-5.15]],"o":[[-1.911,-2.708],[1.099,-3.477],[3.114,4.413],[-0.467,1.478]],"v":[[-9.187,-267.183],[-10.516,-277.036],[-3.435,-285.375],[-5.021,-269.249]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862747669,0.854901969433,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":105,"st":-60,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"white splashes 6","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[100]},{"t":105,"s":[0]}]},"r":{"a":0,"k":99.54},"a":{"a":0,"k":[1.121,-11.827,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[2.915,1.639],[-0.439,2.709],[-2.762,-1.266],[0.258,-2.657]],"o":[[-2.152,-1.211],[0.566,-3.493],[1.834,0.841],[-0.374,3.847]],"v":[[0.236,-176.094],[-2.057,-182.925],[3.531,-188.307],[5.714,-182.155]],"c":true}]},{"t":105,"s":[{"i":[[1.129,1.062],[-0.345,3.296],[-2.657,-2.497],[0.563,-5.372]],"o":[[-2.415,-2.27],[0.38,-3.627],[3.936,3.699],[-0.161,1.542]],"v":[[5.198,-239.911],[1.92,-249.298],[7.185,-258.887],[8.864,-242.771]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[-2.433,-32.344],[1.471,40.713],[-4.408,-2.527],[1.164,-31.322]],"o":[[0,0],[-1.412,-39.1],[2.484,1.424],[-0.718,19.307]],"v":[[1.045,-42.304],[-7.111,-119.798],[1.052,-167.216],[-0.663,-119.462]],"c":true}]},{"t":105,"s":[{"i":[[1.129,1.062],[-0.345,3.296],[-2.657,-2.497],[0.563,-5.372]],"o":[[-2.415,-2.27],[0.38,-3.627],[3.936,3.699],[-0.161,1.542]],"v":[[2.872,-210.009],[-0.406,-219.396],[4.859,-228.985],[6.538,-212.869]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862747669,0.854901969433,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":105,"st":-60,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"white splashes 5","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[100]},{"t":105,"s":[0]}]},"r":{"a":0,"k":77.63},"a":{"a":0,"k":[1.121,-11.827,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[1.463,0.517],[-0.149,1.097],[-1.132,-0.252],[0.096,-1.199]],"o":[[-1.11,-0.392],[0.199,-1.463],[1.187,0.264],[-0.125,1.557]],"v":[[3.823,-195.789],[2.539,-198.382],[4.909,-201.046],[6.439,-198.381]],"c":true}]},{"t":105,"s":[{"i":[[0.372,0.35],[-0.114,1.086],[-0.875,-0.822],[0.185,-1.769]],"o":[[-0.795,-0.748],[0.125,-1.194],[1.296,1.218],[-0.053,0.508]],"v":[[4.894,-216.411],[3.814,-219.502],[5.548,-222.66],[6.101,-217.353]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[1.326,0.836],[-0.749,2.262],[-1.249,-0.536],[0.89,-2.237]],"o":[[-1.156,-0.729],[0.838,-2.534],[2.124,0.912],[-1.049,2.636]],"v":[[5.046,-203.717],[4.806,-209.142],[8.46,-213.457],[9.665,-207.855]],"c":true}]},{"t":105,"s":[{"i":[[0.715,0.672],[-0.218,2.085],[-1.681,-1.58],[0.356,-3.399]],"o":[[-1.528,-1.436],[0.24,-2.295],[2.49,2.34],[-0.102,0.975]],"v":[[6.049,-228.341],[3.975,-234.28],[7.307,-240.347],[8.369,-230.151]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[-4.814,-40.706],[-1.895,51.518],[-4.761,-1.911],[1.795,-38.673]],"o":[[0,0],[1.569,-42.637],[2.891,1.16],[-1.202,25.903]],"v":[[3.306,-29.924],[-6.601,-132.191],[3.292,-191.348],[-0.482,-130.244]],"c":true}]},{"t":105,"s":[{"i":[[1.129,1.062],[-0.345,3.296],[-2.657,-2.497],[0.563,-5.372]],"o":[[-2.415,-2.27],[0.38,-3.627],[3.936,3.699],[-0.161,1.542]],"v":[[3.3,-194.69],[0.023,-204.077],[5.288,-213.666],[6.967,-197.55]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862747669,0.854901969433,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":105,"st":-60,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"white splashes 3","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[100]},{"t":105,"s":[0]}]},"r":{"a":0,"k":35.48},"a":{"a":0,"k":[1.121,-11.827,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[2.259,0.86],[-0.661,4.424],[-2.433,-0.341],[1.208,-4.987]],"o":[[-2.13,-0.811],[0.754,-5.049],[3.542,0.496],[-1.185,4.895]],"v":[[1.284,-197.622],[-0.569,-207.285],[4.734,-217.037],[7.318,-206.171]],"c":true}]},{"t":105,"s":[{"i":[[1.129,1.062],[-0.345,3.296],[-2.657,-2.497],[0.563,-5.372]],"o":[[-2.415,-2.27],[0.38,-3.627],[3.936,3.699],[-0.161,1.542]],"v":[[5.772,-271.189],[2.494,-280.576],[7.759,-290.165],[9.439,-274.049]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[0,0],[-1.091,54.207],[-2.162,-1.244],[2.031,-39.647]],"o":[[0,0],[0.873,-43.398],[3.603,2.074],[-2.522,49.212]],"v":[[2.196,-32.287],[-7.332,-149.424],[2.183,-192.763],[-0.137,-136.646]],"c":true}]},{"t":105,"s":[{"i":[[1.129,1.062],[-0.345,3.296],[-2.657,-2.497],[0.563,-5.372]],"o":[[-2.415,-2.27],[0.38,-3.627],[3.936,3.699],[-0.161,1.542]],"v":[[3.446,-241.287],[0.168,-250.674],[5.433,-260.263],[7.113,-244.146]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862747669,0.854901969433,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":105,"st":-60,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"white splashes 4","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[100]},{"t":105,"s":[0]}]},"r":{"a":0,"k":-3.87},"a":{"a":0,"k":[1.121,-11.827,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[2.46,0.092],[-0.034,1.74],[-1.94,0.288],[-0.039,-1.924]],"o":[[-1.711,-0.064],[0.034,-1.74],[1.94,-0.288],[0.039,1.924]],"v":[[2.715,-178.247],[0.16,-181.736],[3.08,-185.561],[6.197,-182.051]],"c":true}]},{"t":105,"s":[{"i":[[1.129,1.062],[-0.345,3.296],[-2.657,-2.498],[0.563,-5.372]],"o":[[-2.415,-2.27],[0.38,-3.627],[3.936,3.699],[-0.161,1.542]],"v":[[5.626,-224.593],[2.349,-233.98],[7.614,-243.569],[9.293,-227.452]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[2.294,0.008],[0.501,2.89],[-1.583,-0.006],[-0.229,-1.323]],"o":[[-2.933,-0.01],[-0.27,-1.56],[1.343,0.005],[0.392,2.26]],"v":[[1.108,-11.6],[-4.67,-18.237],[-0.404,-20.326],[3.025,-18.209]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[0,0],[0.85,39.256],[-3.929,-0.2],[0.353,-39.178]],"o":[[0,0],[-0.635,-29.308],[6.408,0.327],[-0.331,36.714]],"v":[[1.787,-33.47],[-2.076,-118.748],[1.789,-173.393],[2.474,-118.386]],"c":true}]},{"t":105,"s":[{"i":[[1.129,1.062],[-0.345,3.296],[-2.657,-2.497],[0.563,-5.372]],"o":[[-2.415,-2.27],[0.38,-3.627],[3.936,3.699],[-0.161,1.542]],"v":[[3.3,-194.69],[0.023,-204.077],[5.288,-213.666],[6.967,-197.55]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862747669,0.854901969433,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":105,"st":-60,"bm":0}]},{"id":"comp_3","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"pink","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[100]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[100]},{"t":100,"s":[0]}]},"p":{"a":0,"k":[245.75,225.75,0]},"a":{"a":0,"k":[2.75,-17.25,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.629,0.629,0.629],"y":[0,0,0]},"t":30,"s":[43.235,43.235,100]},{"t":40,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[1.312,8.525],[3.244,0.766],[0.278,-4.348]],"o":[[-1.312,-8.525],[-5.358,-1.265],[-0.278,4.348]],"v":[[6.13,-24.571],[8.951,-66.768],[5.014,-23.638]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[3.55,23.061],[3.244,0.766],[0.753,-11.762]],"o":[[-3.55,-23.061],[-5.358,-1.265],[-0.753,11.762]],"v":[[11.788,-37.733],[14.47,-92.154],[8.769,-35.211]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.113,0.736],[3.244,0.766],[0.024,-0.376]],"o":[[-0.113,-0.736],[-5.358,-1.265],[-0.024,0.376]],"v":[[1.251,-14.165],[19.643,-137.56],[1.155,-14.085]],"c":true}]},{"t":100,"s":[{"i":[[0.002,0.014],[3.244,0.766],[0,-0.007]],"o":[[-0.002,-0.014],[-5.358,-1.265],[0,0.007]],"v":[[29.568,-215.114],[32.47,-250.154],[29.566,-215.113]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-0.775,14.209],[6.351,4.603],[2.652,-16.185]],"o":[[0.775,-14.209],[-4.056,-2.94],[-2.652,16.185]],"v":[[7.231,-23.969],[18.873,-61.186],[6.13,-24.571]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-2.096,38.439],[6.351,4.603],[7.173,-43.783]],"o":[[2.096,-38.439],[-4.056,-2.94],[-7.173,43.783]],"v":[[14.767,-36.105],[37.637,-108.647],[11.788,-37.733]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.067,1.227],[6.351,4.603],[0.229,-1.398]],"o":[[0.067,-1.227],[-4.056,-2.94],[-0.229,1.398]],"v":[[1.932,-14.242],[49.85,-146.868],[1.837,-14.294]],"c":true}]},{"t":100,"s":[{"i":[[-0.001,0.023],[6.351,4.603],[0.004,-0.026]],"o":[[0.001,-0.023],[-4.056,-2.94],[-0.004,0.026]],"v":[[75.069,-225.113],[80.137,-241.647],[75.068,-225.114]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.749,6.829],[6.31,5.609],[1.359,-6.48]],"o":[[-0.749,-6.829],[-3.554,-3.159],[-1.359,6.48]],"v":[[8.596,-23.217],[31.24,-73.607],[7.231,-23.969]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[2.026,18.473],[6.31,5.609],[3.677,-17.531]],"o":[[-2.026,-18.473],[-3.554,-3.159],[-3.677,17.531]],"v":[[18.46,-34.07],[47.481,-104.355],[14.767,-36.105]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.065,0.59],[6.31,5.609],[0.117,-0.56]],"o":[[-0.065,-0.59],[-3.554,-3.159],[-0.117,0.56]],"v":[[2.166,-13.836],[63.43,-142.576],[2.048,-13.901]],"c":true}]},{"t":100,"s":[{"i":[[0.001,0.011],[6.31,5.609],[0.002,-0.011]],"o":[[-0.001,-0.011],[-3.554,-3.159],[-0.002,0.011]],"v":[[84.072,-198.612],[102.981,-237.355],[84.069,-198.613]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-6.199,15.089],[5.581,6.595],[12.758,-17.563]],"o":[[6.199,-15.089],[-2.72,-3.214],[-12.758,17.563]],"v":[[9.486,-22.203],[48.882,-61.987],[8.281,-23.532]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-16.769,40.819],[5.581,6.595],[34.513,-47.512]],"o":[[16.769,-40.819],[-2.72,-3.214],[-34.513,47.512]],"v":[[21.719,-30.475],[103.282,-109.764],[18.46,-34.07]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.535,1.303],[5.581,6.595],[1.102,-1.517]],"o":[[0.535,-1.303],[-2.72,-3.214],[-1.102,1.517]],"v":[[3.204,-13.341],[122.823,-131.604],[3.099,-13.456]],"c":true}]},{"t":100,"s":[{"i":[[-0.01,0.025],[5.581,6.595],[0.021,-0.029]],"o":[[0.01,-0.025],[-2.72,-3.214],[-0.021,0.029]],"v":[[156.574,-169.11],[171.282,-185.764],[156.572,-169.112]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-4.223,9.945],[2.258,6.104],[6.726,-10.205]],"o":[[4.223,-9.945],[-1.817,-4.912],[-6.726,10.205]],"v":[[11.177,-20.424],[48.778,-58.677],[9.801,-21.888]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-11.423,26.904],[2.258,6.104],[18.195,-27.606]],"o":[[11.423,-26.904],[-1.817,-4.912],[-18.195,27.606]],"v":[[25.439,-26.516],[74.48,-79.649],[21.719,-30.475]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.365,0.859],[2.258,6.104],[0.581,-0.882]],"o":[[0.365,-0.859],[-1.817,-4.912],[-0.581,0.882]],"v":[[3.264,-12.932],[105.66,-107.524],[3.146,-13.058]],"c":true}]},{"t":100,"s":[{"i":[[-0.007,0.016],[2.258,6.104],[0.011,-0.017]],"o":[[0.007,-0.016],[-1.817,-4.912],[-0.011,0.017]],"v":[[152.076,-147.108],[182.98,-176.649],[152.074,-147.11]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":5,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-3.334,6.648],[-1.799,5.518],[9.673,-9.931]],"o":[[3.334,-6.648],[1.878,-5.758],[-9.673,9.931]],"v":[[12.897,-19.193],[48.972,-44.204],[11.177,-20.424]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-9.018,17.983],[-1.799,5.518],[26.167,-26.865]],"o":[[9.018,-17.983],[1.878,-5.758],[-26.167,26.865]],"v":[[30.094,-23.186],[81.927,-62.022],[25.439,-26.516]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.288,0.574],[-1.799,5.518],[0.836,-0.858]],"o":[[0.288,-0.574],[1.878,-5.758],[-0.836,0.858]],"v":[[3.883,-12.491],[120.292,-83.862],[3.734,-12.597]],"c":true}]},{"t":100,"s":[{"i":[[-0.005,0.011],[-1.799,5.518],[0.016,-0.016]],"o":[[0.005,-0.011],[1.878,-5.758],[-0.016,0.016]],"v":[[188.579,-121.106],[215.427,-138.022],[188.576,-121.108]],"c":true}]}]},"nm":"Path 6","hd":false},{"ind":6,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-9.869,8.704],[-3.432,7.058],[12.009,-7.852]],"o":[[9.869,-8.704],[2.981,-6.13],[-12.009,7.852]],"v":[[12.832,-16.628],[74.374,-42.527],[12.897,-19.193]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-26.696,23.547],[-3.432,7.058],[32.485,-21.24]],"o":[[26.696,-23.547],[2.981,-6.13],[-32.485,21.24]],"v":[[29.916,-16.247],[117.42,-56.876],[30.094,-23.186]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.852,0.752],[-3.432,7.058],[1.037,-0.678]],"o":[[0.852,-0.752],[2.981,-6.13],[-1.037,0.678]],"v":[[3.897,-11.786],[151.618,-69.521],[3.902,-12.008]],"c":true}]},{"t":100,"s":[{"i":[[-0.016,0.014],[-3.432,7.058],[0.02,-0.013]],"o":[[0.016,-0.014],[2.981,-6.13],[-0.02,0.013]],"v":[[190.079,-83.601],[236.42,-100.876],[190.079,-83.606]],"c":true}]}]},"nm":"Path 7","hd":false},{"ind":7,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-14.199,3.553],[-2.249,4.962],[22.38,-4.873]],"o":[[22.287,-5.577],[4.049,-8.935],[-22.38,4.873]],"v":[[10.861,-15.141],[88.919,-15.854],[12.516,-16.944]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-38.412,9.611],[-2.249,4.962],[60.542,-13.183]],"o":[[60.291,-15.085],[4.049,-8.935],[-60.542,13.183]],"v":[[25.439,-11.372],[154.986,-15.381],[29.916,-16.247]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-1.227,0.307],[-2.249,4.962],[1.933,-0.421]],"o":[[1.925,-0.482],[4.049,-8.935],[-1.933,0.421]],"v":[[3.934,-10.704],[182.862,-13.944],[4.077,-10.859]],"c":true}]},{"t":100,"s":[{"i":[[-0.023,0.006],[-2.249,4.962],[0.036,-0.008]],"o":[[0.036,-0.009],[4.049,-8.935],[-0.036,0.008]],"v":[[204.076,-11.599],[251.986,-10.381],[204.079,-11.601]],"c":true}]}]},"nm":"Path 8","hd":false},{"ind":8,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-4.711,2.16],[-0.839,0.958],[12.251,-0.88]],"o":[[4.711,-2.16],[1.348,-1.539],[-12.251,0.88]],"v":[[9.079,-12.869],[39.383,-12.388],[10.861,-14.826]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-12.744,5.843],[-0.839,0.958],[33.14,-2.38]],"o":[[12.744,-5.843],[1.348,-1.539],[-33.14,2.38]],"v":[[20.619,-6.076],[78.015,-8.131],[25.439,-11.372]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.407,0.187],[-0.839,0.958],[1.058,-0.076]],"o":[[0.407,-0.187],[1.348,-1.539],[-1.058,0.076]],"v":[[4.224,-10.251],[128.305,-2.239],[4.378,-10.421]],"c":true}]},{"t":100,"s":[{"i":[[-0.008,0.004],[-0.839,0.958],[0.02,-0.001]],"o":[[0.008,-0.004],[1.348,-1.539],[-0.02,0.001]],"v":[[238.573,10.405],[253.015,12.369],[238.576,10.401]],"c":true}]}]},"nm":"Path 9","hd":false},{"ind":9,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-19.702,-2.401],[-2.482,3.154],[14.29,0.398]],"o":[[19.702,2.401],[2.595,-3.297],[-14.29,-0.398]],"v":[[9.698,-12.307],[64.597,6.088],[9.395,-13.184]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-53.298,-6.494],[-2.482,3.154],[38.657,1.077]],"o":[[53.298,6.494],[2.595,-3.297],[-38.657,-1.077]],"v":[[21.439,-3.705],[93.137,16.968],[20.619,-6.076]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-1.702,-0.207],[-2.482,3.154],[1.234,0.034]],"o":[[1.702,0.207],[2.595,-3.297],[-1.234,-0.034]],"v":[[3.71,-9.493],[135.812,35.216],[3.683,-9.569]],"c":true}]},{"t":100,"s":[{"i":[[-0.032,-0.004],[-2.482,3.154],[0.023,0.001]],"o":[[0.032,0.004],[2.595,-3.297],[-0.023,-0.001]],"v":[[196.573,63.406],[241.637,80.468],[196.573,63.405]],"c":true}]}]},"nm":"Path 10","hd":false},{"ind":10,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-9.253,-3.386],[-5.064,4.293],[16.792,8.51]],"o":[[9.253,3.386],[4.126,-3.498],[-16.792,-8.51]],"v":[[11.313,-9.614],[54.159,17.809],[9.698,-11.992]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-25.032,-9.16],[-5.064,4.293],[45.426,23.022]],"o":[[25.032,9.16],[4.126,-3.498],[-45.426,-23.022]],"v":[[25.808,2.729],[103.197,50.764],[21.439,-3.705]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.799,-0.292],[-5.064,4.293],[1.451,0.735]],"o":[[0.799,0.292],[4.126,-3.498],[-1.451,-0.735]],"v":[[3.881,-8.702],[137.251,71.886],[3.742,-8.908]],"c":true}]},{"t":100,"s":[{"i":[[-0.015,-0.006],[-5.064,4.293],[0.027,0.014]],"o":[[0.015,0.006],[4.126,-3.498],[-0.027,-0.014]],"v":[[199.076,108.91],[221.697,124.264],[199.073,108.906]],"c":true}]}]},"nm":"Path 11","hd":false},{"ind":11,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-6.526,-3.215],[-3.502,3.162],[9.671,6.481]],"o":[[6.526,3.215],[3.502,-3.162],[-9.671,-6.481]],"v":[[10.491,-8.994],[73.057,40.569],[11.313,-9.929]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-17.653,-8.697],[-3.502,3.162],[26.163,17.532]],"o":[[17.653,8.697],[3.502,-3.162],[-26.163,-17.532]],"v":[[23.585,5.258],[96.236,63.117],[25.808,2.729]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.564,-0.278],[-3.502,3.162],[0.835,0.56]],"o":[[0.564,0.278],[3.502,-3.162],[-0.835,-0.56]],"v":[[3.102,-8.57],[126.267,90.274],[3.173,-8.651]],"c":true}]},{"t":100,"s":[{"i":[[-0.011,-0.005],[-3.502,3.162],[0.016,0.011]],"o":[[0.011,0.005],[3.502,-3.162],[-0.016,-0.011]],"v":[[144.075,112.911],[200.736,157.617],[144.076,112.91]],"c":true}]}]},"nm":"Path 12","hd":false},{"ind":12,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-13.475,-10.482],[-4.458,3.463],[10.073,7.849]],"o":[[13.475,10.482],[4.458,-3.463],[-10.073,-7.849]],"v":[[9.502,-7.951],[52.87,45.892],[10.491,-8.678]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-36.453,-28.355],[-4.458,3.463],[27.249,21.233]],"o":[[36.453,28.355],[4.458,-3.463],[-27.249,-21.233]],"v":[[20.909,7.226],[94.655,93.669],[23.585,5.258]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-1.164,-0.905],[-4.458,3.463],[0.87,0.678]],"o":[[1.164,0.905],[4.458,-3.463],[-0.87,-0.678]],"v":[[3.049,-7.986],[116.926,120.825],[3.134,-8.049]],"c":true}]},{"t":100,"s":[{"i":[[-0.022,-0.017],[-4.458,3.463],[0.016,0.013]],"o":[[0.022,0.017],[4.458,-3.463],[-0.016,-0.013]],"v":[[146.573,153.413],[172.155,188.169],[146.575,153.411]],"c":true}]}]},"nm":"Path 13","hd":false},{"ind":13,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-4.775,-3.087],[-3.488,2.449],[10.229,14.193]],"o":[[4.775,3.087],[3.488,-2.449],[-10.229,-14.193]],"v":[[7.167,-8.135],[48.091,56.332],[9.187,-7.951]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-12.917,-8.352],[-3.488,2.449],[27.671,38.395]],"o":[[12.917,8.352],[3.488,-2.449],[-27.671,-38.395]],"v":[[15.445,6.729],[62.124,74.78],[20.909,7.226]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.412,-0.267],[-3.488,2.449],[0.884,1.226]],"o":[[0.412,0.267],[3.488,-2.449],[-0.884,-1.226]],"v":[[2.321,-8.079],[84.827,113.576],[2.495,-8.063]],"c":true}]},{"t":100,"s":[{"i":[[-0.008,-0.005],[-3.488,2.449],[0.017,0.023]],"o":[[0.008,0.005],[3.488,-2.449],[-0.017,-0.023]],"v":[[103.57,147.412],[141.124,209.78],[103.573,147.413]],"c":true}]}]},"nm":"Path 14","hd":false},{"ind":14,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-5.362,-6.334],[-2.659,0.857],[3.694,6.735]],"o":[[5.361,6.334],[6.495,-2.093],[-3.694,-6.735]],"v":[[5.102,-9.295],[26.477,35.494],[7.167,-8.45]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-14.504,-17.136],[-2.659,0.857],[9.994,18.219]],"o":[[14.504,17.136],[6.495,-2.093],[-9.994,-18.219]],"v":[[9.859,4.445],[41.772,64.349],[15.445,6.729]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.463,-0.547],[-2.659,0.857],[0.319,0.582]],"o":[[0.463,0.547],[6.495,-2.093],[-0.319,-0.582]],"v":[[2.027,-7.553],[61.313,111.91],[2.205,-7.48]],"c":true}]},{"t":100,"s":[{"i":[[-0.009,-0.01],[-2.659,0.857],[0.006,0.011]],"o":[[0.009,0.01],[6.495,-2.093],[-0.006,-0.011]],"v":[[94.566,193.911],[109.772,229.849],[94.57,193.912]],"c":true}]}]},"nm":"Path 15","hd":false},{"ind":15,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-0.43,-4.038],[-5.792,0.94],[0.557,8.126]],"o":[[0.43,4.038],[3.71,-0.602],[-0.557,-8.126]],"v":[[3.69,-10.876],[17.077,55.965],[5.102,-9.295]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-1.164,-10.924],[-5.792,0.94],[1.507,21.981]],"o":[[1.164,10.924],[3.71,-0.602],[-1.507,-21.981]],"v":[[6.039,0.167],[22.28,71.891],[9.859,4.445]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.037,-0.349],[-5.792,0.94],[0.048,0.702]],"o":[[0.037,0.349],[3.71,-0.602],[-0.048,-0.702]],"v":[[1.242,-7.799],[31.62,122.9],[1.364,-7.663]],"c":true}]},{"t":100,"s":[{"i":[[-0.001,-0.007],[-5.792,0.94],[0.001,0.013]],"o":[[0.001,0.007],[3.71,-0.602],[-0.001,-0.013]],"v":[[43.064,185.408],[54.78,249.391],[43.066,185.411]],"c":true}]}]},"nm":"Path 16","hd":false},{"ind":16,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.268,-1.072],[-8.766,2.77],[-1.453,20.282]],"o":[[-0.268,1.072],[8.956,-2.83],[1.453,-20.282]],"v":[[2.258,-8.649],[11.352,48.031],[4.005,-10.876]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[0.726,-2.901],[-8.766,2.77],[-3.93,54.866]],"o":[[-0.726,2.901],[8.956,-2.83],[3.93,-54.866]],"v":[[1.312,6.192],[20.655,115.675],[6.039,0.167]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.023,-0.093],[-8.766,2.77],[-0.125,1.752]],"o":[[-0.023,0.093],[8.956,-2.83],[0.125,-1.752]],"v":[[0.956,-7.06],[24.966,155.333],[1.106,-7.252]],"c":true}]},{"t":100,"s":[{"i":[[0,-0.002],[-8.766,2.77],[-0.002,0.033]],"o":[[0,0.002],[8.956,-2.83],[0.002,-0.033]],"v":[[32.561,227.912],[35.655,253.675],[32.564,227.908]],"c":true}]}]},"nm":"Path 17","hd":false},{"ind":17,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.674,-5.118],[-9.34,-10.304],[-5.656,20.469]],"o":[[-0.674,5.118],[5.335,5.886],[5.656,-20.469]],"v":[[0.319,-7.862],[-7.754,79.004],[2.258,-8.649]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[1.822,-13.845],[-9.34,-10.304],[-15.299,55.372]],"o":[[-1.822,13.845],[5.335,5.886],[15.299,-55.372]],"v":[[-3.933,8.322],[-10.119,142.549],[1.312,6.192]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.058,-0.442],[-9.34,-10.304],[-0.489,1.768]],"o":[[-0.058,0.442],[5.335,5.886],[0.489,-1.768]],"v":[[0.157,-7.436],[-14.573,174.16],[0.325,-7.504]],"c":true}]},{"t":100,"s":[{"i":[[0.001,-0.008],[-9.34,-10.304],[-0.009,0.033]],"o":[[-0.001,0.008],[5.335,5.886],[0.009,-0.033]],"v":[[-16.442,193.413],[-25.619,252.549],[-16.439,193.412]],"c":true}]}]},"nm":"Path 18","hd":false},{"ind":18,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[1.134,-4.491],[-8.529,-8.273],[-6.696,18.034]],"o":[[-1.134,4.491],[3.166,3.071],[6.696,-18.034]],"v":[[-1.761,-9.64],[-13.598,51.106],[0.003,-7.546]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[3.068,-12.148],[-8.529,-8.273],[-18.113,48.786]],"o":[[-3.068,12.148],[3.166,3.071],[18.113,-48.786]],"v":[[-8.707,2.658],[-24.162,110.866],[-3.933,8.322]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.098,-0.388],[-8.529,-8.273],[-0.578,1.558]],"o":[[-0.098,0.388],[3.166,3.071],[0.578,-1.558]],"v":[[-0.446,-7.378],[-34.795,149.374],[-0.293,-7.198]],"c":true}]},{"t":100,"s":[{"i":[[0.002,-0.007],[-8.529,-8.273],[-0.011,0.029]],"o":[[-0.002,0.007],[3.166,3.071],[0.011,-0.029]],"v":[[-51.445,211.91],[-61.162,244.866],[-51.442,211.913]],"c":true}]}]},"nm":"Path 19","hd":false},{"ind":19,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[9.217,-13.161],[-2.977,-4.918],[-5.725,7.174]],"o":[[-9.217,13.161],[1.721,2.843],[5.725,-7.174]],"v":[[-1.154,-12.499],[-42.53,56.34],[-1.761,-9.64]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[24.934,-35.602],[-2.977,-4.918],[-15.488,19.406]],"o":[[-24.934,35.602],[1.721,2.843],[15.488,-19.406]],"v":[[-7.064,-5.076],[-47.418,71.95],[-8.707,2.658]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.796,-1.137],[-2.977,-4.918],[-0.495,0.62]],"o":[[-0.796,1.137],[1.721,2.843],[0.495,-0.62]],"v":[[-0.876,-8.404],[-71.414,113.188],[-0.929,-8.157]],"c":true}]},{"t":100,"s":[{"i":[[0.015,-0.021],[-2.977,-4.918],[-0.009,0.012]],"o":[[-0.015,0.021],[1.721,2.843],[0.009,-0.012]],"v":[[-88.944,151.405],[-130.918,215.45],[-88.945,151.41]],"c":true}]}]},"nm":"Path 20","hd":false},{"ind":20,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[7.626,-7.899],[-8.084,-5.313],[-13.235,7.936]],"o":[[-2.855,2.957],[3.306,2.173],[10.979,-6.583]],"v":[[-4.169,-12.723],[-45.434,37.243],[-0.839,-12.499]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[20.63,-21.369],[-8.084,-5.313],[-35.802,21.467]],"o":[[-7.724,8],[3.306,2.173],[29.699,-17.808]],"v":[[-16.072,-5.684],[-88.796,85.335],[-7.064,-5.076]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.659,-0.682],[-8.084,-5.313],[-1.143,0.686]],"o":[[-0.247,0.255],[3.306,2.173],[0.948,-0.569]],"v":[[-1.962,-8.308],[-113.223,114.36],[-1.674,-8.288]],"c":true}]},{"t":100,"s":[{"i":[[0.012,-0.013],[-8.084,-5.313],[-0.022,0.013]],"o":[[-0.005,0.005],[3.306,2.173],[0.018,-0.011]],"v":[[-150.949,160.405],[-173.796,186.335],[-150.944,160.405]],"c":true}]}]},"nm":"Path 21","hd":false},{"ind":21,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[14.117,-10.21],[-5.628,-14.818],[-24.822,15.796]],"o":[[-14.086,10.188],[3.081,4.733],[21.924,-13.951]],"v":[[-3.765,-15.075],[-82.448,63.651],[-4.484,-12.723]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[38.189,-27.621],[0.797,-25.979],[-67.149,42.73]],"o":[[-38.106,27.561],[-0.163,5.311],[59.307,-37.74]],"v":[[-14.127,-12.046],[-117.611,102.283],[-16.072,-5.684]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[1.219,-0.882],[-2.13,-20.894],[-2.144,1.364]],"o":[[-1.217,0.88],[1.315,5.048],[1.894,-1.205]],"v":[[-1.552,-9.148],[-137.871,121.393],[-1.614,-8.945]],"c":true}]},{"t":100,"s":[{"i":[[0.023,-0.017],[-9.389,-8.283],[-0.04,0.026]],"o":[[-0.023,0.017],[4.981,4.394],[0.036,-0.023]],"v":[[-123.948,110.901],[-188.111,168.783],[-123.949,110.905]],"c":true}]}]},"nm":"Path 22","hd":false},{"ind":22,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[2.794,-1.349],[0.656,-8.742],[-9.144,3.299]],"o":[[-0.388,0.187],[-0.279,3.717],[9.144,-3.298]],"v":[[-4.357,-15.318],[-39.206,7.131],[-3.283,-15.062]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[7.56,-3.648],[1.507,-8.661],[-24.737,8.919]],"o":[[-1.051,0.507],[-0.641,3.682],[24.737,-8.919]],"v":[[-17.032,-12.737],[-65.957,17.111],[-14.127,-12.046]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.241,-0.117],[1.12,-8.698],[-0.79,0.285]],"o":[[-0.034,0.016],[-0.476,3.698],[0.79,-0.285]],"v":[[-2.581,-9.26],[-111.172,47.264],[-2.489,-9.238]],"c":true}]},{"t":100,"s":[{"i":[[0.004,-0.003],[0.158,-8.79],[-0.014,0.008]],"o":[[-0.001,0],[-0.067,3.737],[0.014,-0.008]],"v":[[-196.685,103.922],[-223.294,122.037],[-196.683,103.922]],"c":true}]}]},"nm":"Path 23","hd":false},{"ind":23,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.652,-0.002],[9.621,-23.195],[-30.797,11.034]],"o":[[-0.652,0.002],[1.201,4.343],[30.797,-11.034]],"v":[[-5.938,-16.534],[-91.485,36.162],[-4.524,-15.646]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[1.764,-0.005],[34.94,-49.93],[-83.311,29.848]],"o":[[-1.764,0.005],[-2.448,3.499],[83.311,-29.848]],"v":[[-20.857,-15.139],[-161.337,74.163],[-17.032,-12.737]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.056,0],[23.404,-37.749],[-2.66,0.953]],"o":[[-0.056,0],[-0.786,3.883],[2.66,-0.953]],"v":[[-2.437,-9.678],[-180.879,84.077],[-2.314,-9.602]],"c":true}]},{"t":100,"s":[{"i":[[0.001,0],[-5.203,-7.542],[-0.05,0.018]],"o":[[-0.001,0],[3.337,4.837],[0.05,-0.018]],"v":[[-175.952,77.399],[-229.337,108.663],[-175.95,77.401]],"c":true}]}]},"nm":"Path 24","hd":false},{"ind":24,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[3.079,-0.51],[0.18,-8.491],[-14.645,0.239]],"o":[[-3.079,0.51],[-0.099,4.7],[14.645,-0.239]],"v":[[-8.996,-17.288],[-60.714,-4.682],[-6.253,-16.534]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[8.33,-1.38],[0.18,-8.491],[-39.617,0.647]],"o":[[-8.33,1.38],[-0.099,4.7],[39.617,-0.647]],"v":[[-28.279,-17.178],[-119.213,6.829],[-20.857,-15.139]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.266,-0.044],[0.18,-8.491],[-1.265,0.021]],"o":[[-0.266,0.044],[-0.099,4.7],[1.265,-0.021]],"v":[[-3.288,-10.423],[-157.506,13.654],[-3.051,-10.357]],"c":true}]},{"t":100,"s":[{"i":[[0.005,-0.001],[0.18,-8.491],[-0.024,0]],"o":[[-0.005,0.001],[-0.099,4.7],[0.024,0]],"v":[[-223.707,24.648],[-252.463,30.579],[-223.702,24.649]],"c":true}]}]},"nm":"Path 25","hd":false},{"ind":25,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[5.002,-0.018],[4.376,-11.742],[-21.034,-0.505]],"o":[[-5.002,0.018],[-1,2.682],[21.034,0.505]],"v":[[-8.482,-18.09],[-80.041,-5.651],[-8.996,-16.972]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[13.533,-0.05],[4.376,-11.742],[-56.9,-1.366]],"o":[[-13.533,0.05],[-1,2.682],[56.9,1.366]],"v":[[-26.888,-20.202],[-133.179,-1.393],[-28.279,-17.178]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.432,-0.002],[4.376,-11.742],[-1.817,-0.044]],"o":[[-0.432,0.002],[-1,2.682],[1.817,0.044]],"v":[[-2.957,-10.802],[-167.52,2.271],[-3.002,-10.706]],"c":true}]},{"t":100,"s":[{"i":[[0.008,0],[4.376,-11.742],[-0.034,-0.001]],"o":[[-0.008,0],[-1,2.682],[0.034,0.001]],"v":[[-201.456,2.646],[-252.679,11.357],[-201.457,2.648]],"c":true}]}]},"nm":"Path 26","hd":false},{"ind":26,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[4.015,0.648],[9.285,-5.938],[-12.687,-1.577]],"o":[[-1.837,-0.296],[-5.457,3.49],[12.687,1.577]],"v":[[-7.193,-19.109],[-55.539,-18.566],[-8.167,-18.09]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[10.862,1.752],[9.285,-5.938],[-34.32,-4.266]],"o":[[-4.969,-0.802],[-5.457,3.49],[34.32,4.266]],"v":[[-24.253,-22.959],[-119.714,-21.877],[-26.888,-20.202]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.347,0.056],[9.285,-5.938],[-1.096,-0.136]],"o":[[-0.159,-0.026],[-5.457,3.49],[1.096,0.136]],"v":[[-3.256,-11.254],[-157.719,-22.883],[-3.34,-11.166]],"c":true}]},{"t":100,"s":[{"i":[[0.007,0.001],[9.285,-5.938],[-0.021,-0.003]],"o":[[-0.003,0],[-5.457,3.49],[0.021,0.003]],"v":[[-231.204,-25.606],[-251.964,-25.377],[-231.206,-25.604]],"c":true}]}]},"nm":"Path 27","hd":false},{"ind":27,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[4.393,1.499],[5.601,-5.319],[-6.035,-3.074]],"o":[[-4.393,-1.499],[-2.043,1.94],[7.22,3.678]],"v":[[-5.608,-19.397],[-48.68,-27.903],[-7.508,-19.109]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[11.885,4.055],[5.601,-5.319],[-16.326,-8.316]],"o":[[-11.885,-4.055],[-2.043,1.94],[19.531,9.948]],"v":[[-19.113,-23.736],[-77.851,-34.052],[-24.253,-22.959]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.38,0.13],[5.601,-5.319],[-0.521,-0.266]],"o":[[-0.38,-0.129],[-2.043,1.94],[0.624,0.318]],"v":[[-2.828,-11.723],[-125.77,-43.679],[-2.992,-11.698]],"c":true}]},{"t":100,"s":[{"i":[[0.007,0.002],[5.601,-5.319],[-0.01,-0.005]],"o":[[-0.007,-0.002],[-2.043,1.94],[0.012,0.006]],"v":[[-210.701,-60.106],[-244.601,-67.552],[-210.704,-60.106]],"c":true}]}]},"nm":"Path 28","hd":false},{"ind":28,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[5.221,2.81],[2.362,-11.3],[-20.382,-17.294]],"o":[[-5.221,-2.81],[-1.358,6.576],[20.382,17.294]],"v":[[-7.193,-23.937],[-71.1,-48.972],[-5.293,-19.397]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[14.123,7.602],[4.156,-15.533],[-55.138,-46.784]],"o":[[-14.123,-7.602],[-2.317,8.659],[55.138,46.784]],"v":[[-24.253,-36.018],[-168.703,-94.857],[-19.113,-23.736]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.451,0.243],[3.339,-13.604],[-1.761,-1.494]],"o":[[-0.451,-0.243],[-1.88,7.71],[1.761,1.494]],"v":[[-2.925,-12.804],[-184.078,-101.754],[-2.761,-12.412]],"c":true}]},{"t":100,"s":[{"i":[[0.008,0.005],[1.312,-8.822],[-0.033,-0.028]],"o":[[-0.008,-0.005],[-0.797,5.357],[0.033,0.028]],"v":[[-205.454,-113.613],[-222.203,-118.857],[-205.451,-113.606]],"c":true}]}]},"nm":"Path 29","hd":false},{"ind":29,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[7.26,4.445],[7.011,-4.124],[-10.385,-10.766]],"o":[[-0.326,-0.199],[-7.011,4.124],[16.897,17.517]],"v":[[-5.801,-24.959],[-56.419,-56.973],[-7.193,-23.937]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[19.64,12.023],[7.011,-4.124],[-28.094,-29.123]],"o":[[-0.881,-0.54],[-7.011,4.124],[45.711,47.385]],"v":[[-20.488,-38.784],[-98.519,-86.459],[-24.253,-36.018]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.627,0.384],[7.011,-4.124],[-0.897,-0.93]],"o":[[-0.028,-0.017],[-7.011,4.124],[1.46,1.513]],"v":[[-2.277,-13.13],[-127.401,-106.145],[-2.397,-13.042]],"c":true}]},{"t":100,"s":[{"i":[[0.012,0.007],[7.011,-4.124],[-0.017,-0.018]],"o":[[-0.001,0],[-7.011,4.124],[0.028,0.029]],"v":[[-164.452,-132.115],[-199.019,-154.959],[-164.454,-132.113]],"c":true}]}]},"nm":"Path 30","hd":false},{"ind":30,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[23.866,20.95],[-0.775,-4.182],[-8.843,-10.116]],"o":[[-23.866,-20.95],[0.799,4.308],[8.843,10.116]],"v":[[-4.127,-25.389],[-47.359,-57.266],[-6.116,-25.275]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[64.562,56.675],[-0.775,-4.182],[-23.921,-27.366]],"o":[[-64.562,-56.675],[0.799,4.308],[23.921,27.366]],"v":[[-15.108,-39.093],[-105.227,-105.674],[-20.488,-38.784]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[2.062,1.81],[-0.775,-4.182],[-0.764,-0.874]],"o":[[-2.062,-1.81],[0.799,4.308],[0.764,0.874]],"v":[[-2.169,-13.514],[-128.217,-124.928],[-2.341,-13.504]],"c":true}]},{"t":100,"s":[{"i":[[0.039,0.034],[-0.775,-4.182],[-0.014,-0.016]],"o":[[-0.039,-0.034],[0.799,4.308],[0.014,0.016]],"v":[[-169.449,-161.115],[-185.227,-172.674],[-169.452,-161.115]],"c":true}]}]},"nm":"Path 31","hd":false},{"ind":31,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.861,0.835],[6.082,-5.676],[-10.296,-14.684]],"o":[[-0.861,-0.835],[-2.893,2.699],[10.296,14.684]],"v":[[-0.366,-23.104],[-55.133,-75.787],[-4.127,-25.389]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[2.329,2.258],[6.082,-5.676],[-27.852,-39.724]],"o":[[-2.329,-2.258],[-2.893,2.699],[27.852,39.724]],"v":[[-4.933,-32.911],[-79.889,-105.589],[-15.108,-39.093]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.074,0.072],[6.082,-5.676],[-0.889,-1.268]],"o":[[-0.074,-0.072],[-2.893,2.699],[0.889,1.268]],"v":[[-1.214,-13.207],[-103.885,-130.878],[-1.539,-13.404]],"c":true}]},{"t":100,"s":[{"i":[[0.001,0.001],[6.082,-5.676],[-0.017,-0.024]],"o":[[-0.001,-0.001],[-2.893,2.699],[0.017,0.024]],"v":[[-120.443,-152.612],[-163.389,-193.589],[-120.449,-152.615]],"c":true}]}]},"nm":"Path 32","hd":false},{"ind":32,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[3.542,19.181],[3.073,-0.981],[-0.152,-9.722]],"o":[[-3.542,-19.181],[-2.453,0.783],[0.152,9.722]],"v":[[0.539,-22.899],[-14.028,-61.928],[-0.366,-22.788]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[9.581,51.887],[3.073,-0.981],[-0.412,-26.3]],"o":[[-9.581,-51.887],[-2.453,0.783],[0.412,26.3]],"v":[[-2.483,-33.211],[-31.215,-116.327],[-4.933,-32.911]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.306,1.657],[3.073,-0.981],[-0.013,-0.84]],"o":[[-0.306,-1.657],[-2.453,0.783],[0.013,0.84]],"v":[[-0.517,-14.182],[-44.865,-152.105],[-0.596,-14.172]],"c":true}]},{"t":100,"s":[{"i":[[0.006,0.031],[3.073,-0.981],[0,-0.016]],"o":[[-0.006,-0.031],[-2.453,0.783],[0,0.016]],"v":[[-72.441,-227.612],[-78.715,-240.827],[-72.443,-227.612]],"c":true}]}]},"nm":"Path 33","hd":false},{"ind":33,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.512,3.311],[7.408,-2.515],[0.879,-9.578]],"o":[[-0.512,-3.311],[-4.274,1.451],[-0.879,9.578]],"v":[[1.755,-24.526],[-15.758,-89.049],[0.539,-22.899]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[1.386,8.956],[7.408,-2.515],[2.377,-25.91]],"o":[[-1.386,-8.956],[-4.274,1.451],[-2.377,25.91]],"v":[[0.805,-37.612],[-23.169,-137.772],[-2.483,-33.211]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.044,0.286],[7.408,-2.515],[0.076,-0.827]],"o":[[-0.044,-0.286],[-4.274,1.451],[-0.076,0.827]],"v":[[0.051,-14.02],[-30.928,-169.383],[-0.054,-13.879]],"c":true}]},{"t":100,"s":[{"i":[[0.001,0.005],[7.408,-2.515],[0.001,-0.016]],"o":[[-0.001,-0.005],[-4.274,1.451],[-0.001,0.016]],"v":[[-36.439,-204.114],[-50.169,-247.772],[-36.441,-204.112]],"c":true}]}]},"nm":"Path 34","hd":false},{"ind":34,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[1.275,0.477],[4.481,1.632],[2.961,-6.55],[-1.425,0.478]],"o":[[-2.55,-0.954],[-6.936,-2.526],[-1.48,3.275],[1.425,-0.478]],"v":[[5.014,-23.954],[2.623,-65.932],[2.07,-24.842],[3.754,-13.991]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[3.449,1.291],[4.481,1.632],[8.009,-17.719],[-3.855,1.292]],"o":[[-6.899,-2.581],[-6.936,-2.526],[-4.004,8.859],[3.855,-1.292]],"v":[[8.769,-35.211],[4.357,-117.808],[0.805,-37.612],[5.361,-8.261]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.11,0.041],[4.481,1.632],[0.256,-0.566],[-0.123,0.041]],"o":[[-0.22,-0.082],[-6.936,-2.526],[-0.128,0.283],[0.123,-0.041]],"v":[[0.788,-14.342],[3.064,-156.748],[0.534,-14.419],[0.679,-13.482]],"c":true}]},{"t":100,"s":[{"i":[[0.002,0.001],[4.481,1.632],[0.005,-0.011],[-0.002,0.001]],"o":[[-0.004,-0.002],[-6.936,-2.526],[-0.002,0.005],[0.002,-0.001]],"v":[[1.066,-235.113],[-0.143,-253.308],[1.061,-235.114],[1.064,-235.097]],"c":true}]}]},"nm":"Path 35","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":7,"k":{"a":0,"k":[0.166,1,1,1,0.291,1,0.894,0.947,0.379,1,0.788,0.894,0.522,0.961,0.394,0.72,0.708,0.922,0,0.545,0.865,0.961,0.206,0.508,0.999,1,0.412,0.471,0.166,1,0.291,1,0.379,1,0.522,0.9,0.708,0.8,0.865,0.8,0.999,0.8]}},"s":{"a":0,"k":[-12,-18]},"e":{"a":0,"k":[-123.596,-129.596]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-6.793,2.506]},"a":{"a":0,"k":[-6.793,2.506]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":-4,"op":99,"st":-65,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"white splashes precomp","refId":"comp_2","sr":1,"ks":{"p":{"a":0,"k":[243,243,0]},"a":{"a":0,"k":[243,243,0]}},"ao":0,"w":486,"h":486,"ip":-4,"op":99,"st":-5,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"ogonek5","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[1],"y":[0]},"t":30,"s":[100]},{"t":45,"s":[0]}]},"p":{"a":0,"k":[241,232.25,0]},"a":{"a":0,"k":[-51,43,0]},"s":{"a":1,"k":[{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,15.828]},"t":30,"s":[0,0,100]},{"t":45,"s":[151,151,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[164,164]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":7,"k":{"a":0,"k":[0.166,1,1,1,0.291,1,0.894,0.947,0.379,1,0.788,0.894,0.522,0.961,0.394,0.72,0.708,0.922,0,0.545,0.865,0.961,0.206,0.508,0.999,1,0.412,0.471,0.166,1,0.291,1,0.379,1,0.522,0.9,0.708,0.8,0.865,0.8,0.999,0.8]}},"s":{"a":0,"k":[1,0.5]},"e":{"a":0,"k":[-61.596,-53.096]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 9","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,43]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":30,"op":45,"st":-45,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"rose splashes 80%","sr":1,"ks":{"o":{"a":0,"k":80},"p":{"a":0,"k":[283.287,244.6,0]},"a":{"a":0,"k":[40.287,1.6,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[8.458,2.584],[-1.178,1.927]],"o":[[0,0],[1.178,-1.927]],"v":[[47.396,-2.495],[66.132,8.305]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[17.915,5.472],[-2.495,4.082]],"o":[[0,0],[2.494,-4.082]],"v":[[127.416,14.655],[167.1,37.528]],"c":true}]},{"t":100,"s":[{"i":[[2.922,0.892],[-0.407,0.666]],"o":[[0,0],[0.407,-0.666]],"v":[[231.544,79.714],[238.016,83.445]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-7.371,11.75],[-2.605,-2.954]],"o":[[0,0],[2.605,2.954]],"v":[[-19.417,21.61],[-33.613,50.565]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-16.511,26.322],[-5.835,-6.617]],"o":[[0,0],[5.835,6.617]],"v":[[-54.289,78.774],[-86.09,143.637]],"c":true}]},{"t":100,"s":[{"i":[[-2.019,3.219],[-0.714,-0.809]],"o":[[0,0],[0.714,0.809]],"v":[[-120,208.391],[-123.889,216.322]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[4.272,18.752],[3.489,0]],"o":[[0,0],[-2.919,0]],"v":[[13.337,36.564],[18.266,63.997]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[9.5,41.701],[7.758,0]],"o":[[0,0],[-6.491,0]],"v":[[34.44,120.578],[45.401,181.583]],"c":true}]},{"t":100,"s":[{"i":[[1.211,5.316],[0.989,0]],"o":[[0,0],[-0.827,0]],"v":[[49.981,239.625],[51.378,247.402]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[5.357,10.649],[2.065,-1.671]],"o":[[0,0],[-2.092,1.693]],"v":[[27.785,38.344],[37.752,60.836]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[12.682,25.21],[4.889,-3.955]],"o":[[0,0],[-4.953,4.007]],"v":[[73.965,124.33],[97.561,177.576]],"c":true}]},{"t":100,"s":[{"i":[[1.068,2.124],[0.412,-0.333]],"o":[[0,0],[-0.417,0.338]],"v":[[112.746,219.25],[114.734,223.735]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[7.125,6.764],[2.818,-3.288]],"o":[[0,0],[-1.952,2.278]],"v":[[39.186,19.394],[56.431,42.171]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[16.508,15.671],[6.529,-7.617]],"o":[[0,0],[-4.523,5.277]],"v":[[103.804,73.868],[143.76,126.64]],"c":true}]},{"t":100,"s":[{"i":[[1.631,1.548],[0.645,-0.753]],"o":[[0,0],[-0.447,0.521]],"v":[[178.352,168.75],[182.3,173.964]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":5,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[14.003,-18.483],[-2.995,-2.598]],"o":[[0,0],[2.346,2.035]],"v":[[19.896,-54.149],[37.88,-77.508]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[31.33,-41.355],[-6.701,-5.813]],"o":[[0,0],[5.249,4.553]],"v":[[52.828,-125.71],[93.066,-177.974]],"c":true}]},{"t":100,"s":[{"i":[[3.858,-5.092],[-0.825,-0.716]],"o":[[0,0],[0.646,0.561]],"v":[[119.615,-214],[124.57,-220.435]],"c":true}]}]},"nm":"Path 6","hd":false},{"ind":6,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-6.092,-13.223],[3.225,-0.461]],"o":[[0,0],[-3.225,0.461]],"v":[[-11.7,-53.501],[-23.447,-74.576]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-13.694,-29.721],[7.248,-1.036]],"o":[[0,0],[-7.248,1.036]],"v":[[-33.744,-123.528],[-60.149,-170.902]],"c":true}]},{"t":100,"s":[{"i":[[-1.641,-3.562],[0.869,-0.124]],"o":[[0,0],[-0.869,0.124]],"v":[[-92.793,-229.25],[-95.958,-234.928]],"c":true}]}]},"nm":"Path 7","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.403921574354,0.454901963472,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 7","hd":false},{"ty":"tr","p":{"a":0,"k":[40.287,1.6]},"a":{"a":0,"k":[40.287,1.6]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":-4,"op":99,"st":-65,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"rose splashes 40%","sr":1,"ks":{"o":{"a":0,"k":40},"p":{"a":0,"k":[255.462,250.532,0]},"a":{"a":0,"k":[12.462,7.532,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[2.498,-1.717],[-2.802,-3.967]],"o":[[-1.32,0.907],[0,0]],"v":[[-27.909,-69.864],[-23.485,-62.918]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[5.583,-3.836],[-6.261,-8.864]],"o":[[-2.949,2.026],[0,0]],"v":[[-77.365,-168.395],[-67.479,-152.872]],"c":true}]},{"t":100,"s":[{"i":[[0.692,-0.476],[-0.776,-1.099]],"o":[[-0.366,0.251],[0,0]],"v":[[-104.452,-204.425],[-103.226,-202.5]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-4.758,-1.202],[3.089,-1.172]],"o":[[0,0],[-3.089,1.172]],"v":[[-40.584,-41.866],[-51.165,-45.778]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-10.775,-2.722],[6.996,-2.654]],"o":[[0,0],[-6.996,2.654]],"v":[[-111.836,-92.023],[-135.799,-100.881]],"c":true}]},{"t":100,"s":[{"i":[[-1.235,-0.312],[0.802,-0.304]],"o":[[0,0],[-0.802,0.304]],"v":[[-194.867,-116.25],[-197.612,-117.265]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-6.418,17.711],[-4.713,-1.091]],"o":[[0,0],[2.758,0.638]],"v":[[-22.297,35.041],[-30.452,64.628]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-14.835,40.938],[-10.895,-2.522]],"o":[[0,0],[6.375,1.475]],"v":[[-62.088,115.52],[-80.938,183.907]],"c":true}]},{"t":100,"s":[{"i":[[-1.49,4.112],[-1.094,-0.253]],"o":[[0,0],[0.64,0.148]],"v":[[-89,203.172],[-90.893,210.041]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[1.125,2.009],[-1.205,-2.41]],"o":[[-0.644,-1.15],[1.205,2.41]],"v":[[60.208,34.488],[58.28,36.577]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[2.266,4.046],[-2.428,-4.856]],"o":[[-1.297,-2.316],[2.428,4.856]],"v":[[161.46,114.372],[157.576,118.58]],"c":true}]},{"t":100,"s":[{"i":[[0.457,0.815],[-0.489,-0.978]],"o":[[-0.261,-0.467],[0.489,0.978]],"v":[[178.425,143.467],[177.643,144.315]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.378,1.888],[-0.054,-1.294]],"o":[[-0.244,-1.218],[0.054,1.294]],"v":[[57.201,-37.513],[53.318,-36.704]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[0.721,3.602],[-0.103,-2.47]],"o":[[-0.465,-2.323],[0.103,2.47]],"v":[[152.987,-80.688],[145.577,-79.144]],"c":true}]},{"t":100,"s":[{"i":[[0.177,0.884],[-0.025,-0.606]],"o":[[-0.114,-0.57],[0.025,0.606]],"v":[[207.62,-96.485],[205.801,-96.106]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":5,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-0.033,1.484],[-0.082,-1.894]],"o":[[0.031,-1.392],[0.082,1.894]],"v":[[53.595,-30.294],[48.408,-29.669]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-0.074,3.328],[-0.185,-4.247]],"o":[[0.069,-3.121],[0.185,4.247]],"v":[[143.792,-61.221],[132.159,-59.821]],"c":true}]},{"t":100,"s":[{"i":[[-0.009,0.404],[-0.022,-0.516]],"o":[[0.008,-0.379],[0.022,0.516]],"v":[[216.285,-76.436],[214.873,-76.266]],"c":true}]}]},"nm":"Path 6","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.403921574354,0.454901963472,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 6","hd":false},{"ty":"tr","p":{"a":0,"k":[12.462,7.532]},"a":{"a":0,"k":[12.462,7.532]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":-4,"op":99,"st":-65,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"violet","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[33.333]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[100]},{"t":70,"s":[0]}]},"p":{"a":0,"k":[250.172,221.61,0]},"a":{"a":0,"k":[7.172,-21.39,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.629,0.629,0.629],"y":[0,0,0]},"t":30,"s":[43.235,43.235,100]},{"t":40,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[8.424,-5.071],[2.653,-10.693],[8.493,-0.25],[-0.281,-21.575],[8.206,2.479],[-9.071,-18.319],[10.163,-0.826],[-8.504,-13.367],[7.925,-6.142],[-9.44,-13.344],[2.854,-8.942],[-21.641,-24.17],[6.753,-1.287],[-6.374,-4.022],[10.533,-2.098],[-13.32,-3.511],[-1.839,-6.483],[-21.075,-3.827],[-5.954,-6.816],[-11.932,-2.571],[3.257,-17.575],[-38.942,9.134],[-0.325,-7.706],[-8.742,1.73],[-8.801,-7.785],[-30.134,14.717],[-9.382,-4.129],[-16.279,6.661],[1.088,-8.515],[-7.687,7.097],[-6.012,-8.868],[-10.651,19.166],[-10.35,0.696],[-9.579,21.376],[-8.09,4.605],[-5.131,22.602],[-3.808,-4.758],[-0.748,8.961],[-7.254,-2.788],[2.909,7.968],[-7.635,-1.047],[9.08,17.754],[-7.507,3.193],[9.997,10.579],[-6.351,4.692],[9.781,8.968],[0.178,6.817],[17.454,12.482],[0.03,6.973],[12.265,1.257],[-5.162,4.439],[17.018,1.365],[-5.218,7.12],[24.328,-1.527],[-3.121,9.666],[14.883,-6.778],[10.864,5.993],[11.338,-8.563],[5.168,9.097],[9.336,-10.339],[5.422,9.555],[17.401,-17.591],[8.409,7.467],[2.648,-7.068],[8.104,5.903],[5.776,-17.779],[7.39,-3.513],[1.572,-7.178],[9.221,3.529],[4.343,-6.683]],"o":[[-7.125,5.075],[-0.627,-20.281],[-8.492,0.25],[-7.605,-11.419],[-6.68,-3.509],[-2.4,-2.857],[-8.37,0.053],[-2.455,-2.437],[-7.318,5.151],[-25.313,-18.266],[-0.866,7.957],[-15.111,-8.219],[-6.731,1.147],[0,0],[-8.775,1.322],[-36.623,-3.316],[2.35,6.725],[-4.284,0.119],[5.62,5.119],[0,0],[-0.325,7.953],[-14.317,6.526],[0.333,6.979],[-5.382,3.713],[7.32,4.607],[-4.424,3.372],[7.613,2.532],[-10.42,10.905],[-1.668,7.104],[-2.075,4.92],[4.11,6.36],[-1.424,4.903],[7.699,-1.121],[0,0],[8.176,-4.621],[-1.052,23.086],[4.14,4.652],[5.615,9.885],[7.25,2.786],[5.203,5.546],[7.441,1.093],[15.761,17.732],[7.507,-3.193],[5.904,3.732],[6.351,-4.692],[9.84,5.408],[-0.622,-6.552],[19.337,4.71],[0.329,-7.351],[6.418,-1.574],[5.14,-4.409],[27.522,-2.876],[5.215,-7.117],[4.426,-6.277],[2.875,-9.309],[1.399,-5.562],[-7.079,-5.34],[4.34,-9.001],[-5.002,-8.493],[13.638,-18.255],[-5.312,-9.225],[0.333,-7.822],[-7.414,-6.268],[3.497,-13.498],[-8.104,-5.903],[-0.973,-5.144],[-8.335,3.293],[-2.994,-1.674],[-9.108,-3.513],[-0.006,-3.457]],"v":[[5.228,-95.512],[7.462,-28.929],[-2.195,-87.293],[6.416,-28.961],[-31.261,-81.602],[2.547,-32.204],[-43.849,-81.375],[0.273,-32.419],[-44.643,-71.2],[-1.483,-31.501],[-68.757,-71.437],[-0.116,-26.024],[-42.303,-41.201],[-2.32,-26.028],[-60.656,-35.365],[-6.28,-25.891],[-67.569,-24.781],[-6.096,-24.791],[-61.84,-18.285],[-3.524,-23.253],[-83.167,12.168],[-3.472,-21.52],[-43.616,2.576],[-3.348,-19.306],[-64.141,31.565],[0.002,-18.262],[-50.895,27.819],[3.739,-17.431],[-29.038,27.111],[2.554,-14.294],[-16.457,46.001],[4.192,-11.614],[-10.643,61.853],[6.528,-12.171],[7.719,52.216],[8.895,-14.393],[13.275,34.112],[10.222,-12.355],[26.825,31.666],[12.417,-11.04],[35.999,36.579],[14.675,-10.481],[49.25,44.887],[15.923,-11.135],[54.41,30.884],[17.017,-12.053],[59.976,24.143],[15.598,-15.029],[60.134,6.365],[15.406,-16.076],[56.972,-8.479],[17.766,-17.986],[90.136,-9.191],[19.955,-19.743],[79.759,-35.208],[20.474,-22.64],[63.92,-44.118],[18.736,-24.334],[63.006,-55.052],[17.431,-26.232],[77.149,-67.057],[16.295,-27.947],[47.537,-73.383],[14.878,-29.037],[41.274,-77.714],[13.733,-29.91],[25.971,-72.901],[12.306,-29.047],[19.524,-85.462],[9.122,-30.564]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[8.849,0.01],[7.184,-28.957],[3.052,-1.273],[-0.761,-58.423],[7.839,-5.353],[-24.563,-49.607],[9.192,-3.975],[-23.029,-36.198],[4.861,-7.946],[-25.563,-36.135],[12.25,-6.125],[-58.602,-65.452],[0.423,-3.502],[-17.26,-10.89],[11.497,-5.096],[-36.071,-9.509],[-2.201,-1.041],[-57.07,-10.364],[-1.998,-10.013],[-32.312,-6.962],[9.36,-30.715],[-105.454,24.735],[0.047,-4.178],[-23.672,4.686],[-7.461,-15.968],[-81.601,39.854],[-8.157,-7.347],[-44.083,18.039],[-3.026,-7.306],[-20.815,19.217],[-8.186,-10.792],[-28.842,51.901],[-12.022,-1.924],[-25.94,57.885],[-10.365,1.613],[-13.894,61.205],[-1.581,0.481],[-2.025,24.266],[-5.413,1.006],[7.876,21.576],[-4.573,1.132],[24.589,48.078],[-6.41,1.365],[27.072,28.648],[-4.858,2.346],[26.485,24.286],[-6.451,3.968],[47.266,33.8],[-2.771,2.906],[33.213,3.403],[-1.167,1.667],[46.083,3.696],[-1.333,9],[65.879,-4.134],[-5.083,7.361],[40.303,-18.354],[15.513,2.68],[30.702,-23.187],[1.943,7.096],[25.282,-27.999],[2.5,7.5],[47.12,-47.637],[6.155,7.421],[7.17,-19.139],[5.169,3.176],[15.641,-48.146],[3.103,0.727],[4.257,-19.437],[6.927,0.949],[11.76,-18.098]],"o":[[-5.323,-0.006],[-1.697,-54.919],[-3.052,1.273],[-20.594,-30.923],[-3.728,2.546],[-6.498,-7.737],[-4.346,1.879],[-6.647,-6.6],[-3.22,5.264],[-68.548,-49.463],[-6.872,3.436],[-40.921,-22.258],[-0.377,3.121],[0,0],[-6.756,2.995],[-99.173,-8.979],[3.585,1.695],[-11.602,0.322],[1.08,5.412],[0,0],[-1.42,4.661],[-38.77,17.671],[-0.025,2.213],[-14.575,10.054],[3.439,7.359],[-11.98,9.131],[3.354,3.021],[-28.218,29.53],[1.451,3.503],[-5.619,13.324],[3.041,4.009],[-3.855,13.277],[4.839,0.774],[0,0],[10.589,-1.648],[-2.849,62.516],[2.488,-0.757],[15.206,26.767],[5.413,-1.006],[14.09,15.019],[4.055,-1.004],[42.68,48.017],[6.41,-1.365],[15.989,10.107],[4.858,-2.346],[26.647,14.644],[5.257,-3.234],[52.364,12.754],[3.745,-3.927],[17.38,-4.263],[1.115,-1.592],[74.53,-7.789],[1.333,-9],[11.985,-16.998],[4.419,-6.4],[3.79,-15.061],[-5.258,-0.908],[11.752,-24.375],[-1.495,-5.458],[36.932,-49.434],[-2.202,-6.607],[0.901,-21.183],[-3.464,-4.176],[9.469,-36.552],[-5.169,-3.176],[-2.634,-13.929],[-5.658,-1.325],[-8.107,-4.533],[-6.628,-0.908],[-0.017,-9.362]],"v":[[3.02,-162.999],[8.243,-41.492],[-9.448,-140.273],[5.428,-41.577],[-66.581,-136.474],[-4.982,-50.303],[-94.937,-141.294],[-11.103,-50.883],[-91.631,-117.558],[-15.827,-48.412],[-150.75,-126.625],[-12.148,-33.673],[-76.677,-54.762],[-18.079,-33.683],[-125.935,-48.926],[-28.737,-33.315],[-144.516,-27.935],[-28.241,-30.355],[-129.957,-16.393],[-21.32,-26.218],[-189.443,52.219],[-21.179,-21.553],[-84.613,22.128],[-20.845,-15.595],[-143.927,91.798],[-11.831,-12.788],[-109.552,77.33],[-1.776,-10.549],[-60.574,69.369],[-4.963,-2.109],[-41.055,117.272],[-0.555,5.102],[-30.196,155.83],[5.729,3.604],[8.665,130.425],[12.099,-2.376],[18.32,81.416],[15.669,3.108],[41.962,76.131],[21.577,6.648],[62.49,91.451],[27.654,8.15],[95.923,118.365],[31.011,6.393],[102.975,84.18],[33.954,3.923],[112.957,71.446],[30.136,-4.087],[107.438,31.594],[29.62,-6.903],[94.5,0.667],[35.97,-12.045],[183.167,5],[41.86,-16.772],[147.562,-47.507],[43.258,-24.567],[106.809,-61.148],[38.582,-29.125],[103.057,-80.596],[35.068,-34.233],[141.167,-109],[32.012,-38.849],[76.865,-113.118],[28.198,-41.782],[66.503,-119.657],[25.119,-44.132],[37.324,-105.068],[21.279,-41.807],[29.931,-135.604],[12.71,-45.889]],"c":true}]},{"t":100,"s":[{"i":[[6.52,-0.499],[0,0],[7.552,-1.773],[0,0],[7.419,-3.974],[0,0],[5.063,-2.794],[0,0],[3.869,-4.058],[0,0],[3.25,-5.125],[0,0],[2.323,-7.262],[0,0],[1.065,-7.426],[0,0],[-0.016,-6.935],[0,0],[0.043,-8.393],[0,0],[-1.943,-5.281],[0,0],[-4.613,-6.872],[0,0],[-4.427,-4.702],[0,0],[-5.552,-5.67],[0,0],[-7.074,-5.131],[0,0],[-7.555,-2.728],[0,0],[-8.696,0.33],[0,0],[-5.835,-0.075],[0,0],[-5.179,-0.584],[0,0],[-7.538,4.131],[0,0],[-4.51,1.951],[0,0],[-7.077,4.865],[0,0],[-5.025,3.68],[0,0],[-5.043,6.946],[0,0],[-2.062,4.094],[0,0],[-1.5,7.666],[0,0],[-1.333,7.5],[0,0],[-0.438,6.493],[0,0],[2.309,7.852],[0,0],[1.057,4.404],[0,0],[1.667,6.5],[0,0],[6.365,6.382],[0,0],[3.503,2.843],[0,0],[5.324,3.432],[0,0],[2.931,0.896],[0,0]],"o":[[-6.521,0.499],[0,0],[-7.552,1.773],[0,0],[-7.419,3.974],[0,0],[-5.063,2.794],[0,0],[-3.869,4.058],[0,0],[-3.25,5.125],[0,0],[-2.323,7.262],[0,0],[-1.065,7.426],[0,0],[0.016,6.935],[0,0],[-0.043,8.393],[0,0],[1.943,5.281],[0,0],[4.613,6.872],[0,0],[4.427,4.702],[0,0],[5.552,5.67],[0,0],[7.074,5.131],[0,0],[7.555,2.728],[0,0],[8.696,-0.33],[0,0],[5.835,0.075],[0,0],[5.18,0.584],[0,0],[7.538,-4.131],[0,0],[4.51,-1.951],[0,0],[7.077,-4.865],[0,0],[5.025,-3.68],[0,0],[5.043,-6.946],[0,0],[2.062,-4.094],[0,0],[1.5,-7.666],[0,0],[1.333,-7.5],[0,0],[0.438,-6.493],[0,0],[-2.309,-7.852],[0,0],[-1.057,-4.404],[0,0],[-1.667,-6.5],[0,0],[-6.365,-6.382],[0,0],[-3.503,-2.843],[0,0],[-5.324,-3.432],[0,0],[-2.931,-0.896],[0,0]],"v":[[0.021,-238.999],[6.522,-22.25],[-22.948,-238.773],[6.486,-22.251],[-111.081,-205.974],[6.353,-22.363],[-136.437,-188.794],[6.274,-22.37],[-151.131,-175.058],[6.214,-22.339],[-176.75,-144.125],[6.261,-22.15],[-199.177,-102.262],[6.185,-22.15],[-209.935,-65.926],[6.048,-22.145],[-215.016,-31.435],[6.055,-22.107],[-215.457,-11.893],[6.143,-22.054],[-201.443,57.219],[6.145,-21.994],[-194.113,74.128],[6.149,-21.918],[-171.427,112.298],[6.265,-21.882],[-163.052,122.83],[6.394,-21.853],[-126.074,156.869],[6.353,-21.745],[-65.055,189.772],[6.41,-21.653],[-38.196,196.33],[6.49,-21.672],[9.165,202.425],[6.572,-21.748],[31.821,199.416],[6.618,-21.678],[80.962,187.631],[6.693,-21.633],[105.49,178.451],[6.771,-21.613],[127.923,167.865],[6.814,-21.636],[157.975,144.18],[6.852,-21.668],[174.957,125.946],[6.803,-21.77],[204.438,84.094],[6.796,-21.806],[223.5,34.166],[6.878,-21.872],[227.667,12],[6.953,-21.933],[227.062,-61.507],[6.971,-22.033],[213.309,-104.148],[6.911,-22.091],[198.057,-137.596],[6.866,-22.157],[195.667,-144],[6.827,-22.216],[142.865,-200.118],[6.778,-22.254],[122.503,-211.157],[6.739,-22.284],[84.324,-230.568],[6.689,-22.254],[51.431,-240.604],[6.58,-22.306]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":80},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0.001,0.467,0.031,0.541,0.707,0.435,0.108,0.561,1,0.404,0.184,0.58]}},"s":{"a":0,"k":[4,-20]},"e":{"a":0,"k":[177.39,-20]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 3","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":-4,"op":99,"st":-65,"bm":0}]},{"id":"comp_4","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"picies2","sr":1,"ks":{"p":{"a":0,"k":[257.33,185.73,0]},"a":{"a":0,"k":[-265.67,-10.27,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,-0.35],[53.84,0],[-19.93,11.56],[-1,-40.01]],"o":[[0,50.03],[-31.67,-34.87],[39.26,-0.87],[0.01,0.35]],"v":[[-168.19,-10.27],[-265.67,80.32],[-266.5,-79.82],[-168.2,-11.32]],"c":true}]},{"t":35,"s":[{"i":[[0,-0.004],[0.613,0],[-0.227,0.132],[-0.011,-0.455]],"o":[[0,0.569],[-0.36,-0.397],[0.447,-0.01],[0,0.004]],"v":[[-145.03,70.7],[-146.14,71.731],[-146.149,69.909],[-145.03,70.688]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-2.5,-7],[15.65,-61.05],[-53.84,0]],"o":[[0,0],[0,-50.03],[0,0]],"v":[[-266.5,-79.82],[-363.15,-10.27],[-265.67,-100.86]],"c":true}]},{"t":35,"s":[{"i":[[-0.013,-0.036],[0.08,-0.312],[-0.275,0]],"o":[[0,0],[0,-0.256],[0,0]],"v":[[-383.165,-126.689],[-383.659,-126.333],[-383.161,-126.797]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-0.6,-49.55],[39.26,-0.87],[0,0]],"o":[[-1,-40.01],[-2.5,-7],[53.46,0]],"v":[[-168.2,-11.32],[-266.5,-79.82],[-265.67,-100.86]],"c":true}]},{"t":35,"s":[{"i":[[-0.018,-1.483],[1.175,-0.026],[0,0]],"o":[[-0.03,-1.198],[-0.075,-0.21],[1.6,0]],"v":[[-162.091,-102.75],[-165.034,-104.8],[-165.009,-105.43]],"c":true}]}]},"nm":"Path 3","hd":false},{"ty":"fl","c":{"a":0,"k":[0.749019607843,0,0.690196078431,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 3","hd":false},{"ty":"tr","p":{"a":0,"k":[-265.67,-10.27]},"a":{"a":0,"k":[-265.67,-10.27]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-31.67,-34.87],[0,50.03],[0,0]],"o":[[-53.84,0],[15.65,-61.05],[-19.93,11.56]],"v":[[-265.67,80.32],[-363.15,-10.27],[-266.5,-79.82]],"c":true}]},{"t":35,"s":[{"i":[[-0.099,-0.109],[0,0.156],[0,0]],"o":[[-0.168,0],[0.049,-0.191],[-0.062,0.036]],"v":[[-415.258,90.5],[-415.562,90.217],[-415.26,90]],"c":true}]}]},"nm":"Path 4","hd":false},{"ty":"fl","c":{"a":0,"k":[0.529411764706,0,0.486274539723,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 3","hd":false},{"ty":"tr","p":{"a":0,"k":[-314.41,0.25]},"a":{"a":0,"k":[-314.41,0.25]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"st","c":{"a":0,"k":[0.301960784314,0,0.274509803922,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":24},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-265.67,-10.27]},"a":{"a":0,"k":[-265.67,-10.27]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":30,"op":35,"st":2,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"picies","sr":1,"ks":{"p":{"a":0,"k":[255,191.406,0]},"a":{"a":0,"k":[0,45.406,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":31,"s":[{"i":[[-14.81,-13.46],[11.95,21.92],[1.11,16.11],[-22.44,-7.84],[-11.16,1.15]],"o":[[-22.91,4.11],[-6.67,-12.23],[5.79,24.31],[9.97,3.48],[-0.13,0.68]],"v":[[10.978,133.929],[-49.332,107.219],[-76.002,35.549],[-29.982,86.199],[2.028,89.909]],"c":true}]},{"t":36,"s":[{"i":[[0.042,0.009],[-0.025,-0.045],[-0.002,-0.033],[0.03,0.046],[0.013,0.008]],"o":[[0.048,-0.009],[0.014,0.025],[-0.012,-0.05],[-0.009,-0.013],[-0.02,-0.013]],"v":[[-71.68,195.17],[-71.555,195.225],[-71.5,195.374],[-71.582,195.231],[-71.615,195.198]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":31,"s":[{"i":[[-20.7,35.34],[6.66,-12.23],[15.67,-2.82],[0,0],[-0.13,0.68]],"o":[[-1.11,16.11],[-8.18,15],[0,0],[-14.81,-13.46],[25.91,-2.68]],"v":[[75.998,35.549],[49.338,107.219],[10.988,133.929],[10.978,133.929],[2.028,89.909]],"c":true}]},{"t":36,"s":[{"i":[[-0.149,0.254],[0.048,-0.088],[0.112,-0.02],[0,0],[-0.001,0.005]],"o":[[-0.008,0.116],[-0.059,0.108],[0,0],[0.129,-0.1],[0.093,-0.157]],"v":[[73.967,180.898],[73.775,181.412],[73.5,181.604],[73.5,181.604],[73.665,181.395]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":31,"s":[{"i":[[4.69,-50.46],[25.91,-2.68],[9.97,3.48],[-5.65,29.96],[19.36,23.12]],"o":[[-20.7,35.34],[-11.16,1.15],[25.15,-11.39],[3.71,-19.59],[38.33,2.51]],"v":[[75.998,35.549],[2.028,89.909],[-29.982,86.199],[26.648,21.049],[6.078,-43.891]],"c":true}]},{"t":36,"s":[{"i":[[0.019,-0.202],[0.104,-0.011],[0.04,0.014],[-0.023,0.12],[0.077,0.092]],"o":[[-0.083,0.141],[-0.045,0.005],[0.101,-0.046],[0.015,-0.078],[0.153,0.01]],"v":[[154.424,34.876],[154.128,35.094],[154,35.079],[154.226,34.818],[154.144,34.559]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":31,"s":[{"i":[[3.71,-19.59],[21,-18.31],[-37.83,-2.49]],"o":[[-10.36,-22.61],[4.41,-44.16],[19.36,23.12]],"v":[[26.648,21.049],[-76.002,18.639],[6.078,-43.891]],"c":true}]},{"t":36,"s":[{"i":[[0.012,-0.061],[0.066,-0.057],[-0.118,-0.008]],"o":[[-0.032,-0.071],[0.014,-0.138],[0.06,0.072]],"v":[[1.192,-97.167],[0.872,-97.174],[1.128,-97.369]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":31,"s":[{"i":[[-10.36,-22.61],[25.15,-11.39],[5.79,24.31],[-0.53,5.3]],"o":[[-5.65,29.96],[-22.44,-7.84],[-0.55,-5.97],[21,-18.31]],"v":[[26.648,21.049],[-29.982,86.199],[-76.002,35.549],[-76.002,18.639]],"c":true}]},{"t":36,"s":[{"i":[[-0.156,-0.229],[0.005,-0.198],[0.125,0.524],[-0.011,0.114]],"o":[[0.175,0.257],[-0.483,-0.169],[-0.012,-0.129],[0.113,0.148]],"v":[[-149.053,15.247],[-148.42,15.955],[-149.412,14.864],[-149.412,14.5]],"c":true}]}]},"nm":"Path 5","hd":false},{"ty":"fl","c":{"a":0,"k":[0.749019607843,0,0.690196078431,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 5","hd":false},{"ty":"st","c":{"a":0,"k":[0.301960784314,0,0.274509803922,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":24},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,45.406]},"a":{"a":0,"k":[0,45.406]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":31,"op":36,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"Null 3","sr":1,"ks":{"o":{"a":0,"k":0},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[256]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":21,"s":[257.445]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":22,"s":[251.735]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":23,"s":[262.162]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":24,"s":[256.384]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[256.888]},{"t":29,"s":[254.404]}]},"y":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[198]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":22,"s":[190.425]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":23,"s":[197.133]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":24,"s":[186.523]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[194.791]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":26,"s":[186.058]},{"t":29,"s":[194.761]}]}},"a":{"a":0,"k":[50,50,0]}},"ao":0,"ip":20,"op":30,"st":-60,"bm":0},{"ddd":0,"ind":4,"ty":3,"nm":"Null 19","parent":3,"sr":1,"ks":{"o":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[50,108,0],"to":[0,0,0],"ti":[0,0,0]},{"t":30,"s":[50,1,0]}]}},"ao":0,"ip":0,"op":36,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"blik","parent":6,"sr":1,"ks":{"p":{"a":0,"k":[20.897,-88.771,0]},"a":{"a":0,"k":[19.25,-92,0]},"s":{"a":0,"k":[70,70,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,-13.193],[44.427,-0.298],[-30.5,146.5]],"o":[[0,13.193],[-55,1.5],[-0.5,0]],"v":[[80.444,-33.889],[0,-10],[0,-200]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[0,-61.227],[24.905,-1.3],[-8.202,27.448]],"o":[[0,40.169],[-45,-4.333],[2.417,-2.431]],"v":[[98.282,62.507],[32.976,130.655],[10.893,-24.206]],"c":true}]},{"t":30,"s":[{"i":[[0,-77.778],[23.059,-1.647],[-4.109,3.995]],"o":[[0,50.031],[-47.216,-6.039],[3.294,-3.203]],"v":[[109.918,84.947],[41.535,169.497],[12.437,7.17]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":8},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-131]},{"t":30,"s":[-5]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":8.571},"lc":2,"lj":2,"bm":0,"d":[{"n":"d","nm":"dash","v":{"a":0,"k":2}},{"n":"g","nm":"gap","v":{"a":0,"k":12}},{"n":"d","nm":"dash2","v":{"a":0,"k":297}},{"n":"o","nm":"offset","v":{"a":0,"k":0}}],"nm":"Stroke 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":30,"st":-60,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"rocket_cap","parent":10,"sr":1,"ks":{"p":{"a":0,"k":[0,-33.889,0]},"a":{"a":0,"k":[0,-33.889,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,-13.193],[44.427,-0.298],[-30.5,146.5]],"o":[[0,13.193],[-55,1.5],[-0.5,0]],"v":[[80.444,-33.889],[0,-10],[0,-200]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[0,-61.227],[24.905,-1.3],[-8.202,27.448]],"o":[[0,40.169],[-45,-4.333],[2.417,-2.431]],"v":[[87.389,21.435],[22.083,89.583],[0,-65.278]],"c":true}]},{"t":30,"s":[{"i":[[0,-77.778],[23.059,-1.647],[-4.109,3.995]],"o":[[0,50.031],[-47.216,-6.039],[3.294,-3.203]],"v":[[97.776,39.412],[29.392,123.961],[0.294,-38.366]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.749019607843,0,0.690196078431,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,-13.193],[44.428,0],[0,13.193],[0,0]],"o":[[0,13.193],[-44.428,0],[0,-13.193],[0,0]],"v":[[80.444,-33.889],[0,-10],[-80.444,-33.889],[0,-200]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[0,-61.227],[48.264,0],[0,40.168],[0,0]],"o":[[0,40.168],[-48.264,0],[0,-61.227],[0,0]],"v":[[87.389,21.435],[0,94.167],[-87.389,21.435],[0,-65.278]],"c":true}]},{"t":30,"s":[{"i":[[0,-77.778],[53.838,0],[0,50.031],[0,0]],"o":[[0,50.031],[-53.838,0],[0,-77.778],[0,0]],"v":[[97.776,39.412],[0.294,130],[-97.187,39.412],[0.294,-38.366]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.529411764706,0,0.486274539723,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-105]},"a":{"a":0,"k":[0,-105]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,-13.193],[44.428,0],[0,13.193],[-44.428,0]],"o":[[0,13.193],[-44.428,0],[0,-13.193],[44.428,0]],"v":[[80.444,-33.889],[0,-10],[-80.444,-33.889],[0,-57.778]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[0,-40.168],[48.264,0],[0,40.168],[-48.264,0]],"o":[[0,40.168],[-48.264,0],[0,-40.168],[48.264,0]],"v":[[87.389,21.435],[0,94.167],[-87.389,21.435],[0,-51.296]],"c":true}]},{"t":30,"s":[{"i":[[0,-50.031],[53.838,0],[0,50.031],[-53.838,0]],"o":[[0,50.031],[-53.838,0],[0,-50.031],[53.838,0]],"v":[[97.776,39.412],[0.294,130],[-97.187,39.412],[0.294,-51.176]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"fl","c":{"a":0,"k":[0.749019607843,0,0.690196078431,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 2","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-33.889]},"a":{"a":0,"k":[0,-33.889]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"st","c":{"a":0,"k":[0.3,0,0.276439771465,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":24},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":30,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"rocket body 2 outlines","parent":10,"sr":1,"ks":{"p":{"a":0,"k":[0,71.389,0]},"a":{"a":0,"k":[0,71.389,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[2.222,-10],[7.734,34.804],[0,0]],"o":[[0,0],[-7.676,34.543],[-2.222,-10],[0,0]],"v":[[68.778,-37.222],[59.333,153.333],[-59.333,153.333],[-68.778,-37.222]],"c":true}]},{"t":30,"s":[{"i":[[0,0],[6.667,-12.222],[20.133,36.91],[1.111,16.111]],"o":[[-1.111,16.111],[-20.133,36.911],[-6.667,-12.222],[0,0]],"v":[[76,35.556],[49.333,107.222],[-49.333,107.222],[-76,35.556]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.301960784314,0,0.274509803922,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":12},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":30,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"rocket body 2 shadows","parent":10,"sr":1,"ks":{"o":{"a":0,"k":50},"p":{"a":0,"k":[0,71.389,0]},"a":{"a":0,"k":[0,71.389,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0.667,-19.333],[7.333,22.167],[0,0]],"o":[[0,0],[0.167,0.667],[-3.217,-9.726],[0,0]],"v":[[-13.222,-18.222],[-12.667,177.333],[-59.333,153.333],[-68.778,-37.222]],"c":true}]},{"t":30,"s":[{"i":[[0,0],[0.167,-11.222],[18.833,29.278],[1.111,16.111]],"o":[[-1.111,16.111],[-15.833,-1.222],[-7.532,-11.709],[0,0]],"v":[[8,37.056],[7.333,133.222],[-49.333,107.222],[-76,35.556]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.3,0,0.274509803922,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":30,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"lines2","parent":10,"sr":1,"ks":{"p":{"a":0,"k":[-4.5,135.446,0]},"a":{"a":0,"k":[-4.5,135.446,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[19,23.25],[0,0],[-72.25,-0.5],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-45,-7]],"v":[[-67.5,-34.25],[-66,-10.75],[65.25,58.5],[66.5,12.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[19,23.25],[0,0],[-70.25,-2.5],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-45,-7]],"v":[[-67.5,-34.25],[-67,-26.25],[65.75,25],[68.5,-17]],"c":true}]},{"t":10,"s":[{"i":[[19,23.25],[0,0],[-70.25,-2.5],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-45,-7]],"v":[[-67.5,-34.25],[-67,-26.25],[68.25,-15],[68.5,-17]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[25,44.25],[0,0],[-71.75,9],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50,5]],"v":[[-65,33.75],[-62.5,74.75],[60.75,137],[62.5,98]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[25,44.25],[0,0],[-71.75,10.5],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50,5]],"v":[[-66.5,2.75],[-65,41.25],[61.75,99],[65,63]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[26,30.25],[0,0],[-71.75,10.5],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50,5]],"v":[[-68.5,-6.75],[-66,19.75],[62.75,70.5],[68,37]],"c":true}]},{"t":15,"s":[{"i":[[26,30.25],[0,0],[-54.75,16],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50,5]],"v":[[-68.5,-6.75],[-65.5,15.25],[65.75,49],[71,14]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[20,16.5],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[13.25,13.75],[-0.25,0],[-50.75,19]],"v":[[-56.25,160.5],[-56,160.5],[49.25,167.75],[49.75,168]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[20,16.5],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[13.25,13.75],[-0.25,0],[-50.75,19]],"v":[[-54.5,154.25],[-54.25,154.25],[43,161.5],[43.5,161.75]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[20,16.5],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[13.25,13.75],[-0.25,0],[-50.75,19]],"v":[[-53.75,148],[-53.5,148],[42.25,155.5],[42.75,155.75]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[17.5,26],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[11.75,20.5],[-0.25,0],[-50.75,19]],"v":[[-52.5,136.25],[-52.75,136.25],[42.25,147.75],[42.5,147.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[21.25,35.75],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[11.75,20.5],[-0.25,0],[-50.75,19]],"v":[[-57.25,102.5],[-55,120.25],[43.25,137.5],[44,133.75]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[21.25,35.75],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[11.75,20.5],[-0.25,0],[-50.75,19]],"v":[[-59.25,89.5],[-54.25,106.75],[42.5,128],[48,118.75]],"c":true}]},{"t":30,"s":[{"i":[[21.25,35.75],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[11.75,20.5],[-0.25,0],[-50.75,19]],"v":[[-58.5,71.75],[-53.5,89],[43.25,110.25],[48.75,101]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[19,27.5],[0,0],[-58.5,22],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50.75,19]],"v":[[-58.5,158.5],[-58.25,158.5],[53.5,166],[54,165]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[19,27.5],[0,0],[-58.5,22],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50.75,19]],"v":[[-57.25,147],[-57.25,147.5],[50.5,160],[50,161]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[24.25,45.5],[0,0],[-58.5,22],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50.75,19]],"v":[[-60.5,106.5],[-58.75,138.5],[51,150],[51.25,149.75]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[24.25,45.5],[0,0],[-54.5,29.25],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50.75,19]],"v":[[-62.5,78],[-58.25,106.75],[49,141.75],[53.5,124.75]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[21.25,35.75],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[11.75,20.5],[-0.25,0],[-50.75,19]],"v":[[-62.5,67.5],[-59.5,89.75],[50,121.5],[54.5,104.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[21.25,35.75],[0,0],[-49.75,27],[0,0]],"o":[[-0.25,0.25],[11.75,20.5],[-0.25,0],[-50.75,19]],"v":[[-62.5,63.25],[-60.75,81.25],[52.25,106.5],[57.25,81]],"c":true}]},{"t":30,"s":[{"i":[[21.25,35.75],[0,0],[-49.75,27],[0,0]],"o":[[-0.25,0.25],[11.75,20.5],[-0.25,0],[-50.75,19]],"v":[[-62.5,63.25],[-60.75,70.5],[56.5,84.75],[57.25,81]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[25,44.25],[0,0],[-47,36.25],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50.5,40.25]],"v":[[-61.5,114.75],[-59,155.75],[63.25,158],[63.5,157.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[25,44.25],[0,0],[-55,45.75],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-51,12.25]],"v":[[-62,77.5],[-60.75,112.25],[60.75,151.25],[59.5,132.25]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[25,44.25],[0,0],[-58.5,22],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-51,12.25]],"v":[[-64.25,50.75],[-60.75,80.5],[58.25,128],[60.75,101.25]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[25,39],[0,0],[-58.5,22],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-51,12.25]],"v":[[-65.5,33.75],[-63.75,59.5],[59,101.75],[61.5,76.75]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[25,39],[0,0],[-55.75,26.75],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-51,12.25]],"v":[[-69.5,30],[-65,49],[61,82.75],[63.75,62.25]],"c":true}]},{"t":25,"s":[{"i":[[25,39],[0,0],[-55.75,26.75],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-51,12.25]],"v":[[-69.5,30],[-65,54.75],[62.25,76.25],[63.75,62.25]],"c":true}]}]},"nm":"Path 4","hd":false},{"ty":"fl","c":{"a":0,"k":[0.913725490196,0.81568627451,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":30,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"rocket body","parent":4,"sr":1,"ks":{"p":{"a":0,"k":[0,71.389,0]},"a":{"a":0,"k":[0,71.389,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[2.222,-10],[7.734,34.804],[0,0]],"o":[[0,0],[-7.676,34.543],[-2.222,-10],[0,0]],"v":[[68.778,-37.222],[59.333,153.333],[-59.333,153.333],[-68.778,-37.222]],"c":true}]},{"t":30,"s":[{"i":[[0,0],[6.667,-12.222],[20.133,36.91],[1.111,16.111]],"o":[[-1.111,16.111],[-20.133,36.911],[-6.667,-12.222],[0,0]],"v":[[76,35.556],[49.333,107.222],[-49.333,107.222],[-76,35.556]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.749019607843,0,0.690196078431,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":30,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Shape Layer 11","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[283,390,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[257,472,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[283,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":17,"s":[262,427.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[280,323,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[256.5,371.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[279,288,0],"to":[0,0,0],"ti":[0,0,0]},{"t":31,"s":[259,328,0]}]},"a":{"a":0,"k":[-28,116,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":4,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":10,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":11,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":17,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":18,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":24,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":25,"s":[100,100,100]},{"t":31,"s":[20,20,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[56,56]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862745098,0.854901960784,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-28,116]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":4,"op":31,"st":4,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Shape Layer 10","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[255,394,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[257,472,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13,"s":[258,356,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":19,"s":[262,427.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[255,327,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":26,"s":[256.5,371.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":27,"s":[261,292,0],"to":[0,0,0],"ti":[0,0,0]},{"t":33,"s":[259,328,0]}]},"a":{"a":0,"k":[-28,116,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":6,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":12,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":13,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":19,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":20,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":26,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":27,"s":[100,100,100]},{"t":33,"s":[20,20,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[56,56]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862745098,0.854901960784,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-28,116]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":6,"op":33,"st":6,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"Shape Layer 9","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[231,397,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[252,494,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[232,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":21,"s":[254,428,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[230,336,0],"to":[0,0,0],"ti":[0,0,0]},{"t":28,"s":[256.5,371.5,0]}]},"a":{"a":0,"k":[-28,116,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":8,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":14,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":15,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":21,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":22,"s":[100,100,100]},{"t":28,"s":[20,20,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[56,56]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862745098,0.854901960784,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-28,116]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":8,"op":28,"st":8,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"Shape Layer 8","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[283,390,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[257,472,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9,"s":[283,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[262,427.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[280,323,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[256.5,371.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23,"s":[279,288,0],"to":[0,0,0],"ti":[0,0,0]},{"t":29,"s":[259,328,0]}]},"a":{"a":0,"k":[-28,116,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":2,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":8,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":9,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":15,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":16,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":22,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":23,"s":[100,100,100]},{"t":29,"s":[20,20,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[56,56]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862745098,0.854901960784,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-28,116]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":2,"op":29,"st":2,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[231,397,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[252,494,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":7,"s":[232,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13,"s":[254,428,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[230,336,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[256.5,371.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":21,"s":[234,298,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":27,"s":[259,328,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[239,262,0],"to":[0,0,0],"ti":[0,0,0]},{"t":33,"s":[264,326,0]}]},"a":{"a":0,"k":[-28,116,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":6,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":7,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":13,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":14,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":20,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":21,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":27,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":28,"s":[100,100,100]},{"t":33,"s":[20,20,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[56,56]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862745098,0.854901960784,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-28,116]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":34,"st":0,"bm":0}]},{"id":"comp_5","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"picies2","sr":1,"ks":{"p":{"a":0,"k":[257.33,185.73,0]},"a":{"a":0,"k":[-265.67,-10.27,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,-0.35],[53.84,0],[-19.93,11.56],[-1,-40.01]],"o":[[0,50.03],[-31.67,-34.87],[39.26,-0.87],[0.01,0.35]],"v":[[-168.19,-10.27],[-265.67,80.32],[-266.5,-79.82],[-168.2,-11.32]],"c":true}]},{"t":35,"s":[{"i":[[0,-0.004],[0.613,0],[-0.227,0.132],[-0.011,-0.455]],"o":[[0,0.569],[-0.36,-0.397],[0.447,-0.01],[0,0.004]],"v":[[-145.03,70.7],[-146.14,71.731],[-146.149,69.909],[-145.03,70.688]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-2.5,-7],[15.65,-61.05],[-53.84,0]],"o":[[0,0],[0,-50.03],[0,0]],"v":[[-266.5,-79.82],[-363.15,-10.27],[-265.67,-100.86]],"c":true}]},{"t":35,"s":[{"i":[[-0.013,-0.036],[0.08,-0.312],[-0.275,0]],"o":[[0,0],[0,-0.256],[0,0]],"v":[[-383.165,-126.689],[-383.659,-126.333],[-383.161,-126.797]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-0.6,-49.55],[39.26,-0.87],[0,0]],"o":[[-1,-40.01],[-2.5,-7],[53.46,0]],"v":[[-168.2,-11.32],[-266.5,-79.82],[-265.67,-100.86]],"c":true}]},{"t":35,"s":[{"i":[[-0.018,-1.483],[1.175,-0.026],[0,0]],"o":[[-0.03,-1.198],[-0.075,-0.21],[1.6,0]],"v":[[-162.091,-102.75],[-165.034,-104.8],[-165.009,-105.43]],"c":true}]}]},"nm":"Path 3","hd":false},{"ty":"fl","c":{"a":0,"k":[0.898039275525,0.898039275525,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 3","hd":false},{"ty":"tr","p":{"a":0,"k":[-265.67,-10.27]},"a":{"a":0,"k":[-265.67,-10.27]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-31.67,-34.87],[0,50.03],[0,0]],"o":[[-53.84,0],[15.65,-61.05],[-19.93,11.56]],"v":[[-265.67,80.32],[-363.15,-10.27],[-266.5,-79.82]],"c":true}]},{"t":35,"s":[{"i":[[-0.099,-0.109],[0,0.156],[0,0]],"o":[[-0.168,0],[0.049,-0.191],[-0.062,0.036]],"v":[[-415.258,90.5],[-415.562,90.217],[-415.26,90]],"c":true}]}]},"nm":"Path 4","hd":false},{"ty":"fl","c":{"a":0,"k":[0.807843197093,0.643137254902,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 3","hd":false},{"ty":"tr","p":{"a":0,"k":[-314.41,0.25]},"a":{"a":0,"k":[-314.41,0.25]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"st","c":{"a":0,"k":[0.647058823529,0.305882352941,0,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":24},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-265.67,-10.27]},"a":{"a":0,"k":[-265.67,-10.27]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":30,"op":35,"st":2,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"picies","sr":1,"ks":{"p":{"a":0,"k":[255,191.406,0]},"a":{"a":0,"k":[0,45.406,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":31,"s":[{"i":[[-14.81,-13.46],[11.95,21.92],[1.11,16.11],[-22.44,-7.84],[-11.16,1.15]],"o":[[-22.91,4.11],[-6.67,-12.23],[5.79,24.31],[9.97,3.48],[-0.13,0.68]],"v":[[10.978,133.929],[-49.332,107.219],[-76.002,35.549],[-29.982,86.199],[2.028,89.909]],"c":true}]},{"t":36,"s":[{"i":[[0.042,0.009],[-0.025,-0.045],[-0.002,-0.033],[0.03,0.046],[0.013,0.008]],"o":[[0.048,-0.009],[0.014,0.025],[-0.012,-0.05],[-0.009,-0.013],[-0.02,-0.013]],"v":[[-71.68,195.17],[-71.555,195.225],[-71.5,195.374],[-71.582,195.231],[-71.615,195.198]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":31,"s":[{"i":[[-20.7,35.34],[6.66,-12.23],[15.67,-2.82],[0,0],[-0.13,0.68]],"o":[[-1.11,16.11],[-8.18,15],[0,0],[-14.81,-13.46],[25.91,-2.68]],"v":[[75.998,35.549],[49.338,107.219],[10.988,133.929],[10.978,133.929],[2.028,89.909]],"c":true}]},{"t":36,"s":[{"i":[[-0.149,0.254],[0.048,-0.088],[0.112,-0.02],[0,0],[-0.001,0.005]],"o":[[-0.008,0.116],[-0.059,0.108],[0,0],[0.129,-0.1],[0.093,-0.157]],"v":[[73.967,180.898],[73.775,181.412],[73.5,181.604],[73.5,181.604],[73.665,181.395]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":31,"s":[{"i":[[4.69,-50.46],[25.91,-2.68],[9.97,3.48],[-5.65,29.96],[19.36,23.12]],"o":[[-20.7,35.34],[-11.16,1.15],[25.15,-11.39],[3.71,-19.59],[38.33,2.51]],"v":[[75.998,35.549],[2.028,89.909],[-29.982,86.199],[26.648,21.049],[6.078,-43.891]],"c":true}]},{"t":36,"s":[{"i":[[0.019,-0.202],[0.104,-0.011],[0.04,0.014],[-0.023,0.12],[0.077,0.092]],"o":[[-0.083,0.141],[-0.045,0.005],[0.101,-0.046],[0.015,-0.078],[0.153,0.01]],"v":[[154.424,34.876],[154.128,35.094],[154,35.079],[154.226,34.818],[154.144,34.559]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":31,"s":[{"i":[[3.71,-19.59],[21,-18.31],[-37.83,-2.49]],"o":[[-10.36,-22.61],[4.41,-44.16],[19.36,23.12]],"v":[[26.648,21.049],[-76.002,18.639],[6.078,-43.891]],"c":true}]},{"t":36,"s":[{"i":[[0.012,-0.061],[0.066,-0.057],[-0.118,-0.008]],"o":[[-0.032,-0.071],[0.014,-0.138],[0.06,0.072]],"v":[[1.192,-97.167],[0.872,-97.174],[1.128,-97.369]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":31,"s":[{"i":[[-10.36,-22.61],[25.15,-11.39],[5.79,24.31],[-0.53,5.3]],"o":[[-5.65,29.96],[-22.44,-7.84],[-0.55,-5.97],[21,-18.31]],"v":[[26.648,21.049],[-29.982,86.199],[-76.002,35.549],[-76.002,18.639]],"c":true}]},{"t":36,"s":[{"i":[[-0.156,-0.229],[0.005,-0.198],[0.125,0.524],[-0.011,0.114]],"o":[[0.175,0.257],[-0.483,-0.169],[-0.012,-0.129],[0.113,0.148]],"v":[[-149.053,15.247],[-148.42,15.955],[-149.412,14.864],[-149.412,14.5]],"c":true}]}]},"nm":"Path 5","hd":false},{"ty":"fl","c":{"a":0,"k":[0.898039275525,0.898039275525,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 5","hd":false},{"ty":"st","c":{"a":0,"k":[0.647058823529,0.305882352941,0,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":24},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,45.406]},"a":{"a":0,"k":[0,45.406]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":31,"op":36,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"Null 3","sr":1,"ks":{"o":{"a":0,"k":0},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[256]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":21,"s":[257.445]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":22,"s":[251.735]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":23,"s":[262.162]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":24,"s":[256.384]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[256.888]},{"t":29,"s":[254.404]}]},"y":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[198]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":22,"s":[190.425]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":23,"s":[197.133]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":24,"s":[186.523]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[194.791]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":26,"s":[186.058]},{"t":29,"s":[194.761]}]}},"a":{"a":0,"k":[50,50,0]}},"ao":0,"ip":20,"op":30,"st":-60,"bm":0},{"ddd":0,"ind":4,"ty":3,"nm":"Null 19","parent":3,"sr":1,"ks":{"o":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[50,108,0],"to":[0,0,0],"ti":[0,0,0]},{"t":30,"s":[50,1,0]}]}},"ao":0,"ip":0,"op":36,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"blik","parent":6,"sr":1,"ks":{"p":{"a":0,"k":[20.897,-88.771,0]},"a":{"a":0,"k":[19.25,-92,0]},"s":{"a":0,"k":[70,70,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,-13.193],[44.427,-0.298],[-30.5,146.5]],"o":[[0,13.193],[-55,1.5],[-0.5,0]],"v":[[80.444,-33.889],[0,-10],[0,-200]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[0,-61.227],[24.905,-1.3],[-8.202,27.448]],"o":[[0,40.169],[-45,-4.333],[2.417,-2.431]],"v":[[98.282,62.507],[32.976,130.655],[10.893,-24.206]],"c":true}]},{"t":30,"s":[{"i":[[0,-77.778],[23.059,-1.647],[-4.109,3.995]],"o":[[0,50.031],[-47.216,-6.039],[3.294,-3.203]],"v":[[109.918,84.947],[41.535,169.497],[12.437,7.17]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":8},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-131]},{"t":30,"s":[-5]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":8.571},"lc":2,"lj":2,"bm":0,"d":[{"n":"d","nm":"dash","v":{"a":0,"k":2}},{"n":"g","nm":"gap","v":{"a":0,"k":12}},{"n":"d","nm":"dash2","v":{"a":0,"k":297}},{"n":"o","nm":"offset","v":{"a":0,"k":0}}],"nm":"Stroke 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":30,"st":-60,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"rocket_cap","parent":10,"sr":1,"ks":{"p":{"a":0,"k":[0,-33.889,0]},"a":{"a":0,"k":[0,-33.889,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,-13.193],[44.427,-0.298],[-30.5,146.5]],"o":[[0,13.193],[-55,1.5],[-0.5,0]],"v":[[80.444,-33.889],[0,-10],[0,-200]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[0,-61.227],[24.905,-1.3],[-8.202,27.448]],"o":[[0,40.169],[-45,-4.333],[2.417,-2.431]],"v":[[87.389,21.435],[22.083,89.583],[0,-65.278]],"c":true}]},{"t":30,"s":[{"i":[[0,-77.778],[23.059,-1.647],[-4.109,3.995]],"o":[[0,50.031],[-47.216,-6.039],[3.294,-3.203]],"v":[[97.776,39.412],[29.392,123.961],[0.294,-38.366]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.898039275525,0.898039275525,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,-13.193],[44.428,0],[0,13.193],[0,0]],"o":[[0,13.193],[-44.428,0],[0,-13.193],[0,0]],"v":[[80.444,-33.889],[0,-10],[-80.444,-33.889],[0,-200]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[0,-61.227],[48.264,0],[0,40.168],[0,0]],"o":[[0,40.168],[-48.264,0],[0,-61.227],[0,0]],"v":[[87.389,21.435],[0,94.167],[-87.389,21.435],[0,-65.278]],"c":true}]},{"t":30,"s":[{"i":[[0,-77.778],[53.838,0],[0,50.031],[0,0]],"o":[[0,50.031],[-53.838,0],[0,-77.778],[0,0]],"v":[[97.776,39.412],[0.294,130],[-97.187,39.412],[0.294,-38.366]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.807843197093,0.643137254902,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-105]},"a":{"a":0,"k":[0,-105]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,-13.193],[44.428,0],[0,13.193],[-44.428,0]],"o":[[0,13.193],[-44.428,0],[0,-13.193],[44.428,0]],"v":[[80.444,-33.889],[0,-10],[-80.444,-33.889],[0,-57.778]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[0,-40.168],[48.264,0],[0,40.168],[-48.264,0]],"o":[[0,40.168],[-48.264,0],[0,-40.168],[48.264,0]],"v":[[87.389,21.435],[0,94.167],[-87.389,21.435],[0,-51.296]],"c":true}]},{"t":30,"s":[{"i":[[0,-50.031],[53.838,0],[0,50.031],[-53.838,0]],"o":[[0,50.031],[-53.838,0],[0,-50.031],[53.838,0]],"v":[[97.776,39.412],[0.294,130],[-97.187,39.412],[0.294,-51.176]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"fl","c":{"a":0,"k":[0.898039275525,0.898039275525,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 2","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-33.889]},"a":{"a":0,"k":[0,-33.889]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"st","c":{"a":0,"k":[0.647058823529,0.305882352941,0,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":24},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":30,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"rocket body 2 outlines","parent":10,"sr":1,"ks":{"p":{"a":0,"k":[0,71.389,0]},"a":{"a":0,"k":[0,71.389,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[2.222,-10],[7.734,34.804],[0,0]],"o":[[0,0],[-7.676,34.543],[-2.222,-10],[0,0]],"v":[[68.778,-37.222],[59.333,153.333],[-59.333,153.333],[-68.778,-37.222]],"c":true}]},{"t":30,"s":[{"i":[[0,0],[6.667,-12.222],[20.133,36.91],[1.111,16.111]],"o":[[-1.111,16.111],[-20.133,36.911],[-6.667,-12.222],[0,0]],"v":[[76,35.556],[49.333,107.222],[-49.333,107.222],[-76,35.556]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.647058823529,0.305882352941,0,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":12},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":30,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"rocket body 2 shadows","parent":10,"sr":1,"ks":{"o":{"a":0,"k":50},"p":{"a":0,"k":[0,71.389,0]},"a":{"a":0,"k":[0,71.389,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0.667,-19.333],[7.333,22.167],[0,0]],"o":[[0,0],[0.167,0.667],[-3.217,-9.726],[0,0]],"v":[[-13.222,-18.222],[-12.667,177.333],[-59.333,153.333],[-68.778,-37.222]],"c":true}]},{"t":30,"s":[{"i":[[0,0],[0.167,-11.222],[18.833,29.278],[1.111,16.111]],"o":[[-1.111,16.111],[-15.833,-1.222],[-7.532,-11.709],[0,0]],"v":[[8,37.056],[7.333,133.222],[-49.333,107.222],[-76,35.556]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.713725490196,0.38431372549,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":30,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"lines2","parent":10,"sr":1,"ks":{"p":{"a":0,"k":[-4.5,135.446,0]},"a":{"a":0,"k":[-4.5,135.446,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[19,23.25],[0,0],[-72.25,-0.5],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-45,-7]],"v":[[-67.5,-34.25],[-66,-10.75],[65.25,58.5],[66.5,12.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[19,23.25],[0,0],[-70.25,-2.5],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-45,-7]],"v":[[-67.5,-34.25],[-67,-26.25],[65.75,25],[68.5,-17]],"c":true}]},{"t":10,"s":[{"i":[[19,23.25],[0,0],[-70.25,-2.5],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-45,-7]],"v":[[-67.5,-34.25],[-67,-26.25],[68.25,-15],[68.5,-17]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[25,44.25],[0,0],[-71.75,9],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50,5]],"v":[[-65,33.75],[-62.5,74.75],[60.75,137],[62.5,98]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[25,44.25],[0,0],[-71.75,10.5],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50,5]],"v":[[-66.5,2.75],[-65,41.25],[61.75,99],[65,63]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[26,30.25],[0,0],[-71.75,10.5],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50,5]],"v":[[-68.5,-6.75],[-66,19.75],[62.75,70.5],[68,37]],"c":true}]},{"t":15,"s":[{"i":[[26,30.25],[0,0],[-54.75,16],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50,5]],"v":[[-68.5,-6.75],[-65.5,15.25],[65.75,49],[71,14]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[20,16.5],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[13.25,13.75],[-0.25,0],[-50.75,19]],"v":[[-56.25,160.5],[-56,160.5],[49.25,167.75],[49.75,168]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[20,16.5],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[13.25,13.75],[-0.25,0],[-50.75,19]],"v":[[-54.5,154.25],[-54.25,154.25],[43,161.5],[43.5,161.75]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[20,16.5],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[13.25,13.75],[-0.25,0],[-50.75,19]],"v":[[-53.75,148],[-53.5,148],[42.25,155.5],[42.75,155.75]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[17.5,26],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[11.75,20.5],[-0.25,0],[-50.75,19]],"v":[[-52.5,136.25],[-52.75,136.25],[42.25,147.75],[42.5,147.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[21.25,35.75],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[11.75,20.5],[-0.25,0],[-50.75,19]],"v":[[-57.25,102.5],[-55,120.25],[43.25,137.5],[44,133.75]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[21.25,35.75],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[11.75,20.5],[-0.25,0],[-50.75,19]],"v":[[-59.25,89.5],[-54.25,106.75],[42.5,128],[48,118.75]],"c":true}]},{"t":30,"s":[{"i":[[21.25,35.75],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[11.75,20.5],[-0.25,0],[-50.75,19]],"v":[[-58.5,71.75],[-53.5,89],[43.25,110.25],[48.75,101]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[19,27.5],[0,0],[-58.5,22],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50.75,19]],"v":[[-58.5,158.5],[-58.25,158.5],[53.5,166],[54,165]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[19,27.5],[0,0],[-58.5,22],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50.75,19]],"v":[[-57.25,147],[-57.25,147.5],[50.5,160],[50,161]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[24.25,45.5],[0,0],[-58.5,22],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50.75,19]],"v":[[-60.5,106.5],[-58.75,138.5],[51,150],[51.25,149.75]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[24.25,45.5],[0,0],[-54.5,29.25],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50.75,19]],"v":[[-62.5,78],[-58.25,106.75],[49,141.75],[53.5,124.75]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[21.25,35.75],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[11.75,20.5],[-0.25,0],[-50.75,19]],"v":[[-62.5,67.5],[-59.5,89.75],[50,121.5],[54.5,104.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[21.25,35.75],[0,0],[-49.75,27],[0,0]],"o":[[-0.25,0.25],[11.75,20.5],[-0.25,0],[-50.75,19]],"v":[[-62.5,63.25],[-60.75,81.25],[52.25,106.5],[57.25,81]],"c":true}]},{"t":30,"s":[{"i":[[21.25,35.75],[0,0],[-49.75,27],[0,0]],"o":[[-0.25,0.25],[11.75,20.5],[-0.25,0],[-50.75,19]],"v":[[-62.5,63.25],[-60.75,70.5],[56.5,84.75],[57.25,81]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[25,44.25],[0,0],[-47,36.25],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50.5,40.25]],"v":[[-61.5,114.75],[-59,155.75],[63.25,158],[63.5,157.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[25,44.25],[0,0],[-55,45.75],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-51,12.25]],"v":[[-62,77.5],[-60.75,112.25],[60.75,151.25],[59.5,132.25]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[25,44.25],[0,0],[-58.5,22],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-51,12.25]],"v":[[-64.25,50.75],[-60.75,80.5],[58.25,128],[60.75,101.25]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[25,39],[0,0],[-58.5,22],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-51,12.25]],"v":[[-65.5,33.75],[-63.75,59.5],[59,101.75],[61.5,76.75]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[25,39],[0,0],[-55.75,26.75],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-51,12.25]],"v":[[-69.5,30],[-65,49],[61,82.75],[63.75,62.25]],"c":true}]},{"t":25,"s":[{"i":[[25,39],[0,0],[-55.75,26.75],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-51,12.25]],"v":[[-69.5,30],[-65,54.75],[62.25,76.25],[63.75,62.25]],"c":true}]}]},"nm":"Path 4","hd":false},{"ty":"fl","c":{"a":0,"k":[0.109803921569,0.576470588235,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":30,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"rocket body","parent":4,"sr":1,"ks":{"p":{"a":0,"k":[0,71.389,0]},"a":{"a":0,"k":[0,71.389,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[2.222,-10],[7.734,34.804],[0,0]],"o":[[0,0],[-7.676,34.543],[-2.222,-10],[0,0]],"v":[[68.778,-37.222],[59.333,153.333],[-59.333,153.333],[-68.778,-37.222]],"c":true}]},{"t":30,"s":[{"i":[[0,0],[6.667,-12.222],[20.133,36.91],[1.111,16.111]],"o":[[-1.111,16.111],[-20.133,36.911],[-6.667,-12.222],[0,0]],"v":[[76,35.556],[49.333,107.222],[-49.333,107.222],[-76,35.556]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.898039215686,0.898039215686,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":30,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Shape Layer 11","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[283,390,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[257,472,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[283,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":17,"s":[262,427.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[280,323,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[256.5,371.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[279,288,0],"to":[0,0,0],"ti":[0,0,0]},{"t":31,"s":[259,328,0]}]},"a":{"a":0,"k":[-28,116,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":4,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":10,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":11,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":17,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":18,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":24,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":25,"s":[100,100,100]},{"t":31,"s":[20,20,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[56,56]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862745098,0.854901960784,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-28,116]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":4,"op":31,"st":4,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Shape Layer 10","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[255,394,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[257,472,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13,"s":[258,356,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":19,"s":[262,427.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[255,327,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":26,"s":[256.5,371.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":27,"s":[261,292,0],"to":[0,0,0],"ti":[0,0,0]},{"t":33,"s":[259,328,0]}]},"a":{"a":0,"k":[-28,116,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":6,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":12,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":13,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":19,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":20,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":26,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":27,"s":[100,100,100]},{"t":33,"s":[20,20,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[56,56]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862745098,0.854901960784,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-28,116]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":6,"op":33,"st":6,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"Shape Layer 9","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[231,397,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[252,494,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[232,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":21,"s":[254,428,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[230,336,0],"to":[0,0,0],"ti":[0,0,0]},{"t":28,"s":[256.5,371.5,0]}]},"a":{"a":0,"k":[-28,116,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":8,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":14,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":15,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":21,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":22,"s":[100,100,100]},{"t":28,"s":[20,20,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[56,56]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862745098,0.854901960784,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-28,116]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":8,"op":28,"st":8,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"Shape Layer 8","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[283,390,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[257,472,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9,"s":[283,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[262,427.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[280,323,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[256.5,371.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23,"s":[279,288,0],"to":[0,0,0],"ti":[0,0,0]},{"t":29,"s":[259,328,0]}]},"a":{"a":0,"k":[-28,116,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":2,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":8,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":9,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":15,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":16,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":22,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":23,"s":[100,100,100]},{"t":29,"s":[20,20,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[56,56]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862745098,0.854901960784,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-28,116]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":2,"op":29,"st":2,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[231,397,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[252,494,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":7,"s":[232,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13,"s":[254,428,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[230,336,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[256.5,371.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":21,"s":[234,298,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":27,"s":[259,328,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[239,262,0],"to":[0,0,0],"ti":[0,0,0]},{"t":33,"s":[264,326,0]}]},"a":{"a":0,"k":[-28,116,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":6,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":7,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":13,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":14,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":20,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":21,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":27,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":28,"s":[100,100,100]},{"t":33,"s":[20,20,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[56,56]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862745098,0.854901960784,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-28,116]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":34,"st":0,"bm":0}]},{"id":"comp_6","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"pink","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[100]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[100]},{"t":100,"s":[0]}]},"p":{"a":0,"k":[245.75,225.75,0]},"a":{"a":0,"k":[2.75,-17.25,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.629,0.629,0.629],"y":[0,0,0]},"t":30,"s":[43.235,43.235,100]},{"t":40,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[1.312,8.525],[3.244,0.766],[0.278,-4.348]],"o":[[-1.312,-8.525],[-5.358,-1.265],[-0.278,4.348]],"v":[[6.13,-24.571],[8.951,-66.768],[5.014,-23.638]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[3.55,23.061],[3.244,0.766],[0.753,-11.762]],"o":[[-3.55,-23.061],[-5.358,-1.265],[-0.753,11.762]],"v":[[11.788,-37.733],[14.47,-92.154],[8.769,-35.211]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.113,0.736],[3.244,0.766],[0.024,-0.376]],"o":[[-0.113,-0.736],[-5.358,-1.265],[-0.024,0.376]],"v":[[1.251,-14.165],[19.643,-137.56],[1.155,-14.085]],"c":true}]},{"t":100,"s":[{"i":[[0.002,0.014],[3.244,0.766],[0,-0.007]],"o":[[-0.002,-0.014],[-5.358,-1.265],[0,0.007]],"v":[[29.568,-215.114],[32.47,-250.154],[29.566,-215.113]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-0.775,14.209],[6.351,4.603],[2.652,-16.185]],"o":[[0.775,-14.209],[-4.056,-2.94],[-2.652,16.185]],"v":[[7.231,-23.969],[18.873,-61.186],[6.13,-24.571]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-2.096,38.439],[6.351,4.603],[7.173,-43.783]],"o":[[2.096,-38.439],[-4.056,-2.94],[-7.173,43.783]],"v":[[14.767,-36.105],[37.637,-108.647],[11.788,-37.733]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.067,1.227],[6.351,4.603],[0.229,-1.398]],"o":[[0.067,-1.227],[-4.056,-2.94],[-0.229,1.398]],"v":[[1.932,-14.242],[49.85,-146.868],[1.837,-14.294]],"c":true}]},{"t":100,"s":[{"i":[[-0.001,0.023],[6.351,4.603],[0.004,-0.026]],"o":[[0.001,-0.023],[-4.056,-2.94],[-0.004,0.026]],"v":[[75.069,-225.113],[80.137,-241.647],[75.068,-225.114]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.749,6.829],[6.31,5.609],[1.359,-6.48]],"o":[[-0.749,-6.829],[-3.554,-3.159],[-1.359,6.48]],"v":[[8.596,-23.217],[31.24,-73.607],[7.231,-23.969]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[2.026,18.473],[6.31,5.609],[3.677,-17.531]],"o":[[-2.026,-18.473],[-3.554,-3.159],[-3.677,17.531]],"v":[[18.46,-34.07],[47.481,-104.355],[14.767,-36.105]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.065,0.59],[6.31,5.609],[0.117,-0.56]],"o":[[-0.065,-0.59],[-3.554,-3.159],[-0.117,0.56]],"v":[[2.166,-13.836],[63.43,-142.576],[2.048,-13.901]],"c":true}]},{"t":100,"s":[{"i":[[0.001,0.011],[6.31,5.609],[0.002,-0.011]],"o":[[-0.001,-0.011],[-3.554,-3.159],[-0.002,0.011]],"v":[[84.072,-198.612],[102.981,-237.355],[84.069,-198.613]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-6.199,15.089],[5.581,6.595],[12.758,-17.563]],"o":[[6.199,-15.089],[-2.72,-3.214],[-12.758,17.563]],"v":[[9.486,-22.203],[48.882,-61.987],[8.281,-23.532]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-16.769,40.819],[5.581,6.595],[34.513,-47.512]],"o":[[16.769,-40.819],[-2.72,-3.214],[-34.513,47.512]],"v":[[21.719,-30.475],[103.282,-109.764],[18.46,-34.07]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.535,1.303],[5.581,6.595],[1.102,-1.517]],"o":[[0.535,-1.303],[-2.72,-3.214],[-1.102,1.517]],"v":[[3.204,-13.341],[122.823,-131.604],[3.099,-13.456]],"c":true}]},{"t":100,"s":[{"i":[[-0.01,0.025],[5.581,6.595],[0.021,-0.029]],"o":[[0.01,-0.025],[-2.72,-3.214],[-0.021,0.029]],"v":[[156.574,-169.11],[171.282,-185.764],[156.572,-169.112]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-4.223,9.945],[2.258,6.104],[6.726,-10.205]],"o":[[4.223,-9.945],[-1.817,-4.912],[-6.726,10.205]],"v":[[11.177,-20.424],[48.778,-58.677],[9.801,-21.888]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-11.423,26.904],[2.258,6.104],[18.195,-27.606]],"o":[[11.423,-26.904],[-1.817,-4.912],[-18.195,27.606]],"v":[[25.439,-26.516],[74.48,-79.649],[21.719,-30.475]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.365,0.859],[2.258,6.104],[0.581,-0.882]],"o":[[0.365,-0.859],[-1.817,-4.912],[-0.581,0.882]],"v":[[3.264,-12.932],[105.66,-107.524],[3.146,-13.058]],"c":true}]},{"t":100,"s":[{"i":[[-0.007,0.016],[2.258,6.104],[0.011,-0.017]],"o":[[0.007,-0.016],[-1.817,-4.912],[-0.011,0.017]],"v":[[152.076,-147.108],[182.98,-176.649],[152.074,-147.11]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":5,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-3.334,6.648],[-1.799,5.518],[9.673,-9.931]],"o":[[3.334,-6.648],[1.878,-5.758],[-9.673,9.931]],"v":[[12.897,-19.193],[48.972,-44.204],[11.177,-20.424]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-9.018,17.983],[-1.799,5.518],[26.167,-26.865]],"o":[[9.018,-17.983],[1.878,-5.758],[-26.167,26.865]],"v":[[30.094,-23.186],[81.927,-62.022],[25.439,-26.516]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.288,0.574],[-1.799,5.518],[0.836,-0.858]],"o":[[0.288,-0.574],[1.878,-5.758],[-0.836,0.858]],"v":[[3.883,-12.491],[120.292,-83.862],[3.734,-12.597]],"c":true}]},{"t":100,"s":[{"i":[[-0.005,0.011],[-1.799,5.518],[0.016,-0.016]],"o":[[0.005,-0.011],[1.878,-5.758],[-0.016,0.016]],"v":[[188.579,-121.106],[215.427,-138.022],[188.576,-121.108]],"c":true}]}]},"nm":"Path 6","hd":false},{"ind":6,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-9.869,8.704],[-3.432,7.058],[12.009,-7.852]],"o":[[9.869,-8.704],[2.981,-6.13],[-12.009,7.852]],"v":[[12.832,-16.628],[74.374,-42.527],[12.897,-19.193]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-26.696,23.547],[-3.432,7.058],[32.485,-21.24]],"o":[[26.696,-23.547],[2.981,-6.13],[-32.485,21.24]],"v":[[29.916,-16.247],[117.42,-56.876],[30.094,-23.186]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.852,0.752],[-3.432,7.058],[1.037,-0.678]],"o":[[0.852,-0.752],[2.981,-6.13],[-1.037,0.678]],"v":[[3.897,-11.786],[151.618,-69.521],[3.902,-12.008]],"c":true}]},{"t":100,"s":[{"i":[[-0.016,0.014],[-3.432,7.058],[0.02,-0.013]],"o":[[0.016,-0.014],[2.981,-6.13],[-0.02,0.013]],"v":[[190.079,-83.601],[236.42,-100.876],[190.079,-83.606]],"c":true}]}]},"nm":"Path 7","hd":false},{"ind":7,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-14.199,3.553],[-2.249,4.962],[22.38,-4.873]],"o":[[22.287,-5.577],[4.049,-8.935],[-22.38,4.873]],"v":[[10.861,-15.141],[88.919,-15.854],[12.516,-16.944]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-38.412,9.611],[-2.249,4.962],[60.542,-13.183]],"o":[[60.291,-15.085],[4.049,-8.935],[-60.542,13.183]],"v":[[25.439,-11.372],[154.986,-15.381],[29.916,-16.247]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-1.227,0.307],[-2.249,4.962],[1.933,-0.421]],"o":[[1.925,-0.482],[4.049,-8.935],[-1.933,0.421]],"v":[[3.934,-10.704],[182.862,-13.944],[4.077,-10.859]],"c":true}]},{"t":100,"s":[{"i":[[-0.023,0.006],[-2.249,4.962],[0.036,-0.008]],"o":[[0.036,-0.009],[4.049,-8.935],[-0.036,0.008]],"v":[[204.076,-11.599],[251.986,-10.381],[204.079,-11.601]],"c":true}]}]},"nm":"Path 8","hd":false},{"ind":8,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-4.711,2.16],[-0.839,0.958],[12.251,-0.88]],"o":[[4.711,-2.16],[1.348,-1.539],[-12.251,0.88]],"v":[[9.079,-12.869],[39.383,-12.388],[10.861,-14.826]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-12.744,5.843],[-0.839,0.958],[33.14,-2.38]],"o":[[12.744,-5.843],[1.348,-1.539],[-33.14,2.38]],"v":[[20.619,-6.076],[78.015,-8.131],[25.439,-11.372]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.407,0.187],[-0.839,0.958],[1.058,-0.076]],"o":[[0.407,-0.187],[1.348,-1.539],[-1.058,0.076]],"v":[[4.224,-10.251],[128.305,-2.239],[4.378,-10.421]],"c":true}]},{"t":100,"s":[{"i":[[-0.008,0.004],[-0.839,0.958],[0.02,-0.001]],"o":[[0.008,-0.004],[1.348,-1.539],[-0.02,0.001]],"v":[[238.573,10.405],[253.015,12.369],[238.576,10.401]],"c":true}]}]},"nm":"Path 9","hd":false},{"ind":9,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-19.702,-2.401],[-2.482,3.154],[14.29,0.398]],"o":[[19.702,2.401],[2.595,-3.297],[-14.29,-0.398]],"v":[[9.698,-12.307],[64.597,6.088],[9.395,-13.184]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-53.298,-6.494],[-2.482,3.154],[38.657,1.077]],"o":[[53.298,6.494],[2.595,-3.297],[-38.657,-1.077]],"v":[[21.439,-3.705],[93.137,16.968],[20.619,-6.076]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-1.702,-0.207],[-2.482,3.154],[1.234,0.034]],"o":[[1.702,0.207],[2.595,-3.297],[-1.234,-0.034]],"v":[[3.71,-9.493],[135.812,35.216],[3.683,-9.569]],"c":true}]},{"t":100,"s":[{"i":[[-0.032,-0.004],[-2.482,3.154],[0.023,0.001]],"o":[[0.032,0.004],[2.595,-3.297],[-0.023,-0.001]],"v":[[196.573,63.406],[241.637,80.468],[196.573,63.405]],"c":true}]}]},"nm":"Path 10","hd":false},{"ind":10,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-9.253,-3.386],[-5.064,4.293],[16.792,8.51]],"o":[[9.253,3.386],[4.126,-3.498],[-16.792,-8.51]],"v":[[11.313,-9.614],[54.159,17.809],[9.698,-11.992]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-25.032,-9.16],[-5.064,4.293],[45.426,23.022]],"o":[[25.032,9.16],[4.126,-3.498],[-45.426,-23.022]],"v":[[25.808,2.729],[103.197,50.764],[21.439,-3.705]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.799,-0.292],[-5.064,4.293],[1.451,0.735]],"o":[[0.799,0.292],[4.126,-3.498],[-1.451,-0.735]],"v":[[3.881,-8.702],[137.251,71.886],[3.742,-8.908]],"c":true}]},{"t":100,"s":[{"i":[[-0.015,-0.006],[-5.064,4.293],[0.027,0.014]],"o":[[0.015,0.006],[4.126,-3.498],[-0.027,-0.014]],"v":[[199.076,108.91],[221.697,124.264],[199.073,108.906]],"c":true}]}]},"nm":"Path 11","hd":false},{"ind":11,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-6.526,-3.215],[-3.502,3.162],[9.671,6.481]],"o":[[6.526,3.215],[3.502,-3.162],[-9.671,-6.481]],"v":[[10.491,-8.994],[73.057,40.569],[11.313,-9.929]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-17.653,-8.697],[-3.502,3.162],[26.163,17.532]],"o":[[17.653,8.697],[3.502,-3.162],[-26.163,-17.532]],"v":[[23.585,5.258],[96.236,63.117],[25.808,2.729]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.564,-0.278],[-3.502,3.162],[0.835,0.56]],"o":[[0.564,0.278],[3.502,-3.162],[-0.835,-0.56]],"v":[[3.102,-8.57],[126.267,90.274],[3.173,-8.651]],"c":true}]},{"t":100,"s":[{"i":[[-0.011,-0.005],[-3.502,3.162],[0.016,0.011]],"o":[[0.011,0.005],[3.502,-3.162],[-0.016,-0.011]],"v":[[144.075,112.911],[200.736,157.617],[144.076,112.91]],"c":true}]}]},"nm":"Path 12","hd":false},{"ind":12,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-13.475,-10.482],[-4.458,3.463],[10.073,7.849]],"o":[[13.475,10.482],[4.458,-3.463],[-10.073,-7.849]],"v":[[9.502,-7.951],[52.87,45.892],[10.491,-8.678]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-36.453,-28.355],[-4.458,3.463],[27.249,21.233]],"o":[[36.453,28.355],[4.458,-3.463],[-27.249,-21.233]],"v":[[20.909,7.226],[94.655,93.669],[23.585,5.258]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-1.164,-0.905],[-4.458,3.463],[0.87,0.678]],"o":[[1.164,0.905],[4.458,-3.463],[-0.87,-0.678]],"v":[[3.049,-7.986],[116.926,120.825],[3.134,-8.049]],"c":true}]},{"t":100,"s":[{"i":[[-0.022,-0.017],[-4.458,3.463],[0.016,0.013]],"o":[[0.022,0.017],[4.458,-3.463],[-0.016,-0.013]],"v":[[146.573,153.413],[172.155,188.169],[146.575,153.411]],"c":true}]}]},"nm":"Path 13","hd":false},{"ind":13,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-4.775,-3.087],[-3.488,2.449],[10.229,14.193]],"o":[[4.775,3.087],[3.488,-2.449],[-10.229,-14.193]],"v":[[7.167,-8.135],[48.091,56.332],[9.187,-7.951]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-12.917,-8.352],[-3.488,2.449],[27.671,38.395]],"o":[[12.917,8.352],[3.488,-2.449],[-27.671,-38.395]],"v":[[15.445,6.729],[62.124,74.78],[20.909,7.226]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.412,-0.267],[-3.488,2.449],[0.884,1.226]],"o":[[0.412,0.267],[3.488,-2.449],[-0.884,-1.226]],"v":[[2.321,-8.079],[84.827,113.576],[2.495,-8.063]],"c":true}]},{"t":100,"s":[{"i":[[-0.008,-0.005],[-3.488,2.449],[0.017,0.023]],"o":[[0.008,0.005],[3.488,-2.449],[-0.017,-0.023]],"v":[[103.57,147.412],[141.124,209.78],[103.573,147.413]],"c":true}]}]},"nm":"Path 14","hd":false},{"ind":14,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-5.362,-6.334],[-2.659,0.857],[3.694,6.735]],"o":[[5.361,6.334],[6.495,-2.093],[-3.694,-6.735]],"v":[[5.102,-9.295],[26.477,35.494],[7.167,-8.45]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-14.504,-17.136],[-2.659,0.857],[9.994,18.219]],"o":[[14.504,17.136],[6.495,-2.093],[-9.994,-18.219]],"v":[[9.859,4.445],[41.772,64.349],[15.445,6.729]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.463,-0.547],[-2.659,0.857],[0.319,0.582]],"o":[[0.463,0.547],[6.495,-2.093],[-0.319,-0.582]],"v":[[2.027,-7.553],[61.313,111.91],[2.205,-7.48]],"c":true}]},{"t":100,"s":[{"i":[[-0.009,-0.01],[-2.659,0.857],[0.006,0.011]],"o":[[0.009,0.01],[6.495,-2.093],[-0.006,-0.011]],"v":[[94.566,193.911],[109.772,229.849],[94.57,193.912]],"c":true}]}]},"nm":"Path 15","hd":false},{"ind":15,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-0.43,-4.038],[-5.792,0.94],[0.557,8.126]],"o":[[0.43,4.038],[3.71,-0.602],[-0.557,-8.126]],"v":[[3.69,-10.876],[17.077,55.965],[5.102,-9.295]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-1.164,-10.924],[-5.792,0.94],[1.507,21.981]],"o":[[1.164,10.924],[3.71,-0.602],[-1.507,-21.981]],"v":[[6.039,0.167],[22.28,71.891],[9.859,4.445]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[-0.037,-0.349],[-5.792,0.94],[0.048,0.702]],"o":[[0.037,0.349],[3.71,-0.602],[-0.048,-0.702]],"v":[[1.242,-7.799],[31.62,122.9],[1.364,-7.663]],"c":true}]},{"t":100,"s":[{"i":[[-0.001,-0.007],[-5.792,0.94],[0.001,0.013]],"o":[[0.001,0.007],[3.71,-0.602],[-0.001,-0.013]],"v":[[43.064,185.408],[54.78,249.391],[43.066,185.411]],"c":true}]}]},"nm":"Path 16","hd":false},{"ind":16,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.268,-1.072],[-8.766,2.77],[-1.453,20.282]],"o":[[-0.268,1.072],[8.956,-2.83],[1.453,-20.282]],"v":[[2.258,-8.649],[11.352,48.031],[4.005,-10.876]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[0.726,-2.901],[-8.766,2.77],[-3.93,54.866]],"o":[[-0.726,2.901],[8.956,-2.83],[3.93,-54.866]],"v":[[1.312,6.192],[20.655,115.675],[6.039,0.167]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.023,-0.093],[-8.766,2.77],[-0.125,1.752]],"o":[[-0.023,0.093],[8.956,-2.83],[0.125,-1.752]],"v":[[0.956,-7.06],[24.966,155.333],[1.106,-7.252]],"c":true}]},{"t":100,"s":[{"i":[[0,-0.002],[-8.766,2.77],[-0.002,0.033]],"o":[[0,0.002],[8.956,-2.83],[0.002,-0.033]],"v":[[32.561,227.912],[35.655,253.675],[32.564,227.908]],"c":true}]}]},"nm":"Path 17","hd":false},{"ind":17,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.674,-5.118],[-9.34,-10.304],[-5.656,20.469]],"o":[[-0.674,5.118],[5.335,5.886],[5.656,-20.469]],"v":[[0.319,-7.862],[-7.754,79.004],[2.258,-8.649]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[1.822,-13.845],[-9.34,-10.304],[-15.299,55.372]],"o":[[-1.822,13.845],[5.335,5.886],[15.299,-55.372]],"v":[[-3.933,8.322],[-10.119,142.549],[1.312,6.192]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.058,-0.442],[-9.34,-10.304],[-0.489,1.768]],"o":[[-0.058,0.442],[5.335,5.886],[0.489,-1.768]],"v":[[0.157,-7.436],[-14.573,174.16],[0.325,-7.504]],"c":true}]},{"t":100,"s":[{"i":[[0.001,-0.008],[-9.34,-10.304],[-0.009,0.033]],"o":[[-0.001,0.008],[5.335,5.886],[0.009,-0.033]],"v":[[-16.442,193.413],[-25.619,252.549],[-16.439,193.412]],"c":true}]}]},"nm":"Path 18","hd":false},{"ind":18,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[1.134,-4.491],[-8.529,-8.273],[-6.696,18.034]],"o":[[-1.134,4.491],[3.166,3.071],[6.696,-18.034]],"v":[[-1.761,-9.64],[-13.598,51.106],[0.003,-7.546]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[3.068,-12.148],[-8.529,-8.273],[-18.113,48.786]],"o":[[-3.068,12.148],[3.166,3.071],[18.113,-48.786]],"v":[[-8.707,2.658],[-24.162,110.866],[-3.933,8.322]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.098,-0.388],[-8.529,-8.273],[-0.578,1.558]],"o":[[-0.098,0.388],[3.166,3.071],[0.578,-1.558]],"v":[[-0.446,-7.378],[-34.795,149.374],[-0.293,-7.198]],"c":true}]},{"t":100,"s":[{"i":[[0.002,-0.007],[-8.529,-8.273],[-0.011,0.029]],"o":[[-0.002,0.007],[3.166,3.071],[0.011,-0.029]],"v":[[-51.445,211.91],[-61.162,244.866],[-51.442,211.913]],"c":true}]}]},"nm":"Path 19","hd":false},{"ind":19,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[9.217,-13.161],[-2.977,-4.918],[-5.725,7.174]],"o":[[-9.217,13.161],[1.721,2.843],[5.725,-7.174]],"v":[[-1.154,-12.499],[-42.53,56.34],[-1.761,-9.64]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[24.934,-35.602],[-2.977,-4.918],[-15.488,19.406]],"o":[[-24.934,35.602],[1.721,2.843],[15.488,-19.406]],"v":[[-7.064,-5.076],[-47.418,71.95],[-8.707,2.658]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.796,-1.137],[-2.977,-4.918],[-0.495,0.62]],"o":[[-0.796,1.137],[1.721,2.843],[0.495,-0.62]],"v":[[-0.876,-8.404],[-71.414,113.188],[-0.929,-8.157]],"c":true}]},{"t":100,"s":[{"i":[[0.015,-0.021],[-2.977,-4.918],[-0.009,0.012]],"o":[[-0.015,0.021],[1.721,2.843],[0.009,-0.012]],"v":[[-88.944,151.405],[-130.918,215.45],[-88.945,151.41]],"c":true}]}]},"nm":"Path 20","hd":false},{"ind":20,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[7.626,-7.899],[-8.084,-5.313],[-13.235,7.936]],"o":[[-2.855,2.957],[3.306,2.173],[10.979,-6.583]],"v":[[-4.169,-12.723],[-45.434,37.243],[-0.839,-12.499]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[20.63,-21.369],[-8.084,-5.313],[-35.802,21.467]],"o":[[-7.724,8],[3.306,2.173],[29.699,-17.808]],"v":[[-16.072,-5.684],[-88.796,85.335],[-7.064,-5.076]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.659,-0.682],[-8.084,-5.313],[-1.143,0.686]],"o":[[-0.247,0.255],[3.306,2.173],[0.948,-0.569]],"v":[[-1.962,-8.308],[-113.223,114.36],[-1.674,-8.288]],"c":true}]},{"t":100,"s":[{"i":[[0.012,-0.013],[-8.084,-5.313],[-0.022,0.013]],"o":[[-0.005,0.005],[3.306,2.173],[0.018,-0.011]],"v":[[-150.949,160.405],[-173.796,186.335],[-150.944,160.405]],"c":true}]}]},"nm":"Path 21","hd":false},{"ind":21,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[14.117,-10.21],[-5.628,-14.818],[-24.822,15.796]],"o":[[-14.086,10.188],[3.081,4.733],[21.924,-13.951]],"v":[[-3.765,-15.075],[-82.448,63.651],[-4.484,-12.723]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[38.189,-27.621],[0.797,-25.979],[-67.149,42.73]],"o":[[-38.106,27.561],[-0.163,5.311],[59.307,-37.74]],"v":[[-14.127,-12.046],[-117.611,102.283],[-16.072,-5.684]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[1.219,-0.882],[-2.13,-20.894],[-2.144,1.364]],"o":[[-1.217,0.88],[1.315,5.048],[1.894,-1.205]],"v":[[-1.552,-9.148],[-137.871,121.393],[-1.614,-8.945]],"c":true}]},{"t":100,"s":[{"i":[[0.023,-0.017],[-9.389,-8.283],[-0.04,0.026]],"o":[[-0.023,0.017],[4.981,4.394],[0.036,-0.023]],"v":[[-123.948,110.901],[-188.111,168.783],[-123.949,110.905]],"c":true}]}]},"nm":"Path 22","hd":false},{"ind":22,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[2.794,-1.349],[0.656,-8.742],[-9.144,3.299]],"o":[[-0.388,0.187],[-0.279,3.717],[9.144,-3.298]],"v":[[-4.357,-15.318],[-39.206,7.131],[-3.283,-15.062]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[7.56,-3.648],[1.507,-8.661],[-24.737,8.919]],"o":[[-1.051,0.507],[-0.641,3.682],[24.737,-8.919]],"v":[[-17.032,-12.737],[-65.957,17.111],[-14.127,-12.046]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.241,-0.117],[1.12,-8.698],[-0.79,0.285]],"o":[[-0.034,0.016],[-0.476,3.698],[0.79,-0.285]],"v":[[-2.581,-9.26],[-111.172,47.264],[-2.489,-9.238]],"c":true}]},{"t":100,"s":[{"i":[[0.004,-0.003],[0.158,-8.79],[-0.014,0.008]],"o":[[-0.001,0],[-0.067,3.737],[0.014,-0.008]],"v":[[-196.685,103.922],[-223.294,122.037],[-196.683,103.922]],"c":true}]}]},"nm":"Path 23","hd":false},{"ind":23,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.652,-0.002],[9.621,-23.195],[-30.797,11.034]],"o":[[-0.652,0.002],[1.201,4.343],[30.797,-11.034]],"v":[[-5.938,-16.534],[-91.485,36.162],[-4.524,-15.646]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[1.764,-0.005],[34.94,-49.93],[-83.311,29.848]],"o":[[-1.764,0.005],[-2.448,3.499],[83.311,-29.848]],"v":[[-20.857,-15.139],[-161.337,74.163],[-17.032,-12.737]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.056,0],[23.404,-37.749],[-2.66,0.953]],"o":[[-0.056,0],[-0.786,3.883],[2.66,-0.953]],"v":[[-2.437,-9.678],[-180.879,84.077],[-2.314,-9.602]],"c":true}]},{"t":100,"s":[{"i":[[0.001,0],[-5.203,-7.542],[-0.05,0.018]],"o":[[-0.001,0],[3.337,4.837],[0.05,-0.018]],"v":[[-175.952,77.399],[-229.337,108.663],[-175.95,77.401]],"c":true}]}]},"nm":"Path 24","hd":false},{"ind":24,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[3.079,-0.51],[0.18,-8.491],[-14.645,0.239]],"o":[[-3.079,0.51],[-0.099,4.7],[14.645,-0.239]],"v":[[-8.996,-17.288],[-60.714,-4.682],[-6.253,-16.534]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[8.33,-1.38],[0.18,-8.491],[-39.617,0.647]],"o":[[-8.33,1.38],[-0.099,4.7],[39.617,-0.647]],"v":[[-28.279,-17.178],[-119.213,6.829],[-20.857,-15.139]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.266,-0.044],[0.18,-8.491],[-1.265,0.021]],"o":[[-0.266,0.044],[-0.099,4.7],[1.265,-0.021]],"v":[[-3.288,-10.423],[-157.506,13.654],[-3.051,-10.357]],"c":true}]},{"t":100,"s":[{"i":[[0.005,-0.001],[0.18,-8.491],[-0.024,0]],"o":[[-0.005,0.001],[-0.099,4.7],[0.024,0]],"v":[[-223.707,24.648],[-252.463,30.579],[-223.702,24.649]],"c":true}]}]},"nm":"Path 25","hd":false},{"ind":25,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[5.002,-0.018],[4.376,-11.742],[-21.034,-0.505]],"o":[[-5.002,0.018],[-1,2.682],[21.034,0.505]],"v":[[-8.482,-18.09],[-80.041,-5.651],[-8.996,-16.972]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[13.533,-0.05],[4.376,-11.742],[-56.9,-1.366]],"o":[[-13.533,0.05],[-1,2.682],[56.9,1.366]],"v":[[-26.888,-20.202],[-133.179,-1.393],[-28.279,-17.178]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.432,-0.002],[4.376,-11.742],[-1.817,-0.044]],"o":[[-0.432,0.002],[-1,2.682],[1.817,0.044]],"v":[[-2.957,-10.802],[-167.52,2.271],[-3.002,-10.706]],"c":true}]},{"t":100,"s":[{"i":[[0.008,0],[4.376,-11.742],[-0.034,-0.001]],"o":[[-0.008,0],[-1,2.682],[0.034,0.001]],"v":[[-201.456,2.646],[-252.679,11.357],[-201.457,2.648]],"c":true}]}]},"nm":"Path 26","hd":false},{"ind":26,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[4.015,0.648],[9.285,-5.938],[-12.687,-1.577]],"o":[[-1.837,-0.296],[-5.457,3.49],[12.687,1.577]],"v":[[-7.193,-19.109],[-55.539,-18.566],[-8.167,-18.09]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[10.862,1.752],[9.285,-5.938],[-34.32,-4.266]],"o":[[-4.969,-0.802],[-5.457,3.49],[34.32,4.266]],"v":[[-24.253,-22.959],[-119.714,-21.877],[-26.888,-20.202]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.347,0.056],[9.285,-5.938],[-1.096,-0.136]],"o":[[-0.159,-0.026],[-5.457,3.49],[1.096,0.136]],"v":[[-3.256,-11.254],[-157.719,-22.883],[-3.34,-11.166]],"c":true}]},{"t":100,"s":[{"i":[[0.007,0.001],[9.285,-5.938],[-0.021,-0.003]],"o":[[-0.003,0],[-5.457,3.49],[0.021,0.003]],"v":[[-231.204,-25.606],[-251.964,-25.377],[-231.206,-25.604]],"c":true}]}]},"nm":"Path 27","hd":false},{"ind":27,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[4.393,1.499],[5.601,-5.319],[-6.035,-3.074]],"o":[[-4.393,-1.499],[-2.043,1.94],[7.22,3.678]],"v":[[-5.608,-19.397],[-48.68,-27.903],[-7.508,-19.109]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[11.885,4.055],[5.601,-5.319],[-16.326,-8.316]],"o":[[-11.885,-4.055],[-2.043,1.94],[19.531,9.948]],"v":[[-19.113,-23.736],[-77.851,-34.052],[-24.253,-22.959]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.38,0.13],[5.601,-5.319],[-0.521,-0.266]],"o":[[-0.38,-0.129],[-2.043,1.94],[0.624,0.318]],"v":[[-2.828,-11.723],[-125.77,-43.679],[-2.992,-11.698]],"c":true}]},{"t":100,"s":[{"i":[[0.007,0.002],[5.601,-5.319],[-0.01,-0.005]],"o":[[-0.007,-0.002],[-2.043,1.94],[0.012,0.006]],"v":[[-210.701,-60.106],[-244.601,-67.552],[-210.704,-60.106]],"c":true}]}]},"nm":"Path 28","hd":false},{"ind":28,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[5.221,2.81],[2.362,-11.3],[-20.382,-17.294]],"o":[[-5.221,-2.81],[-1.358,6.576],[20.382,17.294]],"v":[[-7.193,-23.937],[-71.1,-48.972],[-5.293,-19.397]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[14.123,7.602],[4.156,-15.533],[-55.138,-46.784]],"o":[[-14.123,-7.602],[-2.317,8.659],[55.138,46.784]],"v":[[-24.253,-36.018],[-168.703,-94.857],[-19.113,-23.736]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.451,0.243],[3.339,-13.604],[-1.761,-1.494]],"o":[[-0.451,-0.243],[-1.88,7.71],[1.761,1.494]],"v":[[-2.925,-12.804],[-184.078,-101.754],[-2.761,-12.412]],"c":true}]},{"t":100,"s":[{"i":[[0.008,0.005],[1.312,-8.822],[-0.033,-0.028]],"o":[[-0.008,-0.005],[-0.797,5.357],[0.033,0.028]],"v":[[-205.454,-113.613],[-222.203,-118.857],[-205.451,-113.606]],"c":true}]}]},"nm":"Path 29","hd":false},{"ind":29,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[7.26,4.445],[7.011,-4.124],[-10.385,-10.766]],"o":[[-0.326,-0.199],[-7.011,4.124],[16.897,17.517]],"v":[[-5.801,-24.959],[-56.419,-56.973],[-7.193,-23.937]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[19.64,12.023],[7.011,-4.124],[-28.094,-29.123]],"o":[[-0.881,-0.54],[-7.011,4.124],[45.711,47.385]],"v":[[-20.488,-38.784],[-98.519,-86.459],[-24.253,-36.018]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.627,0.384],[7.011,-4.124],[-0.897,-0.93]],"o":[[-0.028,-0.017],[-7.011,4.124],[1.46,1.513]],"v":[[-2.277,-13.13],[-127.401,-106.145],[-2.397,-13.042]],"c":true}]},{"t":100,"s":[{"i":[[0.012,0.007],[7.011,-4.124],[-0.017,-0.018]],"o":[[-0.001,0],[-7.011,4.124],[0.028,0.029]],"v":[[-164.452,-132.115],[-199.019,-154.959],[-164.454,-132.113]],"c":true}]}]},"nm":"Path 30","hd":false},{"ind":30,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[23.866,20.95],[-0.775,-4.182],[-8.843,-10.116]],"o":[[-23.866,-20.95],[0.799,4.308],[8.843,10.116]],"v":[[-4.127,-25.389],[-47.359,-57.266],[-6.116,-25.275]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[64.562,56.675],[-0.775,-4.182],[-23.921,-27.366]],"o":[[-64.562,-56.675],[0.799,4.308],[23.921,27.366]],"v":[[-15.108,-39.093],[-105.227,-105.674],[-20.488,-38.784]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[2.062,1.81],[-0.775,-4.182],[-0.764,-0.874]],"o":[[-2.062,-1.81],[0.799,4.308],[0.764,0.874]],"v":[[-2.169,-13.514],[-128.217,-124.928],[-2.341,-13.504]],"c":true}]},{"t":100,"s":[{"i":[[0.039,0.034],[-0.775,-4.182],[-0.014,-0.016]],"o":[[-0.039,-0.034],[0.799,4.308],[0.014,0.016]],"v":[[-169.449,-161.115],[-185.227,-172.674],[-169.452,-161.115]],"c":true}]}]},"nm":"Path 31","hd":false},{"ind":31,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.861,0.835],[6.082,-5.676],[-10.296,-14.684]],"o":[[-0.861,-0.835],[-2.893,2.699],[10.296,14.684]],"v":[[-0.366,-23.104],[-55.133,-75.787],[-4.127,-25.389]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[2.329,2.258],[6.082,-5.676],[-27.852,-39.724]],"o":[[-2.329,-2.258],[-2.893,2.699],[27.852,39.724]],"v":[[-4.933,-32.911],[-79.889,-105.589],[-15.108,-39.093]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.074,0.072],[6.082,-5.676],[-0.889,-1.268]],"o":[[-0.074,-0.072],[-2.893,2.699],[0.889,1.268]],"v":[[-1.214,-13.207],[-103.885,-130.878],[-1.539,-13.404]],"c":true}]},{"t":100,"s":[{"i":[[0.001,0.001],[6.082,-5.676],[-0.017,-0.024]],"o":[[-0.001,-0.001],[-2.893,2.699],[0.017,0.024]],"v":[[-120.443,-152.612],[-163.389,-193.589],[-120.449,-152.615]],"c":true}]}]},"nm":"Path 32","hd":false},{"ind":32,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[3.542,19.181],[3.073,-0.981],[-0.152,-9.722]],"o":[[-3.542,-19.181],[-2.453,0.783],[0.152,9.722]],"v":[[0.539,-22.899],[-14.028,-61.928],[-0.366,-22.788]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[9.581,51.887],[3.073,-0.981],[-0.412,-26.3]],"o":[[-9.581,-51.887],[-2.453,0.783],[0.412,26.3]],"v":[[-2.483,-33.211],[-31.215,-116.327],[-4.933,-32.911]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.306,1.657],[3.073,-0.981],[-0.013,-0.84]],"o":[[-0.306,-1.657],[-2.453,0.783],[0.013,0.84]],"v":[[-0.517,-14.182],[-44.865,-152.105],[-0.596,-14.172]],"c":true}]},{"t":100,"s":[{"i":[[0.006,0.031],[3.073,-0.981],[0,-0.016]],"o":[[-0.006,-0.031],[-2.453,0.783],[0,0.016]],"v":[[-72.441,-227.612],[-78.715,-240.827],[-72.443,-227.612]],"c":true}]}]},"nm":"Path 33","hd":false},{"ind":33,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.512,3.311],[7.408,-2.515],[0.879,-9.578]],"o":[[-0.512,-3.311],[-4.274,1.451],[-0.879,9.578]],"v":[[1.755,-24.526],[-15.758,-89.049],[0.539,-22.899]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[1.386,8.956],[7.408,-2.515],[2.377,-25.91]],"o":[[-1.386,-8.956],[-4.274,1.451],[-2.377,25.91]],"v":[[0.805,-37.612],[-23.169,-137.772],[-2.483,-33.211]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.044,0.286],[7.408,-2.515],[0.076,-0.827]],"o":[[-0.044,-0.286],[-4.274,1.451],[-0.076,0.827]],"v":[[0.051,-14.02],[-30.928,-169.383],[-0.054,-13.879]],"c":true}]},{"t":100,"s":[{"i":[[0.001,0.005],[7.408,-2.515],[0.001,-0.016]],"o":[[-0.001,-0.005],[-4.274,1.451],[-0.001,0.016]],"v":[[-36.439,-204.114],[-50.169,-247.772],[-36.441,-204.112]],"c":true}]}]},"nm":"Path 34","hd":false},{"ind":34,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[1.275,0.477],[4.481,1.632],[2.961,-6.55],[-1.425,0.478]],"o":[[-2.55,-0.954],[-6.936,-2.526],[-1.48,3.275],[1.425,-0.478]],"v":[[5.014,-23.954],[2.623,-65.932],[2.07,-24.842],[3.754,-13.991]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[3.449,1.291],[4.481,1.632],[8.009,-17.719],[-3.855,1.292]],"o":[[-6.899,-2.581],[-6.936,-2.526],[-4.004,8.859],[3.855,-1.292]],"v":[[8.769,-35.211],[4.357,-117.808],[0.805,-37.612],[5.361,-8.261]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":43,"s":[{"i":[[0.11,0.041],[4.481,1.632],[0.256,-0.566],[-0.123,0.041]],"o":[[-0.22,-0.082],[-6.936,-2.526],[-0.128,0.283],[0.123,-0.041]],"v":[[0.788,-14.342],[3.064,-156.748],[0.534,-14.419],[0.679,-13.482]],"c":true}]},{"t":100,"s":[{"i":[[0.002,0.001],[4.481,1.632],[0.005,-0.011],[-0.002,0.001]],"o":[[-0.004,-0.002],[-6.936,-2.526],[-0.002,0.005],[0.002,-0.001]],"v":[[1.066,-235.113],[-0.143,-253.308],[1.061,-235.114],[1.064,-235.097]],"c":true}]}]},"nm":"Path 35","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":7,"k":{"a":0,"k":[0.166,1,1,1,0.291,0.919,0.91,1,0.379,0.837,0.82,1,0.522,0.444,0.645,0.994,0.708,0.05,0.47,0.988,0.865,0.262,0.465,0.992,0.999,0.474,0.461,0.996,0.166,1,0.291,1,0.379,1,0.522,0.9,0.708,0.8,0.865,0.8,0.999,0.8]}},"s":{"a":0,"k":[-12,-18]},"e":{"a":0,"k":[-123.596,-129.596]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-6.793,2.506]},"a":{"a":0,"k":[-6.793,2.506]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":-4,"op":99,"st":-65,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"white splashes precomp","refId":"comp_2","sr":1,"ks":{"p":{"a":0,"k":[243,243,0]},"a":{"a":0,"k":[243,243,0]}},"ao":0,"w":486,"h":486,"ip":-4,"op":99,"st":-5,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"ogonek5","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[1],"y":[0]},"t":30,"s":[100]},{"t":45,"s":[0]}]},"p":{"a":0,"k":[241,232.25,0]},"a":{"a":0,"k":[-51,43,0]},"s":{"a":1,"k":[{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,15.828]},"t":30,"s":[0,0,100]},{"t":45,"s":[151,151,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[164,164]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":7,"k":{"a":0,"k":[0.166,1,1,1,0.291,0.886,0.886,0.99,0.379,0.773,0.773,0.98,0.522,0.386,0.644,0.967,0.708,0,0.516,0.953,0.865,0.203,0.631,0.969,0.999,0.405,0.746,0.984,0.166,1,0.291,1,0.379,1,0.522,0.9,0.708,0.8,0.865,0.8,0.999,0.8]}},"s":{"a":0,"k":[1,0.5]},"e":{"a":0,"k":[-61.596,-53.096]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 8","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,43]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":30,"op":45,"st":-45,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"rose splashes 80%","sr":1,"ks":{"o":{"a":0,"k":80},"p":{"a":0,"k":[283.287,244.6,0]},"a":{"a":0,"k":[40.287,1.6,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[8.458,2.584],[-1.178,1.927]],"o":[[0,0],[1.178,-1.927]],"v":[[47.396,-2.495],[66.132,8.305]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[17.915,5.472],[-2.495,4.082]],"o":[[0,0],[2.494,-4.082]],"v":[[127.416,14.655],[167.1,37.528]],"c":true}]},{"t":100,"s":[{"i":[[2.922,0.892],[-0.407,0.666]],"o":[[0,0],[0.407,-0.666]],"v":[[231.544,79.714],[238.016,83.445]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-7.371,11.75],[-2.605,-2.954]],"o":[[0,0],[2.605,2.954]],"v":[[-19.417,21.61],[-33.613,50.565]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-16.511,26.322],[-5.835,-6.617]],"o":[[0,0],[5.835,6.617]],"v":[[-54.289,78.774],[-86.09,143.637]],"c":true}]},{"t":100,"s":[{"i":[[-2.019,3.219],[-0.714,-0.809]],"o":[[0,0],[0.714,0.809]],"v":[[-120,208.391],[-123.889,216.322]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[4.272,18.752],[3.489,0]],"o":[[0,0],[-2.919,0]],"v":[[13.337,36.564],[18.266,63.997]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[9.5,41.701],[7.758,0]],"o":[[0,0],[-6.491,0]],"v":[[34.44,120.578],[45.401,181.583]],"c":true}]},{"t":100,"s":[{"i":[[1.211,5.316],[0.989,0]],"o":[[0,0],[-0.827,0]],"v":[[49.981,239.625],[51.378,247.402]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[5.357,10.649],[2.065,-1.671]],"o":[[0,0],[-2.092,1.693]],"v":[[27.785,38.344],[37.752,60.836]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[12.682,25.21],[4.889,-3.955]],"o":[[0,0],[-4.953,4.007]],"v":[[73.965,124.33],[97.561,177.576]],"c":true}]},{"t":100,"s":[{"i":[[1.068,2.124],[0.412,-0.333]],"o":[[0,0],[-0.417,0.338]],"v":[[112.746,219.25],[114.734,223.735]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[7.125,6.764],[2.818,-3.288]],"o":[[0,0],[-1.952,2.278]],"v":[[39.186,19.394],[56.431,42.171]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[16.508,15.671],[6.529,-7.617]],"o":[[0,0],[-4.523,5.277]],"v":[[103.804,73.868],[143.76,126.64]],"c":true}]},{"t":100,"s":[{"i":[[1.631,1.548],[0.645,-0.753]],"o":[[0,0],[-0.447,0.521]],"v":[[178.352,168.75],[182.3,173.964]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":5,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[14.003,-18.483],[-2.995,-2.598]],"o":[[0,0],[2.346,2.035]],"v":[[19.896,-54.149],[37.88,-77.508]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[31.33,-41.355],[-6.701,-5.813]],"o":[[0,0],[5.249,4.553]],"v":[[52.828,-125.71],[93.066,-177.974]],"c":true}]},{"t":100,"s":[{"i":[[3.858,-5.092],[-0.825,-0.716]],"o":[[0,0],[0.646,0.561]],"v":[[119.615,-214],[124.57,-220.435]],"c":true}]}]},"nm":"Path 6","hd":false},{"ind":6,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-6.092,-13.223],[3.225,-0.461]],"o":[[0,0],[-3.225,0.461]],"v":[[-11.7,-53.501],[-23.447,-74.576]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-13.694,-29.721],[7.248,-1.036]],"o":[[0,0],[-7.248,1.036]],"v":[[-33.744,-123.528],[-60.149,-170.902]],"c":true}]},{"t":100,"s":[{"i":[[-1.641,-3.562],[0.869,-0.124]],"o":[[0,0],[-0.869,0.124]],"v":[[-92.793,-229.25],[-95.958,-234.928]],"c":true}]}]},"nm":"Path 7","hd":false},{"ty":"fl","c":{"a":0,"k":[0.399169562845,0.676376941157,0.988235294118,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 7","hd":false},{"ty":"tr","p":{"a":0,"k":[40.287,1.6]},"a":{"a":0,"k":[40.287,1.6]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":-4,"op":99,"st":-65,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"rose splashes 40%","sr":1,"ks":{"o":{"a":0,"k":40},"p":{"a":0,"k":[255.462,250.532,0]},"a":{"a":0,"k":[12.462,7.532,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[2.498,-1.717],[-2.802,-3.967]],"o":[[-1.32,0.907],[0,0]],"v":[[-27.909,-69.864],[-23.485,-62.918]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[5.583,-3.836],[-6.261,-8.864]],"o":[[-2.949,2.026],[0,0]],"v":[[-77.365,-168.395],[-67.479,-152.872]],"c":true}]},{"t":100,"s":[{"i":[[0.692,-0.476],[-0.776,-1.099]],"o":[[-0.366,0.251],[0,0]],"v":[[-104.452,-204.425],[-103.226,-202.5]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-4.758,-1.202],[3.089,-1.172]],"o":[[0,0],[-3.089,1.172]],"v":[[-40.584,-41.866],[-51.165,-45.778]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-10.775,-2.722],[6.996,-2.654]],"o":[[0,0],[-6.996,2.654]],"v":[[-111.836,-92.023],[-135.799,-100.881]],"c":true}]},{"t":100,"s":[{"i":[[-1.235,-0.312],[0.802,-0.304]],"o":[[0,0],[-0.802,0.304]],"v":[[-194.867,-116.25],[-197.612,-117.265]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-6.418,17.711],[-4.713,-1.091]],"o":[[0,0],[2.758,0.638]],"v":[[-22.297,35.041],[-30.452,64.628]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-14.835,40.938],[-10.895,-2.522]],"o":[[0,0],[6.375,1.475]],"v":[[-62.088,115.52],[-80.938,183.907]],"c":true}]},{"t":100,"s":[{"i":[[-1.49,4.112],[-1.094,-0.253]],"o":[[0,0],[0.64,0.148]],"v":[[-89,203.172],[-90.893,210.041]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[1.125,2.009],[-1.205,-2.41]],"o":[[-0.644,-1.15],[1.205,2.41]],"v":[[60.208,34.488],[58.28,36.577]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[2.266,4.046],[-2.428,-4.856]],"o":[[-1.297,-2.316],[2.428,4.856]],"v":[[161.46,114.372],[157.576,118.58]],"c":true}]},{"t":100,"s":[{"i":[[0.457,0.815],[-0.489,-0.978]],"o":[[-0.261,-0.467],[0.489,0.978]],"v":[[178.425,143.467],[177.643,144.315]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.378,1.888],[-0.054,-1.294]],"o":[[-0.244,-1.218],[0.054,1.294]],"v":[[57.201,-37.513],[53.318,-36.704]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[0.721,3.602],[-0.103,-2.47]],"o":[[-0.465,-2.323],[0.103,2.47]],"v":[[152.987,-80.688],[145.577,-79.144]],"c":true}]},{"t":100,"s":[{"i":[[0.177,0.884],[-0.025,-0.606]],"o":[[-0.114,-0.57],[0.025,0.606]],"v":[[207.62,-96.485],[205.801,-96.106]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":5,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-0.033,1.484],[-0.082,-1.894]],"o":[[0.031,-1.392],[0.082,1.894]],"v":[[53.595,-30.294],[48.408,-29.669]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-0.074,3.328],[-0.185,-4.247]],"o":[[0.069,-3.121],[0.185,4.247]],"v":[[143.792,-61.221],[132.159,-59.821]],"c":true}]},{"t":100,"s":[{"i":[[-0.009,0.404],[-0.022,-0.516]],"o":[[0.008,-0.379],[0.022,0.516]],"v":[[216.285,-76.436],[214.873,-76.266]],"c":true}]}]},"nm":"Path 6","hd":false},{"ty":"fl","c":{"a":0,"k":[0.4,0.674509803922,0.988235294118,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 6","hd":false},{"ty":"tr","p":{"a":0,"k":[12.462,7.532]},"a":{"a":0,"k":[12.462,7.532]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":-4,"op":99,"st":-65,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"violet","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[33.333]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[100]},{"t":70,"s":[0]}]},"p":{"a":0,"k":[250.172,221.61,0]},"a":{"a":0,"k":[7.172,-21.39,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.629,0.629,0.629],"y":[0,0,0]},"t":30,"s":[43.235,43.235,100]},{"t":40,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[8.424,-5.071],[2.653,-10.693],[8.493,-0.25],[-0.281,-21.575],[8.206,2.479],[-9.071,-18.319],[10.163,-0.826],[-8.504,-13.367],[7.925,-6.142],[-9.44,-13.344],[2.854,-8.942],[-21.641,-24.17],[6.753,-1.287],[-6.374,-4.022],[10.533,-2.098],[-13.32,-3.511],[-1.839,-6.483],[-21.075,-3.827],[-5.954,-6.816],[-11.932,-2.571],[3.257,-17.575],[-38.942,9.134],[-0.325,-7.706],[-8.742,1.73],[-8.801,-7.785],[-30.134,14.717],[-9.382,-4.129],[-16.279,6.661],[1.088,-8.515],[-7.687,7.097],[-6.012,-8.868],[-10.651,19.166],[-10.35,0.696],[-9.579,21.376],[-8.09,4.605],[-5.131,22.602],[-3.808,-4.758],[-0.748,8.961],[-7.254,-2.788],[2.909,7.968],[-7.635,-1.047],[9.08,17.754],[-7.507,3.193],[9.997,10.579],[-6.351,4.692],[9.781,8.968],[0.178,6.817],[17.454,12.482],[0.03,6.973],[12.265,1.257],[-5.162,4.439],[17.018,1.365],[-5.218,7.12],[24.328,-1.527],[-3.121,9.666],[14.883,-6.778],[10.864,5.993],[11.338,-8.563],[5.168,9.097],[9.336,-10.339],[5.422,9.555],[17.401,-17.591],[8.409,7.467],[2.648,-7.068],[8.104,5.903],[5.776,-17.779],[7.39,-3.513],[1.572,-7.178],[9.221,3.529],[4.343,-6.683]],"o":[[-7.125,5.075],[-0.627,-20.281],[-8.492,0.25],[-7.605,-11.419],[-6.68,-3.509],[-2.4,-2.857],[-8.37,0.053],[-2.455,-2.437],[-7.318,5.151],[-25.313,-18.266],[-0.866,7.957],[-15.111,-8.219],[-6.731,1.147],[0,0],[-8.775,1.322],[-36.623,-3.316],[2.35,6.725],[-4.284,0.119],[5.62,5.119],[0,0],[-0.325,7.953],[-14.317,6.526],[0.333,6.979],[-5.382,3.713],[7.32,4.607],[-4.424,3.372],[7.613,2.532],[-10.42,10.905],[-1.668,7.104],[-2.075,4.92],[4.11,6.36],[-1.424,4.903],[7.699,-1.121],[0,0],[8.176,-4.621],[-1.052,23.086],[4.14,4.652],[5.615,9.885],[7.25,2.786],[5.203,5.546],[7.441,1.093],[15.761,17.732],[7.507,-3.193],[5.904,3.732],[6.351,-4.692],[9.84,5.408],[-0.622,-6.552],[19.337,4.71],[0.329,-7.351],[6.418,-1.574],[5.14,-4.409],[27.522,-2.876],[5.215,-7.117],[4.426,-6.277],[2.875,-9.309],[1.399,-5.562],[-7.079,-5.34],[4.34,-9.001],[-5.002,-8.493],[13.638,-18.255],[-5.312,-9.225],[0.333,-7.822],[-7.414,-6.268],[3.497,-13.498],[-8.104,-5.903],[-0.973,-5.144],[-8.335,3.293],[-2.994,-1.674],[-9.108,-3.513],[-0.006,-3.457]],"v":[[5.228,-95.512],[7.462,-28.929],[-2.195,-87.293],[6.416,-28.961],[-31.261,-81.602],[2.547,-32.204],[-43.849,-81.375],[0.273,-32.419],[-44.643,-71.2],[-1.483,-31.501],[-68.757,-71.437],[-0.116,-26.024],[-42.303,-41.201],[-2.32,-26.028],[-60.656,-35.365],[-6.28,-25.891],[-67.569,-24.781],[-6.096,-24.791],[-61.84,-18.285],[-3.524,-23.253],[-83.167,12.168],[-3.472,-21.52],[-43.616,2.576],[-3.348,-19.306],[-64.141,31.565],[0.002,-18.262],[-50.895,27.819],[3.739,-17.431],[-29.038,27.111],[2.554,-14.294],[-16.457,46.001],[4.192,-11.614],[-10.643,61.853],[6.528,-12.171],[7.719,52.216],[8.895,-14.393],[13.275,34.112],[10.222,-12.355],[26.825,31.666],[12.417,-11.04],[35.999,36.579],[14.675,-10.481],[49.25,44.887],[15.923,-11.135],[54.41,30.884],[17.017,-12.053],[59.976,24.143],[15.598,-15.029],[60.134,6.365],[15.406,-16.076],[56.972,-8.479],[17.766,-17.986],[90.136,-9.191],[19.955,-19.743],[79.759,-35.208],[20.474,-22.64],[63.92,-44.118],[18.736,-24.334],[63.006,-55.052],[17.431,-26.232],[77.149,-67.057],[16.295,-27.947],[47.537,-73.383],[14.878,-29.037],[41.274,-77.714],[13.733,-29.91],[25.971,-72.901],[12.306,-29.047],[19.524,-85.462],[9.122,-30.564]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[8.849,0.01],[7.184,-28.957],[3.052,-1.273],[-0.761,-58.423],[7.839,-5.353],[-24.563,-49.607],[9.192,-3.975],[-23.029,-36.198],[4.861,-7.946],[-25.563,-36.135],[12.25,-6.125],[-58.602,-65.452],[0.423,-3.502],[-17.26,-10.89],[11.497,-5.096],[-36.071,-9.509],[-2.201,-1.041],[-57.07,-10.364],[-1.998,-10.013],[-32.312,-6.962],[9.36,-30.715],[-105.454,24.735],[0.047,-4.178],[-23.672,4.686],[-7.461,-15.968],[-81.601,39.854],[-8.157,-7.347],[-44.083,18.039],[-3.026,-7.306],[-20.815,19.217],[-8.186,-10.792],[-28.842,51.901],[-12.022,-1.924],[-25.94,57.885],[-10.365,1.613],[-13.894,61.205],[-1.581,0.481],[-2.025,24.266],[-5.413,1.006],[7.876,21.576],[-4.573,1.132],[24.589,48.078],[-6.41,1.365],[27.072,28.648],[-4.858,2.346],[26.485,24.286],[-6.451,3.968],[47.266,33.8],[-2.771,2.906],[33.213,3.403],[-1.167,1.667],[46.083,3.696],[-1.333,9],[65.879,-4.134],[-5.083,7.361],[40.303,-18.354],[15.513,2.68],[30.702,-23.187],[1.943,7.096],[25.282,-27.999],[2.5,7.5],[47.12,-47.637],[6.155,7.421],[7.17,-19.139],[5.169,3.176],[15.641,-48.146],[3.103,0.727],[4.257,-19.437],[6.927,0.949],[11.76,-18.098]],"o":[[-5.323,-0.006],[-1.697,-54.919],[-3.052,1.273],[-20.594,-30.923],[-3.728,2.546],[-6.498,-7.737],[-4.346,1.879],[-6.647,-6.6],[-3.22,5.264],[-68.548,-49.463],[-6.872,3.436],[-40.921,-22.258],[-0.377,3.121],[0,0],[-6.756,2.995],[-99.173,-8.979],[3.585,1.695],[-11.602,0.322],[1.08,5.412],[0,0],[-1.42,4.661],[-38.77,17.671],[-0.025,2.213],[-14.575,10.054],[3.439,7.359],[-11.98,9.131],[3.354,3.021],[-28.218,29.53],[1.451,3.503],[-5.619,13.324],[3.041,4.009],[-3.855,13.277],[4.839,0.774],[0,0],[10.589,-1.648],[-2.849,62.516],[2.488,-0.757],[15.206,26.767],[5.413,-1.006],[14.09,15.019],[4.055,-1.004],[42.68,48.017],[6.41,-1.365],[15.989,10.107],[4.858,-2.346],[26.647,14.644],[5.257,-3.234],[52.364,12.754],[3.745,-3.927],[17.38,-4.263],[1.115,-1.592],[74.53,-7.789],[1.333,-9],[11.985,-16.998],[4.419,-6.4],[3.79,-15.061],[-5.258,-0.908],[11.752,-24.375],[-1.495,-5.458],[36.932,-49.434],[-2.202,-6.607],[0.901,-21.183],[-3.464,-4.176],[9.469,-36.552],[-5.169,-3.176],[-2.634,-13.929],[-5.658,-1.325],[-8.107,-4.533],[-6.628,-0.908],[-0.017,-9.362]],"v":[[3.02,-162.999],[8.243,-41.492],[-9.448,-140.273],[5.428,-41.577],[-66.581,-136.474],[-4.982,-50.303],[-94.937,-141.294],[-11.103,-50.883],[-91.631,-117.558],[-15.827,-48.412],[-150.75,-126.625],[-12.148,-33.673],[-76.677,-54.762],[-18.079,-33.683],[-125.935,-48.926],[-28.737,-33.315],[-144.516,-27.935],[-28.241,-30.355],[-129.957,-16.393],[-21.32,-26.218],[-189.443,52.219],[-21.179,-21.553],[-84.613,22.128],[-20.845,-15.595],[-143.927,91.798],[-11.831,-12.788],[-109.552,77.33],[-1.776,-10.549],[-60.574,69.369],[-4.963,-2.109],[-41.055,117.272],[-0.555,5.102],[-30.196,155.83],[5.729,3.604],[8.665,130.425],[12.099,-2.376],[18.32,81.416],[15.669,3.108],[41.962,76.131],[21.577,6.648],[62.49,91.451],[27.654,8.15],[95.923,118.365],[31.011,6.393],[102.975,84.18],[33.954,3.923],[112.957,71.446],[30.136,-4.087],[107.438,31.594],[29.62,-6.903],[94.5,0.667],[35.97,-12.045],[183.167,5],[41.86,-16.772],[147.562,-47.507],[43.258,-24.567],[106.809,-61.148],[38.582,-29.125],[103.057,-80.596],[35.068,-34.233],[141.167,-109],[32.012,-38.849],[76.865,-113.118],[28.198,-41.782],[66.503,-119.657],[25.119,-44.132],[37.324,-105.068],[21.279,-41.807],[29.931,-135.604],[12.71,-45.889]],"c":true}]},{"t":100,"s":[{"i":[[6.52,-0.499],[0,0],[7.552,-1.773],[0,0],[7.419,-3.974],[0,0],[5.063,-2.794],[0,0],[3.869,-4.058],[0,0],[3.25,-5.125],[0,0],[2.323,-7.262],[0,0],[1.065,-7.426],[0,0],[-0.016,-6.935],[0,0],[0.043,-8.393],[0,0],[-1.943,-5.281],[0,0],[-4.613,-6.872],[0,0],[-4.427,-4.702],[0,0],[-5.552,-5.67],[0,0],[-7.074,-5.131],[0,0],[-7.555,-2.728],[0,0],[-8.696,0.33],[0,0],[-5.835,-0.075],[0,0],[-5.179,-0.584],[0,0],[-7.538,4.131],[0,0],[-4.51,1.951],[0,0],[-7.077,4.865],[0,0],[-5.025,3.68],[0,0],[-5.043,6.946],[0,0],[-2.062,4.094],[0,0],[-1.5,7.666],[0,0],[-1.333,7.5],[0,0],[-0.438,6.493],[0,0],[2.309,7.852],[0,0],[1.057,4.404],[0,0],[1.667,6.5],[0,0],[6.365,6.382],[0,0],[3.503,2.843],[0,0],[5.324,3.432],[0,0],[2.931,0.896],[0,0]],"o":[[-6.521,0.499],[0,0],[-7.552,1.773],[0,0],[-7.419,3.974],[0,0],[-5.063,2.794],[0,0],[-3.869,4.058],[0,0],[-3.25,5.125],[0,0],[-2.323,7.262],[0,0],[-1.065,7.426],[0,0],[0.016,6.935],[0,0],[-0.043,8.393],[0,0],[1.943,5.281],[0,0],[4.613,6.872],[0,0],[4.427,4.702],[0,0],[5.552,5.67],[0,0],[7.074,5.131],[0,0],[7.555,2.728],[0,0],[8.696,-0.33],[0,0],[5.835,0.075],[0,0],[5.18,0.584],[0,0],[7.538,-4.131],[0,0],[4.51,-1.951],[0,0],[7.077,-4.865],[0,0],[5.025,-3.68],[0,0],[5.043,-6.946],[0,0],[2.062,-4.094],[0,0],[1.5,-7.666],[0,0],[1.333,-7.5],[0,0],[0.438,-6.493],[0,0],[-2.309,-7.852],[0,0],[-1.057,-4.404],[0,0],[-1.667,-6.5],[0,0],[-6.365,-6.382],[0,0],[-3.503,-2.843],[0,0],[-5.324,-3.432],[0,0],[-2.931,-0.896],[0,0]],"v":[[0.021,-238.999],[6.522,-22.25],[-22.948,-238.773],[6.486,-22.251],[-111.081,-205.974],[6.353,-22.363],[-136.437,-188.794],[6.274,-22.37],[-151.131,-175.058],[6.214,-22.339],[-176.75,-144.125],[6.261,-22.15],[-199.177,-102.262],[6.185,-22.15],[-209.935,-65.926],[6.048,-22.145],[-215.016,-31.435],[6.055,-22.107],[-215.457,-11.893],[6.143,-22.054],[-201.443,57.219],[6.145,-21.994],[-194.113,74.128],[6.149,-21.918],[-171.427,112.298],[6.265,-21.882],[-163.052,122.83],[6.394,-21.853],[-126.074,156.869],[6.353,-21.745],[-65.055,189.772],[6.41,-21.653],[-38.196,196.33],[6.49,-21.672],[9.165,202.425],[6.572,-21.748],[31.821,199.416],[6.618,-21.678],[80.962,187.631],[6.693,-21.633],[105.49,178.451],[6.771,-21.613],[127.923,167.865],[6.814,-21.636],[157.975,144.18],[6.852,-21.668],[174.957,125.946],[6.803,-21.77],[204.438,84.094],[6.796,-21.806],[223.5,34.166],[6.878,-21.872],[227.667,12],[6.953,-21.933],[227.062,-61.507],[6.971,-22.033],[213.309,-104.148],[6.911,-22.091],[198.057,-137.596],[6.866,-22.157],[195.667,-144],[6.827,-22.216],[142.865,-200.118],[6.778,-22.254],[122.503,-211.157],[6.739,-22.284],[84.324,-230.568],[6.689,-22.254],[51.431,-240.604],[6.58,-22.306]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":80},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0.001,0.031,0.138,0.537,0.707,0.106,0.275,0.555,1,0.182,0.412,0.573]}},"s":{"a":0,"k":[4,-20]},"e":{"a":0,"k":[177.39,-20]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 3","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":-4,"op":99,"st":-65,"bm":0}]},{"id":"comp_7","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"picies2","sr":1,"ks":{"p":{"a":0,"k":[257.33,185.73,0]},"a":{"a":0,"k":[-265.67,-10.27,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,-0.35],[53.84,0],[-19.93,11.56],[-1,-40.01]],"o":[[0,50.03],[-31.67,-34.87],[39.26,-0.87],[0.01,0.35]],"v":[[-168.19,-10.27],[-265.67,80.32],[-266.5,-79.82],[-168.2,-11.32]],"c":true}]},{"t":35,"s":[{"i":[[0,-0.004],[0.613,0],[-0.227,0.132],[-0.011,-0.455]],"o":[[0,0.569],[-0.36,-0.397],[0.447,-0.01],[0,0.004]],"v":[[-145.03,70.7],[-146.14,71.731],[-146.149,69.909],[-145.03,70.688]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-2.5,-7],[15.65,-61.05],[-53.84,0]],"o":[[0,0],[0,-50.03],[0,0]],"v":[[-266.5,-79.82],[-363.15,-10.27],[-265.67,-100.86]],"c":true}]},{"t":35,"s":[{"i":[[-0.013,-0.036],[0.08,-0.312],[-0.275,0]],"o":[[0,0],[0,-0.256],[0,0]],"v":[[-383.165,-126.689],[-383.659,-126.333],[-383.161,-126.797]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-0.6,-49.55],[39.26,-0.87],[0,0]],"o":[[-1,-40.01],[-2.5,-7],[53.46,0]],"v":[[-168.2,-11.32],[-266.5,-79.82],[-265.67,-100.86]],"c":true}]},{"t":35,"s":[{"i":[[-0.018,-1.483],[1.175,-0.026],[0,0]],"o":[[-0.03,-1.198],[-0.075,-0.21],[1.6,0]],"v":[[-162.091,-102.75],[-165.034,-104.8],[-165.009,-105.43]],"c":true}]}]},"nm":"Path 3","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.360784313725,0.698039215686,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 3","hd":false},{"ty":"tr","p":{"a":0,"k":[-265.67,-10.27]},"a":{"a":0,"k":[-265.67,-10.27]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-31.67,-34.87],[0,50.03],[0,0]],"o":[[-53.84,0],[15.65,-61.05],[-19.93,11.56]],"v":[[-265.67,80.32],[-363.15,-10.27],[-266.5,-79.82]],"c":true}]},{"t":35,"s":[{"i":[[-0.099,-0.109],[0,0.156],[0,0]],"o":[[-0.168,0],[0.049,-0.191],[-0.062,0.036]],"v":[[-415.258,90.5],[-415.562,90.217],[-415.26,90]],"c":true}]}]},"nm":"Path 4","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.286274509804,0.552941176471,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 3","hd":false},{"ty":"tr","p":{"a":0,"k":[-314.41,0.25]},"a":{"a":0,"k":[-314.41,0.25]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"st","c":{"a":0,"k":[0,0.207843152214,0.400000029919,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":24},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-265.67,-10.27]},"a":{"a":0,"k":[-265.67,-10.27]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":30,"op":35,"st":2,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"picies","sr":1,"ks":{"p":{"a":0,"k":[255,191.406,0]},"a":{"a":0,"k":[0,45.406,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":31,"s":[{"i":[[-14.81,-13.46],[11.95,21.92],[1.11,16.11],[-22.44,-7.84],[-11.16,1.15]],"o":[[-22.91,4.11],[-6.67,-12.23],[5.79,24.31],[9.97,3.48],[-0.13,0.68]],"v":[[10.978,133.929],[-49.332,107.219],[-76.002,35.549],[-29.982,86.199],[2.028,89.909]],"c":true}]},{"t":36,"s":[{"i":[[0.042,0.009],[-0.025,-0.045],[-0.002,-0.033],[0.03,0.046],[0.013,0.008]],"o":[[0.048,-0.009],[0.014,0.025],[-0.012,-0.05],[-0.009,-0.013],[-0.02,-0.013]],"v":[[-71.68,195.17],[-71.555,195.225],[-71.5,195.374],[-71.582,195.231],[-71.615,195.198]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":31,"s":[{"i":[[-20.7,35.34],[6.66,-12.23],[15.67,-2.82],[0,0],[-0.13,0.68]],"o":[[-1.11,16.11],[-8.18,15],[0,0],[-14.81,-13.46],[25.91,-2.68]],"v":[[75.998,35.549],[49.338,107.219],[10.988,133.929],[10.978,133.929],[2.028,89.909]],"c":true}]},{"t":36,"s":[{"i":[[-0.149,0.254],[0.048,-0.088],[0.112,-0.02],[0,0],[-0.001,0.005]],"o":[[-0.008,0.116],[-0.059,0.108],[0,0],[0.129,-0.1],[0.093,-0.157]],"v":[[73.967,180.898],[73.775,181.412],[73.5,181.604],[73.5,181.604],[73.665,181.395]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":31,"s":[{"i":[[4.69,-50.46],[25.91,-2.68],[9.97,3.48],[-5.65,29.96],[19.36,23.12]],"o":[[-20.7,35.34],[-11.16,1.15],[25.15,-11.39],[3.71,-19.59],[38.33,2.51]],"v":[[75.998,35.549],[2.028,89.909],[-29.982,86.199],[26.648,21.049],[6.078,-43.891]],"c":true}]},{"t":36,"s":[{"i":[[0.019,-0.202],[0.104,-0.011],[0.04,0.014],[-0.023,0.12],[0.077,0.092]],"o":[[-0.083,0.141],[-0.045,0.005],[0.101,-0.046],[0.015,-0.078],[0.153,0.01]],"v":[[154.424,34.876],[154.128,35.094],[154,35.079],[154.226,34.818],[154.144,34.559]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":31,"s":[{"i":[[3.71,-19.59],[21,-18.31],[-37.83,-2.49]],"o":[[-10.36,-22.61],[4.41,-44.16],[19.36,23.12]],"v":[[26.648,21.049],[-76.002,18.639],[6.078,-43.891]],"c":true}]},{"t":36,"s":[{"i":[[0.012,-0.061],[0.066,-0.057],[-0.118,-0.008]],"o":[[-0.032,-0.071],[0.014,-0.138],[0.06,0.072]],"v":[[1.192,-97.167],[0.872,-97.174],[1.128,-97.369]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":31,"s":[{"i":[[-10.36,-22.61],[25.15,-11.39],[5.79,24.31],[-0.53,5.3]],"o":[[-5.65,29.96],[-22.44,-7.84],[-0.55,-5.97],[21,-18.31]],"v":[[26.648,21.049],[-29.982,86.199],[-76.002,35.549],[-76.002,18.639]],"c":true}]},{"t":36,"s":[{"i":[[-0.156,-0.229],[0.005,-0.198],[0.125,0.524],[-0.011,0.114]],"o":[[0.175,0.257],[-0.483,-0.169],[-0.012,-0.129],[0.113,0.148]],"v":[[-149.053,15.247],[-148.42,15.955],[-149.412,14.864],[-149.412,14.5]],"c":true}]}]},"nm":"Path 5","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.360784313725,0.698039215686,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 5","hd":false},{"ty":"st","c":{"a":0,"k":[0,0.207843137255,0.4,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":24},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,45.406]},"a":{"a":0,"k":[0,45.406]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":31,"op":36,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"Null 3","sr":1,"ks":{"o":{"a":0,"k":0},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[256]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":21,"s":[257.445]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":22,"s":[251.735]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":23,"s":[262.162]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":24,"s":[256.384]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[256.888]},{"t":29,"s":[254.404]}]},"y":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[198]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":22,"s":[190.425]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":23,"s":[197.133]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":24,"s":[186.523]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[194.791]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":26,"s":[186.058]},{"t":29,"s":[194.761]}]}},"a":{"a":0,"k":[50,50,0]}},"ao":0,"ip":20,"op":30,"st":-60,"bm":0},{"ddd":0,"ind":4,"ty":3,"nm":"Null 19","parent":3,"sr":1,"ks":{"o":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[50,108,0],"to":[0,0,0],"ti":[0,0,0]},{"t":30,"s":[50,1,0]}]}},"ao":0,"ip":0,"op":36,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"blik","parent":6,"sr":1,"ks":{"p":{"a":0,"k":[20.897,-88.771,0]},"a":{"a":0,"k":[19.25,-92,0]},"s":{"a":0,"k":[70,70,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,-13.193],[44.427,-0.298],[-30.5,146.5]],"o":[[0,13.193],[-55,1.5],[-0.5,0]],"v":[[80.444,-33.889],[0,-10],[0,-200]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[0,-61.227],[24.905,-1.3],[-8.202,27.448]],"o":[[0,40.169],[-45,-4.333],[2.417,-2.431]],"v":[[98.282,62.507],[32.976,130.655],[10.893,-24.206]],"c":true}]},{"t":30,"s":[{"i":[[0,-77.778],[23.059,-1.647],[-4.109,3.995]],"o":[[0,50.031],[-47.216,-6.039],[3.294,-3.203]],"v":[[109.918,84.947],[41.535,169.497],[12.437,7.17]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":8},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-131]},{"t":30,"s":[-5]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":8.571},"lc":2,"lj":2,"bm":0,"d":[{"n":"d","nm":"dash","v":{"a":0,"k":2}},{"n":"g","nm":"gap","v":{"a":0,"k":12}},{"n":"d","nm":"dash2","v":{"a":0,"k":297}},{"n":"o","nm":"offset","v":{"a":0,"k":0}}],"nm":"Stroke 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":30,"st":-60,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"rocket_cap","parent":10,"sr":1,"ks":{"p":{"a":0,"k":[0,-33.889,0]},"a":{"a":0,"k":[0,-33.889,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,-13.193],[44.427,-0.298],[-30.5,146.5]],"o":[[0,13.193],[-55,1.5],[-0.5,0]],"v":[[80.444,-33.889],[0,-10],[0,-200]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[0,-61.227],[24.905,-1.3],[-8.202,27.448]],"o":[[0,40.169],[-45,-4.333],[2.417,-2.431]],"v":[[87.389,21.435],[22.083,89.583],[0,-65.278]],"c":true}]},{"t":30,"s":[{"i":[[0,-77.778],[23.059,-1.647],[-4.109,3.995]],"o":[[0,50.031],[-47.216,-6.039],[3.294,-3.203]],"v":[[97.776,39.412],[29.392,123.961],[0.294,-38.366]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.360784313725,0.698039215686,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,-13.193],[44.428,0],[0,13.193],[0,0]],"o":[[0,13.193],[-44.428,0],[0,-13.193],[0,0]],"v":[[80.444,-33.889],[0,-10],[-80.444,-33.889],[0,-200]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[0,-61.227],[48.264,0],[0,40.168],[0,0]],"o":[[0,40.168],[-48.264,0],[0,-61.227],[0,0]],"v":[[87.389,21.435],[0,94.167],[-87.389,21.435],[0,-65.278]],"c":true}]},{"t":30,"s":[{"i":[[0,-77.778],[53.838,0],[0,50.031],[0,0]],"o":[[0,50.031],[-53.838,0],[0,-77.778],[0,0]],"v":[[97.776,39.412],[0.294,130],[-97.187,39.412],[0.294,-38.366]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.286274509804,0.552941176471,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-105]},"a":{"a":0,"k":[0,-105]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,-13.193],[44.428,0],[0,13.193],[-44.428,0]],"o":[[0,13.193],[-44.428,0],[0,-13.193],[44.428,0]],"v":[[80.444,-33.889],[0,-10],[-80.444,-33.889],[0,-57.778]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[0,-40.168],[48.264,0],[0,40.168],[-48.264,0]],"o":[[0,40.168],[-48.264,0],[0,-40.168],[48.264,0]],"v":[[87.389,21.435],[0,94.167],[-87.389,21.435],[0,-51.296]],"c":true}]},{"t":30,"s":[{"i":[[0,-50.031],[53.838,0],[0,50.031],[-53.838,0]],"o":[[0,50.031],[-53.838,0],[0,-50.031],[53.838,0]],"v":[[97.776,39.412],[0.294,130],[-97.187,39.412],[0.294,-51.176]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.360784313725,0.698039215686,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 2","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-33.889]},"a":{"a":0,"k":[0,-33.889]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"st","c":{"a":0,"k":[0,0.207843137255,0.4,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":24},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":30,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"rocket body 2 outlines","parent":10,"sr":1,"ks":{"p":{"a":0,"k":[0,71.389,0]},"a":{"a":0,"k":[0,71.389,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[2.222,-10],[7.734,34.804],[0,0]],"o":[[0,0],[-7.676,34.543],[-2.222,-10],[0,0]],"v":[[68.778,-37.222],[59.333,153.333],[-59.333,153.333],[-68.778,-37.222]],"c":true}]},{"t":30,"s":[{"i":[[0,0],[6.667,-12.222],[20.133,36.91],[1.111,16.111]],"o":[[-1.111,16.111],[-20.133,36.911],[-6.667,-12.222],[0,0]],"v":[[76,35.556],[49.333,107.222],[-49.333,107.222],[-76,35.556]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0,0.207843137255,0.4,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":12},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":30,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"rocket body 2 shadows","parent":10,"sr":1,"ks":{"o":{"a":0,"k":50},"p":{"a":0,"k":[0,71.389,0]},"a":{"a":0,"k":[0,71.389,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0.667,-19.333],[7.333,22.167],[0,0]],"o":[[0,0],[0.167,0.667],[-3.217,-9.726],[0,0]],"v":[[-13.222,-18.222],[-12.667,177.333],[-59.333,153.333],[-68.778,-37.222]],"c":true}]},{"t":30,"s":[{"i":[[0,0],[0.167,-11.222],[18.833,29.278],[1.111,16.111]],"o":[[-1.111,16.111],[-15.833,-1.222],[-7.532,-11.709],[0,0]],"v":[[8,37.056],[7.333,133.222],[-49.333,107.222],[-76,35.556]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.207843137255,0.4,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":30,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"lines2","parent":10,"sr":1,"ks":{"p":{"a":0,"k":[-4.5,135.446,0]},"a":{"a":0,"k":[-4.5,135.446,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[19,23.25],[0,0],[-72.25,-0.5],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-45,-7]],"v":[[-67.5,-34.25],[-66,-10.75],[65.25,58.5],[66.5,12.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[19,23.25],[0,0],[-70.25,-2.5],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-45,-7]],"v":[[-67.5,-34.25],[-67,-26.25],[65.75,25],[68.5,-17]],"c":true}]},{"t":10,"s":[{"i":[[19,23.25],[0,0],[-70.25,-2.5],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-45,-7]],"v":[[-67.5,-34.25],[-67,-26.25],[68.25,-15],[68.5,-17]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[25,44.25],[0,0],[-71.75,9],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50,5]],"v":[[-65,33.75],[-62.5,74.75],[60.75,137],[62.5,98]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[25,44.25],[0,0],[-71.75,10.5],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50,5]],"v":[[-66.5,2.75],[-65,41.25],[61.75,99],[65,63]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[26,30.25],[0,0],[-71.75,10.5],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50,5]],"v":[[-68.5,-6.75],[-66,19.75],[62.75,70.5],[68,37]],"c":true}]},{"t":15,"s":[{"i":[[26,30.25],[0,0],[-54.75,16],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50,5]],"v":[[-68.5,-6.75],[-65.5,15.25],[65.75,49],[71,14]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[20,16.5],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[13.25,13.75],[-0.25,0],[-50.75,19]],"v":[[-56.25,160.5],[-56,160.5],[49.25,167.75],[49.75,168]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[20,16.5],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[13.25,13.75],[-0.25,0],[-50.75,19]],"v":[[-54.5,154.25],[-54.25,154.25],[43,161.5],[43.5,161.75]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[20,16.5],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[13.25,13.75],[-0.25,0],[-50.75,19]],"v":[[-53.75,148],[-53.5,148],[42.25,155.5],[42.75,155.75]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[17.5,26],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[11.75,20.5],[-0.25,0],[-50.75,19]],"v":[[-52.5,136.25],[-52.75,136.25],[42.25,147.75],[42.5,147.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[21.25,35.75],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[11.75,20.5],[-0.25,0],[-50.75,19]],"v":[[-57.25,102.5],[-55,120.25],[43.25,137.5],[44,133.75]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[21.25,35.75],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[11.75,20.5],[-0.25,0],[-50.75,19]],"v":[[-59.25,89.5],[-54.25,106.75],[42.5,128],[48,118.75]],"c":true}]},{"t":30,"s":[{"i":[[21.25,35.75],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[11.75,20.5],[-0.25,0],[-50.75,19]],"v":[[-58.5,71.75],[-53.5,89],[43.25,110.25],[48.75,101]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[19,27.5],[0,0],[-58.5,22],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50.75,19]],"v":[[-58.5,158.5],[-58.25,158.5],[53.5,166],[54,165]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[19,27.5],[0,0],[-58.5,22],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50.75,19]],"v":[[-57.25,147],[-57.25,147.5],[50.5,160],[50,161]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[24.25,45.5],[0,0],[-58.5,22],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50.75,19]],"v":[[-60.5,106.5],[-58.75,138.5],[51,150],[51.25,149.75]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[24.25,45.5],[0,0],[-54.5,29.25],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50.75,19]],"v":[[-62.5,78],[-58.25,106.75],[49,141.75],[53.5,124.75]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[21.25,35.75],[0,0],[-53.25,24.25],[0,0]],"o":[[-0.25,0.25],[11.75,20.5],[-0.25,0],[-50.75,19]],"v":[[-62.5,67.5],[-59.5,89.75],[50,121.5],[54.5,104.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[21.25,35.75],[0,0],[-49.75,27],[0,0]],"o":[[-0.25,0.25],[11.75,20.5],[-0.25,0],[-50.75,19]],"v":[[-62.5,63.25],[-60.75,81.25],[52.25,106.5],[57.25,81]],"c":true}]},{"t":30,"s":[{"i":[[21.25,35.75],[0,0],[-49.75,27],[0,0]],"o":[[-0.25,0.25],[11.75,20.5],[-0.25,0],[-50.75,19]],"v":[[-62.5,63.25],[-60.75,70.5],[56.5,84.75],[57.25,81]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[25,44.25],[0,0],[-47,36.25],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-50.5,40.25]],"v":[[-61.5,114.75],[-59,155.75],[63.25,158],[63.5,157.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[25,44.25],[0,0],[-55,45.75],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-51,12.25]],"v":[[-62,77.5],[-60.75,112.25],[60.75,151.25],[59.5,132.25]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[25,44.25],[0,0],[-58.5,22],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-51,12.25]],"v":[[-64.25,50.75],[-60.75,80.5],[58.25,128],[60.75,101.25]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[25,39],[0,0],[-58.5,22],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-51,12.25]],"v":[[-65.5,33.75],[-63.75,59.5],[59,101.75],[61.5,76.75]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[25,39],[0,0],[-55.75,26.75],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-51,12.25]],"v":[[-69.5,30],[-65,49],[61,82.75],[63.75,62.25]],"c":true}]},{"t":25,"s":[{"i":[[25,39],[0,0],[-55.75,26.75],[0,0]],"o":[[-0.25,0.25],[13.75,25.75],[-0.25,0],[-51,12.25]],"v":[[-69.5,30],[-65,54.75],[62.25,76.25],[63.75,62.25]],"c":true}]}]},"nm":"Path 4","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.360784313725,0.698039215686,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":30,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"rocket body","parent":4,"sr":1,"ks":{"p":{"a":0,"k":[0,71.389,0]},"a":{"a":0,"k":[0,71.389,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[2.222,-10],[7.734,34.804],[0,0]],"o":[[0,0],[-7.676,34.543],[-2.222,-10],[0,0]],"v":[[68.778,-37.222],[59.333,153.333],[-59.333,153.333],[-68.778,-37.222]],"c":true}]},{"t":30,"s":[{"i":[[0,0],[6.667,-12.222],[20.133,36.91],[1.111,16.111]],"o":[[-1.111,16.111],[-20.133,36.911],[-6.667,-12.222],[0,0]],"v":[[76,35.556],[49.333,107.222],[-49.333,107.222],[-76,35.556]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.913725550034,0.815686334348,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":30,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Shape Layer 11","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[283,390,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[257,472,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[283,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":17,"s":[262,427.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[280,323,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[256.5,371.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[279,288,0],"to":[0,0,0],"ti":[0,0,0]},{"t":31,"s":[259,328,0]}]},"a":{"a":0,"k":[-28,116,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":4,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":10,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":11,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":17,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":18,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":24,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":25,"s":[100,100,100]},{"t":31,"s":[20,20,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[56,56]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862745098,0.854901960784,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-28,116]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":4,"op":31,"st":4,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Shape Layer 10","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[255,394,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[257,472,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13,"s":[258,356,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":19,"s":[262,427.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[255,327,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":26,"s":[256.5,371.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":27,"s":[261,292,0],"to":[0,0,0],"ti":[0,0,0]},{"t":33,"s":[259,328,0]}]},"a":{"a":0,"k":[-28,116,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":6,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":12,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":13,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":19,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":20,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":26,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":27,"s":[100,100,100]},{"t":33,"s":[20,20,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[56,56]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862745098,0.854901960784,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-28,116]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":6,"op":33,"st":6,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"Shape Layer 9","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[231,397,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[252,494,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[232,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":21,"s":[254,428,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[230,336,0],"to":[0,0,0],"ti":[0,0,0]},{"t":28,"s":[256.5,371.5,0]}]},"a":{"a":0,"k":[-28,116,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":8,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":14,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":15,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":21,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":22,"s":[100,100,100]},{"t":28,"s":[20,20,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[56,56]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862745098,0.854901960784,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-28,116]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":8,"op":28,"st":8,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"Shape Layer 8","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[283,390,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[257,472,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9,"s":[283,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[262,427.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[280,323,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[256.5,371.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23,"s":[279,288,0],"to":[0,0,0],"ti":[0,0,0]},{"t":29,"s":[259,328,0]}]},"a":{"a":0,"k":[-28,116,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":2,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":8,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":9,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":15,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":16,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":22,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":23,"s":[100,100,100]},{"t":29,"s":[20,20,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[56,56]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862745098,0.854901960784,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-28,116]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":2,"op":29,"st":2,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[231,397,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[252,494,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":7,"s":[232,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13,"s":[254,428,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[230,336,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[256.5,371.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":21,"s":[234,298,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":27,"s":[259,328,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[239,262,0],"to":[0,0,0],"ti":[0,0,0]},{"t":33,"s":[264,326,0]}]},"a":{"a":0,"k":[-28,116,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":6,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":7,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":13,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":14,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":20,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":21,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":27,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":28,"s":[100,100,100]},{"t":33,"s":[20,20,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[56,56]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.956862745098,0.854901960784,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-28,116]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":34,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"1536_fireworks_3 sec","refId":"comp_0","sr":1,"ks":{"p":{"a":0,"k":[256,256,0]},"a":{"a":0,"k":[768,768,0]},"s":{"a":0,"k":[33.333,33.333,100]}},"ao":0,"w":1536,"h":1536,"ip":0,"op":180,"st":0,"bm":0}]} \ No newline at end of file diff --git a/Tests/LottieMesh/Resources/SUPER Fire.json b/Tests/LottieMesh/Resources/SUPER Fire.json deleted file mode 100644 index a98220cd49..0000000000 --- a/Tests/LottieMesh/Resources/SUPER Fire.json +++ /dev/null @@ -1 +0,0 @@ -{"tgs":1,"v":"5.5.2","fr":60,"ip":0,"op":180,"w":512,"h":512,"nm":"!!!1536 scaled 512","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 21","sr":1,"ks":{"p":{"a":0,"k":[1066,1130,0]},"a":{"a":0,"k":[298,362,0]},"s":{"a":0,"k":[33,33,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-100,-560]],"o":[[0,0],[0,0]],"v":[[508,168],[-4,696]],"c":false}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-28,-356]],"o":[[0,0],[0,0]],"v":[[508,168],[244,604]],"c":false}},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[44,-216]],"o":[[0,0],[0,0]],"v":[[508,168],[572,612]],"c":false}},"nm":"Path 3","hd":false},{"ind":3,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-48,-180]],"o":[[0,0],[0,0]],"v":[[508,168],[728,208]],"c":false}},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[92,-480]],"o":[[0,0],[0,0]],"v":[[508,168],[-192,392]],"c":false}},"nm":"Path 5","hd":false},{"ind":5,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[232,-220]],"o":[[0,0],[-194.813,184.737]],"v":[[508,168],[-68,-20]],"c":false}},"nm":"Path 6","hd":false},{"ind":6,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[124,-16]],"o":[[0,0],[0,0]],"v":[[508,168],[112,-288]],"c":false}},"nm":"Path 7","hd":false},{"ind":7,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[508,168],[404,-348]],"c":false}},"nm":"Path 8","hd":false},{"ind":8,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[508,168],[532,-464]],"c":false}},"nm":"Path 9","hd":false},{"ind":9,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[508,168],[700,-100]],"c":false}},"nm":"Path 10","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[4]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":16,"s":[4]}]},"e":{"a":0,"k":5},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[115]},{"t":16,"s":[341]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.960784375668,0.717647075653,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":16},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.996078491211,0.745098054409,0.337254911661,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":32},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":1,"op":16,"st":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 22","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":22,"s":[100]},{"t":32,"s":[0]}]},"p":{"a":0,"k":[1066,1130,0]},"a":{"a":0,"k":[298,362,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[104,-684]],"o":[[0,0],[0,0]],"v":[[508,168],[236,412]],"c":false}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-12,-324]],"o":[[0,0],[0,0]],"v":[[508,168],[356,520]],"c":false}},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[44,-216]],"o":[[0,0],[0,0]],"v":[[508,168],[612,440]],"c":false}},"nm":"Path 3","hd":false},{"ind":3,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-16,-144]],"o":[[0,0],[0,0]],"v":[[508,168],[720,156]],"c":false}},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[92,-480]],"o":[[0,0],[0,0]],"v":[[508,168],[132,336]],"c":false}},"nm":"Path 5","hd":false},{"ind":5,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[236,-128]],"o":[[0,0],[-236,128]],"v":[[508,168],[40,116]],"c":false}},"nm":"Path 6","hd":false},{"ind":6,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[156,-128]],"o":[[0,0],[-156,128]],"v":[[508,168],[-20,-180]],"c":false}},"nm":"Path 7","hd":false},{"ind":7,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[508,168],[244,-192]],"c":false}},"nm":"Path 8","hd":false},{"ind":8,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[508,168],[708,-332]],"c":false}},"nm":"Path 9","hd":false},{"ind":9,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[508,168],[724,-56]],"c":false}},"nm":"Path 10","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[168]},{"t":32,"s":[341]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.956862807274,0.68235296011,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":16},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.996078491211,0.643137276173,0.200000017881,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":32},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":5,"op":32,"st":5,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 20","sr":1,"ks":{"p":{"a":0,"k":[1066,1130,0]},"a":{"a":0,"k":[298,362,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[104,-684]],"o":[[0,0],[0,0]],"v":[[508,168],[88,556]],"c":false}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[508,168],[476,708]],"c":false}},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[44,-216]],"o":[[0,0],[0,0]],"v":[[508,168],[676,580]],"c":false}},"nm":"Path 3","hd":false},{"ind":3,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[20,-228]],"o":[[0,0],[0,0]],"v":[[508,168],[712,288]],"c":false}},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[92,-480]],"o":[[0,0],[0,0]],"v":[[508,168],[24,300]],"c":false}},"nm":"Path 5","hd":false},{"ind":5,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[236,-128]],"o":[[0,0],[-236,128]],"v":[[508,168],[20,36]],"c":false}},"nm":"Path 6","hd":false},{"ind":6,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[508,168],[108,-220]],"c":false}},"nm":"Path 7","hd":false},{"ind":7,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[508,168],[340,-264]],"c":false}},"nm":"Path 8","hd":false},{"ind":8,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[508,168],[644,-444]],"c":false}},"nm":"Path 9","hd":false},{"ind":9,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[508,168],[700,-100]],"c":false}},"nm":"Path 10","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":3,"s":[115]},{"t":18,"s":[341]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.698039233685,0.215686291456,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":16},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.988235354424,0.341176480055,0.074509806931,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":32},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":3,"op":18,"st":3,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"smoke","refId":"comp_1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[90]},{"t":60,"s":[0]}]},"p":{"a":0,"k":[1264,984,0]},"a":{"a":0,"k":[276,668,0]},"s":{"a":0,"k":[141,141,100]}},"ao":0,"w":512,"h":768,"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"03 fire small wide cycle end 2","refId":"comp_2","sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[-0.376]},{"t":87,"s":[4.959]}]},"p":{"a":0,"k":[740,1512.8,0]},"a":{"a":0,"k":[256,512,0]}},"ao":0,"w":512,"h":512,"ip":40,"op":88,"st":40,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"smoke2_36 sek","refId":"comp_5","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":50,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":60,"s":[90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":73,"s":[90]},{"t":86,"s":[0]}]},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[736,1400,0],"to":[0,0,0],"ti":[0,0,0]},{"t":86,"s":[736,880,0]}]},"a":{"a":0,"k":[276,668,0]},"s":{"a":0,"k":[-130,130,100]}},"ao":0,"w":512,"h":768,"ip":50,"op":86,"st":50,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Shape Layer 10","sr":1,"ks":{"p":{"a":0,"k":[527.164,1021,0]},"a":{"a":0,"k":[77.164,253,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[34.527,76.078],[108,-470]],"o":[[-118,-260],[-30.762,133.873]],"v":[[784,-84],[214,-262]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":7,"s":[0]},{"t":27,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":7,"s":[1,1,1,1]},{"t":27,"s":[1,0.905882418156,0.298039227724,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":7,"s":[32]},{"t":27,"s":[8]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":7,"s":[1,0.905882418156,0.298039227724,1]},{"t":27,"s":[0.992156922817,0.321568638086,0.078431375325,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":7,"s":[48]},{"t":27,"s":[24]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":7,"op":27,"st":-73,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Shape Layer 9","sr":1,"ks":{"p":{"a":0,"k":[527.164,1021,0]},"a":{"a":0,"k":[77.164,253,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[3.598,83.469],[108,-470]],"o":[[-10,-232],[-30.762,133.873]],"v":[[756,84],[338,326]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"t":25,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[1,1,1,1]},{"t":25,"s":[1,0.905882418156,0.298039227724,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[32]},{"t":25,"s":[48]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[1,0.905882418156,0.298039227724,1]},{"t":25,"s":[0.992156922817,0.321568638086,0.078431375325,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[48]},{"t":25,"s":[64]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":5,"op":25,"st":-75,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Shape Layer 8","sr":1,"ks":{"p":{"a":0,"k":[527.164,1021,0]},"a":{"a":0,"k":[77.164,253,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[54.505,63.318],[-124,-722]],"o":[[-334,-388],[23.251,135.38]],"v":[[756,84],[-54,706]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[0]},{"t":22,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[1,1,1,1]},{"t":22,"s":[1,0.905882418156,0.298039227724,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[32]},{"t":22,"s":[64]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[1,0.905882418156,0.298039227724,1]},{"t":22,"s":[0.992156922817,0.321568638086,0.078431375325,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[48]},{"t":22,"s":[80]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":2,"op":22,"st":-78,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Shape Layer 16","sr":1,"ks":{"p":{"a":0,"k":[527.164,1021,0]},"a":{"a":0,"k":[77.164,253,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[34.527,76.078],[108,-470]],"o":[[-118,-260],[-30.762,133.873]],"v":[[784,-84],[94,102]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":20,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[1,1,1,1]},{"t":20,"s":[1,0.905882418156,0.298039227724,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[32]},{"t":20,"s":[48]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[1,0.905882418156,0.298039227724,1]},{"t":20,"s":[0.992156922817,0.321568638086,0.078431375325,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[48]},{"t":20,"s":[64]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":1,"op":21,"st":-80,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Shape Layer 18","sr":1,"ks":{"p":{"a":0,"k":[527.164,1021,0]},"a":{"a":0,"k":[77.164,253,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-51.827,65.529],[260,-30]],"o":[[174,-220],[-136.456,15.745]],"v":[[716,52],[426,-438]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[0]},{"t":45,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":26,"s":[1,1,1,1]},{"t":46,"s":[1,0.905882418156,0.298039227724,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[32]},{"t":45,"s":[8]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":26,"s":[1,0.905882418156,0.298039227724,1]},{"t":46,"s":[0.992156922817,0.321568638086,0.078431375325,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[48]},{"t":45,"s":[24]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":26,"op":45,"st":-55,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Shape Layer 7","sr":1,"ks":{"p":{"a":0,"k":[527.164,1021,0]},"a":{"a":0,"k":[77.164,253,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-51.827,65.529],[182,-228]],"o":[[174,-220],[-85.694,107.353]],"v":[[920,-16],[710,-234]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":20,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[1,1,1,1]},{"t":20,"s":[1,0.905882418156,0.298039227724,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[32]},{"t":20,"s":[8]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[1,0.905882418156,0.298039227724,1]},{"t":20,"s":[0.992156922817,0.321568638086,0.078431375325,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[48]},{"t":20,"s":[24]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":1,"op":20,"st":-80,"bm":0},{"ddd":0,"ind":13,"ty":0,"nm":"03 fire small wide1","refId":"comp_6","sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":49,"s":[-4.281]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":58,"s":[0.51]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":62,"s":[3.655]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":89,"s":[-3.133]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":98,"s":[-5.791]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":113,"s":[-0.014]},{"t":142,"s":[0]}]},"p":{"a":0,"k":[1152,1488.8,0]},"a":{"a":0,"k":[256,512,0]}},"ao":0,"w":512,"h":512,"ip":49,"op":173,"st":49,"bm":0},{"ddd":0,"ind":14,"ty":0,"nm":"smoke2_36 sek 2","refId":"comp_9","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":121,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":131,"s":[90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":144,"s":[90]},{"t":157,"s":[0]}]},"p":{"a":0,"k":[1176,1384,0]},"a":{"a":0,"k":[276,668,0]},"s":{"a":0,"k":[130,130,100]}},"ao":0,"w":512,"h":768,"ip":121,"op":157,"st":121,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"Shape Layer 17","sr":1,"ks":{"p":{"a":0,"k":[527.164,1021,0]},"a":{"a":0,"k":[77.164,253,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[28.912,78.384],[204,-678]],"o":[[-90,-244],[-39.577,131.537]],"v":[[804,140],[290,690]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0]},{"t":40,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[1,1,1,1]},{"t":40,"s":[1,0.905882418156,0.298039227724,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[32]},{"t":40,"s":[64]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[1,0.905882418156,0.298039227724,1]},{"t":40,"s":[0.992156922817,0.321568638086,0.078431375325,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[48]},{"t":40,"s":[80]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":20,"op":40,"st":-60,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"Shape Layer 12","sr":1,"ks":{"p":{"a":0,"k":[527.164,1021,0]},"a":{"a":0,"k":[77.164,253,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[28.912,78.384],[-96,-702]],"o":[[-90,-244],[18.611,136.095]],"v":[[900,148],[650,678]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":29,"s":[0]},{"t":49,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":29,"s":[1,1,1,1]},{"t":49,"s":[1,0.905882418156,0.298039227724,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":29,"s":[32]},{"t":49,"s":[64]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":29,"s":[1,0.905882418156,0.298039227724,1]},{"t":49,"s":[0.992156922817,0.321568638086,0.078431375325,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":29,"s":[48]},{"t":49,"s":[80]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":29,"op":49,"st":-51,"bm":0},{"ddd":0,"ind":17,"ty":0,"nm":"02 fire middle","refId":"comp_10","sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":22,"s":[5.472]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":37,"s":[1.181]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":52,"s":[3.12]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":86,"s":[-4.096]},{"t":107,"s":[0]}]},"p":{"a":0,"k":[372,1440,0]},"a":{"a":0,"k":[256,768,0]},"s":{"a":0,"k":[120,120,100]}},"ao":0,"w":512,"h":768,"ip":22,"op":142,"st":22,"bm":0},{"ddd":0,"ind":18,"ty":0,"nm":"smoke","refId":"comp_1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":100,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":110,"s":[90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":140,"s":[90]},{"t":160,"s":[0]}]},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":100,"s":[416,1348,0],"to":[0,0,0],"ti":[0,0,0]},{"t":160,"s":[416,1060,0]}]},"a":{"a":0,"k":[276,668,0]},"s":{"a":0,"k":[145,145,100]}},"ao":0,"w":512,"h":768,"ip":100,"op":160,"st":100,"bm":0},{"ddd":0,"ind":19,"ty":0,"nm":"01 fire small 3","refId":"comp_18","sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[-0.87]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":101,"s":[9.498]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":111,"s":[5.62]},{"t":131,"s":[0]}]},"p":{"a":0,"k":[744,1316,0]},"a":{"a":0,"k":[256,512,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":512,"ip":70,"op":162,"st":70,"bm":0},{"ddd":0,"ind":20,"ty":0,"nm":"smoke 2","refId":"comp_24","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":114,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":124,"s":[90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":154,"s":[90]},{"t":174,"s":[0]}]},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":114,"s":[732,1248.798,0],"to":[0,0,0],"ti":[0,0,0]},{"t":174,"s":[732,1008.798,0]}]},"a":{"a":0,"k":[276,668,0]},"s":{"a":0,"k":[-120,120,100]}},"ao":0,"w":512,"h":768,"ip":114,"op":174,"st":114,"bm":0},{"ddd":0,"ind":21,"ty":4,"nm":"Shape Layer 14","sr":1,"ks":{"p":{"a":0,"k":[527.164,1021,0]},"a":{"a":0,"k":[77.164,253,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[28.912,78.384],[64,-690]],"o":[[-90,-244],[-12.686,136.775]],"v":[[900,148],[298,506]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":50,"s":[0]},{"t":70,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":50,"s":[1,1,1,1]},{"t":70,"s":[1,0.905882418156,0.298039227724,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":50,"s":[32]},{"t":70,"s":[64]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":50,"s":[1,0.905882418156,0.298039227724,1]},{"t":70,"s":[0.992156922817,0.321568638086,0.078431375325,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":50,"s":[48]},{"t":70,"s":[80]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":50,"op":70,"st":-30,"bm":0},{"ddd":0,"ind":22,"ty":0,"nm":"smoke 2","refId":"comp_24","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":71,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":81,"s":[90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":111,"s":[90]},{"t":131,"s":[0]}]},"p":{"a":0,"k":[784,1070.383,0]},"a":{"a":0,"k":[276,668,0]},"s":{"a":0,"k":[120,120,100]}},"ao":0,"w":512,"h":768,"ip":71,"op":131,"st":71,"bm":0},{"ddd":0,"ind":23,"ty":0,"nm":"02 fire middle 3","refId":"comp_25","sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[-0.766]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":42,"s":[5.499]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":55,"s":[3.823]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":71,"s":[5.884]},{"t":106,"s":[1.622]}]},"p":{"a":0,"k":[792,1144,0]},"a":{"a":0,"k":[256,768,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":768,"ip":25,"op":113,"st":25,"bm":0},{"ddd":0,"ind":24,"ty":0,"nm":"smoke2_36 sek 2","refId":"comp_9","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":98,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":108,"s":[90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":121,"s":[90]},{"t":134,"s":[0]}]},"p":{"a":0,"k":[596,764,0]},"a":{"a":0,"k":[276,668,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":768,"ip":98,"op":134,"st":98,"bm":0},{"ddd":0,"ind":25,"ty":0,"nm":"01 fire small wide2","refId":"comp_26","sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":60,"s":[5.76]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":86,"s":[-1.041]},{"t":144,"s":[4.242]}]},"p":{"a":0,"k":[593.436,820.784,0]},"a":{"a":0,"k":[256,512,0]},"s":{"a":0,"k":[-80,80,100]}},"ao":0,"w":512,"h":512,"ip":60,"op":152,"st":60,"bm":0},{"ddd":0,"ind":26,"ty":4,"nm":"Shape Layer 13","sr":1,"ks":{"p":{"a":0,"k":[527.164,1021,0]},"a":{"a":0,"k":[77.164,253,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[34.527,76.078],[108,-470]],"o":[[-118,-260],[-30.762,133.873]],"v":[[784,-84],[94,102]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[0]},{"t":60,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[1,1,1,1]},{"t":60,"s":[1,0.905882418156,0.298039227724,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[32]},{"t":60,"s":[48]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[1,0.905882418156,0.298039227724,1]},{"t":60,"s":[0.992156922817,0.321568638086,0.078431375325,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[48]},{"t":60,"s":[64]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":40,"op":60,"st":-40,"bm":0},{"ddd":0,"ind":27,"ty":0,"nm":"03 fire small wide cycle end 2","refId":"comp_2","sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":35,"s":[-3.983]},{"t":82,"s":[0]}]},"p":{"a":0,"k":[244,1056.8,0]},"a":{"a":0,"k":[256,512,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":512,"ip":35,"op":83,"st":35,"bm":0},{"ddd":0,"ind":28,"ty":0,"nm":"smoke2_36 sek 3","refId":"comp_27","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":57,"s":[90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[90]},{"t":83,"s":[0]}]},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":47,"s":[236,1004,0],"to":[0,0,0],"ti":[0,0,0]},{"t":83,"s":[236,732,0]}]},"a":{"a":0,"k":[276,668,0]},"s":{"a":0,"k":[110,110,100]}},"ao":0,"w":512,"h":768,"ip":47,"op":83,"st":47,"bm":0},{"ddd":0,"ind":29,"ty":0,"nm":"01 fire small","refId":"comp_28","sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[-2.909]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[0.418]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":68,"s":[-6.58]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":80,"s":[-1.563]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":111,"s":[5.03]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":124,"s":[7.741]},{"t":135,"s":[0]}]},"p":{"a":0,"k":[1152.795,555.98,0]},"a":{"a":0,"k":[256,512,0]}},"ao":0,"w":512,"h":512,"ip":20,"op":176,"st":20,"bm":0},{"ddd":0,"ind":30,"ty":0,"nm":"smoke 2","refId":"comp_24","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":129,"s":[90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":159,"s":[90]},{"t":179,"s":[0]}]},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":147,"s":[1192,500,0],"to":[0,0,0],"ti":[0,0,0]},{"t":179,"s":[1192,636,0]}]},"a":{"a":0,"k":[276,668,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":768,"ip":119,"op":179,"st":119,"bm":0},{"ddd":0,"ind":31,"ty":0,"nm":"smoke2_36 sek 2","refId":"comp_9","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":144,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":154,"s":[90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":167,"s":[90]},{"t":180,"s":[0]}]},"p":{"a":0,"k":[296,644,0]},"a":{"a":0,"k":[276,668,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":768,"ip":144,"op":180,"st":144,"bm":0},{"ddd":0,"ind":32,"ty":0,"nm":"02 fire middle 2","refId":"comp_29","sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[1.238]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":37,"s":[-3.101]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":54,"s":[-8.156]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":66,"s":[-5.302]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":89,"s":[4.013]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":107,"s":[8.136]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":116,"s":[4.606]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":131,"s":[-7.186]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":141,"s":[-11.857]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":150,"s":[-7.113]},{"t":165,"s":[5.607]}]},"p":{"a":0,"k":[292.283,677.379,0]},"a":{"a":0,"k":[256,768,0]},"s":{"a":0,"k":[80,80,100]}},"ao":0,"w":512,"h":768,"ip":28,"op":180,"st":28,"bm":0},{"ddd":0,"ind":33,"ty":0,"nm":"smoke2_36 sek","refId":"comp_5","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":50,"s":[90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":63,"s":[90]},{"t":76,"s":[0]}]},"p":{"a":0,"k":[576,848,0]},"a":{"a":0,"k":[276,668,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":768,"ip":40,"op":76,"st":40,"bm":0},{"ddd":0,"ind":34,"ty":0,"nm":"03 fire small wide cycle end 2","refId":"comp_2","sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0.759]},{"t":67,"s":[1.63]}]},"p":{"a":0,"k":[564,920.8,0]},"a":{"a":0,"k":[256,512,0]},"s":{"a":0,"k":[70,70,100]}},"ao":0,"w":512,"h":512,"ip":20,"op":68,"st":20,"bm":0},{"ddd":0,"ind":35,"ty":0,"nm":"02 fire middle 3","refId":"comp_25","sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":27,"s":[-0.899]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":41,"s":[-5.017]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":85,"s":[0.569]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":97,"s":[4.675]},{"t":108,"s":[0.39]}]},"p":{"a":0,"k":[657.596,487.985,0]},"a":{"a":0,"k":[256,768,0]},"s":{"a":0,"k":[50,50,100]}},"ao":0,"w":512,"h":768,"ip":27,"op":115,"st":27,"bm":0},{"ddd":0,"ind":36,"ty":4,"nm":"Shape Layer 19","sr":1,"ks":{"p":{"a":0,"k":[527.164,1021,0]},"a":{"a":0,"k":[77.164,253,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[3.598,83.469],[184,-738]],"o":[[-10,-232],[-33.23,133.282]],"v":[[756,84],[-222,246]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[0]},{"t":35,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[1,1,1,1]},{"t":35,"s":[1,0.905882418156,0.298039227724,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[32]},{"t":35,"s":[48]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[1,0.905882418156,0.298039227724,1]},{"t":35,"s":[0.992156922817,0.321568638086,0.078431375325,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[48]},{"t":35,"s":[64]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":15,"op":35,"st":-65,"bm":0},{"ddd":0,"ind":37,"ty":4,"nm":"Shape Layer 11","sr":1,"ks":{"p":{"a":0,"k":[527.164,1021,0]},"a":{"a":0,"k":[77.164,253,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[3.598,83.469],[496,-534]],"o":[[-10,-232],[-93.483,100.645]],"v":[[756,84],[-206,-122]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":8,"s":[0]},{"t":28,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":8,"s":[1,1,1,1]},{"t":28,"s":[1,0.905882418156,0.298039227724,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":8,"s":[32]},{"t":28,"s":[48]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":8,"s":[1,0.905882418156,0.298039227724,1]},{"t":28,"s":[0.992156922817,0.321568638086,0.078431375325,1]}]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":8,"s":[48]},{"t":28,"s":[64]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":8,"op":28,"st":-72,"bm":0},{"ddd":0,"ind":38,"ty":0,"nm":"03 fire small wide cycle end 2","refId":"comp_2","sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[-1.396]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":51,"s":[0.803]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":56,"s":[2.307]},{"t":92,"s":[0.387]}]},"p":{"a":0,"k":[900,364.8,0]},"a":{"a":0,"k":[256,512,0]},"s":{"a":0,"k":[70,70,100]}},"ao":0,"w":512,"h":512,"ip":45,"op":93,"st":45,"bm":0},{"ddd":0,"ind":39,"ty":0,"nm":"smoke 3","refId":"comp_30","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":80,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":90,"s":[90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":127,"s":[90]},{"t":140,"s":[0]}]},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":80,"s":[662,458,0],"to":[0,0,0],"ti":[0,0,0]},{"t":140,"s":[662,554,0]}]},"a":{"a":0,"k":[276,668,0]},"s":{"a":0,"k":[-90,90,100]}},"ao":0,"w":512,"h":768,"ip":80,"op":140,"st":80,"bm":0},{"ddd":0,"ind":40,"ty":0,"nm":"smoke2_36 sek 2","refId":"comp_9","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":52,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":62,"s":[90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":75,"s":[90]},{"t":88,"s":[0]}]},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":52,"s":[916,324,0],"to":[0,0,0],"ti":[0,0,0]},{"t":88,"s":[916,576,0]}]},"a":{"a":0,"k":[276,668,0]},"s":{"a":0,"k":[90,90,100]}},"ao":0,"w":512,"h":768,"ip":52,"op":88,"st":52,"bm":0},{"ddd":0,"ind":41,"ty":0,"nm":"smoke verkh","refId":"comp_31","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":79,"s":[90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":139,"s":[90]},{"t":180,"s":[0]}]},"p":{"a":0,"k":[768,768,0]},"a":{"a":0,"k":[768,768,0]}},"ao":0,"w":1536,"h":1536,"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":42,"ty":0,"nm":"nakal4","refId":"comp_32","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":90,"s":[100]},{"t":166,"s":[0]}]},"p":{"a":0,"k":[768,768,0]},"a":{"a":0,"k":[768,768,0]}},"ao":0,"w":1536,"h":1536,"ip":0,"op":180,"st":0,"bm":0}]},{"id":"comp_1","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 4","parent":2,"sr":1,"ks":{"p":{"a":0,"k":[-77.334,-175.69,0]},"a":{"a":0,"k":[-77.334,-175.69,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[1.988,3.181],[-19.8,-5.052]],"o":[[-4.889,-7.822],[26.133,6.668]],"v":[[19.329,271.399],[24.3,294.213]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[4.186,6.698],[-41.687,-10.636]],"o":[[-10.293,-16.469],[55.022,14.038]],"v":[[7.834,173.427],[18.299,221.462]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[14.361,7.522],[-30.261,-6.497]],"o":[[-24.248,-12.701],[34.72,7.454]],"v":[[35.139,128.628],[12.401,185.046]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[2.846,1.491],[-5.996,-1.287]],"o":[[-4.805,-2.517],[6.88,1.477]],"v":[[32.578,61.344],[28.072,72.523]],"c":true}]},{"t":31,"s":[{"i":[[2.846,1.491],[-5.996,-1.287]],"o":[[-4.805,-2.517],[6.88,1.477]],"v":[[20.578,-416.656],[16.072,-405.477]],"c":true}]}]},"nm":"Path 7","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[1.795,-2.463],[-22.463,0.136]],"o":[[-6.751,9.267],[21.909,-0.132]],"v":[[28.78,287.216],[26.463,308]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[6.778,-9.304],[-84.843,0.513]],"o":[[-25.5,35],[82.75,-0.5]],"v":[[35,213],[26.25,291.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[41.584,-3.199],[-34.387,-1.279]],"o":[[-37.621,2.894],[34.387,1.279]],"v":[[41.616,212.199],[44.975,259.221]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[9.857,-0.643],[-6.643,-0.429]],"o":[[-9.65,0.629],[6.643,0.429]],"v":[[38.138,192.643],[39.103,203.571]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[3.286,-0.214],[-2.214,-0.143]],"o":[[-3.217,0.21],[2.214,0.143]],"v":[[21.707,90.214],[22.029,93.857]],"c":true}]},{"t":41,"s":[{"i":[[3.286,-0.214],[-2.214,-0.143]],"o":[[-3.217,0.21],[2.214,0.143]],"v":[[19.707,-427.786],[20.029,-424.143]],"c":true}]}]},"nm":"Path 6","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[33.144,5.233],[-24.721,-0.221]],"o":[[-35.022,-5.53],[27.911,0.249]],"v":[[19.555,225.935],[15.319,283.751]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[29.02,-23.268],[-29.148,15.285]],"o":[[-36.827,29.528],[27.206,-14.266]],"v":[[18.827,100.658],[25.794,178.294]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[36.864,-1.646],[-34.889,28.965]],"o":[[-35.938,1.604],[46.08,13.495]],"v":[[1.709,52.646],[-0.595,87.535]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[19.216,-6.659],[-24.954,25.786]],"o":[[-28.004,9.704],[30.5,0]],"v":[[16.732,-8.349],[24.773,17.714]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[12.002,-2.4],[-12.348,2.646]],"o":[[-16.253,3.251],[13.442,-2.88]],"v":[[25.798,-28.588],[29.158,-15.146]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[20.461,-75.516],[21.822,-70.072]],"c":true}]},{"t":51,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[10.461,-427.516],[11.822,-422.072]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[47.81,-67.072],[-25.632,9.313]],"o":[[-93.246,48.698],[25.632,-9.313]],"v":[[-74.754,121.302],[-13.132,182.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[16.302,-91.514],[22.297,-55.155]],"o":[[-51.846,41.486],[63.795,-25.293]],"v":[[-77.654,-43.486],[-14.297,37.655]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[65.508,-110.844],[-62.172,37.016]],"o":[[-46.719,4.672],[111.508,-8.984]],"v":[[-77.508,-81.156],[-31.508,-13.016]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[44.392,-34.904],[-43.656,10.635]],"o":[[-57.534,8.63],[43.656,-10.635]],"v":[[-64.892,-151.596],[-43.156,-90.865]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[45.056,-54.944],[-36.611,29.426]],"o":[[-75.944,-2.944],[74.944,33.426]],"v":[[-55.556,-184.556],[-45.389,-150.426]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[14.5,-3],[-14.25,5.5]],"o":[[-14.5,3],[14.25,-5.5]],"v":[[-44.875,-250.5],[-40.375,-234]],"c":true}]},{"t":60,"s":[{"i":[[1.16,-0.24],[-1.14,0.44]],"o":[[-1.16,0.24],[1.14,-0.44]],"v":[[-45.92,-278.26],[-45.56,-276.94]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[38.071,2.586],[-51.674,3.841],[-14.328,5.136]],"o":[[-71.639,-4.866],[8.07,11.41],[22.966,-8.233]],"v":[[122.074,242.366],[89.943,278.535],[130.995,288.864]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[20.869,-12.454],[-146.79,82.022],[-12.062,30.104]],"o":[[-112.454,-96.052],[15.663,4.937],[27.265,-8.079]],"v":[[157.954,39.052],[123.79,100.978],[169.735,69.137]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[74.55,-67.464],[-92.612,89.3],[0.78,32.797]],"o":[[-175.95,-43.988],[-28.612,-55.2],[57.28,16.797]],"v":[[109.45,-30.036],[99.612,30.2],[150.72,14.703]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[79.321,-50.136],[-20.934,-21.241],[-26.364,9.073]],"o":[[-58.179,-42.136],[17.914,18.177],[76.169,29.621]],"v":[[102.679,-76.364],[46.086,-27.677],[111.331,-23.621]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[62.617,-21.138],[-8.337,-17.814],[-11.971,0.122]],"o":[[-29.444,-9.815],[3.681,7.865],[67.802,31.907]],"v":[[103.66,-125.362],[70.352,-89.569],[93.689,-75.907]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[19.334,0],[-4.841,-5.763],[-4.486,0.179]],"o":[[-13.412,0],[2.137,2.544],[14.647,-0.586]],"v":[[106.839,-168.5],[96.133,-153.01],[106.253,-148.873]],"c":true}]},{"t":60,"s":[{"i":[[5.5,0],[-1.377,-1.639],[-1.276,0.051]],"o":[[-3.815,0],[0.608,0.724],[4.167,-0.167]],"v":[[103.5,-198.125],[100.454,-193.718],[103.333,-192.542]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":5,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[29.237,-0.424],[18.659,-25.84],[-78.306,-8.932],[-2.4,-26.987],[0.564,-42.682],[30.491,12.722]],"o":[[-3.4,-14.426],[-41.473,-22.64],[-12.806,-41.432],[1.048,11.781],[27.835,19.62],[5.813,-25.223]],"v":[[106.649,132.4],[49.973,125.64],[49.306,238.932],[79.9,226.987],[116.436,225.182],[131.993,185.252]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[18.356,-65.631],[1.004,-68.279],[-92.58,7.072],[-16.188,1.28],[-17.016,-0.736],[29.057,17.998]],"o":[[12.356,-49.631],[-116.996,-73.279],[18.803,-1.436],[15.763,-1.246],[26.568,1.15],[25.666,-30.324]],"v":[[114.644,-62.369],[48.996,-92.721],[50.08,17.428],[99.158,-10.713],[145.932,9.85],[160.334,-37.676]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[40.408,-8.621],[8.905,-22.866],[-63.43,19.052],[-2.553,-27.854],[-0.664,-27.112],[37.995,7.577]],"o":[[-15.642,-10.077],[-92.499,-21.673],[-3,-37.948],[6.447,-39.854],[25.263,14.209],[-3.038,-25.183]],"v":[[102.472,-151.481],[53.507,-145.134],[65,-51.052],[106.553,-41.146],[153.664,-66.888],[163.996,-115.133]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[25.979,-38.681],[-12.816,-22.414],[-71.606,9.11],[-7.652,-23.023],[-8.541,18],[3.417,17.237]],"o":[[6.479,-39.181],[-74.816,-41.414],[-4.106,-29.39],[7.848,-31.023],[7.401,-15.598],[-5.628,-28.395]],"v":[[80.521,-174.319],[39.316,-172.086],[38.606,-82.61],[103.652,-89.977],[145.259,-114.137],[152.031,-167.148]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[35.852,-24.204],[6.121,-6.787],[-30.147,-5.617],[-12.364,11.19],[-4.863,14.486],[5.544,11.648]],"o":[[-15.19,-0.044],[-18.957,21.021],[11.029,2.055],[17.765,0.658],[4.214,-12.553],[-9.133,-19.188]],"v":[[66.426,-225.746],[34.942,-214.601],[66.05,-144.055],[101.841,-156.26],[135.809,-181.382],[133.83,-220.533]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[11.188,-2.48],[2.145,-2.133],[-8.537,-4.421],[-3.25,0.288],[-2.088,4.234],[1.472,3.752]],"o":[[-5.164,1.145],[-6.643,6.608],[3.123,1.618],[6.515,-0.577],[1.81,-3.669],[-2.424,-6.18]],"v":[[99.368,-273.52],[88.579,-268.455],[96.629,-247.5],[106.444,-245.289],[119.333,-253.457],[119.83,-265.206]],"c":true}]},{"t":60,"s":[{"i":[[2.469,0.044],[0.542,-0.415],[-1.676,-1.418],[-0.709,0.023],[-0.595,0.897],[0.16,0.894]],"o":[[-1.158,-0.021],[-1.68,1.285],[0.613,0.519],[1.347,-0.043],[0.516,-0.777],[-0.264,-1.473]],"v":[[96.518,-299.069],[93.999,-298.426],[94.96,-292.841],[96.99,-292.023],[99.944,-293.62],[100.503,-296.249]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":6,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-3.499,-24.848],[-6.884,-16.478],[-0.301,7.46],[-4.644,0.228],[-25.003,12.782],[33.341,-4.053]],"o":[[-34.078,22.449],[3.276,7.842],[1.294,-32.082],[19.012,20.499],[85.358,33.282],[-6.489,-19.362]],"v":[[-102.261,200.848],[-133.404,262.516],[-118.294,281.082],[-102.012,287.001],[-31.997,296.218],[-11.544,205.996]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[-0.747,-48.874],[-2.455,-35.731],[-40.679,19.746],[-10.189,-28.494],[-11.643,4.734],[85.039,22.458]],"o":[[-21.211,-29.874],[-35.532,22.641],[-1.215,-25.539],[22.811,19.506],[80.357,20.734],[29.075,-42.399]],"v":[[-100.253,19.874],[-169.932,40.216],[-144.785,98.539],[-93.811,115.494],[-40.357,117.266],[-14.075,40.399]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[4.596,-2.157],[-2.838,-33.101],[-23.392,21.782],[64.52,-34.091],[-7.412,7.454],[7.166,18.65]],"o":[[-43.115,-41.195],[-25.698,17.837],[-20.978,-41.699],[20.24,22.409],[13.576,-13.654],[-5.083,-13.23]],"v":[[-78.36,-37.305],[-165.347,-14.561],[-142.022,35.199],[-94.52,47.591],[-39.74,43.428],[-28.273,-12.423]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[19.586,24.077],[1.481,-16.058],[-4.9,-5.507],[-10.179,0.727],[8.719,-39.971],[89.45,-41.348]],"o":[[-19.447,-7.56],[-0.705,7.642],[4.542,5.105],[-3.153,-23.324],[83.219,35.029],[-14.55,-40.348]],"v":[[-113.586,-84.077],[-147.276,-60.135],[-141.231,-39.204],[-119.347,-31.676],[-66.219,-26.529],[-64.45,-76.152]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[13.764,-16.52],[-0.797,-14.42],[-5.435,-4.397],[-9.222,1.812],[-4.425,5.611],[7.725,10.732]],"o":[[-22.868,-5.87],[0.379,6.863],[5.039,4.077],[14.57,16.763],[8.105,-10.278],[-5.48,-7.613]],"v":[[-80.463,-124.48],[-113.116,-101.094],[-104.345,-83.066],[-82.915,-78.763],[-39.977,-80.996],[-37.614,-117.588]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[9.081,-4.12],[1.941,-4.856],[-0.074,-2.217],[-1.433,-1.373],[-2.412,2.764],[3.877,5.235]],"o":[[-4.629,2.1],[-0.924,2.311],[0.069,2.056],[11.907,11.408],[4.417,-5.063],[-2.75,-3.714]],"v":[[-75.274,-166.527],[-85.585,-154.758],[-86.926,-147.859],[-85.155,-142.261],[-54.718,-143.901],[-53.648,-161.404]],"c":true}]},{"t":60,"s":[{"i":[[0.601,-0.289],[0.013,-0.847],[-0.221,-0.37],[-0.364,-0.269],[-0.704,0.652],[0.934,1.027]],"o":[[-1.307,0.628],[-0.006,0.403],[0.205,0.343],[0.87,0.643],[1.289,-1.194],[-0.662,-0.729]],"v":[[-79.613,-181.42],[-81.494,-179.083],[-81.161,-177.91],[-80.299,-176.982],[-73.89,-177.455],[-73.114,-181.387]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.980392216701,0.972549079446,0.949019667682,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"p":{"a":0,"k":[280.666,670.31,0]},"a":{"a":0,"k":[24.666,286.31,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[20,20,100]},{"t":20,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[1.988,3.181],[-19.8,-5.052]],"o":[[-4.889,-7.822],[26.133,6.668]],"v":[[19.329,271.399],[24.3,294.213]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[6.101,9.761],[-60.75,-15.5]],"o":[[-15,-24],[80.183,20.458]],"v":[[0,163],[15.25,233]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[21,11],[-44.25,-9.5]],"o":[[-35.458,-18.573],[50.771,10.9]],"v":[[35,125],[1.75,207.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[2.846,1.491],[-5.996,-1.287]],"o":[[-4.805,-2.517],[6.88,1.477]],"v":[[32.578,61.344],[28.072,72.523]],"c":true}]},{"t":31,"s":[{"i":[[2.846,1.491],[-5.996,-1.287]],"o":[[-4.805,-2.517],[6.88,1.477]],"v":[[20.578,-416.656],[16.072,-405.477]],"c":true}]}]},"nm":"Path 7","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[1.795,-2.463],[-22.463,0.136]],"o":[[-6.751,9.267],[21.909,-0.132]],"v":[[28.78,287.216],[26.463,308]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[6.778,-9.304],[-84.843,0.513]],"o":[[-25.5,35],[82.75,-0.5]],"v":[[35,213],[26.25,291.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[65,-5],[-53.75,-2]],"o":[[-58.805,4.523],[53.75,2]],"v":[[34,209.5],[39.25,283]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[23,-1.5],[-15.5,-1]],"o":[[-22.516,1.468],[15.5,1]],"v":[[35.5,190.5],[37.75,216]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[3.286,-0.214],[-2.214,-0.143]],"o":[[-3.217,0.21],[2.214,0.143]],"v":[[21.707,90.214],[22.029,93.857]],"c":true}]},{"t":41,"s":[{"i":[[3.286,-0.214],[-2.214,-0.143]],"o":[[-3.217,0.21],[2.214,0.143]],"v":[[19.707,-427.786],[20.029,-424.143]],"c":true}]}]},"nm":"Path 6","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[66.5,10.5],[-49.599,-0.443]],"o":[[-70.267,-11.095],[56,0.5]],"v":[[20.5,199.5],[12,315.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[43.734,-35.066],[-43.928,23.035]],"o":[[-55.5,44.5],[41,-21.5]],"v":[[15.5,72.5],[26,189.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[56,-2.5],[-53,44]],"o":[[-54.592,2.437],[70,20.5]],"v":[[-4.5,51.5],[-8,104.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[34.653,-12.008],[-45,46.5]],"o":[[-50.5,17.5],[55,0]],"v":[[9.5,-11.5],[24,35.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[25,-5],[-25.722,5.512]],"o":[[-33.855,6.771],[28,-6]],"v":[[23,-29],[30,-1]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[20.461,-75.516],[21.822,-70.072]],"c":true}]},{"t":51,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[10.461,-427.516],[11.822,-422.072]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[69.5,-97.5],[-113.5,45]],"o":[[-66.5,8.5],[86.092,-34.134]],"v":[[-90.5,124.5],[-22,227]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[22,-123.5],[-113.5,45]],"o":[[-41.5,7],[86.092,-34.134]],"v":[[-91.5,-56.5],[-6,53]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[29.5,-125],[-86.5,51.5]],"o":[[-65,6.5],[164.5,11.5]],"v":[[-87.5,-87],[-23.5,-7.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[71,-76.5],[-34.5,28]],"o":[[-90,13.5],[106.5,47.5]],"v":[[-68.5,-157.5],[-34.5,-62.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[48.5,-66],[-34.5,28]],"o":[[-81,3],[106.5,47.5]],"v":[[-57.5,-194],[-53,-132]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[29,-6],[-28.5,11]],"o":[[-29,6],[28.5,-11]],"v":[[-46,-251.5],[-37,-218.5]],"c":true}]},{"t":60,"s":[{"i":[[1.16,-0.24],[-1.14,0.44]],"o":[[-1.16,0.24],[1.14,-0.44]],"v":[[-45.92,-278.26],[-45.56,-276.94]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[70.414,4.783],[-95.574,7.104],[-26.5,9.5]],"o":[[-132.5,-9],[14.926,21.104],[42.478,-15.228]],"v":[[116,239],[56.574,305.896],[132.5,325]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[31,-18.5],[-168.751,36.203],[-17.918,44.718]],"o":[[-248,-61.5],[23.267,7.334],[40.5,-12]],"v":[[167,24],[116.251,135.297],[184.5,88]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[101,-20.5],[-133.016,57.816],[-23.678,31.225]],"o":[[-222,-55.5],[17.83,12.614],[53,-13]],"v":[[114.5,-57.5],[94.516,65.184],[159,45]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[108,-21],[-25.045,-34.694],[-37.826,13.017]],"o":[[-46.826,-19.424],[11.058,15.318],[78.5,26]],"v":[[96,-99.5],[40.628,-7.406],[112,2]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[78.5,-26.5],[-10.452,-22.332],[-15.008,0.153]],"o":[[-36.913,-12.304],[4.615,9.86],[85,40]],"v":[[101.5,-126.5],[59.744,-81.628],[89,-64.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[33,0],[-8.262,-9.837],[-7.657,0.306]],"o":[[-22.893,0],[3.648,4.343],[25,-1]],"v":[[103,-170],[84.726,-143.561],[102,-136.5]],"c":true}]},{"t":60,"s":[{"i":[[5.5,0],[-1.377,-1.639],[-1.276,0.051]],"o":[[-3.815,0],[0.608,0.724],[4.167,-0.167]],"v":[[103.5,-198.125],[100.454,-193.718],[103.333,-192.542]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":5,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[40.224,-0.584],[25.671,-35.551],[-117.344,4.076],[-13.845,-2.09],[-15.219,20.222],[41.95,17.503]],"o":[[-4.678,-19.848],[-63.829,-76.551],[7.749,18.334],[16.09,2.429],[38.296,26.994],[7.997,-34.703]],"v":[[108.178,121.348],[34.329,117.551],[25.844,278.924],[70,279],[118.204,260.006],[148.55,186.497]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[42.564,-6.226],[40.813,-50.225],[-85.297,31.262],[-32.063,3.791],[-15.43,23.796],[36.231,22.442]],"o":[[-6.076,-18.138],[-120.187,-27.225],[8.47,17.96],[23.983,5.161],[35.183,9.711],[3.616,-40.885]],"v":[[125.779,-104.633],[37.687,-123.775],[35.297,47.238],[94,72],[154.817,37.789],[175.269,-33.942]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[50.807,-10.84],[11.197,-28.751],[-79.754,23.956],[-21.881,6.283],[-12.991,22.817],[47.774,9.527]],"o":[[-19.668,-12.67],[-116.303,-27.251],[12.896,12.677],[24.971,6.064],[31.765,17.866],[-3.819,-31.664]],"v":[[94.869,-161.73],[33.303,-153.749],[47.754,-35.456],[100,-23],[159.235,-55.366],[172.226,-116.027]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[61.33,-27.94],[13.035,-11.794],[-88,34.498],[-24.613,14.871],[-11.285,23.783],[4.514,22.775]],"o":[[-22.734,-8.189],[-81.69,0.79],[16.437,2.441],[24.287,7.59],[9.779,-20.609],[-7.436,-37.518]],"v":[[69.474,-198.525],[15.69,-190.29],[42.5,-42.998],[104,-60],[158.974,-91.922],[167.923,-161.964]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[49.628,-33.504],[8.473,-9.395],[-41.73,-7.776],[-17.115,15.49],[-6.732,20.052],[7.675,16.124]],"o":[[-21.027,-0.06],[-26.241,29.098],[15.267,2.845],[24.591,0.911],[5.833,-17.376],[-12.642,-26.561]],"v":[[48.978,-222.185],[5.396,-206.758],[48.457,-109.106],[98,-126],[145.019,-160.775],[142.281,-214.968]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[20.595,-4.565],[3.948,-3.927],[-15.714,-8.139],[-5.982,0.53],[-3.844,7.794],[2.709,6.906]],"o":[[-9.506,2.107],[-12.229,12.163],[5.749,2.978],[11.992,-1.063],[3.331,-6.754],[-4.463,-11.376]],"v":[[87.975,-274.467],[68.116,-265.142],[82.934,-226.569],[101,-222.5],[124.726,-237.535],[125.642,-259.162]],"c":true}]},{"t":60,"s":[{"i":[[4.312,0.077],[0.947,-0.725],[-2.927,-2.476],[-1.238,0.04],[-1.039,1.566],[0.28,1.562]],"o":[[-2.023,-0.036],[-2.933,2.244],[1.071,0.906],[2.353,-0.076],[0.9,-1.357],[-0.461,-2.572]],"v":[[95.675,-299.303],[91.277,-298.18],[92.954,-288.429],[96.5,-287],[101.657,-289.789],[102.633,-294.38]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":6,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-4.046,-28.733],[-7.961,-19.055],[-7.058,-4.972],[-5.37,0.263],[-10.383,14.21],[38.553,-4.686]],"o":[[-39.406,25.959],[3.789,9.068],[6.543,4.609],[20.166,40.644],[98.704,38.486],[-7.504,-22.39]],"v":[[-106.454,196.733],[-142.466,268.042],[-124.994,289.511],[-106.166,296.356],[-25.204,307.014],[-1.553,202.686]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[7.754,-10.103],[-2.864,-41.686],[-47.459,23.038],[-17.819,6.695],[-11.001,13.349],[99.213,26.201]],"o":[[-24.746,-34.853],[-41.454,26.415],[10.089,11.703],[9.571,21.591],[45.708,87.19],[-6.546,-22.901]],"v":[[-105.754,6.853],[-187.046,30.585],[-146.041,114.963],[-104.071,125.409],[-34.708,136.81],[-5.213,30.799]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[5.648,-2.651],[-3.488,-40.68],[-28.748,26.769],[-22.563,7.933],[-9.109,9.161],[8.807,22.921]],"o":[[-52.986,-50.627],[-31.582,21.921],[15.404,15.153],[24.874,27.539],[16.685,-16.781],[-6.247,-16.259]],"v":[[-65.014,-36.373],[-171.918,-8.421],[-143.252,52.731],[-84.874,67.961],[-17.551,62.845],[-3.459,-5.795]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[19.861,-24.879],[2.389,-25.903],[-7.904,-8.883],[-16.419,1.173],[-8.619,13.394],[50.602,-9.162]],"o":[[-31.37,-12.195],[-1.137,12.327],[7.327,8.235],[20.153,51.095],[59.455,-7.707],[-9.228,-15.356]],"v":[[-114.361,-87.621],[-168.705,-49.001],[-158.954,-15.239],[-123.653,-3.095],[-37.955,5.207],[-35.102,-74.838]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[20.907,-25.093],[-1.21,-21.904],[-8.256,-6.68],[-14.008,2.753],[-6.721,8.523],[11.735,16.301]],"o":[[-34.736,-8.916],[0.576,10.424],[7.654,6.192],[22.131,25.463],[12.311,-15.612],[-8.324,-11.564]],"v":[[-85.407,-125.907],[-135.006,-90.384],[-121.683,-62.999],[-89.131,-56.463],[-23.909,-59.855],[-20.32,-115.438]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[16.089,-7.299],[3.439,-8.602],[-0.131,-3.928],[-2.539,-2.432],[-4.273,4.897],[6.868,9.275]],"o":[[-8.201,3.721],[-1.637,4.094],[0.122,3.642],[21.095,20.211],[7.826,-8.97],[-4.872,-6.579]],"v":[[-77.089,-167.201],[-95.356,-146.351],[-97.732,-134.128],[-94.595,-124.211],[-40.671,-127.117],[-38.776,-158.124]],"c":true}]},{"t":60,"s":[{"i":[[1.463,-0.702],[0.031,-2.062],[-0.538,-0.9],[-0.887,-0.655],[-1.713,1.587],[2.273,2.5]],"o":[[-3.182,1.528],[-0.015,0.981],[0.498,0.834],[2.118,1.565],[3.138,-2.907],[-1.612,-1.773]],"v":[[-78.589,-181.314],[-83.167,-175.626],[-82.357,-172.772],[-80.258,-170.512],[-64.659,-171.663],[-62.771,-181.234]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.945098099054,0.90588241278,0.792156922583,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.917647118662,0.847058883368,0.721568627451,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":16},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":60,"st":0,"bm":0}]},{"id":"comp_2","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 12","sr":1,"ks":{"o":{"a":0,"k":0},"p":{"a":0,"k":[256,495,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[40,40,100]},{"t":8,"s":[100,100,100]}]}},"ao":0,"ip":0,"op":28,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"core","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":12,"s":[100]},{"t":20,"s":[0]}]},"p":{"a":0,"k":[4.85,-0.729,0]},"a":{"a":0,"k":[-51.438,222.044,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[18.197,16.546],[-41.985,0.511],[69.85,68.547]],"o":[[-53.725,57.166],[41.194,-0.501],[-14.905,76.809]],"v":[[-60.485,-15.773],[2.147,99.863],[27.617,-48.036]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[0.068,-0.429],[-35.745,0.435],[65.918,79.024]],"o":[[-55.611,56.567],[35.071,-0.427],[-0.687,-0.216]],"v":[[-9.918,-30.798],[-3.59,87.929],[-8.638,-29.958]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[0.069,-0.44],[-31.286,-32.594],[40.368,45.715]],"o":[[-28.033,10.909],[28.162,-20.704],[-0.705,-0.222]],"v":[[18.029,13.22],[1.286,91.311],[19.344,14.083]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0.086,-0.545],[-13.305,-8.903],[-4.886,23.805]],"o":[[-34.696,13.502],[52.246,-21.612],[-0.872,-0.275]],"v":[[0.536,-65.183],[9.914,2.038],[2.164,-64.114]],"c":true}]},{"t":20,"s":[{"i":[[0.128,-0.814],[-10.891,-11.311],[27.696,1.589]],"o":[[20.128,26.186],[43.109,-11.311],[-1.304,-0.411]],"v":[[11.872,-133.186],[21.891,-73.689],[14.304,-131.589]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0.121,0.996,0.996,0.898,0.54,0.998,0.951,0.598,0.808,1,0.906,0.298]}},"s":{"a":0,"k":[2.235,29.653]},"e":{"a":0,"k":[6.469,-84.419]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 4","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"second","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":12,"s":[100]},{"t":20,"s":[0]}]},"p":{"a":0,"k":[2,-97,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-1.733,16.717],[-67.889,0.826],[112.946,110.839]],"o":[[-86.872,92.436],[66.609,-0.811],[-1.054,155.839]],"v":[[-75.128,-84.436],[1.891,99.311],[22.054,-139.839]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[0.128,-0.814],[-67.889,0.826],[125.196,150.089]],"o":[[-105.622,107.436],[66.609,-0.811],[-1.304,-0.411]],"v":[[-13.128,-140.186],[-1.109,85.311],[-10.696,-138.589]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[0.128,-0.814],[-57.891,-60.311],[74.696,84.589]],"o":[[-51.872,20.186],[52.109,-38.311],[-1.304,-0.411]],"v":[[33.872,-53.186],[2.891,91.311],[36.304,-51.589]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0.128,-0.814],[-19.891,-13.311],[-7.304,35.589]],"o":[[-51.872,20.186],[78.109,-32.311],[-1.304,-0.411]],"v":[[-2.128,-99.186],[11.891,1.311],[0.304,-97.589]],"c":true}]},{"t":20,"s":[{"i":[[0.128,-0.814],[-10.891,-11.311],[27.696,1.589]],"o":[[20.128,26.186],[43.109,-11.311],[-1.304,-0.411]],"v":[[11.872,-133.186],[21.891,-73.689],[14.304,-131.589]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,0.914,0.239,0.269,1,0.739,0.22,1,1,0.565,0.2]}},"s":{"a":0,"k":[1.76,83.087]},"e":{"a":0,"k":[-8.18,-233.581]},"t":1,"nm":"Gradient Fill 2","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"main 2","parent":1,"sr":1,"ks":{"p":{"a":0,"k":[0,-111,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-49.509,44.388],[123.533,-38.986]],"o":[[-17.509,54.388],[-101.467,-137.986]],"v":[[138.509,-106.388],[72.467,102.986]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-54.652,117.814],[96.514,-88.952]],"o":[[39.848,36.814],[-55.486,-82.702]],"v":[[122.902,-122.064],[135.986,59.452]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-69.795,121.239],[25.494,-72.919]],"o":[[-6.795,25.239],[-9.506,-27.419]],"v":[[118.295,-144.739],[157.506,-8.081]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-47.393,78.823],[5.657,-37.203]],"o":[[-20.938,53.232],[-20.343,-15.203]],"v":[[125.938,-167.732],[170.343,-77.797]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-14.581,12.011],[1.034,-14.274]],"o":[[-8.51,26.743],[-24.109,4.012]],"v":[[132.081,-191.511],[118.466,-138.726]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":19,"s":[{"i":[[-8.189,4.595],[5.817,-6.951]],"o":[[0.811,6.877],[-3.683,-1.451]],"v":[[129.689,-209.345],[131.183,-194.049]],"c":true}]},{"t":20,"s":[{"i":[[-8.189,4.595],[5.817,-6.951]],"o":[[0.811,6.877],[-3.683,-1.451]],"v":[[139.689,-425.345],[141.183,-410.049]],"c":true}]}]},"nm":"Path 10","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[116.786,0],[11,144.667]],"o":[[-317,-127.726],[249,149.667]],"v":[[-1,113.726],[-1,-153.667]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[120.519,-22.794],[-202.241,204.246]],"o":[[-143.695,27.178],[23.759,129.246]],"v":[[0.481,97.794],[0.241,-202.246]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[168.037,-36.863],[3.518,139.825]],"o":[[-193.963,-76.863],[211.518,139.825]],"v":[[1.963,81.863],[-26.518,-251.825]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[78.037,-55.931],[-63.722,35.404]],"o":[[-42.963,-53.931],[-21.722,38.404]],"v":[[2.963,-132.069],[27.722,-295.404]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0.866,0],[-1.78,1.414]],"o":[[-1.004,0],[1.908,1.516]],"v":[[3.963,-346],[3.963,-347.983]],"c":true}]},{"t":17,"s":[{"i":[[0.866,0],[-1.78,1.414]],"o":[[-1.004,0],[1.908,1.516]],"v":[[25.963,-410],[25.963,-411.983]],"c":true}]}]},"nm":"Path 13","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[150.97,-29.436],[-7.904,50.282],[-29.987,22.93],[31.343,22.513]],"o":[[-171.03,-96.436],[20.614,30.124],[20.013,43.93],[96.97,73.457]],"v":[[0.03,113.436],[-84.471,-110.907],[-1.013,-82.93],[77.03,-95.457]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[263.97,-70.436],[41.784,65.595],[-24.463,-11.448],[-17.438,27.049]],"o":[[-288.03,-110.436],[28.054,20.833],[28.268,13.229],[-17.53,86.457]],"v":[[0.03,113.436],[-88.471,-144.407],[-8.648,-91.164],[61.53,-104.457]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[150.97,-29.436],[-32.029,33.907],[-26.789,24.397],[-10.737,32.946]],"o":[[-259.03,-106.436],[23.879,14.802],[25.211,25.397],[-2.03,120.457]],"v":[[0.03,113.436],[-60.471,-110.407],[27.789,-112.897],[118.03,-147.457]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[182.97,-93.436],[-9.029,35.657],[0,0],[-0.28,-0.293]],"o":[[-109.03,-114.436],[-0.029,0.657],[0,0],[30.72,63.707]],"v":[[0.03,113.436],[31.029,-60.657],[31.387,-61.673],[31.28,-60.707]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[122.605,-62.61],[-52.544,10.197],[0,0],[-0.187,-0.196]],"o":[[-64.272,-70.959],[-0.019,0.44],[0,0],[-33.712,34.231]],"v":[[10.772,31.459],[11.544,-98.197],[11.784,-98.878],[11.712,-98.231]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[62.24,-31.783],[36.94,34.738],[0,0],[-0.095,-0.1]],"o":[[-19.515,-27.482],[-0.01,0.223],[0,0],[53.855,13.755]],"v":[[21.515,-50.518],[-7.94,-135.738],[-7.818,-136.083],[-7.855,-135.755]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23,"s":[{"i":[[16.966,-8.664],[-0.837,3.306],[0,0],[-0.026,-0.027]],"o":[[-10.11,-10.611],[-0.003,0.061],[0,0],[2.849,5.907]],"v":[[-8.428,-142],[-5.554,-158.143],[-5.52,-158.237],[-5.53,-158.148]],"c":true}]},{"t":24,"s":[{"i":[[16.966,-8.664],[-0.837,3.306],[0,0],[-0.026,-0.027]],"o":[[-10.11,-10.611],[-0.003,0.061],[0,0],[2.849,5.907]],"v":[[51.572,-404],[54.446,-420.143],[54.48,-420.237],[54.47,-420.148]],"c":true}]}]},"nm":"Path 12","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-264.439,24.845],[-27.183,-36.451]],"o":[[-162.439,62.845],[-14.183,-2.451]],"v":[[74.439,-222.845],[77.183,-95.549]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[{"i":[[-14.552,17.164],[-84.524,-59.247]],"o":[[4.448,44.164],[-177.524,-58.247]],"v":[[76.552,-245.164],[28.524,-132.753]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-46.666,12.482],[4.635,-22.542]],"o":[[58.334,48.482],[-2.365,-44.042]],"v":[[60.666,-261.482],[-6.135,-173.958]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[15.828,22.8],[39.365,-19.981]],"o":[[49.828,24.8],[12.365,-29.981]],"v":[[40.172,-270.8],[41.635,-201.019]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-12.679,33.618],[0.739,-12.776]],"o":[[16.321,12.118],[-23.904,-12.919]],"v":[[19.679,-280.118],[73.404,-244.081]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[-10.439,3.845],[-2.183,-5.451]],"o":[[2.561,7.845],[-14.183,-2.451]],"v":[[-0.561,-309.845],[6.183,-290.549]],"c":true}]},{"t":12,"s":[{"i":[[-10.439,3.845],[-2.183,-5.451]],"o":[[2.561,7.845],[-14.183,-2.451]],"v":[[-2.561,-427.845],[4.183,-408.549]],"c":true}]}]},"nm":"Path 9","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-22.717,11.53],[16.682,-16.21]],"o":[[4.283,8.53],[5.682,-20.71]],"v":[[101.967,-233.03],[88.818,-188.29]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[-6.346,4.147],[-0.366,-3.08]],"o":[[5.543,2.722],[-7.337,1.322]],"v":[[88.467,-258.53],[94.818,-248.54]],"c":true}]},{"t":4,"s":[{"i":[[-6.346,4.147],[-0.366,-3.08]],"o":[[5.543,2.722],[-7.337,1.322]],"v":[[84.467,-412.53],[90.818,-402.54]],"c":true}]}]},"nm":"Path 8","hd":false},{"ind":5,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-46.009,12.388],[74.033,-0.611]],"o":[[-4.509,11.388],[-46.967,-107.486]],"v":[[125.509,71.112],[1.467,113.986]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-9.509,76.388],[74.033,-0.611]],"o":[[26.991,165.888],[-12.967,-113.486]],"v":[[146.509,-65.388],[1.467,113.986]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-3.591,66.715],[93.702,-33.599]],"o":[[45.409,68.715],[-23.798,-99.599]],"v":[[124.591,-101.715],[86.798,98.599]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-28.673,100.041],[18.871,-121.211]],"o":[[23.327,45.041],[-11.272,-74.766]],"v":[[102.673,-138.041],[146.129,39.211]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-86.521,83.201],[0.04,-46.057]],"o":[[-27.521,44.701],[-27.46,-39.557]],"v":[[104.521,-178.201],[147.46,-53.443]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[-78.37,43.361],[-9.836,-38.494]],"o":[[-9.37,31.361],[-29.009,-15.779]],"v":[[124.37,-218.361],[102.836,-129.506]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-12.932,20.02],[8.931,-18.574]],"o":[[5.568,15.02],[-11.069,-5.574]],"v":[[124.432,-260.02],[122.069,-212.426]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":27,"s":[{"i":[[1.395,4.24],[0.439,-5.367]],"o":[[4.195,4.04],[-8.094,-2.167]],"v":[[109.605,-288.64],[115.494,-276.366]],"c":true}]},{"t":28,"s":[{"i":[[1.395,4.24],[0.439,-5.367]],"o":[[4.195,4.04],[-8.094,-2.167]],"v":[[121.605,-432.64],[127.494,-420.366]],"c":true}]}]},"nm":"Path 7","hd":false},{"ind":6,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-8.509,21.888],[109.533,-35.486]],"o":[[30.991,64.388],[-33.467,-95.486]],"v":[[-84.491,-110.888],[-138.033,56.986]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[22.118,49.473],[122.558,9.2]],"o":[[68.618,31.473],[6.558,-41.3]],"v":[[-88.618,-143.473],[-141.058,-21.7]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[1.746,26.057],[53.583,-9.114]],"o":[[69.246,2.557],[28.083,-45.614]],"v":[[-106.746,-172.057],[-96.083,-90.386]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-2.806,9.641],[17.287,-6.213]],"o":[[11.944,10.391],[-8.963,-20.463]],"v":[[-100.944,-206.641],[-102.787,-160.537]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[-0.532,1.368],[6.846,-2.218]],"o":[[1.937,4.024],[-2.092,-5.968]],"v":[[-95.281,-232.141],[-96.877,-218.399]],"c":true}]},{"t":16,"s":[{"i":[[-0.532,1.368],[6.846,-2.218]],"o":[[1.937,4.024],[-2.092,-5.968]],"v":[[-97.281,-426.141],[-98.877,-412.399]],"c":true}]}]},"nm":"Path 6","hd":false},{"ind":7,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[3.874,33.283],[9.817,-3.451]],"o":[[36.874,61.283],[-1.183,-15.951]],"v":[[-146.374,-134.783],[-180.817,-70.549]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-0.126,16.712],[9.817,-3.451]],"o":[[22.088,11.14],[-1.755,-10.237]],"v":[[-156.088,-168.64],[-145.102,-130.049]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":7,"s":[{"i":[[-3.126,4.283],[9.817,-3.451]],"o":[[4.874,3.783],[-2.183,-5.951]],"v":[[-152.874,-195.783],[-156.817,-182.549]],"c":true}]},{"t":8,"s":[{"i":[[-3.126,4.283],[9.817,-3.451]],"o":[[4.874,3.783],[-2.183,-5.951]],"v":[[-156.874,-419.783],[-160.817,-406.549]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":8,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-14.009,113.888],[9.033,-147.486]],"o":[[54.491,45.388],[-75.467,-0.611]],"v":[[-160.991,-29.888],[1.467,113.986]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-32.96,22.468],[39.256,-132.515]],"o":[[9.54,49.968],[-101.244,-47.015]],"v":[[-141.04,-64.468],[-79.756,102.515]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-6.912,19.047],[-5.522,-58.543]],"o":[[45.088,50.047],[-57.022,-89.543]],"v":[[-121.088,-99.047],[-141.978,53.043]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[0.17,68.493],[6.251,-19.805]],"o":[[67.67,54.493],[-2.749,-44.305]],"v":[[-115.67,-138.993],[-160.251,-25.195]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[2.752,16.439],[33.524,-10.567]],"o":[[46.252,26.939],[13.524,-31.067]],"v":[[-121.252,-178.939],[-123.524,-94.933]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[-7.345,7.385],[14.19,-7.615]],"o":[[22.798,13.885],[-2.31,-14.115]],"v":[[-118.655,-218.885],[-120.19,-175.885]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23,"s":[{"i":[[-2.042,3.844],[4.939,-3.65]],"o":[[5.208,4.094],[-3.144,-0.025]],"v":[[-116.708,-248.844],[-117.689,-236.6]],"c":true}]},{"t":24,"s":[{"i":[[-2.042,3.844],[4.939,-3.65]],"o":[[5.208,4.094],[-3.144,-0.025]],"v":[[-120.708,-416.844],[-121.689,-404.6]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.969,0.651,0.024,0.179,0.98,0.476,0.053,1,0.992,0.302,0.082]}},"s":{"a":0,"k":[0,76.641]},"e":{"a":0,"k":[0,-306.457]},"t":1,"nm":"Gradient Fill 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.729411764706,0.007843137255,0.007843137255,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":16},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":28,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"01 fire small left bottom flame (f24) 2","refId":"comp_3","sr":1,"ks":{"p":{"a":0,"k":[246,253.2,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":8,"op":40,"st":8,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"01 fire small left flame twirl","refId":"comp_4","sr":1,"ks":{"p":{"a":0,"k":[246,253.2,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":512,"ip":8,"op":40,"st":8,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"p":{"a":0,"k":[261.799,285,0]},"a":{"a":0,"k":[3.799,5,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-139.238,137.226],[-214.402,227]],"o":[[110.598,-109],[69.514,-73.599]],"v":[[7,157],[22,-231]],"c":false}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[179.736,76.896],[409.598,33]],"o":[[-231.402,-99],[-100.91,-8.13]],"v":[[-11,213],[-186,-187]],"c":false}},"nm":"Path 2","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":9,"s":[0]},{"t":48,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.905882418156,0.298039227724,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":9,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":19,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":38,"s":[8]},{"t":48,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.992156922817,0.321568638086,0.078431375325,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":9,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":19,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":38,"s":[24]},{"t":48,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":9,"op":48,"st":-1,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"p":{"a":0,"k":[261.799,285,0]},"a":{"a":0,"k":[3.799,5,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[179.736,76.896],[302,241]],"o":[[-231.402,-99],[-79.13,-63.147]],"v":[[-11,213],[-186,-187]],"c":false}},"nm":"Path 2","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":9,"s":[0]},{"t":47,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.905882418156,0.298039227724,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":9,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":19,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":37,"s":[8]},{"t":47,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.992156922817,0.321568638086,0.078431375325,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":9,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":19,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":37,"s":[24]},{"t":47,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":9,"op":47,"st":9,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"p":{"a":0,"k":[261.799,285,0]},"a":{"a":0,"k":[3.799,5,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-189.52,47.962],[-214.402,227]],"o":[[407,-103],[69.514,-73.599]],"v":[[7,157],[22,-231]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[0]},{"t":42,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.905882418156,0.298039227724,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":12,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":32,"s":[8]},{"t":42,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.992156922817,0.321568638086,0.078431375325,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":12,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":32,"s":[24]},{"t":42,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":2,"op":42,"st":2,"bm":0}]},{"id":"comp_3","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"main 4","sr":1,"ks":{"p":{"a":0,"k":[256,390,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[65.688,1.123],[19.556,69.083]],"o":[[-41.977,-0.718],[0.056,0.083]],"v":[[0,105],[-106.556,17.417]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[56.878,-11.304],[-71.136,145.476]],"o":[[-40.253,8],[-9.136,65.476]],"v":[[20.467,102.986],[-125.864,-38.476]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[24.289,-77.906],[35.914,15.981]],"o":[[-62.711,-56.906],[43.914,-19.019]],"v":[[-127.289,-23.094],[-176.914,-135.981]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[-7.43,-47.184],[26.355,-15.403]],"o":[[-46.274,-7.369],[16.386,-25.431]],"v":[[-126.57,-102.816],[-189.726,-126.053]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-39.149,-16.463],[16.796,-46.787]],"o":[[-29.838,42.168],[-11.142,-31.843]],"v":[[-151.851,-136.537],[-228.539,-70.125]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[-20.866,7.057],[-9.552,-23.948]],"o":[[2.898,26.635],[-15.271,-8.542]],"v":[[-209.791,-103.936],[-216.762,-51.783]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-3.95,10.396],[-12.397,-3.967]],"o":[[11.308,7.423],[-8.231,3.207]],"v":[[-222.283,-63.444],[-204.202,-43.982]],"c":true}]},{"t":32,"s":[{"i":[[-0.203,0.069],[-0.093,-0.233]],"o":[[0.028,0.259],[-0.148,-0.083]],"v":[[-180.901,-36],[-180.969,-35.493]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.729411764706,0.007843137255,0.007843137255,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":8},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.969,0.651,0.024,0.179,0.98,0.476,0.053,1,0.992,0.302,0.082]}},"s":{"a":0,"k":[-0.803,78.641]},"e":{"a":0,"k":[5.941,-306.457]},"t":1,"nm":"Gradient Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":36,"st":-24,"bm":0}]},{"id":"comp_4","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"main 5","sr":1,"ks":{"p":{"a":0,"k":[281.589,353.586,0]},"a":{"a":0,"k":[-25.411,84.586,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[35.726,95.092],[9.957,-101.911],[-13.539,42.608]],"o":[[76.726,173.092],[-45.603,-22.126],[34,-107]],"v":[[20.274,-202.092],[-45.957,94.911],[-102,-24]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[28.539,79.666],[15.271,-30.792],[63.294,-24.571]],"o":[[89.467,109.928],[-7.636,15.397],[-160.84,62.44]],"v":[[-4.803,-241.627],[21.701,-28.613],[-27.163,-45.068]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[27.035,22.744],[-7.411,-52.025],[34.341,-66.85]],"o":[[98.144,67.203],[4.153,29.149],[-14.835,28.88]],"v":[[-66.918,-295.641],[41.698,-130.341],[-50.963,-169.565]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[38.174,25.145],[-12.055,-52.934],[22.231,-50.177]],"o":[[93.649,22.392],[6.204,24.263],[-19.381,32.447]],"v":[[-114.032,-311.296],[26.995,-175.344],[-61.336,-205.289]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[51.836,22.143],[-19.073,-53.622],[17.852,-32.908]],"o":[[74.884,-18.277],[7.052,19.825],[-21.585,39.789]],"v":[[-157.371,-308.755],[11.042,-212.473],[-74.322,-234.122]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[47.388,-0.852],[-20.633,-42.246],[-6.679,-39.947]],"o":[[79.829,-60.96],[7.911,20.621],[3.631,36.916]],"v":[[-192.262,-284.999],[-12.601,-245.703],[-93.247,-262.886]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[30.066,-23.944],[-27.45,-33.052],[-38.7,-24.364]],"o":[[34.202,-118.986],[16.55,19.928],[26.108,16.437]],"v":[[-220.042,-233.02],[-52.086,-285.918],[-140.713,-280.698]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[21.559,-53.406],[-42.556,-12.395],[-39.454,-8.254]],"o":[[-21.52,-112.915],[26.583,7.597],[22.752,4.36]],"v":[[-217.817,-188.862],[-94.492,-311.817],[-150.4,-267.689]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[-8.081,-76.532],[-44.505,14.451],[-34.813,5.036]],"o":[[-66.788,-82.682],[28.609,-9.29],[15.471,-2.238]],"v":[[-197.892,-143.194],[-148.445,-308.584],[-158.488,-252.15]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[-42.015,-57.434],[-26.817,31.143],[-39.824,16.539]],"o":[[-92.051,-33.255],[19.332,-22.828],[22.33,-8.449]],"v":[[-156.547,-114.04],[-197.313,-273.38],[-168.13,-223.209]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-61.367,-22.786],[-3.433,35.391],[-37.643,36.993]],"o":[[-86.784,22.985],[2.883,-29.724],[23.519,-23.113]],"v":[[-107.757,-110.853],[-225.28,-214.573],[-166.864,-196.295]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":26,"s":[{"i":[[-48.255,8.824],[11.318,24.14],[-10.048,38.28]],"o":[[-47.05,48.04],[-9.546,-20.238],[6.238,-23.914]],"v":[[-81.839,-126.076],[-197.225,-147.861],[-152.608,-158.445]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[-22.599,23.564],[15.508,8.665],[9.557,24.582]],"o":[[-6.616,44.216],[-13.049,-7.172],[-6.032,-15.307]],"v":[[-64.671,-149.022],[-135.368,-115.824],[-115.418,-138.897]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-2.335,16.115],[8.871,-0.948],[11.06,7.121]],"o":[[10.116,19.764],[-7.341,0.908],[-6.895,-4.334]],"v":[[-43.565,-175.386],[-62.601,-141.49],[-61.167,-156.623]],"c":true}]},{"t":32,"s":[{"i":[[-0.068,-0.18],[-0.019,0.193],[0.026,-0.081]],"o":[[-0.145,-0.328],[0.086,0.042],[-0.064,0.203]],"v":[[-36.242,-198.937],[-36.116,-199.5],[-36.01,-199.275]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.729411764706,0.007843137255,0.007843137255,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":8},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.969,0.651,0.024,0.179,0.98,0.476,0.053,1,0.992,0.302,0.082]}},"s":{"a":0,"k":[-0.803,78.641]},"e":{"a":0,"k":[5.941,-306.457]},"t":1,"nm":"Gradient Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":32,"st":-40,"bm":0}]},{"id":"comp_5","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 4","parent":2,"sr":1,"ks":{"p":{"a":0,"k":[-77.334,-175.69,0]},"a":{"a":0,"k":[-77.334,-175.69,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[1.988,3.181],[-19.8,-5.052]],"o":[[-4.889,-7.822],[26.133,6.668]],"v":[[19.329,271.399],[24.3,294.213]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[4.186,6.698],[-41.687,-10.636]],"o":[[-10.293,-16.469],[55.022,14.038]],"v":[[7.834,173.427],[18.299,221.462]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[14.361,7.522],[-30.261,-6.497]],"o":[[-24.248,-12.701],[34.72,7.454]],"v":[[35.139,128.628],[12.401,185.046]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[2.846,1.491],[-5.996,-1.287]],"o":[[-4.805,-2.517],[6.88,1.477]],"v":[[32.578,61.344],[28.072,72.523]],"c":true}]},{"t":19,"s":[{"i":[[2.846,1.491],[-5.996,-1.287]],"o":[[-4.805,-2.517],[6.88,1.477]],"v":[[20.578,-416.656],[16.072,-405.477]],"c":true}]}]},"nm":"Path 7","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[33.144,5.233],[-24.721,-0.221]],"o":[[-35.022,-5.53],[27.911,0.249]],"v":[[19.555,225.935],[15.319,283.751]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[29.02,-23.268],[-29.148,15.285]],"o":[[-36.827,29.528],[27.206,-14.266]],"v":[[18.827,100.658],[25.794,178.294]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[36.864,-1.646],[-34.889,28.965]],"o":[[-35.938,1.604],[46.08,13.495]],"v":[[1.709,52.646],[-0.595,87.535]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[19.216,-6.659],[-24.954,25.786]],"o":[[-28.004,9.704],[30.5,0]],"v":[[16.732,-8.349],[24.773,17.714]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[12.002,-2.4],[-12.348,2.646]],"o":[[-16.253,3.251],[13.442,-2.88]],"v":[[25.798,-28.588],[29.158,-15.146]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[20.461,-75.516],[21.822,-70.072]],"c":true}]},{"t":31,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[10.461,-427.516],[11.822,-422.072]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[47.81,-67.072],[-25.632,9.313]],"o":[[-93.246,48.698],[25.632,-9.313]],"v":[[-74.754,121.302],[-13.132,182.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[16.302,-91.514],[22.297,-55.155]],"o":[[-51.846,41.486],[63.795,-25.293]],"v":[[-77.654,-43.486],[-14.297,37.655]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[65.508,-110.844],[-62.172,37.016]],"o":[[-46.719,4.672],[111.508,-8.984]],"v":[[-77.508,-81.156],[-31.508,-13.016]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[44.392,-34.904],[-43.656,10.635]],"o":[[-57.534,8.63],[43.656,-10.635]],"v":[[-64.892,-151.596],[-43.156,-90.865]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[45.056,-54.944],[-36.611,29.426]],"o":[[-75.944,-2.944],[74.944,33.426]],"v":[[-55.556,-184.556],[-45.389,-150.426]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[14.5,-3],[-14.25,5.5]],"o":[[-14.5,3],[14.25,-5.5]],"v":[[-44.875,-250.5],[-40.375,-234]],"c":true}]},{"t":36,"s":[{"i":[[1.16,-0.24],[-1.14,0.44]],"o":[[-1.16,0.24],[1.14,-0.44]],"v":[[-45.92,-278.26],[-45.56,-276.94]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[38.071,2.586],[-51.674,3.841],[-14.328,5.136]],"o":[[-71.639,-4.866],[8.07,11.41],[22.966,-8.233]],"v":[[122.074,242.366],[89.943,278.535],[130.995,288.864]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[20.869,-12.454],[-146.79,82.022],[-12.062,30.104]],"o":[[-112.454,-96.052],[15.663,4.937],[27.265,-8.079]],"v":[[157.954,39.052],[123.79,100.978],[169.735,69.137]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[74.55,-67.464],[-92.612,89.3],[0.78,32.797]],"o":[[-175.95,-43.988],[-28.612,-55.2],[57.28,16.797]],"v":[[109.45,-30.036],[99.612,30.2],[150.72,14.703]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[79.321,-50.136],[-20.934,-21.241],[-26.364,9.073]],"o":[[-58.179,-42.136],[17.914,18.177],[76.169,29.621]],"v":[[102.679,-76.364],[46.086,-27.677],[111.331,-23.621]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[62.617,-21.138],[-8.337,-17.814],[-11.971,0.122]],"o":[[-29.444,-9.815],[3.681,7.865],[67.802,31.907]],"v":[[103.66,-125.362],[70.352,-89.569],[93.689,-75.907]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[19.334,0],[-4.841,-5.763],[-4.486,0.179]],"o":[[-13.412,0],[2.137,2.544],[14.647,-0.586]],"v":[[106.839,-168.5],[96.133,-153.01],[106.253,-148.873]],"c":true}]},{"t":36,"s":[{"i":[[5.5,0],[-1.377,-1.639],[-1.276,0.051]],"o":[[-3.815,0],[0.608,0.724],[4.167,-0.167]],"v":[[103.5,-198.125],[100.454,-193.718],[103.333,-192.542]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":6,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-3.499,-24.848],[-6.884,-16.478],[-0.301,7.46],[-4.644,0.228],[-25.003,12.782],[33.341,-4.053]],"o":[[-34.078,22.449],[3.276,7.842],[1.294,-32.082],[19.012,20.499],[85.358,33.282],[-6.489,-19.362]],"v":[[-102.261,200.848],[-133.404,262.516],[-118.294,281.082],[-102.012,287.001],[-31.997,296.218],[-11.544,205.996]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[-0.747,-48.874],[-2.455,-35.731],[-40.679,19.746],[-10.189,-28.494],[-11.643,4.734],[85.039,22.458]],"o":[[-21.211,-29.874],[-35.532,22.641],[-1.215,-25.539],[22.811,19.506],[80.357,20.734],[29.075,-42.399]],"v":[[-100.253,19.874],[-169.932,40.216],[-144.785,98.539],[-93.811,115.494],[-40.357,117.266],[-14.075,40.399]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[4.596,-2.157],[-2.838,-33.101],[-23.392,21.782],[64.52,-34.091],[-7.412,7.454],[7.166,18.65]],"o":[[-43.115,-41.195],[-25.698,17.837],[-20.978,-41.699],[20.24,22.409],[13.576,-13.654],[-5.083,-13.23]],"v":[[-78.36,-37.305],[-165.347,-14.561],[-142.022,35.199],[-94.52,47.591],[-39.74,43.428],[-28.273,-12.423]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[19.586,24.077],[1.481,-16.058],[-4.9,-5.507],[-10.179,0.727],[8.719,-39.971],[89.45,-41.348]],"o":[[-19.447,-7.56],[-0.705,7.642],[4.542,5.105],[-3.153,-23.324],[83.219,35.029],[-14.55,-40.348]],"v":[[-113.586,-84.077],[-147.276,-60.135],[-141.231,-39.204],[-119.347,-31.676],[-66.219,-26.529],[-64.45,-76.152]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[13.764,-16.52],[-0.797,-14.42],[-5.435,-4.397],[-9.222,1.812],[-4.425,5.611],[7.725,10.732]],"o":[[-22.868,-5.87],[0.379,6.863],[5.039,4.077],[14.57,16.763],[8.105,-10.278],[-5.48,-7.613]],"v":[[-80.463,-124.48],[-113.116,-101.094],[-104.345,-83.066],[-82.915,-78.763],[-39.977,-80.996],[-37.614,-117.588]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[9.081,-4.12],[1.941,-4.856],[-0.074,-2.217],[-1.433,-1.373],[-2.412,2.764],[3.877,5.235]],"o":[[-4.629,2.1],[-0.924,2.311],[0.069,2.056],[11.907,11.408],[4.417,-5.063],[-2.75,-3.714]],"v":[[-75.274,-166.527],[-85.585,-154.758],[-86.926,-147.859],[-85.155,-142.261],[-54.718,-143.901],[-53.648,-161.404]],"c":true}]},{"t":36,"s":[{"i":[[0.601,-0.289],[0.013,-0.847],[-0.221,-0.37],[-0.364,-0.269],[-0.704,0.652],[0.934,1.027]],"o":[[-1.307,0.628],[-0.006,0.403],[0.205,0.343],[0.87,0.643],[1.289,-1.194],[-0.662,-0.729]],"v":[[-79.613,-181.42],[-81.494,-179.083],[-81.161,-177.91],[-80.299,-176.982],[-73.89,-177.455],[-73.114,-181.387]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.980392216701,0.972549079446,0.949019667682,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":36,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"p":{"a":0,"k":[280.666,670.31,0]},"a":{"a":0,"k":[24.666,286.31,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[20,20,100]},{"t":12,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[1.988,3.181],[-19.8,-5.052]],"o":[[-4.889,-7.822],[26.133,6.668]],"v":[[19.329,271.399],[24.3,294.213]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[6.101,9.761],[-60.75,-15.5]],"o":[[-15,-24],[80.183,20.458]],"v":[[0,163],[15.25,233]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[21,11],[-44.25,-9.5]],"o":[[-35.458,-18.573],[50.771,10.9]],"v":[[35,125],[1.75,207.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[2.846,1.491],[-5.996,-1.287]],"o":[[-4.805,-2.517],[6.88,1.477]],"v":[[32.578,61.344],[28.072,72.523]],"c":true}]},{"t":19,"s":[{"i":[[2.846,1.491],[-5.996,-1.287]],"o":[[-4.805,-2.517],[6.88,1.477]],"v":[[20.578,-416.656],[16.072,-405.477]],"c":true}]}]},"nm":"Path 7","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[66.5,10.5],[-49.599,-0.443]],"o":[[-70.267,-11.095],[56,0.5]],"v":[[20.5,199.5],[12,315.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[43.734,-35.066],[-43.928,23.035]],"o":[[-55.5,44.5],[41,-21.5]],"v":[[15.5,72.5],[26,189.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[56,-2.5],[-53,44]],"o":[[-54.592,2.437],[70,20.5]],"v":[[-4.5,51.5],[-8,104.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[34.653,-12.008],[-45,46.5]],"o":[[-50.5,17.5],[55,0]],"v":[[9.5,-11.5],[24,35.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[25,-5],[-25.722,5.512]],"o":[[-33.855,6.771],[28,-6]],"v":[[23,-29],[30,-1]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[20.461,-75.516],[21.822,-70.072]],"c":true}]},{"t":31,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[10.461,-427.516],[11.822,-422.072]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[69.5,-97.5],[-113.5,45]],"o":[[-66.5,8.5],[86.092,-34.134]],"v":[[-90.5,124.5],[-22,227]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[22,-123.5],[-113.5,45]],"o":[[-41.5,7],[86.092,-34.134]],"v":[[-91.5,-56.5],[-6,53]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[29.5,-125],[-86.5,51.5]],"o":[[-65,6.5],[164.5,11.5]],"v":[[-87.5,-87],[-23.5,-7.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[71,-76.5],[-34.5,28]],"o":[[-90,13.5],[106.5,47.5]],"v":[[-68.5,-157.5],[-34.5,-62.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[48.5,-66],[-34.5,28]],"o":[[-81,3],[106.5,47.5]],"v":[[-57.5,-194],[-53,-132]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[29,-6],[-28.5,11]],"o":[[-29,6],[28.5,-11]],"v":[[-46,-251.5],[-37,-218.5]],"c":true}]},{"t":36,"s":[{"i":[[1.16,-0.24],[-1.14,0.44]],"o":[[-1.16,0.24],[1.14,-0.44]],"v":[[-45.92,-278.26],[-45.56,-276.94]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[70.414,4.783],[-95.574,7.104],[-26.5,9.5]],"o":[[-132.5,-9],[14.926,21.104],[42.478,-15.228]],"v":[[116,239],[56.574,305.896],[132.5,325]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[31,-18.5],[-168.751,36.203],[-17.918,44.718]],"o":[[-248,-61.5],[23.267,7.334],[40.5,-12]],"v":[[167,24],[116.251,135.297],[184.5,88]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[101,-20.5],[-133.016,57.816],[-23.678,31.225]],"o":[[-222,-55.5],[17.83,12.614],[53,-13]],"v":[[114.5,-57.5],[94.516,65.184],[159,45]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[108,-21],[-25.045,-34.694],[-37.826,13.017]],"o":[[-46.826,-19.424],[11.058,15.318],[78.5,26]],"v":[[96,-99.5],[40.628,-7.406],[112,2]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[78.5,-26.5],[-10.452,-22.332],[-15.008,0.153]],"o":[[-36.913,-12.304],[4.615,9.86],[85,40]],"v":[[101.5,-126.5],[59.744,-81.628],[89,-64.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[33,0],[-8.262,-9.837],[-7.657,0.306]],"o":[[-22.893,0],[3.648,4.343],[25,-1]],"v":[[103,-170],[84.726,-143.561],[102,-136.5]],"c":true}]},{"t":36,"s":[{"i":[[5.5,0],[-1.377,-1.639],[-1.276,0.051]],"o":[[-3.815,0],[0.608,0.724],[4.167,-0.167]],"v":[[103.5,-198.125],[100.454,-193.718],[103.333,-192.542]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":6,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-4.046,-28.733],[-7.961,-19.055],[-7.058,-4.972],[-5.37,0.263],[-10.383,14.21],[38.553,-4.686]],"o":[[-39.406,25.959],[3.789,9.068],[6.543,4.609],[20.166,40.644],[98.704,38.486],[-7.504,-22.39]],"v":[[-106.454,196.733],[-142.466,268.042],[-124.994,289.511],[-106.166,296.356],[-25.204,307.014],[-1.553,202.686]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[7.754,-10.103],[-2.864,-41.686],[-47.459,23.038],[-17.819,6.695],[-11.001,13.349],[99.213,26.201]],"o":[[-24.746,-34.853],[-41.454,26.415],[10.089,11.703],[9.571,21.591],[45.708,87.19],[-6.546,-22.901]],"v":[[-105.754,6.853],[-187.046,30.585],[-146.041,114.963],[-104.071,125.409],[-34.708,136.81],[-5.213,30.799]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[5.648,-2.651],[-3.488,-40.68],[-28.748,26.769],[-22.563,7.933],[-9.109,9.161],[8.807,22.921]],"o":[[-52.986,-50.627],[-31.582,21.921],[15.404,15.153],[24.874,27.539],[16.685,-16.781],[-6.247,-16.259]],"v":[[-65.014,-36.373],[-171.918,-8.421],[-143.252,52.731],[-84.874,67.961],[-17.551,62.845],[-3.459,-5.795]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[19.861,-24.879],[2.389,-25.903],[-7.904,-8.883],[-16.419,1.173],[-8.619,13.394],[50.602,-9.162]],"o":[[-31.37,-12.195],[-1.137,12.327],[7.327,8.235],[20.153,51.095],[59.455,-7.707],[-9.228,-15.356]],"v":[[-114.361,-87.621],[-168.705,-49.001],[-158.954,-15.239],[-123.653,-3.095],[-37.955,5.207],[-35.102,-74.838]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[20.907,-25.093],[-1.21,-21.904],[-8.256,-6.68],[-14.008,2.753],[-6.721,8.523],[11.735,16.301]],"o":[[-34.736,-8.916],[0.576,10.424],[7.654,6.192],[22.131,25.463],[12.311,-15.612],[-8.324,-11.564]],"v":[[-85.407,-125.907],[-135.006,-90.384],[-121.683,-62.999],[-89.131,-56.463],[-23.909,-59.855],[-20.32,-115.438]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[16.089,-7.299],[3.439,-8.602],[-0.131,-3.928],[-2.539,-2.432],[-4.273,4.897],[6.868,9.275]],"o":[[-8.201,3.721],[-1.637,4.094],[0.122,3.642],[21.095,20.211],[7.826,-8.97],[-4.872,-6.579]],"v":[[-77.089,-167.201],[-95.356,-146.351],[-97.732,-134.128],[-94.595,-124.211],[-40.671,-127.117],[-38.776,-158.124]],"c":true}]},{"t":36,"s":[{"i":[[1.463,-0.702],[0.031,-2.062],[-0.538,-0.9],[-0.887,-0.655],[-1.713,1.587],[2.273,2.5]],"o":[[-3.182,1.528],[-0.015,0.981],[0.498,0.834],[2.118,1.565],[3.138,-2.907],[-1.612,-1.773]],"v":[[-78.589,-181.314],[-83.167,-175.626],[-82.357,-172.772],[-80.258,-170.512],[-64.659,-171.663],[-62.771,-181.234]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.945098099054,0.90588241278,0.792156922583,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.917647118662,0.847058883368,0.721568627451,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":16},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":36,"st":0,"bm":0}]},{"id":"comp_6","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 12","sr":1,"ks":{"o":{"a":0,"k":0},"p":{"a":0,"k":[256,495,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[40,40,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":8,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":37,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":64,"s":[60,60,100]},{"t":76,"s":[100,100,100]}]}},"ao":0,"ip":0,"op":124,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"03 fire small wide cycle end","parent":1,"refId":"comp_7","sr":1,"ks":{"p":{"a":0,"k":[0,-239,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":64,"op":92,"st":64,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"03 fire small wide cycle","parent":1,"refId":"comp_8","sr":1,"ks":{"p":{"a":0,"k":[0,-239,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":32,"op":64,"st":32,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"03 fire small wide cycle","parent":1,"refId":"comp_8","sr":1,"ks":{"p":{"a":0,"k":[0,-239,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"p":{"a":0,"k":[261.799,285,0]},"a":{"a":0,"k":[3.799,5,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-139.238,137.226],[-214.402,227]],"o":[[110.598,-109],[69.514,-73.599]],"v":[[7,157],[22,-231]],"c":false}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[179.736,76.896],[409.598,33]],"o":[[-231.402,-99],[-100.91,-8.13]],"v":[[-11,213],[-186,-187]],"c":false}},"nm":"Path 2","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":74,"s":[0]},{"t":124,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.905882418156,0.298039227724,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":74,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":84,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":114,"s":[8]},{"t":124,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.992156922817,0.321568638086,0.078431375325,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":74,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":84,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":114,"s":[24]},{"t":124,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":64,"op":124,"st":64,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"p":{"a":0,"k":[261.799,285,0]},"a":{"a":0,"k":[3.799,5,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[179.736,76.896],[302,241]],"o":[[-231.402,-99],[-79.13,-63.147]],"v":[[-11,213],[-186,-187]],"c":false}},"nm":"Path 2","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":64,"s":[0]},{"t":114,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.905882418156,0.298039227724,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":64,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":74,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":104,"s":[8]},{"t":114,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.992156922817,0.321568638086,0.078431375325,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":64,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":74,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":104,"s":[24]},{"t":114,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":64,"op":114,"st":64,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"p":{"a":0,"k":[261.799,285,0]},"a":{"a":0,"k":[3.799,5,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-189.52,47.962],[-214.402,227]],"o":[[407,-103],[69.514,-73.599]],"v":[[7,157],[22,-231]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"t":109,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.905882418156,0.298039227724,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":79,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":99,"s":[8]},{"t":109,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.992156922817,0.321568638086,0.078431375325,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":79,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":99,"s":[24]},{"t":109,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":69,"op":109,"st":69,"bm":0}]},{"id":"comp_7","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"core","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":12,"s":[100]},{"t":20,"s":[0]}]},"p":{"a":0,"k":[260.85,494.271,0]},"a":{"a":0,"k":[-51.438,222.044,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[18.197,16.546],[-41.985,0.511],[69.85,68.547]],"o":[[-53.725,57.166],[41.194,-0.501],[-14.905,76.809]],"v":[[-60.485,-15.773],[2.147,99.863],[27.617,-48.036]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[0.068,-0.429],[-35.745,0.435],[65.918,79.024]],"o":[[-55.611,56.567],[35.071,-0.427],[-0.687,-0.216]],"v":[[-9.918,-30.798],[-3.59,87.929],[-8.638,-29.958]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[0.069,-0.44],[-31.286,-32.594],[40.368,45.715]],"o":[[-28.033,10.909],[28.162,-20.704],[-0.705,-0.222]],"v":[[18.029,13.22],[1.286,91.311],[19.344,14.083]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0.086,-0.545],[-13.305,-8.903],[-4.886,23.805]],"o":[[-34.696,13.502],[52.246,-21.612],[-0.872,-0.275]],"v":[[0.536,-65.183],[9.914,2.038],[2.164,-64.114]],"c":true}]},{"t":20,"s":[{"i":[[0.128,-0.814],[-10.891,-11.311],[27.696,1.589]],"o":[[20.128,26.186],[43.109,-11.311],[-1.304,-0.411]],"v":[[11.872,-133.186],[21.891,-73.689],[14.304,-131.589]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0.121,0.996,0.996,0.898,0.54,0.998,0.951,0.598,0.808,1,0.906,0.298]}},"s":{"a":0,"k":[2.235,29.653]},"e":{"a":0,"k":[6.469,-84.419]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 4","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"second","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":12,"s":[100]},{"t":20,"s":[0]}]},"p":{"a":0,"k":[258,398,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-1.733,16.717],[-67.889,0.826],[112.946,110.839]],"o":[[-86.872,92.436],[66.609,-0.811],[-1.054,155.839]],"v":[[-75.128,-84.436],[1.891,99.311],[22.054,-139.839]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[0.128,-0.814],[-67.889,0.826],[125.196,150.089]],"o":[[-105.622,107.436],[66.609,-0.811],[-1.304,-0.411]],"v":[[-13.128,-140.186],[-1.109,85.311],[-10.696,-138.589]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[0.128,-0.814],[-57.891,-60.311],[74.696,84.589]],"o":[[-51.872,20.186],[52.109,-38.311],[-1.304,-0.411]],"v":[[33.872,-53.186],[2.891,91.311],[36.304,-51.589]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0.128,-0.814],[-19.891,-13.311],[-7.304,35.589]],"o":[[-51.872,20.186],[78.109,-32.311],[-1.304,-0.411]],"v":[[-2.128,-99.186],[11.891,1.311],[0.304,-97.589]],"c":true}]},{"t":20,"s":[{"i":[[0.128,-0.814],[-10.891,-11.311],[27.696,1.589]],"o":[[20.128,26.186],[43.109,-11.311],[-1.304,-0.411]],"v":[[11.872,-133.186],[21.891,-73.689],[14.304,-131.589]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,0.914,0.239,0.269,1,0.739,0.22,1,1,0.565,0.2]}},"s":{"a":0,"k":[1.76,83.087]},"e":{"a":0,"k":[-8.18,-233.581]},"t":1,"nm":"Gradient Fill 2","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"main 2","sr":1,"ks":{"p":{"a":0,"k":[256,384,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-49.509,44.388],[123.533,-38.986]],"o":[[-17.509,54.388],[-101.467,-137.986]],"v":[[138.509,-106.388],[72.467,102.986]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-54.652,117.814],[96.514,-88.952]],"o":[[39.848,36.814],[-55.486,-82.702]],"v":[[122.902,-122.064],[135.986,59.452]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-69.795,121.239],[25.494,-72.919]],"o":[[-6.795,25.239],[-9.506,-27.419]],"v":[[118.295,-144.739],[157.506,-8.081]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-47.393,78.823],[5.657,-37.203]],"o":[[-20.938,53.232],[-20.343,-15.203]],"v":[[125.938,-167.732],[170.343,-77.797]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-14.581,12.011],[1.034,-14.274]],"o":[[-8.51,26.743],[-24.109,4.012]],"v":[[132.081,-191.511],[118.466,-138.726]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":19,"s":[{"i":[[-8.189,4.595],[5.817,-6.951]],"o":[[0.811,6.877],[-3.683,-1.451]],"v":[[129.689,-209.345],[131.183,-194.049]],"c":true}]},{"t":20,"s":[{"i":[[-8.189,4.595],[5.817,-6.951]],"o":[[0.811,6.877],[-3.683,-1.451]],"v":[[139.689,-425.345],[141.183,-410.049]],"c":true}]}]},"nm":"Path 10","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[116.786,0],[11,144.667]],"o":[[-317,-127.726],[249,149.667]],"v":[[-1,113.726],[-1,-153.667]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[120.519,-22.794],[-202.241,204.246]],"o":[[-143.695,27.178],[23.759,129.246]],"v":[[0.481,97.794],[0.241,-202.246]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[168.037,-36.863],[3.518,139.825]],"o":[[-193.963,-76.863],[211.518,139.825]],"v":[[1.963,81.863],[-26.518,-251.825]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[78.037,-55.931],[-63.722,35.404]],"o":[[-42.963,-53.931],[-21.722,38.404]],"v":[[2.963,-132.069],[27.722,-295.404]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0.866,0],[-1.78,1.414]],"o":[[-1.004,0],[1.908,1.516]],"v":[[3.963,-346],[3.963,-347.983]],"c":true}]},{"t":17,"s":[{"i":[[0.866,0],[-1.78,1.414]],"o":[[-1.004,0],[1.908,1.516]],"v":[[25.963,-410],[25.963,-411.983]],"c":true}]}]},"nm":"Path 13","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[150.97,-29.436],[-7.904,50.282],[-29.987,22.93],[31.343,22.513]],"o":[[-171.03,-96.436],[20.614,30.124],[20.013,43.93],[96.97,73.457]],"v":[[0.03,113.436],[-84.471,-110.907],[-1.013,-82.93],[77.03,-95.457]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[263.97,-70.436],[41.784,65.595],[-24.463,-11.448],[-17.438,27.049]],"o":[[-288.03,-110.436],[28.054,20.833],[28.268,13.229],[-17.53,86.457]],"v":[[0.03,113.436],[-88.471,-144.407],[-8.648,-91.164],[61.53,-104.457]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[150.97,-29.436],[-32.029,33.907],[-26.789,24.397],[-10.737,32.946]],"o":[[-259.03,-106.436],[23.879,14.802],[25.211,25.397],[-2.03,120.457]],"v":[[0.03,113.436],[-60.471,-110.407],[27.789,-112.897],[118.03,-147.457]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[182.97,-93.436],[-9.029,35.657],[0,0],[-0.28,-0.293]],"o":[[-109.03,-114.436],[-0.029,0.657],[0,0],[30.72,63.707]],"v":[[0.03,113.436],[31.029,-60.657],[31.387,-61.673],[31.28,-60.707]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[122.605,-62.61],[-52.544,10.197],[0,0],[-0.187,-0.196]],"o":[[-64.272,-70.959],[-0.019,0.44],[0,0],[-33.712,34.231]],"v":[[10.772,31.459],[11.544,-98.197],[11.784,-98.878],[11.712,-98.231]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[62.24,-31.783],[36.94,34.738],[0,0],[-0.095,-0.1]],"o":[[-19.515,-27.482],[-0.01,0.223],[0,0],[53.855,13.755]],"v":[[21.515,-50.518],[-7.94,-135.738],[-7.818,-136.083],[-7.855,-135.755]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23,"s":[{"i":[[16.966,-8.664],[-0.837,3.306],[0,0],[-0.026,-0.027]],"o":[[-10.11,-10.611],[-0.003,0.061],[0,0],[2.849,5.907]],"v":[[-8.428,-142],[-5.554,-158.143],[-5.52,-158.237],[-5.53,-158.148]],"c":true}]},{"t":24,"s":[{"i":[[16.966,-8.664],[-0.837,3.306],[0,0],[-0.026,-0.027]],"o":[[-10.11,-10.611],[-0.003,0.061],[0,0],[2.849,5.907]],"v":[[51.572,-404],[54.446,-420.143],[54.48,-420.237],[54.47,-420.148]],"c":true}]}]},"nm":"Path 12","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-264.439,24.845],[-27.183,-36.451]],"o":[[-162.439,62.845],[-14.183,-2.451]],"v":[[74.439,-222.845],[77.183,-95.549]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[{"i":[[-14.552,17.164],[-84.524,-59.247]],"o":[[4.448,44.164],[-177.524,-58.247]],"v":[[76.552,-245.164],[28.524,-132.753]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-46.666,12.482],[4.635,-22.542]],"o":[[58.334,48.482],[-2.365,-44.042]],"v":[[60.666,-261.482],[-6.135,-173.958]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[15.828,22.8],[39.365,-19.981]],"o":[[49.828,24.8],[12.365,-29.981]],"v":[[40.172,-270.8],[41.635,-201.019]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-12.679,33.618],[0.739,-12.776]],"o":[[16.321,12.118],[-23.904,-12.919]],"v":[[19.679,-280.118],[73.404,-244.081]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[-10.439,3.845],[-2.183,-5.451]],"o":[[2.561,7.845],[-14.183,-2.451]],"v":[[-0.561,-309.845],[6.183,-290.549]],"c":true}]},{"t":12,"s":[{"i":[[-10.439,3.845],[-2.183,-5.451]],"o":[[2.561,7.845],[-14.183,-2.451]],"v":[[-2.561,-427.845],[4.183,-408.549]],"c":true}]}]},"nm":"Path 9","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-22.717,11.53],[16.682,-16.21]],"o":[[4.283,8.53],[5.682,-20.71]],"v":[[101.967,-233.03],[88.818,-188.29]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[-6.346,4.147],[-0.366,-3.08]],"o":[[5.543,2.722],[-7.337,1.322]],"v":[[88.467,-258.53],[94.818,-248.54]],"c":true}]},{"t":4,"s":[{"i":[[-6.346,4.147],[-0.366,-3.08]],"o":[[5.543,2.722],[-7.337,1.322]],"v":[[84.467,-412.53],[90.818,-402.54]],"c":true}]}]},"nm":"Path 8","hd":false},{"ind":5,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-46.009,12.388],[74.033,-0.611]],"o":[[-4.509,11.388],[-46.967,-107.486]],"v":[[125.509,71.112],[1.467,113.986]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-9.509,76.388],[74.033,-0.611]],"o":[[26.991,165.888],[-12.967,-113.486]],"v":[[146.509,-65.388],[1.467,113.986]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-3.591,66.715],[93.702,-33.599]],"o":[[45.409,68.715],[-23.798,-99.599]],"v":[[124.591,-101.715],[86.798,98.599]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-28.673,100.041],[18.871,-121.211]],"o":[[23.327,45.041],[-11.272,-74.766]],"v":[[102.673,-138.041],[146.129,39.211]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-86.521,83.201],[0.04,-46.057]],"o":[[-27.521,44.701],[-27.46,-39.557]],"v":[[104.521,-178.201],[147.46,-53.443]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[-78.37,43.361],[-9.836,-38.494]],"o":[[-9.37,31.361],[-29.009,-15.779]],"v":[[124.37,-218.361],[102.836,-129.506]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-12.932,20.02],[8.931,-18.574]],"o":[[5.568,15.02],[-11.069,-5.574]],"v":[[124.432,-260.02],[122.069,-212.426]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":27,"s":[{"i":[[1.395,4.24],[0.439,-5.367]],"o":[[4.195,4.04],[-8.094,-2.167]],"v":[[109.605,-288.64],[115.494,-276.366]],"c":true}]},{"t":28,"s":[{"i":[[1.395,4.24],[0.439,-5.367]],"o":[[4.195,4.04],[-8.094,-2.167]],"v":[[121.605,-432.64],[127.494,-420.366]],"c":true}]}]},"nm":"Path 7","hd":false},{"ind":6,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-8.509,21.888],[109.533,-35.486]],"o":[[30.991,64.388],[-33.467,-95.486]],"v":[[-84.491,-110.888],[-138.033,56.986]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[22.118,49.473],[122.558,9.2]],"o":[[68.618,31.473],[6.558,-41.3]],"v":[[-88.618,-143.473],[-141.058,-21.7]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[1.746,26.057],[53.583,-9.114]],"o":[[69.246,2.557],[28.083,-45.614]],"v":[[-106.746,-172.057],[-96.083,-90.386]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-2.806,9.641],[17.287,-6.213]],"o":[[11.944,10.391],[-8.963,-20.463]],"v":[[-100.944,-206.641],[-102.787,-160.537]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[-0.532,1.368],[6.846,-2.218]],"o":[[1.937,4.024],[-2.092,-5.968]],"v":[[-95.281,-232.141],[-96.877,-218.399]],"c":true}]},{"t":16,"s":[{"i":[[-0.532,1.368],[6.846,-2.218]],"o":[[1.937,4.024],[-2.092,-5.968]],"v":[[-97.281,-426.141],[-98.877,-412.399]],"c":true}]}]},"nm":"Path 6","hd":false},{"ind":7,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[3.874,33.283],[9.817,-3.451]],"o":[[36.874,61.283],[-1.183,-15.951]],"v":[[-146.374,-134.783],[-180.817,-70.549]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-0.126,16.712],[9.817,-3.451]],"o":[[22.088,11.14],[-1.755,-10.237]],"v":[[-156.088,-168.64],[-145.102,-130.049]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":7,"s":[{"i":[[-3.126,4.283],[9.817,-3.451]],"o":[[4.874,3.783],[-2.183,-5.951]],"v":[[-152.874,-195.783],[-156.817,-182.549]],"c":true}]},{"t":8,"s":[{"i":[[-3.126,4.283],[9.817,-3.451]],"o":[[4.874,3.783],[-2.183,-5.951]],"v":[[-156.874,-419.783],[-160.817,-406.549]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":8,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-14.009,113.888],[9.033,-147.486]],"o":[[54.491,45.388],[-75.467,-0.611]],"v":[[-160.991,-29.888],[1.467,113.986]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-32.96,22.468],[39.256,-132.515]],"o":[[9.54,49.968],[-101.244,-47.015]],"v":[[-141.04,-64.468],[-79.756,102.515]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-6.912,19.047],[-5.522,-58.543]],"o":[[45.088,50.047],[-57.022,-89.543]],"v":[[-121.088,-99.047],[-141.978,53.043]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[0.17,68.493],[6.251,-19.805]],"o":[[67.67,54.493],[-2.749,-44.305]],"v":[[-115.67,-138.993],[-160.251,-25.195]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[2.752,16.439],[33.524,-10.567]],"o":[[46.252,26.939],[13.524,-31.067]],"v":[[-121.252,-178.939],[-123.524,-94.933]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[-7.345,7.385],[14.19,-7.615]],"o":[[22.798,13.885],[-2.31,-14.115]],"v":[[-118.655,-218.885],[-120.19,-175.885]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23,"s":[{"i":[[-2.042,3.844],[4.939,-3.65]],"o":[[5.208,4.094],[-3.144,-0.025]],"v":[[-116.708,-248.844],[-117.689,-236.6]],"c":true}]},{"t":24,"s":[{"i":[[-2.042,3.844],[4.939,-3.65]],"o":[[5.208,4.094],[-3.144,-0.025]],"v":[[-120.708,-416.844],[-121.689,-404.6]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.969,0.651,0.024,0.179,0.98,0.476,0.053,1,0.992,0.302,0.082]}},"s":{"a":0,"k":[0,76.641]},"e":{"a":0,"k":[0,-306.457]},"t":1,"nm":"Gradient Fill 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.729411764706,0.007843137255,0.007843137255,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":16},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":28,"st":0,"bm":0}]},{"id":"comp_8","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"core","sr":1,"ks":{"p":{"a":0,"k":[260.85,494.271,0]},"a":{"a":0,"k":[-51.438,222.044,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[18.197,16.546],[-41.985,0.511],[69.85,68.547]],"o":[[-53.725,57.166],[41.194,-0.501],[-14.905,76.809]],"v":[[-60.485,-15.773],[2.147,99.863],[27.617,-48.036]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-0.979,9.447],[-38.363,0.467],[70.747,84.813]],"o":[[-59.685,60.711],[37.64,-0.458],[-1.585,72.664]],"v":[[-57.538,-18.917],[1.813,98.207],[7.7,-34.227]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-1.042,10.05],[-40.812,0.497],[123.505,80.759]],"o":[[-74.767,73.603],[40.043,-0.487],[-2.738,60.921]],"v":[[-53.637,-37.717],[1.022,99.894],[10.207,-27.711]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-1.085,10.471],[-42.522,0.518],[97.518,74.59]],"o":[[-66.155,67.291],[41.72,-0.508],[-24.524,76.665]],"v":[[-54.263,-34.227],[0.703,99.483],[31.236,-22.892]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-1.154,11.128],[-37.389,-2.202],[40.213,84]],"o":[[-57.827,61.531],[44.266,2.607],[-37.787,100]],"v":[[-49.168,-20.338],[2.1,101.975],[50.498,-47.227]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[-1.21,11.676],[-47.417,0.577],[79.061,75.843]],"o":[[-87.565,74.165],[46.523,-0.566],[-12.086,67.113]],"v":[[-25.098,-59.555],[1.837,97.942],[45.651,-47.629]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-1.123,10.831],[-43.984,0.535],[60.542,59.825]],"o":[[-61.466,81.592],[43.155,-0.525],[-21.739,23.543]],"v":[[-17.822,-70.092],[1.626,101.757],[35.746,-6.212]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[-1.169,11.276],[-45.792,0.557],[69.607,68.523]],"o":[[-61.294,73.648],[44.929,-0.547],[-11.672,64.813]],"v":[[-35.095,-15.72],[-2.741,104.632],[32.82,-31.227]],"c":true}]},{"t":32,"s":[{"i":[[18.197,16.546],[-41.985,0.511],[69.85,68.547]],"o":[[-53.725,57.166],[41.194,-0.501],[-14.905,76.809]],"v":[[-60.485,-15.773],[2.147,99.863],[27.617,-48.036]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0.121,0.996,0.996,0.898,0.54,0.998,0.951,0.598,0.808,1,0.906,0.298]}},"s":{"a":0,"k":[2.235,29.653]},"e":{"a":0,"k":[6.469,-84.419]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 4","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"second","sr":1,"ks":{"p":{"a":0,"k":[258,398,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-1.733,16.717],[-67.889,0.826],[112.946,110.839]],"o":[[-86.872,92.436],[66.609,-0.811],[-1.054,155.839]],"v":[[-75.128,-84.436],[1.891,99.311],[22.054,-139.839]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-1.733,16.717],[-67.889,0.826],[125.196,150.089]],"o":[[-105.622,107.436],[66.609,-0.811],[-2.804,128.589]],"v":[[-57.128,-106.186],[1.891,99.311],[-10.696,-138.589]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-1.733,16.717],[-67.889,0.826],[205.446,134.339]],"o":[[-124.372,122.436],[66.609,-0.811],[-4.554,101.339]],"v":[[-39.128,-127.936],[1.891,99.311],[-9.446,-86.339]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-1.733,16.717],[-67.889,0.826],[155.696,119.089]],"o":[[-105.622,107.436],[66.609,-0.811],[-2.804,128.589]],"v":[[-57.128,-106.186],[1.891,99.311],[20.304,-88.089]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-1.733,16.717],[-67.889,0.826],[132.946,124.839]],"o":[[-86.872,92.436],[66.609,-0.811],[-1.054,155.839]],"v":[[-75.128,-84.436],[1.891,99.311],[46.054,-130.839]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[-1.733,16.717],[-67.889,0.826],[113.196,108.589]],"o":[[-125.372,106.186],[66.609,-0.811],[-17.304,96.089]],"v":[[-16.628,-126.186],[1.891,99.311],[50.304,-99.089]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-1.733,16.717],[-67.889,0.826],[93.446,92.339]],"o":[[-94.872,125.936],[66.609,-0.811],[-33.554,36.339]],"v":[[-28.128,-165.936],[1.891,99.311],[54.554,-67.339]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[-1.733,16.717],[-67.889,0.826],[103.196,101.589]],"o":[[-90.872,109.186],[66.609,-0.811],[-17.304,96.089]],"v":[[-41.628,-73.186],[1.891,99.311],[38.304,-103.589]],"c":true}]},{"t":32,"s":[{"i":[[-1.733,16.717],[-67.889,0.826],[112.946,110.839]],"o":[[-86.872,92.436],[66.609,-0.811],[-1.054,155.839]],"v":[[-75.128,-84.436],[1.891,99.311],[22.054,-139.839]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,0.914,0.239,0.269,1,0.739,0.22,1,1,0.565,0.2]}},"s":{"a":0,"k":[1.76,83.087]},"e":{"a":0,"k":[-8.18,-233.581]},"t":1,"nm":"Gradient Fill 2","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"main 2","sr":1,"ks":{"p":{"a":0,"k":[256,384,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[22.772,0],[-46.805,37.19]],"o":[[-26.404,0],[50.185,39.876]],"v":[[-0.195,113.443],[-0.195,61.306]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[218.597,-71.585],[-101.403,40.181]],"o":[[-122.702,-55.863],[-27.403,50.181]],"v":[[-0.597,113.585],[-0.597,-46.181]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[199.943,-71.044],[38.799,75.924]],"o":[[-170.851,-83.794],[80.799,31.924]],"v":[[-0.799,113.655],[-0.799,-99.924]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[259,-94.726],[-62,81.667]],"o":[[-219,-111.726],[-8,98.667]],"v":[[-1,113.726],[-1,-153.667]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[134.519,-83.794],[18.759,153.246]],"o":[[-355.481,-139.794],[72.759,69.246]],"v":[[0.481,97.794],[-24.759,-201.246]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[186.037,-45.863],[-99.482,95.825]],"o":[[-135.963,-78.863],[-3.482,107.825]],"v":[[1.963,81.863],[-26.518,-251.825]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[27.037,-76.931],[27.278,52.404]],"o":[[-69.963,-68.931],[64.278,42.404]],"v":[[2.963,-132.069],[-15.278,-290.404]],"c":true}]},{"t":32,"s":[{"i":[[0.866,0],[-1.78,1.414]],"o":[[-1.004,0],[1.908,1.516]],"v":[[3.963,-346],[3.963,-347.983]],"c":true}]}]},"nm":"Path 11","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[116.786,0],[11,144.667]],"o":[[-317,-127.726],[249,149.667]],"v":[[-1,113.726],[-1,-153.667]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[120.519,-22.794],[-202.241,204.246]],"o":[[-143.695,27.178],[23.759,129.246]],"v":[[0.481,97.794],[0.241,-202.246]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[168.037,-36.863],[3.518,139.825]],"o":[[-193.963,-76.863],[211.518,139.825]],"v":[[1.963,81.863],[-26.518,-251.825]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[78.037,-55.931],[-63.722,35.404]],"o":[[-42.963,-53.931],[-21.722,38.404]],"v":[[2.963,-132.069],[27.722,-295.404]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0.866,0],[-1.78,1.414]],"o":[[-1.004,0],[1.908,1.516]],"v":[[3.963,-346],[3.963,-347.983]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":17,"s":[{"i":[[22.772,0],[-46.805,37.19]],"o":[[-26.404,0],[50.185,39.876]],"v":[[-0.195,113.443],[-0.195,61.306]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[224.785,-62.65],[-21.215,44.341]],"o":[[-196.215,-40.65],[178.785,45.341]],"v":[[-0.785,113.65],[-35.785,-108.341]],"c":true}]},{"t":32,"s":[{"i":[[116.786,0],[11,144.667]],"o":[[-317,-127.726],[249,149.667]],"v":[[-1,113.726],[-1,-153.667]],"c":true}]}]},"nm":"Path 13","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[150.97,-29.436],[-7.904,50.282],[-29.987,22.93],[31.343,22.513]],"o":[[-171.03,-96.436],[20.614,30.124],[20.013,43.93],[96.97,73.457]],"v":[[0.03,113.436],[-84.471,-110.907],[-1.013,-82.93],[77.03,-95.457]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[263.97,-70.436],[41.784,65.595],[-24.463,-11.448],[-17.438,27.049]],"o":[[-288.03,-110.436],[28.054,20.833],[28.268,13.229],[-17.53,86.457]],"v":[[0.03,113.436],[-88.471,-144.407],[-8.648,-91.164],[61.53,-104.457]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[150.97,-29.436],[-32.029,33.907],[-26.789,24.397],[-10.737,32.946]],"o":[[-259.03,-106.436],[23.879,14.802],[25.211,25.397],[-2.03,120.457]],"v":[[0.03,113.436],[-60.471,-110.407],[27.789,-112.897],[118.03,-147.457]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[218.97,-95.436],[-14.029,92.657],[-25.376,-0.07],[-14.356,48.358]],"o":[[-215.03,-101.436],[18.775,49.248],[29.323,0.081],[-14.779,118.707]],"v":[[0.03,113.436],[-38.971,-132.657],[31.387,-61.673],[103.28,-138.707]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[286.97,-161.436],[36.471,78.407],[-18.903,53.783],[-17.706,24.637]],"o":[[-171.03,-96.436],[27.358,41.478],[30.097,36.783],[-27.529,116.957]],"v":[[0.03,113.436],[-40.471,-166.407],[29.903,-158.783],[103.53,-179.957]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[245.97,-124.769],[28.471,22.365],[-36.809,25.898],[5.725,49.721]],"o":[[-86.03,-19.436],[-9.524,-43.439],[42.535,-29.926],[44.679,76.748]],"v":[[0.03,113.436],[-126.471,65.635],[-66.412,-30.863],[18.321,-138.748]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[204.97,-88.102],[14.284,64.345],[-5.622,33.725],[-11.854,28.702]],"o":[[-178.03,-22.436],[27.474,40.898],[20.378,16.725],[116.887,36.54]],"v":[[0.03,113.436],[-122.471,-54.324],[-77.378,-76.725],[-34.887,-117.54]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[163.97,-51.436],[3.19,57.313],[-43.708,-7.429],[-12.405,-37.168]],"o":[[-189.03,-2.436],[27.532,40.608],[50.507,8.584],[-25.405,42.832]],"v":[[0.03,113.436],[-103.471,-80.782],[8.103,-95.227],[146.405,-9.832]],"c":true}]},{"t":32,"s":[{"i":[[150.97,-29.436],[-7.904,50.282],[-29.987,22.93],[31.343,22.513]],"o":[[-171.03,-96.436],[20.614,30.124],[20.013,43.93],[96.97,73.457]],"v":[[0.03,113.436],[-84.471,-110.907],[-1.013,-82.93],[77.03,-95.457]],"c":true}]}]},"nm":"Path 12","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-49.509,44.388],[123.533,-38.986]],"o":[[-17.509,54.388],[-101.467,-137.986]],"v":[[138.509,-106.388],[72.467,102.986]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-54.652,117.814],[96.514,-88.952]],"o":[[39.848,36.814],[-55.486,-82.702]],"v":[[122.902,-122.064],[135.986,59.452]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-69.795,121.239],[25.494,-72.919]],"o":[[-6.795,25.239],[-9.506,-27.419]],"v":[[118.295,-144.739],[157.506,-8.081]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-47.393,78.823],[5.657,-37.203]],"o":[[-20.938,53.232],[-20.343,-15.203]],"v":[[125.938,-167.732],[170.343,-77.797]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-14.581,12.011],[1.034,-14.274]],"o":[[-8.51,26.743],[-24.109,4.012]],"v":[[132.081,-191.511],[118.466,-138.726]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":19,"s":[{"i":[[-8.189,4.595],[5.817,-6.951]],"o":[[0.811,6.877],[-3.683,-1.451]],"v":[[129.689,-209.345],[131.183,-194.049]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,113.375],[0,113.375]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-86.754,-66.493],[91.266,-0.305]],"o":[[3.246,27.507],[-58.234,-74.493]],"v":[[143.754,52.493],[0.734,113.68]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[-85.509,72.388],[223.533,-7.986]],"o":[[-30.509,37.388],[-116.467,-148.986]],"v":[[157.509,-90.388],[1.467,113.986]],"c":true}]},{"t":32,"s":[{"i":[[-49.509,44.388],[123.533,-38.986]],"o":[[-17.509,54.388],[-101.467,-137.986]],"v":[[138.509,-106.388],[72.467,102.986]],"c":true}]}]},"nm":"Path 10","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-264.439,24.845],[-27.183,-36.451]],"o":[[-162.439,62.845],[-14.183,-2.451]],"v":[[74.439,-222.845],[77.183,-95.549]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[{"i":[[-14.552,17.164],[-84.524,-59.247]],"o":[[4.448,44.164],[-177.524,-58.247]],"v":[[76.552,-245.164],[28.524,-132.753]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-46.666,12.482],[4.635,-22.542]],"o":[[58.334,48.482],[-2.365,-44.042]],"v":[[60.666,-261.482],[-6.135,-173.958]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[15.828,22.8],[39.365,-19.981]],"o":[[49.828,24.8],[12.365,-29.981]],"v":[[40.172,-270.8],[41.635,-201.019]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-12.679,33.618],[0.739,-12.776]],"o":[[16.321,12.118],[-23.904,-12.919]],"v":[[19.679,-280.118],[73.404,-244.081]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[-10.439,3.845],[-2.183,-5.451]],"o":[[2.561,7.845],[-14.183,-2.451]],"v":[[-0.561,-309.845],[6.183,-290.549]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,113.375],[0,113.375]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-132.754,-64.493],[63.266,-0.305]],"o":[[-3.754,90.507],[-6.484,-56.743]],"v":[[147.754,12.493],[0.734,113.68]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[21.491,112.388],[126.533,-0.611]],"o":[[224.491,166.388],[-116.467,-148.986]],"v":[[19.509,-136.388],[1.467,113.986]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-135.819,196.207],[121.294,-153.141]],"o":[[1.181,45.207],[-18.706,-90.141]],"v":[[-5.181,-172.207],[113.706,89.141]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[-234.629,86.526],[12.555,-77.796]],"o":[[-114.629,48.526],[-16.445,-46.296]],"v":[[13.629,-194.526],[144.445,-6.204]],"c":true}]},{"t":32,"s":[{"i":[[-264.439,24.845],[-27.183,-36.451]],"o":[[-162.439,62.845],[-14.183,-2.451]],"v":[[74.439,-222.845],[77.183,-95.549]],"c":true}]}]},"nm":"Path 9","hd":false},{"ind":5,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-22.717,11.53],[16.682,-16.21]],"o":[[4.283,8.53],[5.682,-20.71]],"v":[[101.967,-233.03],[88.818,-188.29]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[-6.346,4.147],[-0.366,-3.08]],"o":[[5.543,2.722],[-7.337,1.322]],"v":[[88.467,-258.53],[94.818,-248.54]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,113.375],[0,113.375]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-41.254,-51.493],[37.016,-0.305]],"o":[[-39.254,39.882],[-6.484,-56.743]],"v":[[127.254,73.493],[0.734,113.68]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-9.509,76.388],[74.033,-0.611]],"o":[[74.491,136.388],[-12.967,-113.486]],"v":[[125.509,-54.388],[1.467,113.986]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-14.3,65.117],[72.063,-37.031]],"o":[[78.7,80.617],[-49.687,-74.281]],"v":[[100.3,-82.617],[86.937,99.031]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[-19.092,53.845],[64.593,-56.075]],"o":[[34.908,50.845],[-86.407,-35.075]],"v":[[71.092,-114.845],[102.407,15.075]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-74.384,65.24],[3.789,-34.954]],"o":[[-8.384,53.24],[-113.211,7.046]],"v":[[64.384,-155.24],[122.211,-37.046]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[-91.175,30.135],[-24.514,-25.332]],"o":[[-16.175,23.135],[-53.764,-6.832]],"v":[[92.175,-192.135],[75.514,-110.668]],"c":true}]},{"t":32,"s":[{"i":[[-22.717,11.53],[16.682,-16.21]],"o":[[4.283,8.53],[5.682,-20.71]],"v":[[101.967,-233.03],[88.818,-188.29]],"c":true}]}]},"nm":"Path 8","hd":false},{"ind":6,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-46.009,12.388],[74.033,-0.611]],"o":[[-4.509,11.388],[-46.967,-107.486]],"v":[[125.509,71.112],[1.467,113.986]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-9.509,76.388],[74.033,-0.611]],"o":[[26.991,165.888],[-12.967,-113.486]],"v":[[146.509,-65.388],[1.467,113.986]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-3.591,66.715],[93.702,-33.599]],"o":[[45.409,68.715],[-23.798,-99.599]],"v":[[124.591,-101.715],[86.798,98.599]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-28.673,100.041],[18.871,-121.211]],"o":[[23.327,45.041],[-11.272,-74.766]],"v":[[102.673,-138.041],[146.129,39.211]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-86.521,83.201],[0.04,-46.057]],"o":[[-27.521,44.701],[-27.46,-39.557]],"v":[[104.521,-178.201],[147.46,-53.443]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[-78.37,43.361],[-9.836,-38.494]],"o":[[-9.37,31.361],[-29.009,-15.779]],"v":[[124.37,-218.361],[102.836,-129.506]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-12.932,20.02],[8.931,-18.574]],"o":[[5.568,15.02],[-11.069,-5.574]],"v":[[124.432,-260.02],[122.069,-212.426]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":27,"s":[{"i":[[1.395,4.24],[0.439,-5.367]],"o":[[4.195,4.04],[-8.094,-2.167]],"v":[[109.605,-288.64],[115.494,-276.366]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,113.375],[0,113.375]],"c":true}]},{"t":32,"s":[{"i":[[-46.009,12.388],[74.033,-0.611]],"o":[[-4.509,11.388],[-46.967,-107.486]],"v":[[125.509,71.112],[1.467,113.986]],"c":true}]}]},"nm":"Path 7","hd":false},{"ind":7,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,113.375],[0,113.375]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[9.996,54.507],[18.766,-61.243]],"o":[[81.496,-58.493],[-74.234,-0.305]],"v":[[-144.496,36.493],[0.734,113.68]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-160.009,110.388],[37.533,-122.486]],"o":[[30.991,64.388],[-148.467,-0.611]],"v":[[-60.991,-109.388],[1.467,113.986]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-28.931,37.131],[101.093,-74.23]],"o":[[26.297,53.685],[-138.907,-85.98]],"v":[[-39.069,-135.131],[-92.093,95.98]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[31.646,69.873],[164.653,-25.974]],"o":[[21.603,42.982],[-10.847,-128.474]],"v":[[-41.146,-166.873],[-145.653,27.974]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[51.14,50.249],[196.48,-6.735]],"o":[[31.14,23.249],[83.48,-70.235]],"v":[[-68.14,-193.749],[-119.48,-62.265]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[2.634,39.625],[104.807,-64.995]],"o":[[12.214,21.576],[27.307,-41.495]],"v":[[-85.134,-224.625],[-44.307,-124.505]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[-5.015,14.537],[29.527,-18.684]],"o":[[-0.515,20.037],[-20.473,-28.184]],"v":[[-63.485,-252.537],[-76.027,-200.316]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":31,"s":[{"i":[[-2.751,4.345],[10.817,-1.201]],"o":[[3.999,2.845],[1.067,-3.951]],"v":[[-54.249,-276.095],[-57.817,-259.799]],"c":true}]},{"t":32,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,113.375],[0,113.375]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":8,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-8.509,21.888],[109.533,-35.486]],"o":[[30.991,64.388],[-33.467,-95.486]],"v":[[-84.491,-110.888],[-138.033,56.986]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[22.118,49.473],[122.558,9.2]],"o":[[68.618,31.473],[6.558,-41.3]],"v":[[-88.618,-143.473],[-141.058,-21.7]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[1.746,26.057],[53.583,-9.114]],"o":[[69.246,2.557],[28.083,-45.614]],"v":[[-106.746,-172.057],[-96.083,-90.386]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-2.806,9.641],[17.287,-6.213]],"o":[[11.944,10.391],[-8.963,-20.463]],"v":[[-100.944,-206.641],[-102.787,-160.537]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[-0.532,1.368],[6.846,-2.218]],"o":[[1.937,4.024],[-2.092,-5.968]],"v":[[-95.281,-232.141],[-96.877,-218.399]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,113.375],[0,113.375]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[27.996,30.007],[18.766,-61.243]],"o":[[67.996,-36.993],[-98.734,-0.305]],"v":[[-125.996,67.993],[0.734,113.68]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-59.009,91.388],[37.533,-122.486]],"o":[[30.991,64.388],[-130.467,-0.611]],"v":[[-123.991,-53.388],[1.467,113.986]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[-33.759,56.638],[73.533,-78.986]],"o":[[30.991,64.388],[-111.217,-27.486]],"v":[[-104.241,-82.138],[-77.783,97.986]],"c":true}]},{"t":32,"s":[{"i":[[-8.509,21.888],[109.533,-35.486]],"o":[[30.991,64.388],[-33.467,-95.486]],"v":[[-84.491,-110.888],[-138.033,56.986]],"c":true}]}]},"nm":"Path 6","hd":false},{"ind":9,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[3.874,33.283],[9.817,-3.451]],"o":[[36.874,61.283],[-1.183,-15.951]],"v":[[-146.374,-134.783],[-180.817,-70.549]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-0.126,16.712],[9.817,-3.451]],"o":[[22.088,11.14],[-1.755,-10.237]],"v":[[-156.088,-168.64],[-145.102,-130.049]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":7,"s":[{"i":[[-3.126,4.283],[9.817,-3.451]],"o":[[4.874,3.783],[-2.183,-5.951]],"v":[[-152.874,-195.783],[-156.817,-182.549]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,113.375],[0,113.375]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[15.496,26.632],[4.516,-73.743]],"o":[[72.746,-25.993],[-37.734,-0.305]],"v":[[-106.496,84.743],[0.734,113.68]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[32.991,88.388],[46.533,-190.986]],"o":[[43.991,27.388],[-75.467,-0.611]],"v":[[-175.991,-1.388],[1.467,113.986]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[7.991,54.888],[48.283,-114.236]],"o":[[34.991,53.388],[-57.467,-13.798]],"v":[[-185.491,-36.888],[-72.283,105.236]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-17.009,21.388],[-35.967,-67.486]],"o":[[64.991,42.388],[-39.467,-26.986]],"v":[[-179.991,-74.388],[-130.033,65.486]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[-21.318,2.586],[43.425,-41.969]],"o":[[3.682,27.586],[-35.575,-40.469]],"v":[[-156.182,-99.086],[-170.925,4.469]],"c":true}]},{"t":32,"s":[{"i":[[3.874,33.283],[9.817,-3.451]],"o":[[36.874,61.283],[-1.183,-15.951]],"v":[[-146.374,-134.783],[-180.817,-70.549]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":10,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-14.009,113.888],[9.033,-147.486]],"o":[[54.491,45.388],[-75.467,-0.611]],"v":[[-160.991,-29.888],[1.467,113.986]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-32.96,22.468],[39.256,-132.515]],"o":[[9.54,49.968],[-101.244,-47.015]],"v":[[-141.04,-64.468],[-79.756,102.515]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-6.912,19.047],[-5.522,-58.543]],"o":[[45.088,50.047],[-57.022,-89.543]],"v":[[-121.088,-99.047],[-141.978,53.043]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[0.17,68.493],[6.251,-19.805]],"o":[[67.67,54.493],[-2.749,-44.305]],"v":[[-115.67,-138.993],[-160.251,-25.195]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[2.752,16.439],[33.524,-10.567]],"o":[[46.252,26.939],[13.524,-31.067]],"v":[[-121.252,-178.939],[-123.524,-94.933]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[-7.345,7.385],[14.19,-7.615]],"o":[[22.798,13.885],[-2.31,-14.115]],"v":[[-118.655,-218.885],[-120.19,-175.885]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23,"s":[{"i":[[-2.042,3.844],[4.939,-3.65]],"o":[[5.208,4.094],[-3.144,-0.025]],"v":[[-116.708,-248.844],[-117.689,-236.6]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,113.375],[0,113.375]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[27.996,23.757],[4.516,-73.743]],"o":[[27.246,22.694],[-37.734,-0.305]],"v":[[-118.996,80.243],[0.734,113.68]],"c":true}]},{"t":32,"s":[{"i":[[-14.009,113.888],[9.033,-147.486]],"o":[[54.491,45.388],[-75.467,-0.611]],"v":[[-160.991,-29.888],[1.467,113.986]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.969,0.651,0.024,0.179,0.98,0.476,0.053,1,0.992,0.302,0.082]}},"s":{"a":0,"k":[0,76.641]},"e":{"a":0,"k":[0,-306.457]},"t":1,"nm":"Gradient Fill 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.729411764706,0.007843137255,0.007843137255,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":16},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":32,"st":0,"bm":0}]},{"id":"comp_9","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 4","parent":2,"sr":1,"ks":{"p":{"a":0,"k":[-77.334,-175.69,0]},"a":{"a":0,"k":[-77.334,-175.69,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[1.988,3.181],[-19.8,-5.052]],"o":[[-4.889,-7.822],[26.133,6.668]],"v":[[19.329,271.399],[24.3,294.213]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[4.186,6.698],[-41.687,-10.636]],"o":[[-10.293,-16.469],[55.022,14.038]],"v":[[7.834,173.427],[18.299,221.462]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[14.361,7.522],[-30.261,-6.497]],"o":[[-24.248,-12.701],[34.72,7.454]],"v":[[35.139,128.628],[12.401,185.046]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[2.846,1.491],[-5.996,-1.287]],"o":[[-4.805,-2.517],[6.88,1.477]],"v":[[32.578,61.344],[28.072,72.523]],"c":true}]},{"t":19,"s":[{"i":[[2.846,1.491],[-5.996,-1.287]],"o":[[-4.805,-2.517],[6.88,1.477]],"v":[[20.578,-416.656],[16.072,-405.477]],"c":true}]}]},"nm":"Path 7","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[33.144,5.233],[-24.721,-0.221]],"o":[[-35.022,-5.53],[27.911,0.249]],"v":[[19.555,225.935],[15.319,283.751]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[29.02,-23.268],[-29.148,15.285]],"o":[[-36.827,29.528],[27.206,-14.266]],"v":[[18.827,100.658],[25.794,178.294]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[36.864,-1.646],[-34.889,28.965]],"o":[[-35.938,1.604],[46.08,13.495]],"v":[[1.709,52.646],[-0.595,87.535]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[19.216,-6.659],[-24.954,25.786]],"o":[[-28.004,9.704],[30.5,0]],"v":[[16.732,-8.349],[24.773,17.714]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[12.002,-2.4],[-12.348,2.646]],"o":[[-16.253,3.251],[13.442,-2.88]],"v":[[25.798,-28.588],[29.158,-15.146]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[20.461,-75.516],[21.822,-70.072]],"c":true}]},{"t":31,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[10.461,-427.516],[11.822,-422.072]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[47.81,-67.072],[-25.632,9.313]],"o":[[-93.246,48.698],[25.632,-9.313]],"v":[[-74.754,121.302],[-13.132,182.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[16.302,-91.514],[22.297,-55.155]],"o":[[-51.846,41.486],[63.795,-25.293]],"v":[[-77.654,-43.486],[-14.297,37.655]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[65.508,-110.844],[-62.172,37.016]],"o":[[-46.719,4.672],[111.508,-8.984]],"v":[[-77.508,-81.156],[-31.508,-13.016]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[44.392,-34.904],[-43.656,10.635]],"o":[[-57.534,8.63],[43.656,-10.635]],"v":[[-64.892,-151.596],[-43.156,-90.865]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[45.056,-54.944],[-36.611,29.426]],"o":[[-75.944,-2.944],[74.944,33.426]],"v":[[-55.556,-184.556],[-45.389,-150.426]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[14.5,-3],[-14.25,5.5]],"o":[[-14.5,3],[14.25,-5.5]],"v":[[-44.875,-250.5],[-40.375,-234]],"c":true}]},{"t":36,"s":[{"i":[[1.16,-0.24],[-1.14,0.44]],"o":[[-1.16,0.24],[1.14,-0.44]],"v":[[-45.92,-278.26],[-45.56,-276.94]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[38.071,2.586],[-51.674,3.841],[-14.328,5.136]],"o":[[-71.639,-4.866],[8.07,11.41],[22.966,-8.233]],"v":[[122.074,242.366],[89.943,278.535],[130.995,288.864]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[20.869,-12.454],[-146.79,82.022],[-12.062,30.104]],"o":[[-112.454,-96.052],[15.663,4.937],[27.265,-8.079]],"v":[[157.954,39.052],[123.79,100.978],[169.735,69.137]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[74.55,-67.464],[-92.612,89.3],[0.78,32.797]],"o":[[-175.95,-43.988],[-28.612,-55.2],[57.28,16.797]],"v":[[109.45,-30.036],[99.612,30.2],[150.72,14.703]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[79.321,-50.136],[-20.934,-21.241],[-26.364,9.073]],"o":[[-58.179,-42.136],[17.914,18.177],[76.169,29.621]],"v":[[102.679,-76.364],[46.086,-27.677],[111.331,-23.621]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[62.617,-21.138],[-8.337,-17.814],[-11.971,0.122]],"o":[[-29.444,-9.815],[3.681,7.865],[67.802,31.907]],"v":[[103.66,-125.362],[70.352,-89.569],[93.689,-75.907]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[19.334,0],[-4.841,-5.763],[-4.486,0.179]],"o":[[-13.412,0],[2.137,2.544],[14.647,-0.586]],"v":[[106.839,-168.5],[96.133,-153.01],[106.253,-148.873]],"c":true}]},{"t":36,"s":[{"i":[[5.5,0],[-1.377,-1.639],[-1.276,0.051]],"o":[[-3.815,0],[0.608,0.724],[4.167,-0.167]],"v":[[103.5,-198.125],[100.454,-193.718],[103.333,-192.542]],"c":true}]}]},"nm":"Path 3","hd":false},{"ty":"fl","c":{"a":0,"k":[0.980392216701,0.972549079446,0.949019667682,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":36,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"p":{"a":0,"k":[280.666,670.31,0]},"a":{"a":0,"k":[24.666,286.31,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[20,20,100]},{"t":12,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[1.988,3.181],[-19.8,-5.052]],"o":[[-4.889,-7.822],[26.133,6.668]],"v":[[19.329,271.399],[24.3,294.213]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[6.101,9.761],[-60.75,-15.5]],"o":[[-15,-24],[80.183,20.458]],"v":[[0,163],[15.25,233]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[21,11],[-44.25,-9.5]],"o":[[-35.458,-18.573],[50.771,10.9]],"v":[[35,125],[1.75,207.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[2.846,1.491],[-5.996,-1.287]],"o":[[-4.805,-2.517],[6.88,1.477]],"v":[[32.578,61.344],[28.072,72.523]],"c":true}]},{"t":19,"s":[{"i":[[2.846,1.491],[-5.996,-1.287]],"o":[[-4.805,-2.517],[6.88,1.477]],"v":[[20.578,-416.656],[16.072,-405.477]],"c":true}]}]},"nm":"Path 7","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[66.5,10.5],[-49.599,-0.443]],"o":[[-70.267,-11.095],[56,0.5]],"v":[[20.5,199.5],[12,315.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[43.734,-35.066],[-43.928,23.035]],"o":[[-55.5,44.5],[41,-21.5]],"v":[[15.5,72.5],[26,189.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[56,-2.5],[-53,44]],"o":[[-54.592,2.437],[70,20.5]],"v":[[-4.5,51.5],[-8,104.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[34.653,-12.008],[-45,46.5]],"o":[[-50.5,17.5],[55,0]],"v":[[9.5,-11.5],[24,35.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[25,-5],[-25.722,5.512]],"o":[[-33.855,6.771],[28,-6]],"v":[[23,-29],[30,-1]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[20.461,-75.516],[21.822,-70.072]],"c":true}]},{"t":31,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[10.461,-427.516],[11.822,-422.072]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[69.5,-97.5],[-113.5,45]],"o":[[-66.5,8.5],[86.092,-34.134]],"v":[[-90.5,124.5],[-22,227]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[22,-123.5],[-113.5,45]],"o":[[-41.5,7],[86.092,-34.134]],"v":[[-91.5,-56.5],[-6,53]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[29.5,-125],[-86.5,51.5]],"o":[[-65,6.5],[164.5,11.5]],"v":[[-87.5,-87],[-23.5,-7.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[71,-76.5],[-34.5,28]],"o":[[-90,13.5],[106.5,47.5]],"v":[[-68.5,-157.5],[-34.5,-62.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[48.5,-66],[-34.5,28]],"o":[[-81,3],[106.5,47.5]],"v":[[-57.5,-194],[-53,-132]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[29,-6],[-28.5,11]],"o":[[-29,6],[28.5,-11]],"v":[[-46,-251.5],[-37,-218.5]],"c":true}]},{"t":36,"s":[{"i":[[1.16,-0.24],[-1.14,0.44]],"o":[[-1.16,0.24],[1.14,-0.44]],"v":[[-45.92,-278.26],[-45.56,-276.94]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[70.414,4.783],[-95.574,7.104],[-26.5,9.5]],"o":[[-132.5,-9],[14.926,21.104],[42.478,-15.228]],"v":[[116,239],[56.574,305.896],[132.5,325]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[31,-18.5],[-168.751,36.203],[-17.918,44.718]],"o":[[-248,-61.5],[23.267,7.334],[40.5,-12]],"v":[[167,24],[116.251,135.297],[184.5,88]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[101,-20.5],[-133.016,57.816],[-23.678,31.225]],"o":[[-222,-55.5],[17.83,12.614],[53,-13]],"v":[[114.5,-57.5],[94.516,65.184],[159,45]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[108,-21],[-25.045,-34.694],[-37.826,13.017]],"o":[[-46.826,-19.424],[11.058,15.318],[78.5,26]],"v":[[96,-99.5],[40.628,-7.406],[112,2]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[78.5,-26.5],[-10.452,-22.332],[-15.008,0.153]],"o":[[-36.913,-12.304],[4.615,9.86],[85,40]],"v":[[101.5,-126.5],[59.744,-81.628],[89,-64.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[33,0],[-8.262,-9.837],[-7.657,0.306]],"o":[[-22.893,0],[3.648,4.343],[25,-1]],"v":[[103,-170],[84.726,-143.561],[102,-136.5]],"c":true}]},{"t":36,"s":[{"i":[[5.5,0],[-1.377,-1.639],[-1.276,0.051]],"o":[[-3.815,0],[0.608,0.724],[4.167,-0.167]],"v":[[103.5,-198.125],[100.454,-193.718],[103.333,-192.542]],"c":true}]}]},"nm":"Path 3","hd":false},{"ty":"fl","c":{"a":0,"k":[0.945098099054,0.90588241278,0.792156922583,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.917647118662,0.847058883368,0.721568627451,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":16},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":36,"st":0,"bm":0}]},{"id":"comp_10","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 12","sr":1,"ks":{"o":{"a":0,"k":0},"p":{"a":0,"k":[256,743,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[40,40,100]},{"t":8,"s":[100,100,100]}]}},"ao":0,"ip":0,"op":124,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"02 fire middle end","parent":1,"refId":"comp_11","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":64,"op":120,"st":64,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"02 fire middle cycle","parent":1,"refId":"comp_12","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":32,"op":64,"st":32,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"02 fire middle cycle","parent":1,"refId":"comp_12","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"02 fire middle flame2","parent":1,"refId":"comp_13","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":768,"ip":56,"op":84,"st":56,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"02 fire middle flame2","parent":1,"refId":"comp_13","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":40,"op":68,"st":40,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"02 fire middle flame2","parent":1,"refId":"comp_13","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":8,"op":36,"st":8,"bm":0},{"ddd":0,"ind":10,"ty":0,"nm":"02 fire middle center (f0)","parent":1,"refId":"comp_14","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":32,"op":52,"st":32,"bm":0},{"ddd":0,"ind":11,"ty":0,"nm":"02 fire middle center (f0)","parent":1,"refId":"comp_14","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":0,"nm":"02 fire middle flame1","parent":1,"refId":"comp_15","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":768,"ip":48,"op":80,"st":48,"bm":0},{"ddd":0,"ind":13,"ty":0,"nm":"02 fire middle flame1","parent":1,"refId":"comp_15","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":768,"ip":16,"op":48,"st":16,"bm":0},{"ddd":0,"ind":14,"ty":0,"nm":"02 fire middle flame1","parent":1,"refId":"comp_15","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":64,"op":96,"st":64,"bm":0},{"ddd":0,"ind":15,"ty":0,"nm":"02 fire middle flame1","parent":1,"refId":"comp_15","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":32,"op":64,"st":32,"bm":0},{"ddd":0,"ind":16,"ty":0,"nm":"02 fire middle flame1","parent":1,"refId":"comp_15","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":17,"ty":0,"nm":"02 fire middle cycle outlines","parent":1,"refId":"comp_16","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":32,"op":64,"st":32,"bm":0},{"ddd":0,"ind":18,"ty":0,"nm":"02 fire middle cycle outlines","parent":1,"refId":"comp_16","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":19,"ty":0,"nm":"02 fire middle end outl","parent":1,"refId":"comp_17","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":64,"op":120,"st":64,"bm":0}]},{"id":"comp_11","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"core","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":16,"s":[100]},{"t":24,"s":[0]}]},"p":{"a":0,"k":[274.85,745.271,0]},"a":{"a":0,"k":[-51.438,222.044,0]},"s":{"a":0,"k":[105,95,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[39.58,85.812],[-17.902,-31.376],[35.171,-7.543],[-2.194,65.424]],"o":[[43.523,37.586],[39.788,69.735],[-35.171,7.543],[52.37,41.33]],"v":[[-66.479,-127.536],[10.277,-47.479],[1.953,96.976],[-79.778,6.061]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-15.574,60.55],[-15.66,-19.896],[25.722,9.327],[-25.407,35.637]],"o":[[7.827,46.973],[-7.577,42.996],[-28.847,-10.9],[25.303,12.329]],"v":[[43.874,-48.891],[35.16,30.819],[-29.181,91.286],[-49.602,7.388]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-6.961,62.286],[-30.452,4.961],[24.958,0.245],[-12.047,39.13]],"o":[[19.353,55.986],[-8.301,31.7],[-31.851,-0.312],[12.828,-41.664]],"v":[[58.023,-65.56],[49.622,36.723],[-4.6,92.405],[-47.296,23.087]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-17.454,42.526],[-34.556,14.996],[25.974,2.407],[-19.446,40.683]],"o":[[-0.104,53.98],[-4.433,35.663],[-30.206,-2.441],[13.297,-30.334]],"v":[[52.684,-50.822],[51.554,40.778],[-6.488,96.151],[-35.222,17.746]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-26.361,22.765],[-31.719,25.032],[21.302,4.569],[-24.099,42.237]],"o":[[-23.972,51.974],[1.329,39.626],[-21.302,-4.569],[10.843,-19.004]],"v":[[17.471,-41.347],[25.526,39.57],[-23.977,94.634],[-29.019,7.142]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-29.207,38.421],[-3.981,-35.119],[30.141,7.395],[1.651,26.371]],"o":[[-5.586,49.574],[3.738,32.983],[-23.618,-5.795],[27.791,11.116]],"v":[[-22.517,-66.798],[59.641,26.63],[5.786,69.708],[-34.552,13.884]],"c":true}]},{"t":32,"s":[{"i":[[39.58,85.812],[-17.902,-31.376],[35.171,-7.543],[-2.194,65.424]],"o":[[43.523,37.586],[39.788,69.735],[-35.171,7.543],[52.37,41.33]],"v":[[-66.479,-127.536],[10.277,-47.479],[1.953,96.976],[-79.778,6.061]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0.121,0.996,0.996,0.898,0.54,0.998,0.951,0.598,0.808,1,0.906,0.298]}},"s":{"a":0,"k":[2.235,29.653]},"e":{"a":0,"k":[6.469,-84.419]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 4","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":24,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"second 3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":8,"s":[0]}]},"p":{"a":0,"k":[256,646,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-26.872,112.436],[85.609,-106.311]],"o":[[-132.872,139.436],[144.609,-112.311]],"v":[[-39.128,-254.436],[2.391,101.311]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[21.128,83.436],[24.609,-91.311]],"o":[[-66.872,120.436],[89.609,-97.311]],"v":[[-38.128,-277.436],[1.391,-14.689]],"c":true}]},{"t":8,"s":[{"i":[[-67.629,106.783],[-100.629,-42.475]],"o":[[-145.629,104.783],[79.371,-115.475]],"v":[[-1.371,-455.783],[40.629,-170.525]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,0.914,0.239,0.269,1,0.739,0.22,1,1,0.565,0.2]}},"s":{"a":0,"k":[1.76,83.087]},"e":{"a":0,"k":[-8.18,-233.581]},"t":1,"nm":"Gradient Fill 2","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":8,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"second 4","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":8,"s":[100]},{"t":16,"s":[0]}]},"p":{"a":0,"k":[256,646,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[118.077,110.131],[-2.665,-39.018],[-39.734,-0.002]],"o":[[23.538,87.153],[2.856,41.822],[41.124,-0.187]],"v":[[-75.077,-142.131],[-73.567,32.169],[2.391,101.311]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[31.424,67.656],[1.546,-80.008],[-41.88,2.007]],"o":[[-31.576,70.656],[-0.913,47.276],[-41.995,-86.699]],"v":[[-3.424,-109.656],[-79.56,13.318],[-15.005,96.699]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[68.553,128.061],[24.049,-18.868],[-7.331,-43.72]],"o":[[1.954,34.086],[-26.589,20.861],[36.8,-35.069]],"v":[[56.681,-178.293],[10.775,-106.916],[-35.362,-20.93]],"c":true}]},{"t":16,"s":[{"i":[[94.686,138.159],[18.186,-23.426],[-5.386,-53.715]],"o":[[5.853,35.136],[-21.233,27.351],[66.001,-69.723]],"v":[[49.314,-279.159],[20.764,-198.478],[-19.001,-88.277]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,0.914,0.239,0.269,1,0.739,0.22,1,1,0.565,0.2]}},"s":{"a":0,"k":[1.76,83.087]},"e":{"a":0,"k":[-8.18,-233.581]},"t":1,"nm":"Gradient Fill 2","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":16,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"second 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":24,"s":[100]},{"t":32,"s":[0]}]},"p":{"a":0,"k":[256,646,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-39.734,-0.002],[160.903,157.868]],"o":[[41.124,-0.187],[-19.097,53.868]],"v":[[2.391,101.311],[-4.903,-122.868]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-67.071,-173.409],[49.406,97]],"o":[[46.247,-0.728],[-30.594,55]],"v":[[-15.929,99.409],[68.594,-56]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-133.939,-164.639],[53.824,142]],"o":[[43.111,-1.157],[-67.176,52]],"v":[[-16.561,96.506],[99.176,-107]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-86.439,-134.572],[55.824,140.5]],"o":[[43.111,-1.157],[-67.176,52]],"v":[[1.939,90.506],[75.676,-97]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-38.939,-104.506],[57.824,139]],"o":[[43.111,-1.157],[-67.176,52]],"v":[[-14.061,95.506],[52.176,-87]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-7.939,-57.506],[22.824,36]],"o":[[165.061,-85.506],[-106.176,65]],"v":[[21.939,89.506],[-24.824,-88]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[-30.439,-41.506],[22.824,36]],"o":[[33.561,-64.506],[-87.176,60.5]],"v":[[36.439,-22.494],[-35.824,-155]],"c":true}]},{"t":32,"s":[{"i":[[-52.939,-25.506],[22.824,36]],"o":[[11.061,-51.506],[-68.176,56]],"v":[[16.939,-94.494],[-46.824,-222]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,0.914,0.239,0.269,1,0.739,0.22,1,1,0.565,0.2]}},"s":{"a":0,"k":[1.76,83.087]},"e":{"a":0,"k":[-8.18,-233.581]},"t":1,"nm":"Gradient Fill 2","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"main","sr":1,"ks":{"p":{"a":0,"k":[256,646,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-11,-132.312],[-7.172,51.412],[38.165,58.396]],"o":[[53.567,-0.243],[9.183,-65.826],[111.313,177.997]],"v":[[3,102.312],[103.012,7.782],[15.687,-158.997]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-67,-154.909],[-14.398,53.118],[34.043,59.847]],"o":[[53.821,-0.847],[14.42,-57.831],[57.066,132.408]],"v":[[1,101.909],[112.427,3.393],[63.934,-166.408]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-167.999,-206.506],[-21.624,54.823],[29.921,61.299]],"o":[[54.074,-1.451],[19.657,-49.837],[2.818,86.819]],"v":[[-1.001,101.506],[121.841,-0.997],[112.182,-173.819]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-53.567,-0.243],[-3.669,53.71],[-22.247,82.372]],"o":[[53.5,0.243],[3.016,-44.152],[-202.747,113.872]],"v":[[-1,100.312],[99.011,8.806],[99.747,-160.872]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[40.815,-60.506],[1.22,63.133],[20.655,68.222]],"o":[[47.827,1.284],[-1.871,-96.875],[-162.345,140.111]],"v":[[1.185,101.506],[101.739,0.856],[-42.655,-165.222]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[-31.558,-50.003],[16.799,28.099],[2.217,48.282]],"o":[[21.885,-45.314],[-28.06,-46.936],[-150.456,128.631]],"v":[[46.558,7.003],[45.21,-99.803],[-42.044,-227.187]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":32,"s":[{"i":[[-37.931,-26.5],[13.635,23.185],[-8.736,55.472]],"o":[[8.571,-34.721],[-24.653,-41.921],[-138.568,117.152]],"v":[[29.931,-75.5],[16.564,-159.567],[-41.432,-289.152]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-28.177,-18.494],[2.319,19.339],[-10.19,30.389]],"o":[[-1.177,-21.494],[-3.873,-32.303],[-112.29,82.582]],"v":[[-32.823,-251.506],[-43.018,-315.512],[-29.71,-405.582]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[-2.92,-10.108],[-2.262,14.82],[-5.034,15.394]],"o":[[-0.505,-10.745],[2.5,-16.382],[-42.542,41.191]],"v":[[-54.2,-359.009],[-45.238,-393.82],[-38.849,-439.797]],"c":true}]},{"t":48,"s":[{"i":[[0.382,-0.567],[0.061,0.36],[0.121,0.4]],"o":[[0.168,0.005],[-0.102,-0.602],[-1.521,1.313]],"v":[[-47.577,-471.512],[-47.458,-472.128],[-47.987,-474.011]],"c":true}]}]},"nm":"Path 6","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[21.747,64.872],[-3.016,-44.152],[-53.5,0.243]],"o":[[22.247,82.372],[3.669,53.71],[79,-87.312]],"v":[[-97.747,-158.872],[-97.011,10.806],[3,102.312]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[113.161,97.111],[1.871,-96.875],[-47.828,1.284]],"o":[[14.417,96.532],[-1.22,63.133],[-66.999,-121.506]],"v":[[-20.161,-165.111],[-101.555,0.856],[-1.001,101.506]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[118.424,157.27],[28.288,-30.267],[-18.903,-57.031]],"o":[[9.7,45.245],[-31.276,33.464],[42.001,-54.614]],"v":[[46.576,-222.27],[-0.075,-117.135],[-44.001,7.614]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[94.686,138.159],[18.186,-23.426],[-5.386,-53.715]],"o":[[5.853,35.136],[-21.233,27.351],[66.001,-69.723]],"v":[[49.314,-279.159],[20.764,-198.478],[-19.001,-88.277]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[66.949,92.127],[9.993,-15.964],[-5.063,-48.024]],"o":[[5.051,35.121],[-11.668,18.639],[43.6,-13.156]],"v":[[49.051,-336.127],[35.336,-268.89],[15.4,-183.844]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[42.212,58.095],[5.432,-11.165],[-9.453,-39.448]],"o":[[3.788,26.341],[-6.342,13.036],[32.7,-9.867]],"v":[[48.788,-393.095],[41.523,-344.806],[38.55,-278.766]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[28.141,38.73],[1.328,-7.479],[-6.302,-26.298]],"o":[[2.526,17.561],[-1.551,8.732],[21.8,-6.578]],"v":[[48.525,-450.063],[48.103,-417.802],[51.7,-373.687]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":36,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[48,-564],[48,-564],[48,-564]],"c":true}]},{"t":37,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[47,-692],[47,-692],[47,-692]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":-31,"s":[{"i":[[-30.231,72.598],[-53.5,0.243]],"o":[[-74.692,48.796],[53.567,-0.243]],"v":[[3,-341],[3,102.312]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-104,239],[95,-118.312]],"o":[[-279,185],[275,-162.312]],"v":[[3,-341],[3,102.312]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-85.814,172.891],[-2.814,-80.393]],"o":[[-167.814,138.391],[153.186,-122.893]],"v":[[0.814,-398.391],[-2.186,-7.107]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-67.629,106.783],[-100.629,-42.475]],"o":[[-145.629,104.783],[79.371,-115.475]],"v":[[-1.371,-455.783],[40.629,-170.525]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-45.595,72.231],[-66.633,-28.88]],"o":[[-92.443,67.174],[-18.443,-69.55]],"v":[[-3.557,-513.174],[-36.557,-332.45]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[7.743,54.566],[-32.638,-15.285]],"o":[[-48.214,34.909],[20.243,-33.963]],"v":[[-5.743,-570.566],[-53.243,-478.037]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[-1.529,3.129],[1.357,-1.69]],"o":[[-3.986,2.643],[3.929,-2.319]],"v":[[-7.929,-627.957],[-7.929,-621.624]],"c":true}]},{"t":21,"s":[{"i":[[-1.529,3.129],[1.357,-1.69]],"o":[[-3.986,2.643],[3.929,-2.319]],"v":[[-8.857,-693.957],[-8.857,-687.624]],"c":true}]}]},"nm":"Path 5","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.969,0.651,0.024,0.179,0.98,0.476,0.053,1,0.992,0.302,0.082]}},"s":{"a":0,"k":[0,78.641]},"e":{"a":0,"k":[0,-396.457]},"t":1,"nm":"Gradient Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":48,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"p":{"a":0,"k":[263.799,525,0]},"a":{"a":0,"k":[3.799,5,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-188.981,50.042],[-214.402,227]],"o":[[321,-85],[69.514,-73.599]],"v":[[-33,77],[102,-415]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"t":45,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.905882418156,0.298039227724,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":35,"s":[8]},{"t":45,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.992156922817,0.321568638086,0.078431375325,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":35,"s":[24]},{"t":45,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":5,"op":45,"st":5,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"p":{"a":0,"k":[263.799,525,0]},"a":{"a":0,"k":[3.799,5,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[179.736,76.896],[302,241]],"o":[[-231.402,-99.001],[-79.13,-63.147]],"v":[[-23,57],[-182,-463]],"c":false}},"nm":"Path 2","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":56,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.905882418156,0.298039227724,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":46,"s":[8]},{"t":56,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.992156922817,0.321568638086,0.078431375325,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":46,"s":[24]},{"t":56,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":56,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"p":{"a":0,"k":[263.799,525,0]},"a":{"a":0,"k":[3.799,5,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-139.238,137.226],[-214.402,227]],"o":[[110.598,-109],[69.514,-73.599]],"v":[[7,157],[-82,-267]],"c":false}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[179.736,76.896],[409.598,33]],"o":[[-231.402,-99],[-100.91,-8.13]],"v":[[-11,213],[-178,-415]],"c":false}},"nm":"Path 2","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":19,"s":[0]},{"t":51,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.905882418156,0.298039227724,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":19,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":29,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":41,"s":[8]},{"t":51,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.992156922817,0.321568638086,0.078431375325,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":19,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":29,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":41,"s":[24]},{"t":51,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":5,"op":56,"st":9,"bm":0}]},{"id":"comp_12","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"core","sr":1,"ks":{"p":{"a":0,"k":[274.85,745.271,0]},"a":{"a":0,"k":[-51.438,222.044,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[105,95,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":8,"s":[95,105,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":16,"s":[105,95,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":24,"s":[95,105,100]},{"t":32,"s":[105,95,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[39.58,85.812],[-17.902,-31.376],[35.171,-7.543],[-2.194,65.424]],"o":[[43.523,37.586],[39.788,69.735],[-35.171,7.543],[52.37,41.33]],"v":[[-66.479,-127.536],[10.277,-47.479],[1.953,96.976],[-79.778,6.061]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[8.841,78.454],[-43.981,17.591],[37.377,-9.171],[-5.916,52.199]],"o":[[46.223,60.804],[-2.613,41.734],[-47.701,11.703],[6.299,-55.579]],"v":[[47.252,-119.276],[66.299,8.408],[2.462,96.754],[-82.769,28.58]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-43.523,37.586],[-52.37,41.33],[35.171,7.543],[-39.788,69.735]],"o":[[-39.58,85.812],[2.194,65.424],[-35.171,-7.543],[17.902,-31.376]],"v":[[24.695,-129.694],[37.994,3.903],[-43.738,94.818],[-52.061,-49.637]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-46.223,60.804],[-6.299,-55.579],[47.701,11.703],[2.613,41.734]],"o":[[-8.841,78.454],[5.916,52.199],[-37.377,-9.171],[43.981,17.591]],"v":[[-86.843,-117.324],[43.178,30.532],[-42.053,98.707],[-105.89,10.361]],"c":true}]},{"t":32,"s":[{"i":[[39.58,85.812],[-17.902,-31.376],[35.171,-7.543],[-2.194,65.424]],"o":[[43.523,37.586],[39.788,69.735],[-35.171,7.543],[52.37,41.33]],"v":[[-66.479,-127.536],[10.277,-47.479],[1.953,96.976],[-79.778,6.061]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0.121,0.996,0.996,0.898,0.54,0.998,0.951,0.598,0.808,1,0.906,0.298]}},"s":{"a":0,"k":[2.235,29.653]},"e":{"a":0,"k":[6.469,-84.419]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 4","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"second","sr":1,"ks":{"p":{"a":0,"k":[256,646,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-4.799,46.292],[-11.907,-44.087],[-2.665,-39.018],[-39.734,-0.002],[-5.575,39.963],[38.678,59.179]],"o":[[-31.898,32.4],[23.538,87.153],[2.856,41.822],[41.124,-0.187],[7.47,-53.546],[-27.683,-42.356]],"v":[[-39.128,-254.436],[-75.077,-142.131],[-73.567,32.169],[2.391,101.311],[78.742,27.466],[-4.903,-122.868]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[44.885,22.093],[-5.643,-37.784],[1.546,-80.008],[-41.88,2.007],[-23.325,59.137],[24.767,50.739]],"o":[[5.683,20.276],[16.655,111.521],[-0.913,47.276],[36.128,-1.732],[19.026,-48.238],[-37.411,-76.64]],"v":[[-11.07,-221.853],[10.576,-146.656],[-70.56,23.318],[6.995,98.699],[106.516,12.197],[97.615,-141.292]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[31.898,32.4],[27.683,-42.356],[-7.47,-53.546],[-41.124,-0.187],[-2.856,41.822],[-23.538,87.153]],"o":[[4.799,46.292],[-38.677,59.18],[5.575,39.963],[39.734,-0.002],[2.665,-39.018],[11.907,-44.087]],"v":[[39.57,-255.936],[5.345,-124.368],[-78.301,25.966],[-1.949,99.811],[74.008,30.669],[75.518,-143.631]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-5.683,20.276],[37.411,-76.64],[-19.026,-48.238],[-36.128,-1.732],[0.913,47.276],[-16.655,111.521]],"o":[[-49.292,56.853],[-24.766,50.74],[23.327,59.137],[41.88,2.007],[-1.546,-80.008],[5.643,-37.784]],"v":[[39.292,-260.853],[-95.393,-140.292],[-104.293,13.197],[-4.773,99.699],[72.782,24.318],[-8.354,-145.656]],"c":true}]},{"t":32,"s":[{"i":[[-4.799,46.292],[-11.907,-44.087],[-2.665,-39.018],[-39.734,-0.002],[-5.575,39.963],[38.678,59.179]],"o":[[-31.898,32.4],[23.538,87.153],[2.856,41.822],[41.124,-0.187],[7.47,-53.546],[-27.683,-42.356]],"v":[[-39.128,-254.436],[-75.077,-142.131],[-73.567,32.169],[2.391,101.311],[78.742,27.466],[-4.903,-122.868]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,0.914,0.239,0.269,1,0.739,0.22,1,1,0.565,0.2]}},"s":{"a":0,"k":[1.76,83.087]},"e":{"a":0,"k":[-8.18,-233.581]},"t":1,"nm":"Gradient Fill 2","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"main","sr":1,"ks":{"p":{"a":0,"k":[256,646,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-30.231,72.598],[-20.387,-75.483],[-3.016,-44.152],[-53.5,0.243],[-7.172,51.412],[38.165,58.396]],"o":[[-74.692,48.796],[22.247,82.372],[3.669,53.71],[53.567,-0.243],[9.183,-65.826],[-35.462,-54.259]],"v":[[3,-341],[-97.747,-158.872],[-97.011,10.806],[3,102.312],[103.012,7.782],[15.687,-158.997]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[55.583,73],[-8.766,-58.692],[1.871,-96.875],[-47.827,1.284],[-21.624,54.823],[29.921,61.299]],"o":[[-3.417,72],[14.417,96.532],[-1.22,63.133],[54.074,-1.451],[19.657,-49.837],[-55.886,-114.495]],"v":[[-46.583,-299],[-20.161,-165.111],[-101.555,0.856],[-1.001,101.506],[121.841,-0.997],[112.182,-173.819]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[74.692,48.796],[35.462,-54.259],[-9.183,-65.826],[-53.567,-0.243],[-3.669,53.71],[-22.247,82.372]],"o":[[30.231,72.598],[-38.165,58.396],[7.172,51.412],[53.5,0.243],[3.016,-44.152],[20.387,-75.483]],"v":[[-1,-343],[-13.687,-160.997],[-101.012,5.782],[-1,100.312],[99.011,8.806],[99.747,-160.872]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[75.232,135],[55.886,-114.495],[-19.657,-49.837],[-54.074,-1.451],[1.22,63.133],[-14.417,96.532]],"o":[[-1.768,121],[-29.921,61.299],[21.624,54.823],[47.828,1.284],[-1.871,-96.875],[8.766,-58.692]],"v":[[48.768,-397],[-111.998,-173.819],[-121.657,-0.997],[1.185,101.506],[101.739,0.856],[20.345,-165.111]],"c":true}]},{"t":32,"s":[{"i":[[-30.231,72.598],[-20.387,-75.483],[-3.016,-44.152],[-53.5,0.243],[-7.172,51.412],[38.165,58.396]],"o":[[-74.692,48.796],[22.247,82.372],[3.669,53.71],[53.567,-0.243],[9.183,-65.826],[-35.462,-54.259]],"v":[[3,-341],[-97.747,-158.872],[-97.011,10.806],[3,102.312],[103.012,7.782],[15.687,-158.997]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.969,0.651,0.024,0.179,0.98,0.476,0.053,1,0.992,0.302,0.082]}},"s":{"a":0,"k":[0,78.641]},"e":{"a":0,"k":[0,-396.457]},"t":1,"nm":"Gradient Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":32,"st":0,"bm":0}]},{"id":"comp_13","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"main","sr":1,"ks":{"p":{"a":0,"k":[256,646,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[54.161,45.111],[1.871,-96.875]],"o":[[14.417,96.532],[153.555,-0.856]],"v":[[-20.161,-165.111],[-101.555,0.856]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[68.115,181.651],[-13.746,-186.897]],"o":[[10.298,68.951],[24.254,-68.897]],"v":[[-20.115,-283.651],[-98.254,-1.103]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[122.069,81.381],[-53.048,-69.338]],"o":[[56.069,69.381],[14.552,-41.338]],"v":[[-74.069,-401.381],[-62.952,-256.662]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[38.046,52.921],[-35.365,-46.226]],"o":[[8.046,37.921],[9.701,-27.559]],"v":[[-64.046,-481.921],[-48.635,-364.441]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[10.023,27.46],[6.317,-21.779]],"o":[[-5.977,27.46],[24.317,-27.779]],"v":[[-49.023,-535.46],[-34.317,-472.221]],"c":true}]},{"t":28,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-20,-580],[-20,-580]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.729411764706,0.007843137255,0.007843137255,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":8},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.969,0.651,0.024,0.179,0.98,0.476,0.053,1,0.992,0.302,0.082]}},"s":{"a":0,"k":[0,78.641]},"e":{"a":0,"k":[0,-396.457]},"t":1,"nm":"Gradient Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":28,"st":-8,"bm":0}]},{"id":"comp_14","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"main","sr":1,"ks":{"p":{"a":0,"k":[256,646,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":-31,"s":[{"i":[[-30.231,72.598],[-53.5,0.243]],"o":[[-74.692,48.796],[53.567,-0.243]],"v":[[3,-341],[3,102.312]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-104,239],[95,-118.312]],"o":[[-279,185],[275,-162.312]],"v":[[3,-341],[3,102.312]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-85.814,172.891],[-2.814,-80.393]],"o":[[-167.814,138.391],[153.186,-122.893]],"v":[[0.814,-398.391],[-2.186,-7.107]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-67.629,106.783],[-100.629,-42.475]],"o":[[-145.629,104.783],[79.371,-115.475]],"v":[[-1.371,-455.783],[40.629,-170.525]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-45.595,72.231],[-66.633,-28.88]],"o":[[-92.443,67.174],[-18.443,-69.55]],"v":[[-3.557,-513.174],[-36.557,-332.45]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[7.743,54.566],[-32.638,-15.285]],"o":[[-48.214,34.909],[20.243,-33.963]],"v":[[-5.743,-570.566],[-53.243,-478.037]],"c":true}]},{"t":20,"s":[{"i":[[-1.529,3.129],[1.357,-1.69]],"o":[[-3.986,2.643],[3.929,-2.319]],"v":[[-7.929,-627.957],[-7.929,-621.624]],"c":true}]}]},"nm":"Path 5","hd":false},{"ty":"st","c":{"a":0,"k":[0.729411764706,0.007843137255,0.007843137255,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":8},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.969,0.651,0.024,0.179,0.98,0.476,0.053,1,0.992,0.302,0.082]}},"s":{"a":0,"k":[0,78.641]},"e":{"a":0,"k":[0,-396.457]},"t":1,"nm":"Gradient Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":20,"st":0,"bm":0}]},{"id":"comp_15","layers":[{"ddd":0,"ind":3,"ty":4,"nm":"main 2","sr":1,"ks":{"p":{"a":0,"k":[256,646,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-27,-142.312],[38.313,64.997]],"o":[[114,-7.312],[-17.687,108.997]],"v":[[2,101.312],[71.687,-78.997]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-27,-142.312],[116.313,86.997]],"o":[[152,4.688],[74.313,100.997]],"v":[[2,101.312],[123.687,-214.997]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[69.667,-86.208],[12.542,44.665]],"o":[[181.667,-126.208],[-53.458,52.665]],"v":[[132.333,-147.792],[87.458,-400.665]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-59.667,-35.104],[56.771,51.332]],"o":[[-33.667,-59.104],[-43.229,37.332]],"v":[[111.667,-428.896],[85.229,-555.332]],"c":true}]},{"t":32,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[89,-623],[89,-623]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.729411764706,0.007843137255,0.007843137255,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":8},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.969,0.651,0.024,0.179,0.98,0.476,0.053,1,0.992,0.302,0.082]}},"s":{"a":0,"k":[0,78.641]},"e":{"a":0,"k":[0,-396.457]},"t":1,"nm":"Gradient Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":32,"st":0,"bm":0}]},{"id":"comp_16","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"main","sr":1,"ks":{"p":{"a":0,"k":[256,646,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-30.231,72.598],[-20.387,-75.483],[-3.016,-44.152],[-53.5,0.243],[-7.172,51.412],[38.165,58.396]],"o":[[-74.692,48.796],[22.247,82.372],[3.669,53.71],[53.567,-0.243],[9.183,-65.826],[-35.462,-54.259]],"v":[[3,-341],[-97.747,-158.872],[-97.011,10.806],[3,102.312],[103.012,7.782],[15.687,-158.997]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[55.583,73],[-8.766,-58.692],[1.871,-96.875],[-47.827,1.284],[-21.624,54.823],[29.921,61.299]],"o":[[-3.417,72],[14.417,96.532],[-1.22,63.133],[54.074,-1.451],[19.657,-49.837],[-55.886,-114.495]],"v":[[-46.583,-299],[-20.161,-165.111],[-101.555,0.856],[-1.001,101.506],[121.841,-0.997],[112.182,-173.819]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[74.692,48.796],[35.462,-54.259],[-9.183,-65.826],[-53.567,-0.243],[-3.669,53.71],[-22.247,82.372]],"o":[[30.231,72.598],[-38.165,58.396],[7.172,51.412],[53.5,0.243],[3.016,-44.152],[20.387,-75.483]],"v":[[-1,-343],[-13.687,-160.997],[-101.012,5.782],[-1,100.312],[99.011,8.806],[99.747,-160.872]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[75.232,135],[55.886,-114.495],[-19.657,-49.837],[-54.074,-1.451],[1.22,63.133],[-14.417,96.532]],"o":[[-1.768,121],[-29.921,61.299],[21.624,54.823],[47.828,1.284],[-1.871,-96.875],[8.766,-58.692]],"v":[[48.768,-397],[-111.998,-173.819],[-121.657,-0.997],[1.185,101.506],[101.739,0.856],[20.345,-165.111]],"c":true}]},{"t":32,"s":[{"i":[[-30.231,72.598],[-20.387,-75.483],[-3.016,-44.152],[-53.5,0.243],[-7.172,51.412],[38.165,58.396]],"o":[[-74.692,48.796],[22.247,82.372],[3.669,53.71],[53.567,-0.243],[9.183,-65.826],[-35.462,-54.259]],"v":[[3,-341],[-97.747,-158.872],[-97.011,10.806],[3,102.312],[103.012,7.782],[15.687,-158.997]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.729411764706,0.007843137255,0.007843137255,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":16},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":32,"st":0,"bm":0}]},{"id":"comp_17","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"main","sr":1,"ks":{"p":{"a":0,"k":[256,646,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-11,-132.312],[-7.172,51.412],[38.165,58.396]],"o":[[53.567,-0.243],[9.183,-65.826],[111.313,177.997]],"v":[[3,102.312],[103.012,7.782],[15.687,-158.997]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-67,-154.909],[-14.398,53.118],[34.043,59.847]],"o":[[53.821,-0.847],[14.42,-57.831],[57.066,132.408]],"v":[[1,101.909],[112.427,3.393],[63.934,-166.408]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-167.999,-206.506],[-21.624,54.823],[29.921,61.299]],"o":[[54.074,-1.451],[19.657,-49.837],[2.818,86.819]],"v":[[-1.001,101.506],[121.841,-0.997],[112.182,-173.819]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-53.567,-0.243],[-3.669,53.71],[-22.247,82.372]],"o":[[53.5,0.243],[3.016,-44.152],[-202.747,113.872]],"v":[[-1,100.312],[99.011,8.806],[99.747,-160.872]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[40.815,-60.506],[1.22,63.133],[20.655,68.222]],"o":[[47.827,1.284],[-1.871,-96.875],[-162.345,140.111]],"v":[[1.185,101.506],[101.739,0.856],[-42.655,-165.222]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[-31.558,-50.003],[16.799,28.099],[2.217,48.282]],"o":[[21.885,-45.314],[-28.06,-46.936],[-150.456,128.631]],"v":[[46.558,7.003],[45.21,-99.803],[-42.044,-227.187]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":32,"s":[{"i":[[-37.931,-26.5],[13.635,23.185],[-8.736,55.472]],"o":[[8.571,-34.721],[-24.653,-41.921],[-138.568,117.152]],"v":[[29.931,-75.5],[16.564,-159.567],[-41.432,-289.152]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[-28.177,-18.494],[2.319,19.339],[-10.19,30.389]],"o":[[-1.177,-21.494],[-3.873,-32.303],[-112.29,82.582]],"v":[[-32.823,-251.506],[-43.018,-315.512],[-29.71,-405.582]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[-2.92,-10.108],[-2.262,14.82],[-5.034,15.394]],"o":[[-0.505,-10.745],[2.5,-16.382],[-42.542,41.191]],"v":[[-54.2,-359.009],[-45.238,-393.82],[-38.849,-439.797]],"c":true}]},{"t":48,"s":[{"i":[[0.382,-0.567],[0.061,0.36],[0.121,0.4]],"o":[[0.168,0.005],[-0.102,-0.602],[-1.521,1.313]],"v":[[-47.577,-471.512],[-47.458,-472.128],[-47.987,-474.011]],"c":true}]}]},"nm":"Path 6","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[21.747,64.872],[-3.016,-44.152],[-53.5,0.243]],"o":[[22.247,82.372],[3.669,53.71],[79,-87.312]],"v":[[-97.747,-158.872],[-97.011,10.806],[3,102.312]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[113.161,97.111],[1.871,-96.875],[-47.828,1.284]],"o":[[14.417,96.532],[-1.22,63.133],[-66.999,-121.506]],"v":[[-20.161,-165.111],[-101.555,0.856],[-1.001,101.506]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[118.424,157.27],[28.288,-30.267],[-18.903,-57.031]],"o":[[9.7,45.245],[-31.276,33.464],[42.001,-54.614]],"v":[[46.576,-222.27],[-0.075,-117.135],[-44.001,7.614]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[94.686,138.159],[18.186,-23.426],[-5.386,-53.715]],"o":[[5.853,35.136],[-21.233,27.351],[66.001,-69.723]],"v":[[49.314,-279.159],[20.764,-198.478],[-19.001,-88.277]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[66.949,92.127],[9.993,-15.964],[-5.063,-48.024]],"o":[[5.051,35.121],[-11.668,18.639],[43.6,-13.156]],"v":[[49.051,-336.127],[35.336,-268.89],[15.4,-183.844]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[42.212,58.095],[5.432,-11.165],[-9.453,-39.448]],"o":[[3.788,26.341],[-6.342,13.036],[32.7,-9.867]],"v":[[48.788,-393.095],[41.523,-344.806],[38.55,-278.766]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[28.141,38.73],[1.328,-7.479],[-6.302,-26.298]],"o":[[2.526,17.561],[-1.551,8.732],[21.8,-6.578]],"v":[[48.525,-450.063],[48.103,-417.802],[51.7,-373.687]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":36,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[48,-564],[48,-564],[48,-564]],"c":true}]},{"t":37,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[47,-692],[47,-692],[47,-692]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":-31,"s":[{"i":[[-30.231,72.598],[-53.5,0.243]],"o":[[-74.692,48.796],[53.567,-0.243]],"v":[[3,-341],[3,102.312]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-104,239],[95,-118.312]],"o":[[-279,185],[275,-162.312]],"v":[[3,-341],[3,102.312]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-85.814,172.891],[-2.814,-80.393]],"o":[[-167.814,138.391],[153.186,-122.893]],"v":[[0.814,-398.391],[-2.186,-7.107]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-67.629,106.783],[-100.629,-42.475]],"o":[[-145.629,104.783],[79.371,-115.475]],"v":[[-1.371,-455.783],[40.629,-170.525]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-45.595,72.231],[-66.633,-28.88]],"o":[[-92.443,67.174],[-18.443,-69.55]],"v":[[-3.557,-513.174],[-36.557,-332.45]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[7.743,54.566],[-32.638,-15.285]],"o":[[-48.214,34.909],[20.243,-33.963]],"v":[[-5.743,-570.566],[-53.243,-478.037]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[-1.529,3.129],[1.357,-1.69]],"o":[[-3.986,2.643],[3.929,-2.319]],"v":[[-7.929,-627.957],[-7.929,-621.624]],"c":true}]},{"t":21,"s":[{"i":[[-1.529,3.129],[1.357,-1.69]],"o":[[-3.986,2.643],[3.929,-2.319]],"v":[[-8.857,-693.957],[-8.857,-687.624]],"c":true}]}]},"nm":"Path 5","hd":false},{"ty":"st","c":{"a":0,"k":[0.729411764706,0.007843137255,0.007843137255,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":16},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":48,"st":0,"bm":0}]},{"id":"comp_18","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 14","sr":1,"ks":{"o":{"a":0,"k":0},"p":{"a":0,"k":[256,488,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[40,40,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":8,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":30,"s":[60,60,100]},{"t":42,"s":[100,100,100]}]}},"ao":0,"ip":0,"op":92,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"01 fire small cycle","parent":1,"refId":"comp_19","sr":1,"ks":{"p":{"a":0,"k":[0,-232,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"01 fire small end","parent":1,"refId":"comp_20","sr":1,"ks":{"p":{"a":0,"k":[0,24,0]},"a":{"a":0,"k":[256,512,0]}},"ao":0,"w":512,"h":512,"ip":32,"op":92,"st":32,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"01 fire small left bottom flame (f24)","parent":1,"refId":"comp_22","sr":1,"ks":{"p":{"a":0,"k":[0,-232,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":512,"ip":8,"op":40,"st":8,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"01 fire small left bottom flame (f24) 2","parent":1,"refId":"comp_3","sr":1,"ks":{"p":{"a":0,"k":[0,-232,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":24,"op":56,"st":24,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"01 fire small left flame (f0)","parent":1,"refId":"comp_21","sr":1,"ks":{"p":{"a":0,"k":[0,-232,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":512,"ip":16,"op":48,"st":16,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"01 fire small left flame (f0)","parent":1,"refId":"comp_21","sr":1,"ks":{"p":{"a":0,"k":[0,-232,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":0,"nm":"01 fire small cycle outlines","parent":1,"refId":"comp_23","sr":1,"ks":{"p":{"a":0,"k":[0,-232,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":0,"nm":"01 fire small left flame twirl","parent":1,"refId":"comp_4","sr":1,"ks":{"p":{"a":0,"k":[0,-232,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":512,"ip":24,"op":56,"st":24,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Shape Layer 6","sr":1,"ks":{"p":{"a":0,"k":[259.799,261,0]},"a":{"a":0,"k":[3.799,5,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-88.418,174.357],[70,-25]],"o":[[107,-211],[-95.339,34.05]],"v":[[-77,179],[-204,-167]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":4,"s":[0]},{"t":44,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.98431378603,0.4392157197,0.058823533356,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":4,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":14,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":34,"s":[8]},{"t":44,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.729411764706,0.007843137255,0.007843137255,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":4,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":14,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":34,"s":[24]},{"t":44,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":4,"op":44,"st":4,"bm":0}]},{"id":"comp_19","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"core 2","sr":1,"ks":{"p":{"a":0,"k":[255.562,491.044,0]},"a":{"a":0,"k":[-51.438,222.044,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[105,95,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":8,"s":[95,105,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":16,"s":[105,95,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":24,"s":[95,105,100]},{"t":32,"s":[105,95,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-32.312,54.845],[-50.481,50.293],[35.394,-6.415],[6.415,35.394]],"o":[[46.16,46.426],[17.507,77.667],[-35.394,6.415],[-6.415,-35.394]],"v":[[-32.108,-101.639],[64.059,-13.213],[18.468,99.426],[-57.235,46.954]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-9.049,78.431],[-46.813,7.238],[38.482,-0.52],[-11.655,36.679]],"o":[[31.347,69.649],[-11.942,40.075],[-49.111,0.664],[15.601,23.049]],"v":[[45.915,-106],[82.625,29.003],[0.537,100.708],[-67.157,15.096]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[3.588,91.907],[6.435,-34.983],[34.983,6.435],[-15.694,53.155]],"o":[[25.664,58.408],[-6.435,34.983],[-34.983,-6.435],[12.057,48.134]],"v":[[29.902,-105.564],[55.722,46.071],[-19.272,97.762],[-62.997,-18.375]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-54.662,56.939],[-21.112,21.663],[36.819,0.63],[3.619,48.777]],"o":[[5.338,56.939],[20.993,48.329],[-32.5,-0.556],[57.303,3.063]],"v":[[-7.338,-108.939],[56.925,4.101],[-1.758,100.061],[-76.227,19.844]],"c":true}]},{"t":32,"s":[{"i":[[-32.312,54.845],[-50.481,50.293],[35.394,-6.415],[6.415,35.394]],"o":[[46.16,46.426],[17.507,77.667],[-35.394,6.415],[-6.415,-35.394]],"v":[[-32.108,-101.639],[64.059,-13.213],[18.468,99.426],[-57.235,46.954]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0.121,0.996,0.996,0.898,0.54,0.998,0.951,0.598,0.808,1,0.906,0.298]}},"s":{"a":0,"k":[2.235,29.653]},"e":{"a":0,"k":[6.469,-84.419]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 4","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"second 2","sr":1,"ks":{"p":{"a":0,"k":[256,390,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-41.664,72.163],[-8.214,-67.826],[46.519,-9.139],[9.031,45.968]],"o":[[23.707,82.394],[5.632,46.51],[-46.519,9.139],[-9.031,-45.968]],"v":[[-41.859,-162.622],[87.483,-0.511],[19.606,99.271],[-80.977,32.585]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-17.507,82.469],[18.015,-60.597],[47.482,-0.642],[-14.381,45.257]],"o":[[42.493,69.469],[-14.703,49.456],[-60.597,0.819],[27.842,-87.619]],"v":[[41.507,-150.469],[91.801,20.75],[0.906,101],[-82.619,-4.635]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-24.427,64.616],[9.005,-44.596],[44.596,9.005],[-9.492,44.494]],"o":[[41.516,68.877],[-9.005,44.596],[-44.596,-9.005],[13.048,-61.165]],"v":[[43.143,-159.265],[78.884,33.269],[-18.169,97.711],[-82.611,0.658]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-66.821,80.403],[-15.912,-60.669],[49.77,0.851],[10.874,42.57]],"o":[[-1.821,86.403],[12.892,49.156],[-43.931,-0.751],[-13.53,-52.967]],"v":[[0.821,-184.403],[78.597,2.76],[-0.727,99],[-81.47,33.052]],"c":true}]},{"t":32,"s":[{"i":[[-41.664,72.163],[-8.214,-67.826],[46.519,-9.139],[9.031,45.968]],"o":[[23.707,82.394],[5.632,46.51],[-46.519,9.139],[-9.031,-45.968]],"v":[[-41.859,-162.622],[87.483,-0.511],[19.606,99.271],[-80.977,32.585]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,0.914,0.239,0.269,1,0.739,0.22,1,1,0.565,0.2]}},"s":{"a":0,"k":[1.76,83.087]},"e":{"a":0,"k":[-3.18,-152.581]},"t":1,"nm":"Gradient Fill 2","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"main 2","sr":1,"ks":{"p":{"a":0,"k":[256,390,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-50.591,89.284],[-10.423,-83.922],[56.878,-11.304],[11.304,56.878]],"o":[[29.463,101.948],[7.147,57.548],[-56.878,11.304],[-11.304,-56.878]],"v":[[-43.991,-227.888],[102.986,-20.467],[20.467,102.986],[-102.986,20.467]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[35.726,95.092],[22,-74],[57.985,-0.784],[-17.561,55.267]],"o":[[96.726,88.092],[-17.955,60.395],[-74,1],[34,-107]],"v":[[20.274,-202.092],[111,7],[0,105],[-102,-24]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-29.463,101.948],[11.304,-56.878],[56.878,11.304],[-7.147,57.548]],"o":[[50.591,89.284],[-11.304,56.878],[-56.878,-11.304],[10.423,-83.922]],"v":[[43.237,-226.763],[102.232,21.592],[-21.221,104.111],[-103.74,-19.342]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-117.832,89.347],[-21.001,-80.072],[65.687,1.123],[14.352,56.186]],"o":[[-46.372,107.609],[17.016,64.878],[-57.982,-0.992],[-17.858,-69.908]],"v":[[31.079,-257.165],[104.694,-22.02],[0,105],[-106.567,17.96]],"c":true}]},{"t":32,"s":[{"i":[[-50.591,89.284],[-10.423,-83.922],[56.878,-11.304],[11.304,56.878]],"o":[[29.463,101.948],[7.147,57.548],[-56.878,11.304],[-11.304,-56.878]],"v":[[-43.991,-227.888],[102.986,-20.467],[20.467,102.986],[-102.986,20.467]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.969,0.651,0.024,0.179,0.98,0.476,0.053,1,0.992,0.302,0.082]}},"s":{"a":0,"k":[0,76.641]},"e":{"a":0,"k":[0,-306.457]},"t":1,"nm":"Gradient Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":32,"st":0,"bm":0}]},{"id":"comp_20","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"core 3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":8,"s":[0]}]},"p":{"a":0,"k":[255.562,491.044,0]},"a":{"a":0,"k":[-51.438,222.044,0]},"s":{"a":0,"k":[105,95,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-32.312,54.845],[-50.481,50.293],[35.394,-6.415]],"o":[[46.16,46.426],[17.507,77.667],[-35.394,6.415]],"v":[[-32.108,-101.639],[64.059,-13.213],[18.468,99.426]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-20.681,66.638],[-25.24,25.147],[36.938,-3.468]],"o":[[19.636,60.922],[8.753,38.833],[-42.253,3.539]],"v":[[6.903,-103.82],[25.247,13.158],[-2.402,97.962]],"c":true}]},{"t":8,"s":[{"i":[[-9.049,78.431],[0,0],[38.482,-0.52]],"o":[[-6.888,75.419],[0,0],[-49.111,0.664]],"v":[[45.915,-106],[26.435,33.213],[-23.272,96.498]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0.121,0.996,0.996,0.898,0.54,0.998,0.951,0.598,0.808,1,0.906,0.298]}},"s":{"a":0,"k":[2.235,29.653]},"e":{"a":0,"k":[6.469,-84.419]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 4","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":8,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"core 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":8,"s":[100]},{"t":16,"s":[0]}]},"p":{"a":0,"k":[255.562,491.044,0]},"a":{"a":0,"k":[-51.438,222.044,0]},"s":{"a":0,"k":[105,95,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-32.312,54.845],[35.394,-6.415],[6.415,35.394]],"o":[[46.16,46.426],[-35.394,6.415],[-6.415,-35.394]],"v":[[-32.108,-101.639],[18.468,99.426],[-57.235,46.954]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-9.049,78.431],[38.482,-0.52],[-11.655,36.679]],"o":[[14.108,81.288],[-49.111,0.664],[15.601,23.049]],"v":[[36.441,-78.381],[-17.358,97.851],[-58.736,-34.428]],"c":true}]},{"t":16,"s":[{"i":[[25.316,-83.965],[-0.749,0.077],[-76.071,58.32]],"o":[[0.554,1.298],[-34.983,-6.435],[-36.071,64.636]],"v":[[-53.908,77.594],[-54.51,76.71],[-37.283,-93.112]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0.121,0.996,0.996,0.898,0.54,0.998,0.951,0.598,0.808,1,0.906,0.298]}},"s":{"a":0,"k":[2.235,29.653]},"e":{"a":0,"k":[6.469,-84.419]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 4","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":16,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"second 3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":8,"s":[0]}]},"p":{"a":0,"k":[256,390,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-41.664,72.163],[-8.214,-67.826],[46.519,-9.139]],"o":[[23.707,82.394],[5.632,46.51],[-46.519,9.139]],"v":[[-41.859,-162.622],[87.483,-0.511],[19.606,99.271]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-20.764,35.76],[-4.177,-33.677],[23.13,-4.558]],"o":[[11.758,40.899],[2.873,23.062],[-23.024,4.548]],"v":[[0.057,-155.184],[26.715,-70.788],[1.037,-22.218]],"c":true}]},{"t":8,"s":[{"i":[[0.137,-0.643],[-0.14,0.473],[-0.258,0.023]],"o":[[-0.191,-0.596],[0.115,-0.386],[0.471,-0.043]],"v":[[41.973,-147.746],[41.947,-149.066],[42.469,-149.707]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,0.914,0.239,0.269,1,0.739,0.22,1,1,0.565,0.2]}},"s":{"a":0,"k":[1.76,83.087]},"e":{"a":0,"k":[-3.18,-152.581]},"t":1,"nm":"Gradient Fill 2","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":9,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"second 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":8,"s":[100]},{"t":16,"s":[0]}]},"p":{"a":0,"k":[256,390,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-41.664,72.163],[46.519,-9.139],[9.031,45.968]],"o":[[23.707,82.394],[-46.519,9.139],[-9.031,-45.968]],"v":[[-41.859,-162.622],[19.606,99.271],[-80.977,32.585]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-17.507,82.469],[47.482,-0.642],[-14.381,45.257]],"o":[[42.493,69.469],[-60.597,0.819],[52.619,38.635]],"v":[[41.507,-150.469],[-14.094,99],[-74.619,-94.635]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-41.143,-111.735],[0.169,1.289],[-67.389,81.342]],"o":[[-0.143,-0.735],[-65.831,-26.711],[73.611,83.342]],"v":[[-48.857,72.735],[-50.169,74.711],[-15.611,-173.342]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-66.821,80.403],[49.77,0.851],[10.874,42.57]],"o":[[-1.821,86.403],[-43.931,-0.751],[-13.53,-52.967]],"v":[[0.821,-184.403],[-0.727,99],[-81.47,33.052]],"c":true}]},{"t":32,"s":[{"i":[[-41.664,72.163],[46.519,-9.139],[9.031,45.968]],"o":[[23.707,82.394],[-46.519,9.139],[-9.031,-45.968]],"v":[[-41.859,-162.622],[19.606,99.271],[-80.977,32.585]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,0.914,0.239,0.269,1,0.739,0.22,1,1,0.565,0.2]}},"s":{"a":0,"k":[1.76,83.087]},"e":{"a":0,"k":[-3.18,-152.581]},"t":1,"nm":"Gradient Fill 2","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":16,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"main 3","sr":1,"ks":{"p":{"a":0,"k":[256,390,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-50.971,89.067],[103.972,-17.072],[11.061,56.925]],"o":[[29.028,102.073],[-57.224,9.396],[-11.061,-56.925]],"v":[[-43.019,-228.074],[20.028,103.072],[-103.072,20.028]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[43.726,82.092],[57.988,0.518],[-95,149]],"o":[[96.726,88.092],[-112,-1],[-13,51]],"v":[[20.274,-202.092],[0,105],[-73,-138]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-73.494,-115.442],[-0.965,-0.071],[38.023,130.049]],"o":[[-0.535,-0.415],[-233.934,-85.156],[139.015,154.085]],"v":[[-22.502,103.407],[-20.072,103.064],[-32.976,-260.047]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[2.021,-34.999],[-0.33,-1.295],[-11.432,87.746]],"o":[[1.079,2.35],[22.732,-106.281],[55.537,140.785]],"v":[[-87.963,-63.052],[-89.633,-60.758],[17.544,-277.742]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-0.698,-28.573],[0.066,0.496],[-9.356,59.089]],"o":[[-0.427,-0.075],[-30.434,-80.504],[51.697,91.245]],"v":[[-35.323,-171.675],[-35.566,-171.996],[29.381,-304.076]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[24.936,-19.259],[-0.649,-0.657],[-15.013,31.882]],"o":[[-0.129,0.159],[-25.448,-61.739],[35.94,46.05]],"v":[[-26.053,-248.746],[-26.036,-246.932],[35.021,-337.818]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":32,"s":[{"i":[[-0.582,0.228],[-0.296,-0.089],[0.406,0.513]],"o":[[-0.319,0.299],[-1.172,0.037],[0.462,0.188]],"v":[[21.662,-367.612],[21.457,-366.849],[20.741,-368.45]],"c":true}]},{"t":33,"s":[{"i":[[-0.582,0.228],[-0.296,-0.089],[0.406,0.513]],"o":[[-0.319,0.299],[-1.172,0.037],[0.462,0.188]],"v":[[22.119,-425.612],[21.915,-424.849],[21.199,-426.45]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[35.726,95.092],[9.957,-101.911],[-1.117,44.693]],"o":[[71.726,197.092],[-45.603,-22.126],[3,-120]],"v":[[-42.726,-223.092],[-45.957,94.911],[-102,-24]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[35.726,95.092],[9.957,-101.911],[-13.539,42.608]],"o":[[76.726,173.092],[-45.603,-22.126],[34,-107]],"v":[[20.274,-202.092],[-45.957,94.911],[-102,-24]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[28.539,79.666],[15.271,-30.792],[63.294,-24.571]],"o":[[89.467,109.928],[-7.636,15.397],[-160.84,62.44]],"v":[[-4.803,-241.627],[21.701,-28.613],[-27.163,-45.068]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[27.035,22.744],[-7.411,-52.025],[34.341,-66.85]],"o":[[98.144,67.203],[4.153,29.149],[-14.835,28.88]],"v":[[-66.918,-295.641],[41.698,-130.341],[-50.963,-169.565]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[38.174,25.145],[-12.055,-52.934],[22.231,-50.177]],"o":[[93.649,22.392],[6.204,24.263],[-19.381,32.447]],"v":[[-114.032,-311.296],[26.995,-175.344],[-61.336,-205.289]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[51.836,22.143],[-19.073,-53.622],[17.852,-32.908]],"o":[[74.884,-18.277],[7.052,19.825],[-21.585,39.789]],"v":[[-157.371,-308.755],[11.042,-212.473],[-74.322,-234.122]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[47.388,-0.852],[-20.633,-42.246],[-6.679,-39.947]],"o":[[79.829,-60.96],[7.911,20.621],[3.631,36.916]],"v":[[-192.262,-284.999],[-12.601,-245.703],[-93.247,-262.886]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[30.066,-23.944],[-27.45,-33.052],[-38.7,-24.364]],"o":[[34.202,-118.986],[16.55,19.928],[26.108,16.437]],"v":[[-220.042,-233.02],[-52.086,-285.918],[-140.713,-280.698]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":26,"s":[{"i":[[21.559,-53.406],[-42.556,-12.395],[-39.454,-8.254]],"o":[[-21.52,-112.915],[26.583,7.597],[22.752,4.36]],"v":[[-217.817,-188.862],[-94.492,-311.817],[-150.4,-267.689]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[-8.081,-76.532],[-44.505,14.451],[-34.813,5.036]],"o":[[-66.788,-82.682],[28.609,-9.29],[15.471,-2.238]],"v":[[-197.892,-143.194],[-148.445,-308.584],[-158.488,-252.15]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-42.015,-57.434],[-26.817,31.143],[-39.824,16.539]],"o":[[-92.051,-33.255],[19.332,-22.828],[22.33,-8.449]],"v":[[-156.547,-114.04],[-197.313,-273.38],[-168.13,-223.209]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":32,"s":[{"i":[[-61.367,-22.786],[-3.433,35.391],[-37.643,36.993]],"o":[[-86.784,22.985],[2.883,-29.724],[23.519,-23.113]],"v":[[-107.757,-110.853],[-225.28,-214.573],[-166.864,-196.295]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":34,"s":[{"i":[[-48.255,8.824],[11.318,24.14],[-10.048,38.28]],"o":[[-47.05,48.04],[-9.546,-20.238],[6.238,-23.914]],"v":[[-81.839,-126.076],[-197.225,-147.861],[-152.608,-158.445]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":36,"s":[{"i":[[-22.599,23.564],[15.508,8.665],[9.557,24.582]],"o":[[-6.616,44.216],[-13.049,-7.172],[-6.032,-15.307]],"v":[[-64.671,-149.022],[-135.368,-115.824],[-115.418,-138.897]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":38,"s":[{"i":[[-2.335,16.115],[8.871,-0.948],[11.06,7.121]],"o":[[10.116,19.764],[-7.341,0.908],[-6.895,-4.334]],"v":[[-43.565,-175.386],[-62.601,-141.49],[-61.167,-156.623]],"c":true}]},{"t":40,"s":[{"i":[[-0.068,-0.18],[-0.019,0.193],[0.026,-0.081]],"o":[[-0.145,-0.328],[0.086,0.042],[-0.064,0.203]],"v":[[-36.242,-198.937],[-36.116,-199.5],[-36.01,-199.275]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.969,0.651,0.024,0.179,0.98,0.476,0.053,1,0.992,0.302,0.082]}},"s":{"a":0,"k":[-0.803,78.641]},"e":{"a":0,"k":[5.941,-306.457]},"t":1,"nm":"Gradient Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":40,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"01 fire small left flame (f0)","refId":"comp_21","sr":1,"ks":{"p":{"a":0,"k":[256,256,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":512,"ip":-16,"op":16,"st":-16,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"main 4","sr":1,"ks":{"p":{"a":0,"k":[256,390,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-50.971,89.067],[103.972,-17.072],[11.061,56.925]],"o":[[29.028,102.073],[-57.224,9.396],[-11.061,-56.925]],"v":[[-43.019,-228.074],[20.028,103.072],[-103.072,20.028]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[43.726,82.092],[57.988,0.518],[-95,149]],"o":[[96.726,88.092],[-112,-1],[-13,51]],"v":[[20.274,-202.092],[0,105],[-73,-138]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-73.494,-115.442],[-0.965,-0.071],[38.023,130.049]],"o":[[-0.535,-0.415],[-233.934,-85.156],[139.015,154.085]],"v":[[-22.502,103.407],[-20.072,103.064],[-32.976,-260.047]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[2.021,-34.999],[-0.33,-1.295],[-11.432,87.746]],"o":[[1.079,2.35],[22.732,-106.281],[55.537,140.785]],"v":[[-87.963,-63.052],[-89.633,-60.758],[17.544,-277.742]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-0.698,-28.573],[0.066,0.496],[-9.356,59.089]],"o":[[-0.427,-0.075],[-30.434,-80.504],[51.697,91.245]],"v":[[-35.323,-171.675],[-35.566,-171.996],[29.381,-304.076]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[24.936,-19.259],[-0.649,-0.657],[-15.013,31.882]],"o":[[-0.129,0.159],[-25.448,-61.739],[35.94,46.05]],"v":[[-26.053,-248.746],[-26.036,-246.932],[35.021,-337.818]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":32,"s":[{"i":[[-0.582,0.228],[-0.296,-0.089],[0.406,0.513]],"o":[[-0.319,0.299],[-1.172,0.037],[0.462,0.188]],"v":[[21.662,-367.612],[21.457,-366.849],[20.741,-368.45]],"c":true}]},{"t":33,"s":[{"i":[[-0.582,0.228],[-0.296,-0.089],[0.406,0.513]],"o":[[-0.319,0.299],[-1.172,0.037],[0.462,0.188]],"v":[[22.119,-425.612],[21.915,-424.849],[21.199,-426.45]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[35.726,95.092],[9.957,-101.911],[-1.117,44.693]],"o":[[71.726,197.092],[-45.603,-22.126],[3,-120]],"v":[[-42.726,-223.092],[-45.957,94.911],[-102,-24]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[35.726,95.092],[9.957,-101.911],[-13.539,42.608]],"o":[[76.726,173.092],[-45.603,-22.126],[34,-107]],"v":[[20.274,-202.092],[-45.957,94.911],[-102,-24]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[28.539,79.666],[15.271,-30.792],[63.294,-24.571]],"o":[[89.467,109.928],[-7.636,15.397],[-160.84,62.44]],"v":[[-4.803,-241.627],[21.701,-28.613],[-27.163,-45.068]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[27.035,22.744],[-7.411,-52.025],[34.341,-66.85]],"o":[[98.144,67.203],[4.153,29.149],[-14.835,28.88]],"v":[[-66.918,-295.641],[41.698,-130.341],[-50.963,-169.565]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[38.174,25.145],[-12.055,-52.934],[22.231,-50.177]],"o":[[93.649,22.392],[6.204,24.263],[-19.381,32.447]],"v":[[-114.032,-311.296],[26.995,-175.344],[-61.336,-205.289]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[51.836,22.143],[-19.073,-53.622],[17.852,-32.908]],"o":[[74.884,-18.277],[7.052,19.825],[-21.585,39.789]],"v":[[-157.371,-308.755],[11.042,-212.473],[-74.322,-234.122]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[47.388,-0.852],[-20.633,-42.246],[-6.679,-39.947]],"o":[[79.829,-60.96],[7.911,20.621],[3.631,36.916]],"v":[[-192.262,-284.999],[-12.601,-245.703],[-93.247,-262.886]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[30.066,-23.944],[-27.45,-33.052],[-38.7,-24.364]],"o":[[34.202,-118.986],[16.55,19.928],[26.108,16.437]],"v":[[-220.042,-233.02],[-52.086,-285.918],[-140.713,-280.698]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":26,"s":[{"i":[[21.559,-53.406],[-42.556,-12.395],[-39.454,-8.254]],"o":[[-21.52,-112.915],[26.583,7.597],[22.752,4.36]],"v":[[-217.817,-188.862],[-94.492,-311.817],[-150.4,-267.689]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[-8.081,-76.532],[-44.505,14.451],[-34.813,5.036]],"o":[[-66.788,-82.682],[28.609,-9.29],[15.471,-2.238]],"v":[[-197.892,-143.194],[-148.445,-308.584],[-158.488,-252.15]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[-42.015,-57.434],[-26.817,31.143],[-39.824,16.539]],"o":[[-92.051,-33.255],[19.332,-22.828],[22.33,-8.449]],"v":[[-156.547,-114.04],[-197.313,-273.38],[-168.13,-223.209]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":32,"s":[{"i":[[-61.367,-22.786],[-3.433,35.391],[-37.643,36.993]],"o":[[-86.784,22.985],[2.883,-29.724],[23.519,-23.113]],"v":[[-107.757,-110.853],[-225.28,-214.573],[-166.864,-196.295]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":34,"s":[{"i":[[-48.255,8.824],[11.318,24.14],[-10.048,38.28]],"o":[[-47.05,48.04],[-9.546,-20.238],[6.238,-23.914]],"v":[[-81.839,-126.076],[-197.225,-147.861],[-152.608,-158.445]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":36,"s":[{"i":[[-22.599,23.564],[15.508,8.665],[9.557,24.582]],"o":[[-6.616,44.216],[-13.049,-7.172],[-6.032,-15.307]],"v":[[-64.671,-149.022],[-135.368,-115.824],[-115.418,-138.897]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":38,"s":[{"i":[[-2.335,16.115],[8.871,-0.948],[11.06,7.121]],"o":[[10.116,19.764],[-7.341,0.908],[-6.895,-4.334]],"v":[[-43.565,-175.386],[-62.601,-141.49],[-61.167,-156.623]],"c":true}]},{"t":40,"s":[{"i":[[-0.068,-0.18],[-0.019,0.193],[0.026,-0.081]],"o":[[-0.145,-0.328],[0.086,0.042],[-0.064,0.203]],"v":[[-36.242,-198.937],[-36.116,-199.5],[-36.01,-199.275]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"st","c":{"a":0,"k":[0.729411764706,0.007843137255,0.007843137255,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":16},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":40,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"p":{"a":0,"k":[259.799,261,0]},"a":{"a":0,"k":[3.799,5,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-189.52,47.962],[-214.402,227]],"o":[[407,-103],[69.514,-73.599]],"v":[[7,157],[22,-231]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"t":45,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.905882418156,0.298039227724,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":35,"s":[8]},{"t":45,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.992156922817,0.321568638086,0.078431375325,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":35,"s":[24]},{"t":45,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":5,"op":45,"st":5,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"p":{"a":0,"k":[259.799,261,0]},"a":{"a":0,"k":[3.799,5,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[179.736,76.896],[302,241]],"o":[[-231.402,-99],[-79.13,-63.147]],"v":[[-11,213],[-186,-187]],"c":false}},"nm":"Path 2","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":50,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.905882418156,0.298039227724,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[8]},{"t":50,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.992156922817,0.321568638086,0.078431375325,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[24]},{"t":50,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":50,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"p":{"a":0,"k":[259.799,261,0]},"a":{"a":0,"k":[3.799,5,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-139.238,137.226],[-214.402,227]],"o":[[110.598,-109],[69.514,-73.599]],"v":[[7,157],[22,-231]],"c":false}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[179.736,76.896],[409.598,33]],"o":[[-231.402,-99],[-100.91,-8.13]],"v":[[-11,213],[-186,-187]],"c":false}},"nm":"Path 2","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":60,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.905882418156,0.298039227724,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":50,"s":[8]},{"t":60,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.992156922817,0.321568638086,0.078431375325,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":50,"s":[24]},{"t":60,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":60,"st":0,"bm":0}]},{"id":"comp_21","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"main 3","sr":1,"ks":{"p":{"a":0,"k":[256,390,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-50.971,89.067],[56.925,-11.061],[11.061,56.925]],"o":[[29.028,102.073],[-56.925,11.061],[-11.061,-56.925]],"v":[[-51.019,-212.091],[20.028,103.072],[-103.072,20.028]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[43.726,82.092],[57.988,0.518],[-95,149]],"o":[[96.726,88.092],[-112,-1],[-13,51]],"v":[[30.274,-165.092],[0,105],[-73,-138]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-73.494,-115.442],[-0.965,-0.071],[38.023,130.049]],"o":[[-0.535,-0.415],[-233.934,-85.156],[139.015,154.085]],"v":[[-22.502,103.407],[-20.072,103.064],[-32.976,-260.047]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[2.021,-34.999],[-0.33,-1.295],[-11.432,87.746]],"o":[[1.079,2.35],[22.732,-106.281],[55.537,140.785]],"v":[[-87.963,-63.052],[-89.633,-60.758],[17.544,-277.742]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-0.698,-28.573],[0.066,0.496],[-9.356,59.089]],"o":[[-0.427,-0.075],[-30.434,-80.504],[51.697,91.245]],"v":[[-35.323,-171.675],[-35.566,-171.996],[29.381,-304.076]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[{"i":[[24.936,-19.259],[-0.649,-0.657],[-15.013,31.882]],"o":[[-0.129,0.159],[-25.448,-61.739],[35.94,46.05]],"v":[[-26.053,-248.746],[-26.036,-246.932],[35.021,-337.818]],"c":true}]},{"t":32,"s":[{"i":[[-0.582,0.228],[-0.296,-0.089],[0.406,0.513]],"o":[[-0.319,0.299],[-1.172,0.037],[0.462,0.188]],"v":[[21.662,-367.612],[21.457,-366.849],[20.741,-368.45]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.729411764706,0.007843137255,0.007843137255,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":8},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.969,0.651,0.024,0.179,0.98,0.476,0.053,1,0.992,0.302,0.082]}},"s":{"a":0,"k":[-0.803,78.641]},"e":{"a":0,"k":[5.941,-306.457]},"t":1,"nm":"Gradient Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":32,"st":0,"bm":0}]},{"id":"comp_22","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"main 3","sr":1,"ks":{"p":{"a":0,"k":[256,390,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[65.688,1.123],[19.556,69.083]],"o":[[-41.977,-0.718],[0.056,0.083]],"v":[[0,105],[-106.556,17.417]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[56.878,-11.304],[-71.136,145.476]],"o":[[-40.253,8],[-9.136,65.476]],"v":[[20.467,102.986],[-125.864,-38.476]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[57.289,-92.906],[-55.086,48.981]],"o":[[-57.711,-67.906],[-28.086,62.981]],"v":[[-145.289,-16.094],[-126.914,-181.981]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-8.455,-33.826],[3.464,50.987]],"o":[[-46.455,-37.826],[67.464,52.987]],"v":[[-122.545,-106.174],[-129.464,-218.987]],"c":true}]},{"t":32,"s":[{"i":[[-0.557,-0.338],[-1.599,1.221]],"o":[[-0.348,0.005],[-0.55,0.457]],"v":[[-130.801,-267.255],[-131.014,-268.993]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.729411764706,0.007843137255,0.007843137255,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":8},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.969,0.651,0.024,0.179,0.98,0.476,0.053,1,0.992,0.302,0.082]}},"s":{"a":0,"k":[-0.803,78.641]},"e":{"a":0,"k":[5.941,-306.457]},"t":1,"nm":"Gradient Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":32,"st":-24,"bm":0}]},{"id":"comp_23","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"main outlines","sr":1,"ks":{"p":{"a":0,"k":[256,390,0]},"a":{"a":0,"k":[-51,121,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-50.591,89.284],[-10.423,-83.922],[56.878,-11.304],[11.304,56.878]],"o":[[29.463,101.948],[7.147,57.548],[-56.878,11.304],[-11.304,-56.878]],"v":[[-43.991,-227.888],[102.986,-20.467],[20.467,102.986],[-102.986,20.467]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[35.726,95.092],[22,-74],[57.985,-0.784],[-17.561,55.267]],"o":[[96.726,88.092],[-17.955,60.395],[-74,1],[34,-107]],"v":[[20.274,-202.092],[111,7],[0,105],[-102,-24]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[-29.463,101.948],[11.304,-56.878],[56.878,11.304],[-7.147,57.548]],"o":[[50.591,89.284],[-11.304,56.878],[-56.878,-11.304],[10.423,-83.922]],"v":[[43.237,-226.763],[102.232,21.592],[-21.221,104.111],[-103.74,-19.342]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-117.832,89.347],[-21.001,-80.072],[65.687,1.123],[14.352,56.186]],"o":[[-46.372,107.609],[17.016,64.878],[-57.982,-0.992],[-17.858,-69.908]],"v":[[31.079,-257.165],[104.694,-22.02],[0,105],[-106.567,17.96]],"c":true}]},{"t":32,"s":[{"i":[[-50.591,89.284],[-10.423,-83.922],[56.878,-11.304],[11.304,56.878]],"o":[[29.463,101.948],[7.147,57.548],[-56.878,11.304],[-11.304,-56.878]],"v":[[-43.991,-227.888],[102.986,-20.467],[20.467,102.986],[-102.986,20.467]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.729411764706,0.007843137255,0.007843137255,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":16},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"ty":"tr","p":{"a":0,"k":[-51,121]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":0,"op":32,"st":0,"bm":0}]},{"id":"comp_24","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 4","parent":2,"sr":1,"ks":{"p":{"a":0,"k":[-77.334,-175.69,0]},"a":{"a":0,"k":[-77.334,-175.69,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[1.988,3.181],[-19.8,-5.052]],"o":[[-4.889,-7.822],[26.133,6.668]],"v":[[19.329,271.399],[24.3,294.213]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[4.186,6.698],[-41.687,-10.636]],"o":[[-10.293,-16.469],[55.022,14.038]],"v":[[7.834,173.427],[18.299,221.462]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[14.361,7.522],[-30.261,-6.497]],"o":[[-24.248,-12.701],[34.72,7.454]],"v":[[35.139,128.628],[12.401,185.046]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[2.846,1.491],[-5.996,-1.287]],"o":[[-4.805,-2.517],[6.88,1.477]],"v":[[32.578,61.344],[28.072,72.523]],"c":true}]},{"t":31,"s":[{"i":[[2.846,1.491],[-5.996,-1.287]],"o":[[-4.805,-2.517],[6.88,1.477]],"v":[[20.578,-416.656],[16.072,-405.477]],"c":true}]}]},"nm":"Path 7","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[33.144,5.233],[-24.721,-0.221]],"o":[[-35.022,-5.53],[27.911,0.249]],"v":[[19.555,225.935],[15.319,283.751]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[29.02,-23.268],[-29.148,15.285]],"o":[[-36.827,29.528],[27.206,-14.266]],"v":[[18.827,100.658],[25.794,178.294]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[36.864,-1.646],[-34.889,28.965]],"o":[[-35.938,1.604],[46.08,13.495]],"v":[[1.709,52.646],[-0.595,87.535]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[19.216,-6.659],[-24.954,25.786]],"o":[[-28.004,9.704],[30.5,0]],"v":[[16.732,-8.349],[24.773,17.714]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[12.002,-2.4],[-12.348,2.646]],"o":[[-16.253,3.251],[13.442,-2.88]],"v":[[25.798,-28.588],[29.158,-15.146]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[20.461,-75.516],[21.822,-70.072]],"c":true}]},{"t":51,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[10.461,-427.516],[11.822,-422.072]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[47.81,-67.072],[-25.632,9.313]],"o":[[-93.246,48.698],[25.632,-9.313]],"v":[[-74.754,121.302],[-13.132,182.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[16.302,-91.514],[22.297,-55.155]],"o":[[-51.846,41.486],[63.795,-25.293]],"v":[[-77.654,-43.486],[-14.297,37.655]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[65.508,-110.844],[-62.172,37.016]],"o":[[-46.719,4.672],[111.508,-8.984]],"v":[[-77.508,-81.156],[-31.508,-13.016]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[44.392,-34.904],[-43.656,10.635]],"o":[[-57.534,8.63],[43.656,-10.635]],"v":[[-64.892,-151.596],[-43.156,-90.865]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[45.056,-54.944],[-36.611,29.426]],"o":[[-75.944,-2.944],[74.944,33.426]],"v":[[-55.556,-184.556],[-45.389,-150.426]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[14.5,-3],[-14.25,5.5]],"o":[[-14.5,3],[14.25,-5.5]],"v":[[-44.875,-250.5],[-40.375,-234]],"c":true}]},{"t":60,"s":[{"i":[[1.16,-0.24],[-1.14,0.44]],"o":[[-1.16,0.24],[1.14,-0.44]],"v":[[-45.92,-278.26],[-45.56,-276.94]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[38.071,2.586],[-51.674,3.841],[-14.328,5.136]],"o":[[-71.639,-4.866],[8.07,11.41],[22.966,-8.233]],"v":[[122.074,242.366],[89.943,278.535],[130.995,288.864]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[20.869,-12.454],[-146.79,82.022],[-12.062,30.104]],"o":[[-112.454,-96.052],[15.663,4.937],[27.265,-8.079]],"v":[[157.954,39.052],[123.79,100.978],[169.735,69.137]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[74.55,-67.464],[-92.612,89.3],[0.78,32.797]],"o":[[-175.95,-43.988],[-28.612,-55.2],[57.28,16.797]],"v":[[109.45,-30.036],[99.612,30.2],[150.72,14.703]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[79.321,-50.136],[-20.934,-21.241],[-26.364,9.073]],"o":[[-58.179,-42.136],[17.914,18.177],[76.169,29.621]],"v":[[102.679,-76.364],[46.086,-27.677],[111.331,-23.621]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[62.617,-21.138],[-8.337,-17.814],[-11.971,0.122]],"o":[[-29.444,-9.815],[3.681,7.865],[67.802,31.907]],"v":[[103.66,-125.362],[70.352,-89.569],[93.689,-75.907]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[19.334,0],[-4.841,-5.763],[-4.486,0.179]],"o":[[-13.412,0],[2.137,2.544],[14.647,-0.586]],"v":[[106.839,-168.5],[96.133,-153.01],[106.253,-148.873]],"c":true}]},{"t":60,"s":[{"i":[[5.5,0],[-1.377,-1.639],[-1.276,0.051]],"o":[[-3.815,0],[0.608,0.724],[4.167,-0.167]],"v":[[103.5,-198.125],[100.454,-193.718],[103.333,-192.542]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":6,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-3.499,-24.848],[-6.884,-16.478],[-0.301,7.46],[-4.644,0.228],[-25.003,12.782],[33.341,-4.053]],"o":[[-34.078,22.449],[3.276,7.842],[1.294,-32.082],[19.012,20.499],[85.358,33.282],[-6.489,-19.362]],"v":[[-102.261,200.848],[-133.404,262.516],[-118.294,281.082],[-102.012,287.001],[-31.997,296.218],[-11.544,205.996]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[-0.747,-48.874],[-2.455,-35.731],[-40.679,19.746],[-10.189,-28.494],[-11.643,4.734],[85.039,22.458]],"o":[[-21.211,-29.874],[-35.532,22.641],[-1.215,-25.539],[22.811,19.506],[80.357,20.734],[29.075,-42.399]],"v":[[-100.253,19.874],[-169.932,40.216],[-144.785,98.539],[-93.811,115.494],[-40.357,117.266],[-14.075,40.399]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[4.596,-2.157],[-2.838,-33.101],[-23.392,21.782],[64.52,-34.091],[-7.412,7.454],[7.166,18.65]],"o":[[-43.115,-41.195],[-25.698,17.837],[-20.978,-41.699],[20.24,22.409],[13.576,-13.654],[-5.083,-13.23]],"v":[[-78.36,-37.305],[-165.347,-14.561],[-142.022,35.199],[-94.52,47.591],[-39.74,43.428],[-28.273,-12.423]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[19.586,24.077],[1.481,-16.058],[-4.9,-5.507],[-10.179,0.727],[8.719,-39.971],[89.45,-41.348]],"o":[[-19.447,-7.56],[-0.705,7.642],[4.542,5.105],[-3.153,-23.324],[83.219,35.029],[-14.55,-40.348]],"v":[[-113.586,-84.077],[-147.276,-60.135],[-141.231,-39.204],[-119.347,-31.676],[-66.219,-26.529],[-64.45,-76.152]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[13.764,-16.52],[-0.797,-14.42],[-5.435,-4.397],[-9.222,1.812],[-4.425,5.611],[7.725,10.732]],"o":[[-22.868,-5.87],[0.379,6.863],[5.039,4.077],[14.57,16.763],[8.105,-10.278],[-5.48,-7.613]],"v":[[-80.463,-124.48],[-113.116,-101.094],[-104.345,-83.066],[-82.915,-78.763],[-39.977,-80.996],[-37.614,-117.588]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[9.081,-4.12],[1.941,-4.856],[-0.074,-2.217],[-1.433,-1.373],[-2.412,2.764],[3.877,5.235]],"o":[[-4.629,2.1],[-0.924,2.311],[0.069,2.056],[11.907,11.408],[4.417,-5.063],[-2.75,-3.714]],"v":[[-75.274,-166.527],[-85.585,-154.758],[-86.926,-147.859],[-85.155,-142.261],[-54.718,-143.901],[-53.648,-161.404]],"c":true}]},{"t":60,"s":[{"i":[[0.601,-0.289],[0.013,-0.847],[-0.221,-0.37],[-0.364,-0.269],[-0.704,0.652],[0.934,1.027]],"o":[[-1.307,0.628],[-0.006,0.403],[0.205,0.343],[0.87,0.643],[1.289,-1.194],[-0.662,-0.729]],"v":[[-79.613,-181.42],[-81.494,-179.083],[-81.161,-177.91],[-80.299,-176.982],[-73.89,-177.455],[-73.114,-181.387]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.980392216701,0.972549079446,0.949019667682,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"p":{"a":0,"k":[280.666,670.31,0]},"a":{"a":0,"k":[24.666,286.31,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[20,20,100]},{"t":20,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[1.988,3.181],[-19.8,-5.052]],"o":[[-4.889,-7.822],[26.133,6.668]],"v":[[19.329,271.399],[24.3,294.213]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[6.101,9.761],[-60.75,-15.5]],"o":[[-15,-24],[80.183,20.458]],"v":[[0,163],[15.25,233]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[21,11],[-44.25,-9.5]],"o":[[-35.458,-18.573],[50.771,10.9]],"v":[[35,125],[1.75,207.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[2.846,1.491],[-5.996,-1.287]],"o":[[-4.805,-2.517],[6.88,1.477]],"v":[[32.578,61.344],[28.072,72.523]],"c":true}]},{"t":31,"s":[{"i":[[2.846,1.491],[-5.996,-1.287]],"o":[[-4.805,-2.517],[6.88,1.477]],"v":[[20.578,-416.656],[16.072,-405.477]],"c":true}]}]},"nm":"Path 7","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[66.5,10.5],[-49.599,-0.443]],"o":[[-70.267,-11.095],[56,0.5]],"v":[[20.5,199.5],[12,315.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[43.734,-35.066],[-43.928,23.035]],"o":[[-55.5,44.5],[41,-21.5]],"v":[[15.5,72.5],[26,189.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[56,-2.5],[-53,44]],"o":[[-54.592,2.437],[70,20.5]],"v":[[-4.5,51.5],[-8,104.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[34.653,-12.008],[-45,46.5]],"o":[[-50.5,17.5],[55,0]],"v":[[9.5,-11.5],[24,35.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[25,-5],[-25.722,5.512]],"o":[[-33.855,6.771],[28,-6]],"v":[[23,-29],[30,-1]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[20.461,-75.516],[21.822,-70.072]],"c":true}]},{"t":51,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[10.461,-427.516],[11.822,-422.072]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[69.5,-97.5],[-113.5,45]],"o":[[-66.5,8.5],[86.092,-34.134]],"v":[[-90.5,124.5],[-22,227]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[22,-123.5],[-113.5,45]],"o":[[-41.5,7],[86.092,-34.134]],"v":[[-91.5,-56.5],[-6,53]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[29.5,-125],[-86.5,51.5]],"o":[[-65,6.5],[164.5,11.5]],"v":[[-87.5,-87],[-23.5,-7.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[71,-76.5],[-34.5,28]],"o":[[-90,13.5],[106.5,47.5]],"v":[[-68.5,-157.5],[-34.5,-62.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[48.5,-66],[-34.5,28]],"o":[[-81,3],[106.5,47.5]],"v":[[-57.5,-194],[-53,-132]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[29,-6],[-28.5,11]],"o":[[-29,6],[28.5,-11]],"v":[[-46,-251.5],[-37,-218.5]],"c":true}]},{"t":60,"s":[{"i":[[1.16,-0.24],[-1.14,0.44]],"o":[[-1.16,0.24],[1.14,-0.44]],"v":[[-45.92,-278.26],[-45.56,-276.94]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[70.414,4.783],[-95.574,7.104],[-26.5,9.5]],"o":[[-132.5,-9],[14.926,21.104],[42.478,-15.228]],"v":[[116,239],[56.574,305.896],[132.5,325]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[31,-18.5],[-168.751,36.203],[-17.918,44.718]],"o":[[-248,-61.5],[23.267,7.334],[40.5,-12]],"v":[[167,24],[116.251,135.297],[184.5,88]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[101,-20.5],[-133.016,57.816],[-23.678,31.225]],"o":[[-222,-55.5],[17.83,12.614],[53,-13]],"v":[[114.5,-57.5],[94.516,65.184],[159,45]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[108,-21],[-25.045,-34.694],[-37.826,13.017]],"o":[[-46.826,-19.424],[11.058,15.318],[78.5,26]],"v":[[96,-99.5],[40.628,-7.406],[112,2]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[78.5,-26.5],[-10.452,-22.332],[-15.008,0.153]],"o":[[-36.913,-12.304],[4.615,9.86],[85,40]],"v":[[101.5,-126.5],[59.744,-81.628],[89,-64.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[33,0],[-8.262,-9.837],[-7.657,0.306]],"o":[[-22.893,0],[3.648,4.343],[25,-1]],"v":[[103,-170],[84.726,-143.561],[102,-136.5]],"c":true}]},{"t":60,"s":[{"i":[[5.5,0],[-1.377,-1.639],[-1.276,0.051]],"o":[[-3.815,0],[0.608,0.724],[4.167,-0.167]],"v":[[103.5,-198.125],[100.454,-193.718],[103.333,-192.542]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":6,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-4.046,-28.733],[-7.961,-19.055],[-7.058,-4.972],[-5.37,0.263],[-10.383,14.21],[38.553,-4.686]],"o":[[-39.406,25.959],[3.789,9.068],[6.543,4.609],[20.166,40.644],[98.704,38.486],[-7.504,-22.39]],"v":[[-106.454,196.733],[-142.466,268.042],[-124.994,289.511],[-106.166,296.356],[-25.204,307.014],[-1.553,202.686]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[7.754,-10.103],[-2.864,-41.686],[-47.459,23.038],[-17.819,6.695],[-11.001,13.349],[99.213,26.201]],"o":[[-24.746,-34.853],[-41.454,26.415],[10.089,11.703],[9.571,21.591],[45.708,87.19],[-6.546,-22.901]],"v":[[-105.754,6.853],[-187.046,30.585],[-146.041,114.963],[-104.071,125.409],[-34.708,136.81],[-5.213,30.799]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[5.648,-2.651],[-3.488,-40.68],[-28.748,26.769],[-22.563,7.933],[-9.109,9.161],[8.807,22.921]],"o":[[-52.986,-50.627],[-31.582,21.921],[15.404,15.153],[24.874,27.539],[16.685,-16.781],[-6.247,-16.259]],"v":[[-65.014,-36.373],[-171.918,-8.421],[-143.252,52.731],[-84.874,67.961],[-17.551,62.845],[-3.459,-5.795]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[19.861,-24.879],[2.389,-25.903],[-7.904,-8.883],[-16.419,1.173],[-8.619,13.394],[50.602,-9.162]],"o":[[-31.37,-12.195],[-1.137,12.327],[7.327,8.235],[20.153,51.095],[59.455,-7.707],[-9.228,-15.356]],"v":[[-114.361,-87.621],[-168.705,-49.001],[-158.954,-15.239],[-123.653,-3.095],[-37.955,5.207],[-35.102,-74.838]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[20.907,-25.093],[-1.21,-21.904],[-8.256,-6.68],[-14.008,2.753],[-6.721,8.523],[11.735,16.301]],"o":[[-34.736,-8.916],[0.576,10.424],[7.654,6.192],[22.131,25.463],[12.311,-15.612],[-8.324,-11.564]],"v":[[-85.407,-125.907],[-135.006,-90.384],[-121.683,-62.999],[-89.131,-56.463],[-23.909,-59.855],[-20.32,-115.438]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[16.089,-7.299],[3.439,-8.602],[-0.131,-3.928],[-2.539,-2.432],[-4.273,4.897],[6.868,9.275]],"o":[[-8.201,3.721],[-1.637,4.094],[0.122,3.642],[21.095,20.211],[7.826,-8.97],[-4.872,-6.579]],"v":[[-77.089,-167.201],[-95.356,-146.351],[-97.732,-134.128],[-94.595,-124.211],[-40.671,-127.117],[-38.776,-158.124]],"c":true}]},{"t":60,"s":[{"i":[[1.463,-0.702],[0.031,-2.062],[-0.538,-0.9],[-0.887,-0.655],[-1.713,1.587],[2.273,2.5]],"o":[[-3.182,1.528],[-0.015,0.981],[0.498,0.834],[2.118,1.565],[3.138,-2.907],[-1.612,-1.773]],"v":[[-78.589,-181.314],[-83.167,-175.626],[-82.357,-172.772],[-80.258,-170.512],[-64.659,-171.663],[-62.771,-181.234]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.945098099054,0.90588241278,0.792156922583,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.917647118662,0.847058883368,0.721568627451,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":16},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":60,"st":0,"bm":0}]},{"id":"comp_25","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 12","sr":1,"ks":{"o":{"a":0,"k":0},"p":{"a":0,"k":[256,743,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[40,40,100]},{"t":8,"s":[100,100,100]}]}},"ao":0,"ip":0,"op":88,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"02 fire middle end","parent":1,"refId":"comp_11","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":32,"op":88,"st":32,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"02 fire middle cycle","parent":1,"refId":"comp_12","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"02 fire middle flame2","parent":1,"refId":"comp_13","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":768,"ip":24,"op":52,"st":24,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"02 fire middle flame2","parent":1,"refId":"comp_13","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":8,"op":36,"st":8,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"02 fire middle center (f0)","parent":1,"refId":"comp_14","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":0,"nm":"02 fire middle flame1","parent":1,"refId":"comp_15","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":768,"ip":16,"op":48,"st":16,"bm":0},{"ddd":0,"ind":9,"ty":0,"nm":"02 fire middle flame1","parent":1,"refId":"comp_15","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":32,"op":64,"st":32,"bm":0},{"ddd":0,"ind":10,"ty":0,"nm":"02 fire middle flame1","parent":1,"refId":"comp_15","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":0,"nm":"02 fire middle cycle outlines","parent":1,"refId":"comp_16","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":0,"nm":"02 fire middle end outl","parent":1,"refId":"comp_17","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":32,"op":88,"st":32,"bm":0}]},{"id":"comp_26","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 14","sr":1,"ks":{"o":{"a":0,"k":0},"p":{"a":0,"k":[256,488,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[40,40,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":8,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":30,"s":[60,60,100]},{"t":42,"s":[100,100,100]}]}},"ao":0,"ip":0,"op":92,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"03 fire small wide cycle end","parent":1,"refId":"comp_7","sr":1,"ks":{"p":{"a":0,"k":[0,-232,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":32,"op":60,"st":32,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"03 fire small wide cycle","parent":1,"refId":"comp_8","sr":1,"ks":{"p":{"a":0,"k":[0,-232,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"01 fire small left bottom flame (f24) 2","parent":1,"refId":"comp_3","sr":1,"ks":{"p":{"a":0,"k":[0,-232,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":32,"op":64,"st":32,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"01 fire small left flame twirl","parent":1,"refId":"comp_4","sr":1,"ks":{"p":{"a":0,"k":[0,-232,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":512,"ip":32,"op":64,"st":32,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"p":{"a":0,"k":[261.799,285,0]},"a":{"a":0,"k":[3.799,5,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-139.238,137.226],[-214.402,227]],"o":[[110.598,-109],[69.514,-73.599]],"v":[[7,157],[22,-231]],"c":false}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[179.736,76.896],[409.598,33]],"o":[[-231.402,-99],[-100.91,-8.13]],"v":[[-11,213],[-186,-187]],"c":false}},"nm":"Path 2","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":43,"s":[0]},{"t":93,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.905882418156,0.298039227724,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":43,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":53,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":83,"s":[8]},{"t":93,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.992156922817,0.321568638086,0.078431375325,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":43,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":53,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":83,"s":[24]},{"t":93,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":33,"op":93,"st":33,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"p":{"a":0,"k":[261.799,285,0]},"a":{"a":0,"k":[3.799,5,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[179.736,76.896],[302,241]],"o":[[-231.402,-99],[-79.13,-63.147]],"v":[[-11,213],[-186,-187]],"c":false}},"nm":"Path 2","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":33,"s":[0]},{"t":83,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.905882418156,0.298039227724,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":33,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":43,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":73,"s":[8]},{"t":83,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.992156922817,0.321568638086,0.078431375325,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":33,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":43,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":73,"s":[24]},{"t":83,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":33,"op":83,"st":33,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"p":{"a":0,"k":[261.799,285,0]},"a":{"a":0,"k":[3.799,5,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-189.52,47.962],[-214.402,227]],"o":[[407,-103],[69.514,-73.599]],"v":[[7,157],[22,-231]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":38,"s":[0]},{"t":78,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.905882418156,0.298039227724,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":38,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":48,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":68,"s":[8]},{"t":78,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.992156922817,0.321568638086,0.078431375325,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":38,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":48,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":68,"s":[24]},{"t":78,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":38,"op":78,"st":38,"bm":0}]},{"id":"comp_27","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 4","parent":2,"sr":1,"ks":{"p":{"a":0,"k":[-77.334,-175.69,0]},"a":{"a":0,"k":[-77.334,-175.69,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[33.144,5.233],[-24.721,-0.221]],"o":[[-35.022,-5.53],[27.911,0.249]],"v":[[19.555,225.935],[15.319,283.751]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[29.02,-23.268],[-29.148,15.285]],"o":[[-36.827,29.528],[27.206,-14.266]],"v":[[18.827,100.658],[25.794,178.294]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[36.864,-1.646],[-34.889,28.965]],"o":[[-35.938,1.604],[46.08,13.495]],"v":[[1.709,52.646],[-0.595,87.535]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[19.216,-6.659],[-24.954,25.786]],"o":[[-28.004,9.704],[30.5,0]],"v":[[16.732,-8.349],[24.773,17.714]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[12.002,-2.4],[-12.348,2.646]],"o":[[-16.253,3.251],[13.442,-2.88]],"v":[[25.798,-28.588],[29.158,-15.146]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[20.461,-75.516],[21.822,-70.072]],"c":true}]},{"t":31,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[10.461,-427.516],[11.822,-422.072]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[47.81,-67.072],[-25.632,9.313]],"o":[[-93.246,48.698],[25.632,-9.313]],"v":[[-74.754,121.302],[-13.132,182.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[16.302,-91.514],[22.297,-55.155]],"o":[[-51.846,41.486],[63.795,-25.293]],"v":[[-77.654,-43.486],[-14.297,37.655]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[65.508,-110.844],[-62.172,37.016]],"o":[[-46.719,4.672],[111.508,-8.984]],"v":[[-77.508,-81.156],[-31.508,-13.016]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[44.392,-34.904],[-43.656,10.635]],"o":[[-57.534,8.63],[43.656,-10.635]],"v":[[-64.892,-151.596],[-43.156,-90.865]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[45.056,-54.944],[-36.611,29.426]],"o":[[-75.944,-2.944],[74.944,33.426]],"v":[[-55.556,-184.556],[-45.389,-150.426]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[14.5,-3],[-14.25,5.5]],"o":[[-14.5,3],[14.25,-5.5]],"v":[[-44.875,-250.5],[-40.375,-234]],"c":true}]},{"t":36,"s":[{"i":[[1.16,-0.24],[-1.14,0.44]],"o":[[-1.16,0.24],[1.14,-0.44]],"v":[[-45.92,-278.26],[-45.56,-276.94]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[38.071,2.586],[-51.674,3.841],[-14.328,5.136]],"o":[[-71.639,-4.866],[8.07,11.41],[22.966,-8.233]],"v":[[122.074,242.366],[89.943,278.535],[130.995,288.864]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[20.869,-12.454],[-146.79,82.022],[-12.062,30.104]],"o":[[-112.454,-96.052],[15.663,4.937],[27.265,-8.079]],"v":[[157.954,39.052],[123.79,100.978],[169.735,69.137]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[74.55,-67.464],[-92.612,89.3],[0.78,32.797]],"o":[[-175.95,-43.988],[-28.612,-55.2],[57.28,16.797]],"v":[[109.45,-30.036],[99.612,30.2],[150.72,14.703]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[79.321,-50.136],[-20.934,-21.241],[-26.364,9.073]],"o":[[-58.179,-42.136],[17.914,18.177],[76.169,29.621]],"v":[[102.679,-76.364],[46.086,-27.677],[111.331,-23.621]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[62.617,-21.138],[-8.337,-17.814],[-11.971,0.122]],"o":[[-29.444,-9.815],[3.681,7.865],[67.802,31.907]],"v":[[103.66,-125.362],[70.352,-89.569],[93.689,-75.907]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[19.334,0],[-4.841,-5.763],[-4.486,0.179]],"o":[[-13.412,0],[2.137,2.544],[14.647,-0.586]],"v":[[106.839,-168.5],[96.133,-153.01],[106.253,-148.873]],"c":true}]},{"t":36,"s":[{"i":[[5.5,0],[-1.377,-1.639],[-1.276,0.051]],"o":[[-3.815,0],[0.608,0.724],[4.167,-0.167]],"v":[[103.5,-198.125],[100.454,-193.718],[103.333,-192.542]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":5,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[29.237,-0.424],[18.659,-25.84],[-78.306,-8.932],[-2.4,-26.987],[0.564,-42.682],[30.491,12.722]],"o":[[-3.4,-14.426],[-41.473,-22.64],[-12.806,-41.432],[1.048,11.781],[27.835,19.62],[5.813,-25.223]],"v":[[106.649,132.4],[49.973,125.64],[49.306,238.932],[79.9,226.987],[116.436,225.182],[131.993,185.252]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[18.356,-65.631],[1.004,-68.279],[-92.58,7.072],[-16.188,1.28],[-17.016,-0.736],[29.057,17.998]],"o":[[12.356,-49.631],[-116.996,-73.279],[18.803,-1.436],[15.763,-1.246],[26.568,1.15],[25.666,-30.324]],"v":[[114.644,-62.369],[48.996,-92.721],[50.08,17.428],[99.158,-10.713],[145.932,9.85],[160.334,-37.676]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[40.408,-8.621],[8.905,-22.866],[-63.43,19.052],[-2.553,-27.854],[-0.664,-27.112],[37.995,7.577]],"o":[[-15.642,-10.077],[-92.499,-21.673],[-3,-37.948],[6.447,-39.854],[25.263,14.209],[-3.038,-25.183]],"v":[[102.472,-151.481],[53.507,-145.134],[65,-51.052],[106.553,-41.146],[153.664,-66.888],[163.996,-115.133]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[25.979,-38.681],[-12.816,-22.414],[-71.606,9.11],[-7.652,-23.023],[-8.541,18],[3.417,17.237]],"o":[[6.479,-39.181],[-74.816,-41.414],[-4.106,-29.39],[7.848,-31.023],[7.401,-15.598],[-5.628,-28.395]],"v":[[80.521,-174.319],[39.316,-172.086],[38.606,-82.61],[103.652,-89.977],[145.259,-114.137],[152.031,-167.148]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[35.852,-24.204],[6.121,-6.787],[-30.147,-5.617],[-12.364,11.19],[-4.863,14.486],[5.544,11.648]],"o":[[-15.19,-0.044],[-18.957,21.021],[11.029,2.055],[17.765,0.658],[4.214,-12.553],[-9.133,-19.188]],"v":[[66.426,-225.746],[34.942,-214.601],[66.05,-144.055],[101.841,-156.26],[135.809,-181.382],[133.83,-220.533]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[11.188,-2.48],[2.145,-2.133],[-8.537,-4.421],[-3.25,0.288],[-2.088,4.234],[1.472,3.752]],"o":[[-5.164,1.145],[-6.643,6.608],[3.123,1.618],[6.515,-0.577],[1.81,-3.669],[-2.424,-6.18]],"v":[[99.368,-273.52],[88.579,-268.455],[96.629,-247.5],[106.444,-245.289],[119.333,-253.457],[119.83,-265.206]],"c":true}]},{"t":36,"s":[{"i":[[2.469,0.044],[0.542,-0.415],[-1.676,-1.418],[-0.709,0.023],[-0.595,0.897],[0.16,0.894]],"o":[[-1.158,-0.021],[-1.68,1.285],[0.613,0.519],[1.347,-0.043],[0.516,-0.777],[-0.264,-1.473]],"v":[[96.518,-299.069],[93.999,-298.426],[94.96,-292.841],[96.99,-292.023],[99.944,-293.62],[100.503,-296.249]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"fl","c":{"a":0,"k":[0.980392216701,0.972549079446,0.949019667682,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":36,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"p":{"a":0,"k":[280.666,670.31,0]},"a":{"a":0,"k":[24.666,286.31,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[20,20,100]},{"t":12,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[66.5,10.5],[-49.599,-0.443]],"o":[[-70.267,-11.095],[56,0.5]],"v":[[20.5,199.5],[12,315.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[43.734,-35.066],[-43.928,23.035]],"o":[[-55.5,44.5],[41,-21.5]],"v":[[15.5,72.5],[26,189.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[56,-2.5],[-53,44]],"o":[[-54.592,2.437],[70,20.5]],"v":[[-4.5,51.5],[-8,104.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[34.653,-12.008],[-45,46.5]],"o":[[-50.5,17.5],[55,0]],"v":[[9.5,-11.5],[24,35.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[25,-5],[-25.722,5.512]],"o":[[-33.855,6.771],[28,-6]],"v":[[23,-29],[30,-1]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[20.461,-75.516],[21.822,-70.072]],"c":true}]},{"t":31,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[10.461,-427.516],[11.822,-422.072]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[69.5,-97.5],[-113.5,45]],"o":[[-66.5,8.5],[86.092,-34.134]],"v":[[-90.5,124.5],[-22,227]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[22,-123.5],[-113.5,45]],"o":[[-41.5,7],[86.092,-34.134]],"v":[[-91.5,-56.5],[-6,53]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[29.5,-125],[-86.5,51.5]],"o":[[-65,6.5],[164.5,11.5]],"v":[[-87.5,-87],[-23.5,-7.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[71,-76.5],[-34.5,28]],"o":[[-90,13.5],[106.5,47.5]],"v":[[-68.5,-157.5],[-34.5,-62.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[48.5,-66],[-34.5,28]],"o":[[-81,3],[106.5,47.5]],"v":[[-57.5,-194],[-53,-132]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[29,-6],[-28.5,11]],"o":[[-29,6],[28.5,-11]],"v":[[-46,-251.5],[-37,-218.5]],"c":true}]},{"t":36,"s":[{"i":[[1.16,-0.24],[-1.14,0.44]],"o":[[-1.16,0.24],[1.14,-0.44]],"v":[[-45.92,-278.26],[-45.56,-276.94]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[70.414,4.783],[-95.574,7.104],[-26.5,9.5]],"o":[[-132.5,-9],[14.926,21.104],[42.478,-15.228]],"v":[[116,239],[56.574,305.896],[132.5,325]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[31,-18.5],[-168.751,36.203],[-17.918,44.718]],"o":[[-248,-61.5],[23.267,7.334],[40.5,-12]],"v":[[167,24],[116.251,135.297],[184.5,88]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[101,-20.5],[-133.016,57.816],[-23.678,31.225]],"o":[[-222,-55.5],[17.83,12.614],[53,-13]],"v":[[114.5,-57.5],[94.516,65.184],[159,45]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[108,-21],[-25.045,-34.694],[-37.826,13.017]],"o":[[-46.826,-19.424],[11.058,15.318],[78.5,26]],"v":[[96,-99.5],[40.628,-7.406],[112,2]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[78.5,-26.5],[-10.452,-22.332],[-15.008,0.153]],"o":[[-36.913,-12.304],[4.615,9.86],[85,40]],"v":[[101.5,-126.5],[59.744,-81.628],[89,-64.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[33,0],[-8.262,-9.837],[-7.657,0.306]],"o":[[-22.893,0],[3.648,4.343],[25,-1]],"v":[[103,-170],[84.726,-143.561],[102,-136.5]],"c":true}]},{"t":36,"s":[{"i":[[5.5,0],[-1.377,-1.639],[-1.276,0.051]],"o":[[-3.815,0],[0.608,0.724],[4.167,-0.167]],"v":[[103.5,-198.125],[100.454,-193.718],[103.333,-192.542]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":5,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[40.224,-0.584],[25.671,-35.551],[-117.344,4.076],[-13.845,-2.09],[-15.219,20.222],[41.95,17.503]],"o":[[-4.678,-19.848],[-63.829,-76.551],[7.749,18.334],[16.09,2.429],[38.296,26.994],[7.997,-34.703]],"v":[[108.178,121.348],[34.329,117.551],[25.844,278.924],[70,279],[118.204,260.006],[148.55,186.497]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[42.564,-6.226],[40.813,-50.225],[-85.297,31.262],[-32.063,3.791],[-15.43,23.796],[36.231,22.442]],"o":[[-6.076,-18.138],[-120.187,-27.225],[8.47,17.96],[23.983,5.161],[35.183,9.711],[3.616,-40.885]],"v":[[125.779,-104.633],[37.687,-123.775],[35.297,47.238],[94,72],[154.817,37.789],[175.269,-33.942]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[50.807,-10.84],[11.197,-28.751],[-79.754,23.956],[-21.881,6.283],[-12.991,22.817],[47.774,9.527]],"o":[[-19.668,-12.67],[-116.303,-27.251],[12.896,12.677],[24.971,6.064],[31.765,17.866],[-3.819,-31.664]],"v":[[94.869,-161.73],[33.303,-153.749],[47.754,-35.456],[100,-23],[159.235,-55.366],[172.226,-116.027]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[61.33,-27.94],[13.035,-11.794],[-88,34.498],[-24.613,14.871],[-11.285,23.783],[4.514,22.775]],"o":[[-22.734,-8.189],[-81.69,0.79],[16.437,2.441],[24.287,7.59],[9.779,-20.609],[-7.436,-37.518]],"v":[[69.474,-198.525],[15.69,-190.29],[42.5,-42.998],[104,-60],[158.974,-91.922],[167.923,-161.964]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[49.628,-33.504],[8.473,-9.395],[-41.73,-7.776],[-17.115,15.49],[-6.732,20.052],[7.675,16.124]],"o":[[-21.027,-0.06],[-26.241,29.098],[15.267,2.845],[24.591,0.911],[5.833,-17.376],[-12.642,-26.561]],"v":[[48.978,-222.185],[5.396,-206.758],[48.457,-109.106],[98,-126],[145.019,-160.775],[142.281,-214.968]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[20.595,-4.565],[3.948,-3.927],[-15.714,-8.139],[-5.982,0.53],[-3.844,7.794],[2.709,6.906]],"o":[[-9.506,2.107],[-12.229,12.163],[5.749,2.978],[11.992,-1.063],[3.331,-6.754],[-4.463,-11.376]],"v":[[87.975,-274.467],[68.116,-265.142],[82.934,-226.569],[101,-222.5],[124.726,-237.535],[125.642,-259.162]],"c":true}]},{"t":36,"s":[{"i":[[4.312,0.077],[0.947,-0.725],[-2.927,-2.476],[-1.238,0.04],[-1.039,1.566],[0.28,1.562]],"o":[[-2.023,-0.036],[-2.933,2.244],[1.071,0.906],[2.353,-0.076],[0.9,-1.357],[-0.461,-2.572]],"v":[[95.675,-299.303],[91.277,-298.18],[92.954,-288.429],[96.5,-287],[101.657,-289.789],[102.633,-294.38]],"c":true}]}]},"nm":"Path 2","hd":false},{"ty":"fl","c":{"a":0,"k":[0.945098099054,0.90588241278,0.792156922583,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.917647118662,0.847058883368,0.721568627451,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":16},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":36,"st":0,"bm":0}]},{"id":"comp_28","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 13","sr":1,"ks":{"o":{"a":0,"k":0},"p":{"a":0,"k":[256,494,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[40,40,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":8,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":67,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":94,"s":[60,60,100]},{"t":106,"s":[100,100,100]}]}},"ao":0,"ip":0,"op":156,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"01 fire small cycle","parent":1,"refId":"comp_19","sr":1,"ks":{"p":{"a":0,"k":[0,-238,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":64,"op":96,"st":64,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"01 fire small cycle","parent":1,"refId":"comp_19","sr":1,"ks":{"p":{"a":0,"k":[0,-238,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":32,"op":64,"st":32,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"01 fire small cycle","parent":1,"refId":"comp_19","sr":1,"ks":{"p":{"a":0,"k":[0,-238,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"01 fire small end","parent":1,"refId":"comp_20","sr":1,"ks":{"p":{"a":0,"k":[0,18,0]},"a":{"a":0,"k":[256,512,0]}},"ao":0,"w":512,"h":512,"ip":96,"op":156,"st":96,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"01 fire small left bottom flame (f24)","parent":1,"refId":"comp_22","sr":1,"ks":{"p":{"a":0,"k":[0,-238,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":512,"ip":72,"op":104,"st":72,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"01 fire small left bottom flame (f24) 2","parent":1,"refId":"comp_3","sr":1,"ks":{"p":{"a":0,"k":[0,-238,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":512,"ip":40,"op":72,"st":40,"bm":0},{"ddd":0,"ind":8,"ty":0,"nm":"01 fire small left bottom flame (f24)","parent":1,"refId":"comp_22","sr":1,"ks":{"p":{"a":0,"k":[0,-238,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":512,"ip":8,"op":40,"st":8,"bm":0},{"ddd":0,"ind":9,"ty":0,"nm":"01 fire small left bottom flame (f24) 2","parent":1,"refId":"comp_3","sr":1,"ks":{"p":{"a":0,"k":[0,-238,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":88,"op":120,"st":88,"bm":0},{"ddd":0,"ind":10,"ty":0,"nm":"01 fire small left bottom flame (f24)","parent":1,"refId":"comp_22","sr":1,"ks":{"p":{"a":0,"k":[0,-238,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":56,"op":88,"st":56,"bm":0},{"ddd":0,"ind":11,"ty":0,"nm":"01 fire small left bottom flame (f24) 2","parent":1,"refId":"comp_3","sr":1,"ks":{"p":{"a":0,"k":[0,-238,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":24,"op":56,"st":24,"bm":0},{"ddd":0,"ind":12,"ty":0,"nm":"01 fire small left flame (f0)","parent":1,"refId":"comp_21","sr":1,"ks":{"p":{"a":0,"k":[0,-238,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":512,"ip":80,"op":112,"st":80,"bm":0},{"ddd":0,"ind":13,"ty":0,"nm":"01 fire small left flame (f0)","parent":1,"refId":"comp_21","sr":1,"ks":{"p":{"a":0,"k":[0,-238,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":512,"ip":48,"op":80,"st":48,"bm":0},{"ddd":0,"ind":14,"ty":0,"nm":"01 fire small left flame (f0)","parent":1,"refId":"comp_21","sr":1,"ks":{"p":{"a":0,"k":[0,-238,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":512,"ip":16,"op":48,"st":16,"bm":0},{"ddd":0,"ind":15,"ty":0,"nm":"01 fire small left flame (f0)","parent":1,"refId":"comp_21","sr":1,"ks":{"p":{"a":0,"k":[0,-238,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":64,"op":96,"st":64,"bm":0},{"ddd":0,"ind":16,"ty":0,"nm":"01 fire small left flame (f0)","parent":1,"refId":"comp_21","sr":1,"ks":{"p":{"a":0,"k":[0,-238,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":32,"op":64,"st":32,"bm":0},{"ddd":0,"ind":17,"ty":0,"nm":"01 fire small left flame (f0)","parent":1,"refId":"comp_21","sr":1,"ks":{"p":{"a":0,"k":[0,-238,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":18,"ty":0,"nm":"01 fire small cycle outlines","parent":1,"refId":"comp_23","sr":1,"ks":{"p":{"a":0,"k":[0,-238,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":64,"op":96,"st":64,"bm":0},{"ddd":0,"ind":19,"ty":0,"nm":"01 fire small cycle outlines","parent":1,"refId":"comp_23","sr":1,"ks":{"p":{"a":0,"k":[0,-238,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":32,"op":64,"st":32,"bm":0},{"ddd":0,"ind":20,"ty":0,"nm":"01 fire small cycle outlines","parent":1,"refId":"comp_23","sr":1,"ks":{"p":{"a":0,"k":[0,-238,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":21,"ty":0,"nm":"01 fire small left flame twirl","parent":1,"refId":"comp_4","sr":1,"ks":{"p":{"a":0,"k":[0,-238,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":512,"ip":88,"op":120,"st":88,"bm":0},{"ddd":0,"ind":22,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"p":{"a":0,"k":[259.799,261,0]},"a":{"a":0,"k":[3.799,5,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-54.44,187.762],[218,5]],"o":[[49,-169],[-101.211,-2.321]],"v":[[7,157],[-220,-181]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[0]},{"t":55,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.98431378603,0.4392157197,0.058823533356,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[8]},{"t":55,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.729411764706,0.007843137255,0.007843137255,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[24]},{"t":55,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":15,"op":55,"st":15,"bm":0},{"ddd":0,"ind":23,"ty":4,"nm":"Shape Layer 6","sr":1,"ks":{"p":{"a":0,"k":[259.799,261,0]},"a":{"a":0,"k":[3.799,5,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-88.418,174.357],[70,-25]],"o":[[107,-211],[-95.339,34.05]],"v":[[-77,179],[-204,-167]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":61,"s":[0]},{"t":101,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.98431378603,0.4392157197,0.058823533356,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":61,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":71,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":91,"s":[8]},{"t":101,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.729411764706,0.007843137255,0.007843137255,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":61,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":71,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":91,"s":[24]},{"t":101,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":61,"op":101,"st":61,"bm":0},{"ddd":0,"ind":24,"ty":4,"nm":"Shape Layer 5","sr":1,"ks":{"p":{"a":0,"k":[259.799,261,0]},"a":{"a":0,"k":[3.799,5,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-16.887,194.764],[12,-97]],"o":[[15,-173],[-12.429,100.471]],"v":[[-77,179],[-238,85]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":34,"s":[0]},{"t":74,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.98431378603,0.4392157197,0.058823533356,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":34,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":44,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":64,"s":[8]},{"t":74,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.729411764706,0.007843137255,0.007843137255,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":34,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":44,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":64,"s":[24]},{"t":74,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":34,"op":74,"st":34,"bm":0},{"ddd":0,"ind":25,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"p":{"a":0,"k":[259.799,261,0]},"a":{"a":0,"k":[3.799,5,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[52.962,188.184],[-174,-29]],"o":[[-47,-167],[99.86,16.643]],"v":[[7,157],[224,-213]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":0,"k":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":6,"s":[0]},{"t":46,"s":[350]}]},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.98431378603,0.4392157197,0.058823533356,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":6,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":16,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":36,"s":[8]},{"t":46,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.729411764706,0.007843137255,0.007843137255,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":6,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":16,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":36,"s":[24]},{"t":46,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 2","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":6,"op":46,"st":6,"bm":0}]},{"id":"comp_29","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 12","sr":1,"ks":{"o":{"a":0,"k":0},"p":{"a":0,"k":[256,743,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[40,40,100]},{"t":8,"s":[100,100,100]}]}},"ao":0,"ip":0,"op":124,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"02 fire middle end","parent":1,"refId":"comp_11","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":96,"op":152,"st":96,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"02 fire middle cycle","parent":1,"refId":"comp_12","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":64,"op":96,"st":64,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"02 fire middle cycle","parent":1,"refId":"comp_12","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":32,"op":64,"st":32,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"02 fire middle cycle","parent":1,"refId":"comp_12","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"02 fire middle flame2","parent":1,"refId":"comp_13","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":72,"op":100,"st":72,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"02 fire middle center (f0)","parent":1,"refId":"comp_14","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":768,"ip":80,"op":100,"st":80,"bm":0},{"ddd":0,"ind":8,"ty":0,"nm":"02 fire middle center (f0)","parent":1,"refId":"comp_14","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":32,"op":52,"st":32,"bm":0},{"ddd":0,"ind":9,"ty":0,"nm":"02 fire middle flame2","parent":1,"refId":"comp_13","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":8,"op":36,"st":8,"bm":0},{"ddd":0,"ind":10,"ty":0,"nm":"02 fire middle flame1","parent":1,"refId":"comp_15","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":64,"op":96,"st":64,"bm":0},{"ddd":0,"ind":11,"ty":0,"nm":"02 fire middle flame1","parent":1,"refId":"comp_15","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":768,"ip":80,"op":112,"st":80,"bm":0},{"ddd":0,"ind":12,"ty":0,"nm":"02 fire middle flame1","parent":1,"refId":"comp_15","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]},"s":{"a":0,"k":[-100,100,100]}},"ao":0,"w":512,"h":768,"ip":48,"op":80,"st":48,"bm":0},{"ddd":0,"ind":13,"ty":0,"nm":"02 fire middle flame1","parent":1,"refId":"comp_15","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":0,"nm":"02 fire middle cycle outlines","parent":1,"refId":"comp_16","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":64,"op":96,"st":64,"bm":0},{"ddd":0,"ind":15,"ty":0,"nm":"02 fire middle cycle outlines","parent":1,"refId":"comp_16","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":32,"op":64,"st":32,"bm":0},{"ddd":0,"ind":16,"ty":0,"nm":"02 fire middle cycle outlines","parent":1,"refId":"comp_16","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":17,"ty":0,"nm":"02 fire middle end outl","parent":1,"refId":"comp_17","sr":1,"ks":{"p":{"a":0,"k":[0,-359,0]},"a":{"a":0,"k":[256,384,0]}},"ao":0,"w":512,"h":768,"ip":96,"op":152,"st":96,"bm":0}]},{"id":"comp_30","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 4","parent":2,"sr":1,"ks":{"p":{"a":0,"k":[-77.334,-175.69,0]},"a":{"a":0,"k":[-77.334,-175.69,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[1.795,-2.463],[-22.463,0.136]],"o":[[-6.751,9.267],[21.909,-0.132]],"v":[[28.78,287.216],[26.463,308]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[6.778,-9.304],[-84.843,0.513]],"o":[[-25.5,35],[82.75,-0.5]],"v":[[35,213],[26.25,291.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[41.584,-3.199],[-34.387,-1.279]],"o":[[-37.621,2.894],[34.387,1.279]],"v":[[41.616,212.199],[44.975,259.221]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[9.857,-0.643],[-6.643,-0.429]],"o":[[-9.65,0.629],[6.643,0.429]],"v":[[38.138,192.643],[39.103,203.571]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[3.286,-0.214],[-2.214,-0.143]],"o":[[-3.217,0.21],[2.214,0.143]],"v":[[21.707,90.214],[22.029,93.857]],"c":true}]},{"t":41,"s":[{"i":[[3.286,-0.214],[-2.214,-0.143]],"o":[[-3.217,0.21],[2.214,0.143]],"v":[[19.707,-427.786],[20.029,-424.143]],"c":true}]}]},"nm":"Path 6","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[33.144,5.233],[-24.721,-0.221]],"o":[[-35.022,-5.53],[27.911,0.249]],"v":[[19.555,225.935],[15.319,283.751]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[29.02,-23.268],[-29.148,15.285]],"o":[[-36.827,29.528],[27.206,-14.266]],"v":[[18.827,100.658],[25.794,178.294]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[36.864,-1.646],[-34.889,28.965]],"o":[[-35.938,1.604],[46.08,13.495]],"v":[[1.709,52.646],[-0.595,87.535]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[19.216,-6.659],[-24.954,25.786]],"o":[[-28.004,9.704],[30.5,0]],"v":[[16.732,-8.349],[24.773,17.714]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[12.002,-2.4],[-12.348,2.646]],"o":[[-16.253,3.251],[13.442,-2.88]],"v":[[25.798,-28.588],[29.158,-15.146]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[20.461,-75.516],[21.822,-70.072]],"c":true}]},{"t":51,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[10.461,-427.516],[11.822,-422.072]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":5,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[29.237,-0.424],[18.659,-25.84],[-78.306,-8.932],[-2.4,-26.987],[0.564,-42.682],[30.491,12.722]],"o":[[-3.4,-14.426],[-41.473,-22.64],[-12.806,-41.432],[1.048,11.781],[27.835,19.62],[5.813,-25.223]],"v":[[106.649,132.4],[49.973,125.64],[49.306,238.932],[79.9,226.987],[116.436,225.182],[131.993,185.252]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[18.356,-65.631],[1.004,-68.279],[-92.58,7.072],[-16.188,1.28],[-17.016,-0.736],[29.057,17.998]],"o":[[12.356,-49.631],[-116.996,-73.279],[18.803,-1.436],[15.763,-1.246],[26.568,1.15],[25.666,-30.324]],"v":[[114.644,-62.369],[48.996,-92.721],[50.08,17.428],[99.158,-10.713],[145.932,9.85],[160.334,-37.676]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[40.408,-8.621],[8.905,-22.866],[-63.43,19.052],[-2.553,-27.854],[-0.664,-27.112],[37.995,7.577]],"o":[[-15.642,-10.077],[-92.499,-21.673],[-3,-37.948],[6.447,-39.854],[25.263,14.209],[-3.038,-25.183]],"v":[[102.472,-151.481],[53.507,-145.134],[65,-51.052],[106.553,-41.146],[153.664,-66.888],[163.996,-115.133]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[25.979,-38.681],[-12.816,-22.414],[-71.606,9.11],[-7.652,-23.023],[-8.541,18],[3.417,17.237]],"o":[[6.479,-39.181],[-74.816,-41.414],[-4.106,-29.39],[7.848,-31.023],[7.401,-15.598],[-5.628,-28.395]],"v":[[80.521,-174.319],[39.316,-172.086],[38.606,-82.61],[103.652,-89.977],[145.259,-114.137],[152.031,-167.148]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[35.852,-24.204],[6.121,-6.787],[-30.147,-5.617],[-12.364,11.19],[-4.863,14.486],[5.544,11.648]],"o":[[-15.19,-0.044],[-18.957,21.021],[11.029,2.055],[17.765,0.658],[4.214,-12.553],[-9.133,-19.188]],"v":[[66.426,-225.746],[34.942,-214.601],[66.05,-144.055],[101.841,-156.26],[135.809,-181.382],[133.83,-220.533]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[11.188,-2.48],[2.145,-2.133],[-8.537,-4.421],[-3.25,0.288],[-2.088,4.234],[1.472,3.752]],"o":[[-5.164,1.145],[-6.643,6.608],[3.123,1.618],[6.515,-0.577],[1.81,-3.669],[-2.424,-6.18]],"v":[[99.368,-273.52],[88.579,-268.455],[96.629,-247.5],[106.444,-245.289],[119.333,-253.457],[119.83,-265.206]],"c":true}]},{"t":60,"s":[{"i":[[2.469,0.044],[0.542,-0.415],[-1.676,-1.418],[-0.709,0.023],[-0.595,0.897],[0.16,0.894]],"o":[[-1.158,-0.021],[-1.68,1.285],[0.613,0.519],[1.347,-0.043],[0.516,-0.777],[-0.264,-1.473]],"v":[[96.518,-299.069],[93.999,-298.426],[94.96,-292.841],[96.99,-292.023],[99.944,-293.62],[100.503,-296.249]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":6,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-3.499,-24.848],[-6.884,-16.478],[-0.301,7.46],[-4.644,0.228],[-25.003,12.782],[33.341,-4.053]],"o":[[-34.078,22.449],[3.276,7.842],[1.294,-32.082],[19.012,20.499],[85.358,33.282],[-6.489,-19.362]],"v":[[-102.261,200.848],[-133.404,262.516],[-118.294,281.082],[-102.012,287.001],[-31.997,296.218],[-11.544,205.996]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[-0.747,-48.874],[-2.455,-35.731],[-40.679,19.746],[-10.189,-28.494],[-11.643,4.734],[85.039,22.458]],"o":[[-21.211,-29.874],[-35.532,22.641],[-1.215,-25.539],[22.811,19.506],[80.357,20.734],[29.075,-42.399]],"v":[[-100.253,19.874],[-169.932,40.216],[-144.785,98.539],[-93.811,115.494],[-40.357,117.266],[-14.075,40.399]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[4.596,-2.157],[-2.838,-33.101],[-23.392,21.782],[64.52,-34.091],[-7.412,7.454],[7.166,18.65]],"o":[[-43.115,-41.195],[-25.698,17.837],[-20.978,-41.699],[20.24,22.409],[13.576,-13.654],[-5.083,-13.23]],"v":[[-78.36,-37.305],[-165.347,-14.561],[-142.022,35.199],[-94.52,47.591],[-39.74,43.428],[-28.273,-12.423]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[19.586,24.077],[1.481,-16.058],[-4.9,-5.507],[-10.179,0.727],[8.719,-39.971],[89.45,-41.348]],"o":[[-19.447,-7.56],[-0.705,7.642],[4.542,5.105],[-3.153,-23.324],[83.219,35.029],[-14.55,-40.348]],"v":[[-113.586,-84.077],[-147.276,-60.135],[-141.231,-39.204],[-119.347,-31.676],[-66.219,-26.529],[-64.45,-76.152]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[13.764,-16.52],[-0.797,-14.42],[-5.435,-4.397],[-9.222,1.812],[-4.425,5.611],[7.725,10.732]],"o":[[-22.868,-5.87],[0.379,6.863],[5.039,4.077],[14.57,16.763],[8.105,-10.278],[-5.48,-7.613]],"v":[[-80.463,-124.48],[-113.116,-101.094],[-104.345,-83.066],[-82.915,-78.763],[-39.977,-80.996],[-37.614,-117.588]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[9.081,-4.12],[1.941,-4.856],[-0.074,-2.217],[-1.433,-1.373],[-2.412,2.764],[3.877,5.235]],"o":[[-4.629,2.1],[-0.924,2.311],[0.069,2.056],[11.907,11.408],[4.417,-5.063],[-2.75,-3.714]],"v":[[-75.274,-166.527],[-85.585,-154.758],[-86.926,-147.859],[-85.155,-142.261],[-54.718,-143.901],[-53.648,-161.404]],"c":true}]},{"t":60,"s":[{"i":[[0.601,-0.289],[0.013,-0.847],[-0.221,-0.37],[-0.364,-0.269],[-0.704,0.652],[0.934,1.027]],"o":[[-1.307,0.628],[-0.006,0.403],[0.205,0.343],[0.87,0.643],[1.289,-1.194],[-0.662,-0.729]],"v":[[-79.613,-181.42],[-81.494,-179.083],[-81.161,-177.91],[-80.299,-176.982],[-73.89,-177.455],[-73.114,-181.387]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.980392216701,0.972549079446,0.949019667682,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"p":{"a":0,"k":[280.666,670.31,0]},"a":{"a":0,"k":[24.666,286.31,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[20,20,100]},{"t":20,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[1.795,-2.463],[-22.463,0.136]],"o":[[-6.751,9.267],[21.909,-0.132]],"v":[[28.78,287.216],[26.463,308]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[6.778,-9.304],[-84.843,0.513]],"o":[[-25.5,35],[82.75,-0.5]],"v":[[35,213],[26.25,291.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[65,-5],[-53.75,-2]],"o":[[-58.805,4.523],[53.75,2]],"v":[[34,209.5],[39.25,283]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[23,-1.5],[-15.5,-1]],"o":[[-22.516,1.468],[15.5,1]],"v":[[35.5,190.5],[37.75,216]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[3.286,-0.214],[-2.214,-0.143]],"o":[[-3.217,0.21],[2.214,0.143]],"v":[[21.707,90.214],[22.029,93.857]],"c":true}]},{"t":41,"s":[{"i":[[3.286,-0.214],[-2.214,-0.143]],"o":[[-3.217,0.21],[2.214,0.143]],"v":[[19.707,-427.786],[20.029,-424.143]],"c":true}]}]},"nm":"Path 6","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[66.5,10.5],[-49.599,-0.443]],"o":[[-70.267,-11.095],[56,0.5]],"v":[[20.5,199.5],[12,315.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[43.734,-35.066],[-43.928,23.035]],"o":[[-55.5,44.5],[41,-21.5]],"v":[[15.5,72.5],[26,189.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[56,-2.5],[-53,44]],"o":[[-54.592,2.437],[70,20.5]],"v":[[-4.5,51.5],[-8,104.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[34.653,-12.008],[-45,46.5]],"o":[[-50.5,17.5],[55,0]],"v":[[9.5,-11.5],[24,35.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[25,-5],[-25.722,5.512]],"o":[[-33.855,6.771],[28,-6]],"v":[[23,-29],[30,-1]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[20.461,-75.516],[21.822,-70.072]],"c":true}]},{"t":51,"s":[{"i":[[4.861,-0.972],[-5.001,1.072]],"o":[[-6.583,1.317],[5.444,-1.167]],"v":[[10.461,-427.516],[11.822,-422.072]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":5,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[40.224,-0.584],[25.671,-35.551],[-117.344,4.076],[-13.845,-2.09],[-15.219,20.222],[41.95,17.503]],"o":[[-4.678,-19.848],[-63.829,-76.551],[7.749,18.334],[16.09,2.429],[38.296,26.994],[7.997,-34.703]],"v":[[108.178,121.348],[34.329,117.551],[25.844,278.924],[70,279],[118.204,260.006],[148.55,186.497]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[42.564,-6.226],[40.813,-50.225],[-85.297,31.262],[-32.063,3.791],[-15.43,23.796],[36.231,22.442]],"o":[[-6.076,-18.138],[-120.187,-27.225],[8.47,17.96],[23.983,5.161],[35.183,9.711],[3.616,-40.885]],"v":[[125.779,-104.633],[37.687,-123.775],[35.297,47.238],[94,72],[154.817,37.789],[175.269,-33.942]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[50.807,-10.84],[11.197,-28.751],[-79.754,23.956],[-21.881,6.283],[-12.991,22.817],[47.774,9.527]],"o":[[-19.668,-12.67],[-116.303,-27.251],[12.896,12.677],[24.971,6.064],[31.765,17.866],[-3.819,-31.664]],"v":[[94.869,-161.73],[33.303,-153.749],[47.754,-35.456],[100,-23],[159.235,-55.366],[172.226,-116.027]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[61.33,-27.94],[13.035,-11.794],[-88,34.498],[-24.613,14.871],[-11.285,23.783],[4.514,22.775]],"o":[[-22.734,-8.189],[-81.69,0.79],[16.437,2.441],[24.287,7.59],[9.779,-20.609],[-7.436,-37.518]],"v":[[69.474,-198.525],[15.69,-190.29],[42.5,-42.998],[104,-60],[158.974,-91.922],[167.923,-161.964]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[49.628,-33.504],[8.473,-9.395],[-41.73,-7.776],[-17.115,15.49],[-6.732,20.052],[7.675,16.124]],"o":[[-21.027,-0.06],[-26.241,29.098],[15.267,2.845],[24.591,0.911],[5.833,-17.376],[-12.642,-26.561]],"v":[[48.978,-222.185],[5.396,-206.758],[48.457,-109.106],[98,-126],[145.019,-160.775],[142.281,-214.968]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[20.595,-4.565],[3.948,-3.927],[-15.714,-8.139],[-5.982,0.53],[-3.844,7.794],[2.709,6.906]],"o":[[-9.506,2.107],[-12.229,12.163],[5.749,2.978],[11.992,-1.063],[3.331,-6.754],[-4.463,-11.376]],"v":[[87.975,-274.467],[68.116,-265.142],[82.934,-226.569],[101,-222.5],[124.726,-237.535],[125.642,-259.162]],"c":true}]},{"t":60,"s":[{"i":[[4.312,0.077],[0.947,-0.725],[-2.927,-2.476],[-1.238,0.04],[-1.039,1.566],[0.28,1.562]],"o":[[-2.023,-0.036],[-2.933,2.244],[1.071,0.906],[2.353,-0.076],[0.9,-1.357],[-0.461,-2.572]],"v":[[95.675,-299.303],[91.277,-298.18],[92.954,-288.429],[96.5,-287],[101.657,-289.789],[102.633,-294.38]],"c":true}]}]},"nm":"Path 2","hd":false},{"ind":6,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[-4.046,-28.733],[-7.961,-19.055],[-7.058,-4.972],[-5.37,0.263],[-10.383,14.21],[38.553,-4.686]],"o":[[-39.406,25.959],[3.789,9.068],[6.543,4.609],[20.166,40.644],[98.704,38.486],[-7.504,-22.39]],"v":[[-106.454,196.733],[-142.466,268.042],[-124.994,289.511],[-106.166,296.356],[-25.204,307.014],[-1.553,202.686]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[7.754,-10.103],[-2.864,-41.686],[-47.459,23.038],[-17.819,6.695],[-11.001,13.349],[99.213,26.201]],"o":[[-24.746,-34.853],[-41.454,26.415],[10.089,11.703],[9.571,21.591],[45.708,87.19],[-6.546,-22.901]],"v":[[-105.754,6.853],[-187.046,30.585],[-146.041,114.963],[-104.071,125.409],[-34.708,136.81],[-5.213,30.799]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[5.648,-2.651],[-3.488,-40.68],[-28.748,26.769],[-22.563,7.933],[-9.109,9.161],[8.807,22.921]],"o":[[-52.986,-50.627],[-31.582,21.921],[15.404,15.153],[24.874,27.539],[16.685,-16.781],[-6.247,-16.259]],"v":[[-65.014,-36.373],[-171.918,-8.421],[-143.252,52.731],[-84.874,67.961],[-17.551,62.845],[-3.459,-5.795]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[19.861,-24.879],[2.389,-25.903],[-7.904,-8.883],[-16.419,1.173],[-8.619,13.394],[50.602,-9.162]],"o":[[-31.37,-12.195],[-1.137,12.327],[7.327,8.235],[20.153,51.095],[59.455,-7.707],[-9.228,-15.356]],"v":[[-114.361,-87.621],[-168.705,-49.001],[-158.954,-15.239],[-123.653,-3.095],[-37.955,5.207],[-35.102,-74.838]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[20.907,-25.093],[-1.21,-21.904],[-8.256,-6.68],[-14.008,2.753],[-6.721,8.523],[11.735,16.301]],"o":[[-34.736,-8.916],[0.576,10.424],[7.654,6.192],[22.131,25.463],[12.311,-15.612],[-8.324,-11.564]],"v":[[-85.407,-125.907],[-135.006,-90.384],[-121.683,-62.999],[-89.131,-56.463],[-23.909,-59.855],[-20.32,-115.438]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[16.089,-7.299],[3.439,-8.602],[-0.131,-3.928],[-2.539,-2.432],[-4.273,4.897],[6.868,9.275]],"o":[[-8.201,3.721],[-1.637,4.094],[0.122,3.642],[21.095,20.211],[7.826,-8.97],[-4.872,-6.579]],"v":[[-77.089,-167.201],[-95.356,-146.351],[-97.732,-134.128],[-94.595,-124.211],[-40.671,-127.117],[-38.776,-158.124]],"c":true}]},{"t":60,"s":[{"i":[[1.463,-0.702],[0.031,-2.062],[-0.538,-0.9],[-0.887,-0.655],[-1.713,1.587],[2.273,2.5]],"o":[[-3.182,1.528],[-0.015,0.981],[0.498,0.834],[2.118,1.565],[3.138,-2.907],[-1.612,-1.773]],"v":[[-78.589,-181.314],[-83.167,-175.626],[-82.357,-172.772],[-80.258,-170.512],[-64.659,-171.663],[-62.771,-181.234]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.945098099054,0.90588241278,0.792156922583,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.917647118662,0.847058883368,0.721568627451,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":16},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":60,"st":0,"bm":0}]},{"id":"comp_31","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 33","sr":1,"ks":{"p":{"a":0,"k":[768,866.288,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[-0.223,-3.321],[-0.789,2.008]],"o":[[0.105,1.566],[1.017,-2.585]],"v":[[237.765,-1024.967],[243.141,-1024.24]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":69,"s":[{"i":[[-0.223,-3.321],[-0.789,2.007]],"o":[[0.105,1.566],[1.017,-2.585]],"v":[[237.765,-1008.967],[243.141,-1008.24]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":70,"s":[{"i":[[-0.223,-3.321],[-0.789,2.007]],"o":[[0.105,1.566],[1.017,-2.585]],"v":[[236.346,-524.967],[241.722,-524.24]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":80,"s":[{"i":[[-5.342,-59.486],[-13.348,36.334]],"o":[[2.52,28.059],[17.191,-46.793]],"v":[[201.885,-588.486],[298.618,-577.622]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":140,"s":[{"i":[[-15.455,-60.898],[-7.814,39.963]],"o":[[7.29,28.725],[10.063,-51.467]],"v":[[200.215,-675.425],[302.5,-680.251]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":169,"s":[{"i":[[-0.254,-0.782],[-0.068,0.529]],"o":[[0.12,0.369],[0.088,-0.681]],"v":[[241.36,-761.666],[242.692,-761.816]],"c":true}]},{"t":170,"s":[{"i":[[-0.254,-0.782],[-0.068,0.529]],"o":[[0.12,0.369],[0.088,-0.681]],"v":[[233.36,-1093.666],[234.692,-1093.816]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[-1.677,-2.156],[0.08,0.867]],"o":[[0.531,0.683],[-0.184,-1.997]],"v":[[70.838,-933.52],[73.428,-934.29]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":69,"s":[{"i":[[-1.677,-2.156],[0.08,0.867]],"o":[[0.531,0.683],[-0.184,-1.997]],"v":[[70.838,-917.52],[73.428,-918.29]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":70,"s":[{"i":[[-1.677,-2.155],[0.08,0.867]],"o":[[0.531,0.683],[-0.184,-1.997]],"v":[[69.419,-433.52],[72.009,-434.29]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":80,"s":[{"i":[[-87.333,-120.809],[2.702,47.44]],"o":[[27.678,38.287],[-6.226,-109.313]],"v":[[61.469,-509.574],[204.247,-546.629]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":137,"s":[{"i":[[-55.487,-140.171],[-9.175,47.17]],"o":[[17.585,44.423],[21.142,-108.689]],"v":[[49.539,-656.724],[198.711,-657.457]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":166,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[97,-813.288],[97,-813.288]],"c":true}]},{"t":167,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[89,-1145.288],[89,-1145.288]],"c":true}]}]},"nm":"Path 6","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":38,"s":[{"i":[[-0.952,-1.6],[0.095,0.829],[0.849,-0.064]],"o":[[0.752,1.263],[-0.147,-1.287],[-1.042,0.079]],"v":[[288.935,-941.517],[291.86,-942.421],[289.911,-944.178]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":39,"s":[{"i":[[-0.952,-1.6],[0.095,0.829],[0.849,-0.064]],"o":[[0.752,1.263],[-0.147,-1.287],[-1.042,0.079]],"v":[[290.163,-421.517],[293.089,-422.421],[291.14,-424.178]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":49,"s":[{"i":[[-32.583,-59.201],[2.431,30.186],[29.696,-4.505]],"o":[[25.726,46.741],[-3.774,-46.858],[-36.454,5.531]],"v":[[243.801,-477.029],[345.699,-520.67],[279.392,-580.665]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":65,"s":[{"i":[[-337.602,-308.185],[1.22,28.334],[28.569,-4.104]],"o":[[36.9,33.685],[-1.948,-45.216],[54.848,-243.641]],"v":[[226.602,-500.102],[323.428,-535.807],[262.152,-589.647]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":131,"s":[{"i":[[-77.185,-118.131],[-7.472,28.942],[29.09,7.932]],"o":[[12.775,48.988],[11.584,-45.259],[-13.578,-58.019]],"v":[[211.69,-594.276],[319.765,-589.067],[277.087,-671.871]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":160,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[253,-706.288],[253,-706.288],[253,-706.288]],"c":true}]},{"t":161,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[245,-1038.288],[245,-1038.288],[245,-1038.288]],"c":true}]}]},"nm":"Path 7","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":38,"s":[{"i":[[-0.302,-3.573],[-0.364,1.897]],"o":[[0.136,1.605],[0.569,-2.965]],"v":[[361.212,-954.714],[365.543,-954.336]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":39,"s":[{"i":[[-0.302,-3.573],[-0.364,1.897]],"o":[[0.136,1.605],[0.569,-2.965]],"v":[[362.44,-434.714],[366.772,-434.336]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":49,"s":[{"i":[[-5.794,-68.569],[-6.98,36.395]],"o":[[2.602,30.794],[10.911,-56.893]],"v":[[328.696,-513.456],[416.726,-492.683]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":134,"s":[{"i":[[-6.145,-72.718],[-7.403,38.597]],"o":[[2.76,32.658],[11.572,-60.336]],"v":[[333.438,-602.657],[422.337,-592.885]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":163,"s":[{"i":[[-0.268,-3.169],[-0.323,1.682]],"o":[[0.12,1.423],[0.504,-2.629]],"v":[[363.189,-680.242],[367.007,-679.969]],"c":true}]},{"t":164,"s":[{"i":[[-0.268,-3.169],[-0.323,1.682]],"o":[[0.12,1.423],[0.504,-2.629]],"v":[[355.189,-1012.242],[359.007,-1011.969]],"c":true}]}]},"nm":"Path 8","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[0.451,-3.031],[-0.374,2.746],[0.732,0.578]],"o":[[-0.293,1.969],[0.132,-0.969],[-1.903,-1.504]],"v":[[-115.69,-1002.268],[-108.475,-1001.515],[-109.524,-1003.874]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":49,"s":[{"i":[[0.451,-3.031],[-0.374,2.746],[0.732,0.578]],"o":[[-0.293,1.969],[0.132,-0.969],[-1.903,-1.504]],"v":[[-115.69,-986.268],[-108.475,-985.515],[-109.524,-987.874]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[0.451,-3.031],[-0.374,2.746],[0.732,0.578]],"o":[[-0.293,1.969],[0.132,-0.969],[-1.903,-1.504]],"v":[[-113.69,-406.268],[-106.475,-405.515],[-107.524,-407.874]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":61,"s":[{"i":[[-38.977,-43.803],[-18.145,94.928],[25.732,11.42]],"o":[[-61.102,47.212],[5.038,-26.359],[-66.939,-29.709]],"v":[[-124.176,-501.216],[68.711,-476.216],[31.224,-532.319]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":78,"s":[{"i":[[-38.42,-54.458],[40.229,71.489],[22.204,12.329]],"o":[[-55.143,52.759],[10.318,-27.739],[-57.76,-32.072]],"v":[[-154.392,-522.524],[13.156,-502.776],[-11.161,-562.514]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":87,"s":[{"i":[[-31.344,-50.289],[32.722,62.873],[19.508,8.479]],"o":[[-42.344,46.274],[7.643,-24.152],[-50.748,-22.057]],"v":[[-138.728,-548.891],[9.639,-555.16],[-13.613,-603.415]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":103,"s":[{"i":[[-103.459,-215.483],[29.308,65.598],[87.719,-26.389]],"o":[[-32.992,48.05],[89.898,-26.744],[35.043,-130.317]],"v":[[-156.394,-589.554],[1.997,-601.886],[-48.501,-691.933]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":137,"s":[{"i":[[-50.993,-134.357],[21.184,70.149],[49.266,-5.489]],"o":[[-6.59,50.834],[32.255,-26.174],[-23.829,-64.578]],"v":[[-168.525,-671.122],[9.598,-703.002],[-39.751,-769.304]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":166,"s":[{"i":[[-0.108,-0.527],[0.103,0.476],[0.156,0.054]],"o":[[0.07,0.342],[-0.036,-0.168],[-0.406,-0.139]],"v":[[-106.568,-820.63],[-105.325,-820.921],[-105.641,-821.251]],"c":true}]},{"t":167,"s":[{"i":[[-0.108,-0.527],[0.103,0.476],[0.156,0.053]],"o":[[0.07,0.343],[-0.036,-0.168],[-0.406,-0.139]],"v":[[-114.568,-1152.63],[-113.325,-1152.921],[-113.641,-1153.251]],"c":true}]}]},"nm":"Path 9","hd":false},{"ind":5,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-348,-1054.288],[-348,-1054.288],[-348,-1054.288],[-348,-1054.288],[-348,-1054.288]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":49,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-348,-1038.288],[-348,-1038.288],[-348,-1038.288],[-348,-1038.288],[-348,-1038.288]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-346,-458.288],[-346,-458.288],[-346,-458.288],[-346,-458.288],[-346,-458.288]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":61,"s":[{"i":[[-72.133,20.573],[-8.441,30.069],[31.859,-22.763],[17.807,-4.46],[2.557,-14.832]],"o":[[3.162,25.784],[13.284,-47.323],[-19.821,-11.076],[-26.195,6.561],[-3.16,18.33]],"v":[[-299.075,-502.593],[-195.743,-489.357],[-284.594,-545.552],[-342.838,-553.106],[-391.704,-513.472]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":67,"s":[{"i":[[-69.352,19.195],[-8.604,30.649],[32.519,-23.063],[17.232,-5.947],[2.551,-17.614]],"o":[[3.223,26.282],[13.526,-48.185],[-19.2,-10.538],[-25.349,8.749],[-3.153,21.768]],"v":[[-298.27,-489.569],[-192.944,-494.287],[-283.404,-551.637],[-339.784,-556.016],[-387.153,-508.491]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":78,"s":[{"i":[[-87.455,39.11],[-10.617,37.82],[40.23,-28.143],[74.369,-68.916],[-149.028,-56.534]],"o":[[3.977,32.431],[16.659,-59.344],[73.273,-62.648],[-69.881,-63.413],[-83.346,71.374]],"v":[[-322.273,-531.398],[-202.302,-500.697],[-313.69,-571.626],[-394.369,-658.372],[-451.841,-602.482]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":138,"s":[{"i":[[-22.947,-1.251],[-14.249,31.232],[38.78,-17.282],[17.801,-16.201],[-32.86,-23.753]],"o":[[-0.923,28.539],[22.113,-48.468],[18.246,-11.706],[-17.81,-15.984],[-25.777,19.408]],"v":[[-294.35,-597.819],[-183.587,-570.926],[-268.963,-647.742],[-289.754,-669.037],[-322.236,-647.134]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":167,"s":[{"i":[[-0.038,-0.307],[-0.27,0.962],[1.044,-0.652],[0.07,-0.079],[0.052,-0.173]],"o":[[0.101,0.825],[0.417,-1.486],[-0.088,0.055],[-0.103,0.117],[-0.064,0.214]],"v":[[-251.554,-688.673],[-248.249,-688.249],[-251.034,-690.086],[-251.272,-689.885],[-251.508,-689.452]],"c":true}]},{"t":168,"s":[{"i":[[-0.038,-0.307],[-0.27,0.962],[1.044,-0.652],[0.07,-0.079],[0.052,-0.173]],"o":[[0.101,0.825],[0.417,-1.486],[-0.088,0.055],[-0.103,0.117],[-0.064,0.214]],"v":[[-259.554,-1020.673],[-256.249,-1020.249],[-259.034,-1022.086],[-259.272,-1021.885],[-259.508,-1021.452]],"c":true}]}]},"nm":"Path 10","hd":false},{"ind":6,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[-0.86,-2.468],[0.177,1.253]],"o":[[0.171,0.491],[-0.306,-2.172]],"v":[[-404.14,-929.549],[-401.505,-930.201]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":105,"s":[{"i":[[-0.86,-2.468],[0.177,1.253]],"o":[[0.171,0.491],[-0.306,-2.172]],"v":[[-404.14,-913.549],[-401.505,-914.201]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":106,"s":[{"i":[[-0.86,-2.468],[0.177,1.253]],"o":[[0.171,0.491],[-0.306,-2.172]],"v":[[-390.14,-545.549],[-387.505,-546.201]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[{"i":[[-23.735,-79.476],[3.806,39.955]],"o":[[4.724,15.818],[-6.599,-69.274]],"v":[[-420.545,-614.352],[-336.102,-631.243]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":137,"s":[{"i":[[-25.221,-84.449],[4.044,42.455]],"o":[[5.02,16.807],[-7.011,-73.609]],"v":[[-423.425,-606.795],[-333.698,-624.743]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":166,"s":[{"i":[[-1.636,-5.48],[0.262,2.755]],"o":[[0.326,1.091],[-0.455,-4.776]],"v":[[-397.322,-690.878],[-391.5,-692.042]],"c":true}]},{"t":167,"s":[{"i":[[-1.636,-5.48],[0.262,2.755]],"o":[[0.326,1.091],[-0.455,-4.776]],"v":[[-405.322,-1022.878],[-399.5,-1024.042]],"c":true}]}]},"nm":"Path 11","hd":false},{"ind":7,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[0.444,-2.846],[-0.06,2.162],[0.67,0.546]],"o":[[-0.438,2.81],[0.029,-1.027],[-1.558,-1.269]],"v":[[-486.367,-920.217],[-480.556,-919.65],[-481.655,-922.052]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":61,"s":[{"i":[[0.444,-2.846],[-0.06,2.162],[0.67,0.546]],"o":[[-0.439,2.81],[0.029,-1.027],[-1.558,-1.269]],"v":[[-486.367,-904.217],[-480.556,-903.65],[-481.655,-906.052]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":62,"s":[{"i":[[0.444,-2.846],[-0.06,2.162],[0.67,0.546]],"o":[[-0.439,2.81],[0.029,-1.027],[-1.558,-1.269]],"v":[[-485.852,-462.217],[-480.041,-461.65],[-481.14,-464.052]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":72,"s":[{"i":[[6.07,-38.905],[-0.827,29.56],[9.164,7.465]],"o":[[-5.995,38.42],[0.393,-14.04],[-21.298,-17.349]],"v":[[-513.324,-514.208],[-433.885,-506.456],[-448.904,-539.292]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":140,"s":[{"i":[[5.401,-34.613],[-0.736,26.299],[8.153,6.641]],"o":[[-5.333,34.182],[0.349,-12.492],[-18.949,-15.435]],"v":[[-506.688,-592.469],[-436.012,-585.573],[-449.374,-614.787]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":169,"s":[{"i":[[0.25,-1.604],[-0.034,1.218],[0.378,0.308]],"o":[[-0.247,1.584],[0.016,-0.579],[-0.878,-0.715]],"v":[[-487.492,-654.871],[-484.218,-654.552],[-484.837,-655.905]],"c":true}]},{"t":170,"s":[{"i":[[0.25,-1.604],[-0.034,1.218],[0.378,0.308]],"o":[[-0.247,1.584],[0.016,-0.579],[-0.878,-0.715]],"v":[[-495.492,-986.871],[-492.218,-986.552],[-492.837,-987.905]],"c":true}]}]},"nm":"Path 12","hd":false},{"ind":8,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[-0.439,-2.059],[0.427,1.684]],"o":[[0.386,1.811],[-0.592,-2.334]],"v":[[-553.821,-921.181],[-549.938,-921.954]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":105,"s":[{"i":[[-0.439,-2.059],[0.427,1.684]],"o":[[0.386,1.811],[-0.592,-2.334]],"v":[[-553.821,-905.181],[-549.938,-905.954]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":106,"s":[{"i":[[-0.439,-2.059],[0.427,1.684]],"o":[[0.386,1.811],[-0.592,-2.334]],"v":[[-539.821,-537.181],[-535.938,-537.954]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[{"i":[[-5.879,-16.594],[5.372,13.497]],"o":[[5.172,14.6],[-7.446,-18.709]],"v":[[-545.121,-570.309],[-513.765,-580.939]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":139,"s":[{"i":[[-5.901,-16.658],[5.392,13.549]],"o":[[5.192,14.656],[-7.474,-18.781]],"v":[[-544.278,-567.943],[-512.801,-578.614]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":168,"s":[{"i":[[-1.05,-2.965],[0.96,2.412]],"o":[[0.924,2.609],[-1.33,-3.343]],"v":[[-542.164,-610.896],[-536.561,-612.796]],"c":true}]},{"t":169,"s":[{"i":[[-1.05,-2.965],[0.96,2.412]],"o":[[0.924,2.609],[-1.33,-3.343]],"v":[[-550.164,-942.896],[-544.561,-944.796]],"c":true}]}]},"nm":"Path 13","hd":false},{"ind":9,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[0.436,1.704],[0.799,-0.276]],"o":[[-0.306,-1.196],[-0.603,0.208]],"v":[[-610.147,-971.692],[-612.448,-973.012]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":61,"s":[{"i":[[0.436,1.704],[0.799,-0.276]],"o":[[-0.306,-1.196],[-0.603,0.208]],"v":[[-610.147,-955.692],[-612.448,-957.012]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":62,"s":[{"i":[[0.436,1.704],[0.799,-0.276]],"o":[[-0.306,-1.196],[-0.603,0.208]],"v":[[-609.632,-513.692],[-611.933,-515.012]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":72,"s":[{"i":[[-2.489,13.826],[4.33,-5.184]],"o":[[2.181,-12.115],[-13.423,16.072]],"v":[[-591.165,-538.614],[-612.812,-549.155]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[{"i":[[-2.612,20.204],[6.681,-12.817]],"o":[[2.756,-21.685],[-15.352,28.585]],"v":[[-568.224,-566.492],[-617.715,-589.605]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":165,"s":[{"i":[[-0.049,0.416],[0.139,-0.29]],"o":[[0.055,-0.466],[-0.295,0.614]],"v":[[-601.382,-612.704],[-602.49,-613.218]],"c":true}]},{"t":166,"s":[{"i":[[-0.049,0.416],[0.139,-0.29]],"o":[[0.055,-0.466],[-0.295,0.614]],"v":[[-609.382,-944.704],[-610.49,-945.218]],"c":true}]}]},"nm":"Path 16","hd":false},{"ind":10,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[0.164,-3.557],[-0.651,2.476]],"o":[[-0.175,3.776],[0.806,-3.065]],"v":[[-288.352,-1112.731],[-283.559,-1111.922]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":49,"s":[{"i":[[0.164,-3.557],[-0.651,2.476]],"o":[[-0.175,3.776],[0.806,-3.065]],"v":[[-288.351,-1096.731],[-283.559,-1095.922]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[0.164,-3.557],[-0.651,2.476]],"o":[[-0.175,3.776],[0.806,-3.065]],"v":[[-286.351,-516.731],[-281.559,-515.922]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":61,"s":[{"i":[[2.324,-50.247],[-9.202,34.979]],"o":[[-2.467,53.341],[11.393,-43.304]],"v":[[-306.993,-580.629],[-239.292,-569.199]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":133,"s":[{"i":[[2.153,-46.558],[-8.527,32.41]],"o":[[-2.286,49.424],[10.556,-40.125]],"v":[[-319.121,-667.712],[-256.391,-657.122]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":162,"s":[{"i":[[0.056,-1.207],[-0.221,0.84]],"o":[[-0.059,1.281],[0.274,-1.04]],"v":[[-311.688,-727.569],[-310.062,-727.295]],"c":true}]},{"t":163,"s":[{"i":[[0.056,-1.207],[-0.221,0.84]],"o":[[-0.059,1.281],[0.274,-1.04]],"v":[[-319.688,-1059.569],[-318.062,-1059.295]],"c":true}]}]},"nm":"Path 14","hd":false},{"ind":11,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[-0.099,-1.547],[0.122,0.483]],"o":[[-0.233,0.571],[0.37,-0.986]],"v":[[7.57,-944.741],[9.108,-944.508]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":69,"s":[{"i":[[-0.099,-1.547],[0.122,0.483]],"o":[[-0.233,0.571],[0.37,-0.986]],"v":[[7.57,-928.741],[9.109,-928.508]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":70,"s":[{"i":[[-0.099,-1.547],[0.122,0.483]],"o":[[-0.233,0.571],[0.37,-0.986]],"v":[[6.151,-444.741],[7.689,-444.508]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":80,"s":[{"i":[[-10.573,-109.271],[-19.225,37.116]],"o":[[3.245,47.089],[35.066,-67.698]],"v":[[-33.245,-508.877],[74.434,-492.59]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":132,"s":[{"i":[[-0.865,-86.858],[0.432,24.939]],"o":[[-5.822,31.41],[13.995,-55.319]],"v":[[-31.132,-607.48],[56.494,-594.226]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":161,"s":[{"i":[[0.113,-4.165],[-0.19,1.123]],"o":[[-0.04,1.484],[0.449,-2.651]],"v":[[-3.119,-699.053],[1.124,-698.411]],"c":true}]},{"t":162,"s":[{"i":[[0.113,-4.165],[-0.19,1.124]],"o":[[-0.04,1.484],[0.449,-2.651]],"v":[[-11.119,-1031.053],[-6.876,-1030.411]],"c":true}]}]},"nm":"Path 15","hd":false},{"ind":12,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":38,"s":[{"i":[[-0.098,-0.633],[-0.857,0.923],[1.79,-0.946]],"o":[[0.077,0.496],[1.653,0.298],[-0.823,-0.12]],"v":[[470.629,-994.934],[473.075,-994.626],[472.283,-995.63]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":39,"s":[{"i":[[-0.098,-0.633],[-0.857,0.923],[1.79,-0.946]],"o":[[0.077,0.496],[1.653,0.298],[-0.823,-0.12]],"v":[[471.857,-474.934],[474.304,-474.626],[473.511,-475.63]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":49,"s":[{"i":[[-7.221,-46.494],[-87.892,79.766],[198.581,-167.912]],"o":[[5.652,36.39],[194.108,50.766],[-51.203,-77.754]],"v":[[345.774,-588.494],[504.892,-567.053],[451.346,-603.347]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":138,"s":[{"i":[[-6.848,-44.095],[-19.098,36.808],[60.094,-29.415]],"o":[[5.258,33.857],[38.602,-12.297],[-34.472,-23.002]],"v":[[317.847,-666.223],[428.931,-655.095],[390.612,-711.041]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":167,"s":[{"i":[[-0.076,-0.492],[-0.1,0.334],[0.27,0.078]],"o":[[0.058,0.376],[0.097,-0.326],[-0.341,-0.098]],"v":[[351.49,-743.717],[352.564,-743.622],[352.167,-744.254]],"c":true}]},{"t":168,"s":[{"i":[[-0.076,-0.492],[-0.1,0.334],[0.27,0.078]],"o":[[0.058,0.376],[0.097,-0.326],[-0.341,-0.098]],"v":[[343.49,-1075.717],[344.564,-1075.622],[344.167,-1076.254]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":13,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[-1.266,-4.784],[-0.496,1.775]],"o":[[0.615,2.322],[1.11,-3.968]],"v":[[456.064,-939.834],[460.091,-939.764]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":125,"s":[{"i":[[-1.266,-4.784],[-0.496,1.775]],"o":[[0.615,2.322],[1.11,-3.968]],"v":[[456.064,-923.834],[460.091,-923.764]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":126,"s":[{"i":[[-1.015,-3.834],[-0.398,1.422]],"o":[[0.493,1.861],[0.89,-3.18]],"v":[[457.428,-614.453],[460.656,-614.398]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":137,"s":[{"i":[[-17.438,-65.879],[-6.836,24.44]],"o":[[8.463,31.973],[15.284,-54.644]],"v":[[437.705,-661.26],[493.165,-660.3]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":169,"s":[{"i":[[-0.455,-1.719],[-0.178,0.638]],"o":[[0.221,0.834],[0.399,-1.425]],"v":[[455.358,-734.122],[456.805,-734.097]],"c":true}]},{"t":170,"s":[{"i":[[-0.455,-1.719],[-0.178,0.638]],"o":[[0.221,0.834],[0.399,-1.425]],"v":[[447.358,-1066.122],[448.805,-1066.097]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":14,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[0.929,-6.009],[-0.89,3.766]],"o":[[-0.943,6.104],[1.317,-5.57]],"v":[[523.547,-932.234],[530.691,-929.895]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":125,"s":[{"i":[[0.929,-6.009],[-0.89,3.766]],"o":[[-0.943,6.104],[1.317,-5.57]],"v":[[523.547,-916.234],[530.691,-913.895]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":126,"s":[{"i":[[0.863,-5.584],[-0.827,3.5]],"o":[[-0.877,5.673],[1.224,-5.176]],"v":[[524.877,-606.867],[531.515,-604.694]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":137,"s":[{"i":[[4.466,-28.896],[-4.281,18.109]],"o":[[-4.537,29.354],[6.332,-26.783]],"v":[[517.241,-652.642],[551.594,-641.397]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":168,"s":[{"i":[[0.101,-0.651],[-0.096,0.408]],"o":[[-0.102,0.661],[0.143,-0.603]],"v":[[533.63,-697.949],[534.404,-697.695]],"c":true}]},{"t":169,"s":[{"i":[[0.101,-0.651],[-0.096,0.408]],"o":[[-0.102,0.661],[0.143,-0.603]],"v":[[525.63,-1029.949],[526.404,-1029.696]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":15,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[-0.994,-4.026],[0.15,3.139]],"o":[[0.58,2.348],[-0.173,-3.625]],"v":[[597.52,-990.395],[602.541,-991.428]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":125,"s":[{"i":[[-0.994,-4.026],[0.15,3.139]],"o":[[0.58,2.348],[-0.173,-3.625]],"v":[[597.52,-974.395],[602.541,-975.428]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":126,"s":[{"i":[[-0.914,-3.7],[0.137,2.885]],"o":[[0.533,2.158],[-0.159,-3.331]],"v":[[598.914,-665.492],[603.528,-666.441]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":137,"s":[{"i":[[-5.486,-22.219],[0.825,17.323]],"o":[[3.2,12.957],[-0.953,-20.006]],"v":[[595.718,-699.245],[623.426,-704.944]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":166,"s":[{"i":[[-0.343,-1.389],[0.052,1.083]],"o":[[0.2,0.81],[-0.06,-1.25]],"v":[[611.212,-749.097],[612.944,-749.454]],"c":true}]},{"t":167,"s":[{"i":[[-0.343,-1.389],[0.052,1.083]],"o":[[0.2,0.81],[-0.06,-1.25]],"v":[[603.212,-1081.097],[604.944,-1081.454]],"c":true}]}]},"nm":"Path 17","hd":false},{"ty":"fl","c":{"a":0,"k":[0.980392216701,0.972549079446,0.949019667682,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":39,"op":170,"st":-6,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 31","sr":1,"ks":{"p":{"a":0,"k":[768,866.288,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[-0.223,-3.321],[-0.789,2.007]],"o":[[0.105,1.566],[1.017,-2.585]],"v":[[237.765,-1004.967],[243.141,-1004.24]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":69,"s":[{"i":[[-0.223,-3.321],[-0.789,2.007]],"o":[[0.105,1.566],[1.017,-2.585]],"v":[[237.765,-1008.967],[243.141,-1008.24]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":70,"s":[{"i":[[-0.223,-3.321],[-0.789,2.007]],"o":[[0.105,1.566],[1.017,-2.585]],"v":[[236.346,-524.967],[241.722,-524.24]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":80,"s":[{"i":[[-7.579,-84.395],[-18.938,51.549]],"o":[[3.575,39.808],[24.389,-66.388]],"v":[[171.688,-576.426],[308.927,-561.012]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":140,"s":[{"i":[[-20.874,-82.249],[-10.553,53.975]],"o":[[9.846,38.796],[13.591,-69.512]],"v":[[174.345,-662.15],[312.493,-668.667]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":169,"s":[{"i":[[-0.254,-0.782],[-0.068,0.529]],"o":[[0.12,0.369],[0.088,-0.681]],"v":[[241.36,-761.666],[242.692,-761.816]],"c":true}]},{"t":170,"s":[{"i":[[-0.254,-0.782],[-0.068,0.529]],"o":[[0.12,0.369],[0.088,-0.681]],"v":[[233.36,-1093.666],[234.692,-1093.816]],"c":true}]}]},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":39,"s":[{"i":[[-1.677,-2.156],[0.08,0.867]],"o":[[0.531,0.683],[-0.184,-1.997]],"v":[[70.838,-913.52],[73.428,-914.29]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":69,"s":[{"i":[[-1.677,-2.156],[0.08,0.867]],"o":[[0.531,0.683],[-0.184,-1.997]],"v":[[70.838,-917.52],[73.428,-918.29]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":70,"s":[{"i":[[-1.677,-2.155],[0.08,0.867]],"o":[[0.531,0.683],[-0.184,-1.997]],"v":[[69.419,-433.52],[72.009,-434.29]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":80,"s":[{"i":[[-121.422,-167.964],[3.756,65.958]],"o":[[38.481,53.231],[-8.656,-151.98]],"v":[[23.4,-471.434],[221.908,-522.952]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":137,"s":[{"i":[[-76.553,-193.388],[-12.659,65.078]],"o":[[24.261,61.288],[29.168,-149.954]],"v":[[7.222,-628.036],[213.028,-629.047]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":166,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[97,-813.288],[97,-813.288]],"c":true}]},{"t":167,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[89,-1145.288],[89,-1145.288]],"c":true}]}]},"nm":"Path 6","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":38,"s":[{"i":[[-0.952,-1.6],[0.095,0.829],[0.849,-0.064]],"o":[[0.752,1.263],[-0.147,-1.287],[-1.042,0.079]],"v":[[288.935,-941.517],[291.86,-942.421],[289.911,-944.178]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":39,"s":[{"i":[[-0.952,-1.6],[0.095,0.829],[0.849,-0.064]],"o":[[0.752,1.263],[-0.147,-1.287],[-1.042,0.079]],"v":[[290.163,-421.517],[293.089,-422.421],[291.14,-424.178]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":49,"s":[{"i":[[-45.167,-82.063],[3.371,41.844],[41.164,-6.245]],"o":[[35.661,64.792],[-5.232,-64.954],[-50.531,7.666]],"v":[[215.147,-440.242],[356.396,-500.737],[264.483,-583.9]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":65,"s":[{"i":[[-401.836,-369.422],[1.794,41.659],[42.005,-6.035]],"o":[[54.08,49.718],[-2.863,-66.482],[69.366,-256.413]],"v":[[211.836,-454.866],[354.199,-507.363],[262.634,-593.875]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":131,"s":[{"i":[[-107.287,-164.201],[-10.386,40.23],[40.435,11.025]],"o":[[17.757,68.093],[16.102,-62.91],[-18.874,-80.647]],"v":[[186.999,-565.801],[337.223,-558.562],[277.901,-673.658]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":160,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[253,-706.288],[253,-706.288],[253,-706.288]],"c":true}]},{"t":161,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[245,-1038.288],[245,-1038.288],[245,-1038.288]],"c":true}]}]},"nm":"Path 7","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":38,"s":[{"i":[[-0.302,-3.573],[-0.364,1.897]],"o":[[0.136,1.605],[0.569,-2.965]],"v":[[361.212,-954.714],[365.543,-954.336]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":39,"s":[{"i":[[-0.302,-3.573],[-0.364,1.897]],"o":[[0.136,1.605],[0.569,-2.965]],"v":[[362.44,-434.714],[366.772,-434.336]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":49,"s":[{"i":[[-8.568,-101.394],[-10.322,53.817]],"o":[[3.848,45.535],[16.135,-84.128]],"v":[[298.871,-499.176],[429.041,-468.458]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":134,"s":[{"i":[[-8.568,-101.393],[-10.322,53.817]],"o":[[3.848,45.535],[16.135,-84.128]],"v":[[305.22,-590.579],[429.173,-576.953]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":163,"s":[{"i":[[-0.268,-3.169],[-0.323,1.682]],"o":[[0.12,1.423],[0.504,-2.629]],"v":[[363.189,-680.242],[367.007,-679.969]],"c":true}]},{"t":164,"s":[{"i":[[-0.268,-3.169],[-0.323,1.682]],"o":[[0.12,1.423],[0.504,-2.629]],"v":[[355.189,-1012.242],[359.007,-1011.969]],"c":true}]}]},"nm":"Path 8","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[0.451,-3.031],[-0.374,2.746],[0.732,0.578]],"o":[[-0.293,1.969],[0.132,-0.969],[-1.903,-1.504]],"v":[[-115.69,-982.268],[-108.475,-981.515],[-109.524,-983.874]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":49,"s":[{"i":[[0.451,-3.031],[-0.374,2.746],[0.732,0.578]],"o":[[-0.293,1.969],[0.132,-0.969],[-1.903,-1.504]],"v":[[-115.69,-986.268],[-108.475,-985.515],[-109.524,-987.874]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[0.451,-3.031],[-0.374,2.746],[0.732,0.578]],"o":[[-0.293,1.969],[0.132,-0.969],[-1.903,-1.504]],"v":[[-113.69,-406.268],[-106.475,-405.515],[-107.524,-407.874]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":61,"s":[{"i":[[-54.569,-61.327],[-25.403,132.904],[36.026,15.989]],"o":[[-85.546,66.099],[7.054,-36.904],[-93.718,-41.593]],"v":[[-185.454,-488.386],[84.597,-453.384],[32.114,-531.931]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":78,"s":[{"i":[[-48.548,-68.814],[50.834,90.334],[28.057,15.579]],"o":[[-69.68,66.666],[13.038,-35.052],[-72.986,-40.527]],"v":[[-191.665,-512.042],[20.051,-487.088],[-10.676,-562.574]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":87,"s":[{"i":[[-45.36,-72.777],[47.355,90.989],[28.232,12.271]],"o":[[-61.28,66.967],[11.061,-34.953],[-73.443,-31.921]],"v":[[-194.953,-524.565],[19.762,-533.638],[-13.887,-603.472]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":103,"s":[{"i":[[-145.34,-302.713],[41.172,92.153],[123.229,-37.072]],"o":[[-46.347,67.501],[126.289,-37.571],[49.229,-183.072]],"v":[[-200.799,-553.393],[21.711,-570.717],[-49.229,-697.216]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":137,"s":[{"i":[[-68.967,-181.716],[28.651,94.876],[66.631,-7.424]],"o":[[-8.912,68.753],[43.624,-35.4],[-32.228,-87.341]],"v":[[-213.474,-636.521],[27.433,-679.637],[-39.31,-769.31]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":166,"s":[{"i":[[-0.108,-0.527],[0.103,0.476],[0.156,0.054]],"o":[[0.07,0.342],[-0.036,-0.168],[-0.406,-0.139]],"v":[[-106.568,-820.63],[-105.325,-820.921],[-105.641,-821.251]],"c":true}]},{"t":167,"s":[{"i":[[-0.108,-0.527],[0.103,0.476],[0.156,0.053]],"o":[[0.07,0.343],[-0.036,-0.168],[-0.406,-0.139]],"v":[[-114.568,-1152.63],[-113.325,-1152.921],[-113.641,-1153.251]],"c":true}]}]},"nm":"Path 9","hd":false},{"ind":5,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-348,-1034.288],[-348,-1034.288],[-348,-1034.288],[-348,-1034.288],[-348,-1034.288]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":49,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-348,-1038.288],[-348,-1038.288],[-348,-1038.288],[-348,-1038.288],[-348,-1038.288]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-346,-458.288],[-346,-458.288],[-346,-458.288],[-346,-458.288],[-346,-458.288]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":61,"s":[{"i":[[-104.979,29.94],[-12.284,43.761],[46.367,-33.128],[25.915,-6.491],[3.721,-21.586]],"o":[[4.602,37.525],[19.334,-68.873],[-28.847,-16.12],[-38.123,9.548],[-4.599,26.676]],"v":[[-331.896,-483.607],[-181.51,-464.343],[-310.819,-546.127],[-395.586,-557.122],[-466.704,-499.439]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":67,"s":[{"i":[[-99.022,27.407],[-12.284,43.761],[46.431,-32.929],[24.604,-8.492],[3.643,-25.149]],"o":[[4.602,37.525],[19.313,-68.8],[-27.414,-15.046],[-36.194,12.492],[-4.502,31.08]],"v":[[-331.327,-464.449],[-180.941,-471.185],[-310.101,-553.07],[-390.602,-559.322],[-458.235,-491.466]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":78,"s":[{"i":[[-101.193,45.253],[-12.284,43.761],[46.55,-32.564],[61.721,-77.489],[-172.438,-65.414]],"o":[[4.602,37.525],[19.276,-68.666],[84.783,-72.489],[-80.858,-73.373],[-96.438,82.586]],"v":[[-330.285,-479.91],[-179.898,-483.727],[-308.783,-565.799],[-391.721,-670.799],[-497.562,-596.874]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":138,"s":[{"i":[[-30.277,3.035],[-12.284,43.761],[47.217,-30.505],[19.991,-24.82],[-47.826,-24.387]],"o":[[4.602,37.526],[19.064,-67.912],[21.489,-19.029],[-26.554,-17.286],[-29.773,30.639]],"v":[[-323.461,-577.504],[-173.074,-564.867],[-300.409,-647.987],[-331.944,-671.615],[-369.983,-636.347]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":167,"s":[{"i":[[-0.038,-0.307],[-0.27,0.962],[1.044,-0.652],[0.07,-0.079],[0.052,-0.173]],"o":[[0.101,0.825],[0.417,-1.486],[-0.088,0.055],[-0.103,0.117],[-0.064,0.214]],"v":[[-251.554,-688.673],[-248.249,-688.249],[-251.034,-690.086],[-251.272,-689.885],[-251.508,-689.452]],"c":true}]},{"t":168,"s":[{"i":[[-0.038,-0.307],[-0.27,0.962],[1.044,-0.652],[0.07,-0.079],[0.052,-0.173]],"o":[[0.101,0.825],[0.417,-1.486],[-0.088,0.055],[-0.103,0.117],[-0.064,0.214]],"v":[[-259.554,-1020.673],[-256.249,-1020.249],[-259.034,-1022.086],[-259.272,-1021.885],[-259.508,-1021.452]],"c":true}]}]},"nm":"Path 10","hd":false},{"ind":6,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[-0.86,-2.468],[0.177,1.253]],"o":[[0.171,0.491],[-0.306,-2.172]],"v":[[-404.14,-909.549],[-401.505,-910.201]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":105,"s":[{"i":[[-0.86,-2.468],[0.177,1.253]],"o":[[0.171,0.491],[-0.306,-2.172]],"v":[[-404.14,-913.549],[-401.505,-914.201]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":106,"s":[{"i":[[-0.86,-2.468],[0.177,1.253]],"o":[[0.171,0.491],[-0.306,-2.172]],"v":[[-390.14,-545.549],[-387.505,-546.201]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[{"i":[[-35.184,-117.811],[5.642,59.227]],"o":[[7.003,23.447],[-9.781,-102.689]],"v":[[-444.926,-584.477],[-319.751,-609.515]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":137,"s":[{"i":[[-35.184,-117.811],[5.642,59.227]],"o":[[7.003,23.447],[-9.781,-102.689]],"v":[[-444.926,-584.477],[-319.751,-609.515]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":166,"s":[{"i":[[-1.636,-5.48],[0.262,2.755]],"o":[[0.326,1.091],[-0.455,-4.776]],"v":[[-397.322,-690.878],[-391.5,-692.042]],"c":true}]},{"t":167,"s":[{"i":[[-1.636,-5.48],[0.262,2.755]],"o":[[0.326,1.091],[-0.455,-4.776]],"v":[[-405.322,-1022.878],[-399.5,-1024.042]],"c":true}]}]},"nm":"Path 11","hd":false},{"ind":7,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[0.444,-2.846],[-0.06,2.162],[0.67,0.546]],"o":[[-0.439,2.81],[0.029,-1.027],[-1.558,-1.269]],"v":[[-486.367,-900.217],[-480.556,-899.65],[-481.655,-902.052]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":61,"s":[{"i":[[0.444,-2.846],[-0.06,2.162],[0.67,0.546]],"o":[[-0.439,2.81],[0.029,-1.027],[-1.558,-1.269]],"v":[[-486.367,-904.217],[-480.556,-903.65],[-481.655,-906.052]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":62,"s":[{"i":[[0.444,-2.846],[-0.06,2.162],[0.67,0.546]],"o":[[-0.439,2.81],[0.029,-1.027],[-1.558,-1.269]],"v":[[-485.852,-462.217],[-480.041,-461.65],[-481.14,-464.052]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":72,"s":[{"i":[[8.215,-52.651],[-1.119,40.004],[12.402,10.102]],"o":[[-8.113,51.994],[0.532,-19.001],[-28.823,-23.478]],"v":[[-535.698,-505.876],[-428.191,-495.387],[-448.517,-539.824]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":140,"s":[{"i":[[8.215,-52.651],[-1.119,40.004],[12.402,10.102]],"o":[[-8.113,51.994],[0.532,-19.001],[-28.823,-23.478]],"v":[[-535.187,-581.621],[-427.681,-571.131],[-448.006,-615.568]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":169,"s":[{"i":[[0.25,-1.604],[-0.034,1.218],[0.378,0.308]],"o":[[-0.247,1.584],[0.016,-0.579],[-0.878,-0.715]],"v":[[-487.492,-654.871],[-484.218,-654.552],[-484.837,-655.905]],"c":true}]},{"t":170,"s":[{"i":[[0.25,-1.604],[-0.034,1.218],[0.378,0.308]],"o":[[-0.247,1.584],[0.016,-0.579],[-0.878,-0.715]],"v":[[-495.492,-986.871],[-492.218,-986.552],[-492.837,-987.905]],"c":true}]}]},"nm":"Path 12","hd":false},{"ind":8,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[-0.439,-2.059],[0.427,1.684]],"o":[[0.386,1.811],[-0.592,-2.334]],"v":[[-553.821,-901.181],[-549.938,-901.954]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":105,"s":[{"i":[[-0.439,-2.059],[0.427,1.684]],"o":[[0.386,1.811],[-0.592,-2.334]],"v":[[-553.821,-905.181],[-549.938,-905.954]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":106,"s":[{"i":[[-0.439,-2.059],[0.427,1.684]],"o":[[0.386,1.811],[-0.592,-2.334]],"v":[[-539.821,-537.181],[-535.938,-537.954]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[{"i":[[-10.898,-30.763],[9.958,25.023]],"o":[[9.589,27.066],[-13.803,-34.684]],"v":[[-561.449,-554.104],[-503.318,-573.81]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":139,"s":[{"i":[[-10.898,-30.763],[9.958,25.023]],"o":[[9.589,27.066],[-13.803,-34.684]],"v":[[-561.449,-554.104],[-503.318,-573.81]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":168,"s":[{"i":[[-1.05,-2.965],[0.96,2.412]],"o":[[0.924,2.609],[-1.33,-3.343]],"v":[[-542.164,-610.896],[-536.561,-612.796]],"c":true}]},{"t":169,"s":[{"i":[[-1.05,-2.965],[0.96,2.412]],"o":[[0.924,2.609],[-1.33,-3.343]],"v":[[-550.164,-942.896],[-544.561,-944.796]],"c":true}]}]},"nm":"Path 13","hd":false},{"ind":9,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[0.436,1.704],[0.799,-0.276]],"o":[[-0.306,-1.196],[-0.603,0.208]],"v":[[-610.147,-951.692],[-612.448,-953.012]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":61,"s":[{"i":[[0.436,1.704],[0.799,-0.276]],"o":[[-0.306,-1.196],[-0.603,0.208]],"v":[[-610.147,-955.692],[-612.448,-957.012]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":62,"s":[{"i":[[0.436,1.704],[0.799,-0.276]],"o":[[-0.306,-1.196],[-0.603,0.208]],"v":[[-609.632,-513.692],[-611.933,-515.012]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":72,"s":[{"i":[[-3.808,21.156],[6.625,-7.932]],"o":[[3.337,-18.538],[-20.54,24.592]],"v":[[-588.337,-533.75],[-621.46,-549.88]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[{"i":[[-3.841,29.71],[9.825,-18.847]],"o":[[4.052,-31.888],[-22.576,42.035]],"v":[[-562.684,-558.118],[-635.461,-592.108]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":165,"s":[{"i":[[-0.049,0.416],[0.139,-0.29]],"o":[[0.055,-0.466],[-0.295,0.614]],"v":[[-601.382,-612.704],[-602.49,-613.218]],"c":true}]},{"t":166,"s":[{"i":[[-0.049,0.416],[0.139,-0.29]],"o":[[0.055,-0.466],[-0.295,0.614]],"v":[[-609.382,-944.704],[-610.49,-945.218]],"c":true}]}]},"nm":"Path 16","hd":false},{"ind":10,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[0.164,-3.557],[-0.651,2.476]],"o":[[-0.175,3.776],[0.806,-3.065]],"v":[[-288.351,-1092.731],[-283.559,-1091.922]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":49,"s":[{"i":[[0.164,-3.557],[-0.651,2.476]],"o":[[-0.175,3.776],[0.806,-3.065]],"v":[[-288.351,-1096.731],[-283.559,-1095.922]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[0.164,-3.557],[-0.651,2.476]],"o":[[-0.175,3.776],[0.806,-3.065]],"v":[[-286.351,-516.731],[-281.559,-515.922]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":61,"s":[{"i":[[3.542,-76.579],[-14.024,53.309]],"o":[[-3.76,81.294],[17.363,-65.997]],"v":[[-336.336,-570.327],[-233.157,-552.908]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":133,"s":[{"i":[[3.542,-76.579],[-14.025,53.309]],"o":[[-3.76,81.294],[17.363,-65.997]],"v":[[-350.151,-655.09],[-246.971,-637.671]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":162,"s":[{"i":[[0.056,-1.207],[-0.221,0.84]],"o":[[-0.059,1.281],[0.274,-1.04]],"v":[[-311.688,-727.569],[-310.062,-727.295]],"c":true}]},{"t":163,"s":[{"i":[[0.056,-1.207],[-0.221,0.84]],"o":[[-0.059,1.281],[0.274,-1.04]],"v":[[-319.688,-1059.569],[-318.062,-1059.295]],"c":true}]}]},"nm":"Path 14","hd":false},{"ind":11,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":39,"s":[{"i":[[-0.099,-1.547],[0.122,0.483]],"o":[[-0.233,0.571],[0.37,-0.986]],"v":[[7.57,-924.741],[9.109,-924.508]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":69,"s":[{"i":[[-0.099,-1.547],[0.122,0.483]],"o":[[-0.233,0.571],[0.37,-0.986]],"v":[[7.57,-928.741],[9.109,-928.508]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":70,"s":[{"i":[[-0.099,-1.547],[0.122,0.483]],"o":[[-0.233,0.571],[0.37,-0.986]],"v":[[6.151,-444.741],[7.689,-444.508]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":80,"s":[{"i":[[-13.333,-137.796],[16.967,45.148]],"o":[[-27.679,51.531],[39.391,-87.843]],"v":[[-68.009,-495.852],[67.779,-475.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":132,"s":[{"i":[[-1.34,-134.598],[0.67,38.646]],"o":[[-9.022,48.674],[21.687,-85.725]],"v":[[-68.58,-590.547],[67.208,-570.009]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":161,"s":[{"i":[[0.113,-4.165],[-0.19,1.123]],"o":[[-0.04,1.484],[0.449,-2.651]],"v":[[-3.119,-699.053],[1.124,-698.411]],"c":true}]},{"t":162,"s":[{"i":[[0.113,-4.165],[-0.19,1.124]],"o":[[-0.04,1.484],[0.449,-2.651]],"v":[[-11.119,-1031.053],[-6.876,-1030.411]],"c":true}]}]},"nm":"Path 15","hd":false},{"ind":12,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":38,"s":[{"i":[[-0.098,-0.633],[-0.857,0.923],[1.79,-0.946]],"o":[[0.077,0.496],[1.653,0.298],[-0.823,-0.12]],"v":[[470.629,-994.934],[473.075,-994.626],[472.283,-995.63]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":39,"s":[{"i":[[-0.098,-0.633],[-0.857,0.923],[1.79,-0.946]],"o":[[0.077,0.496],[1.653,0.298],[-0.823,-0.12]],"v":[[471.857,-474.934],[474.304,-474.626],[473.511,-475.63]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":49,"s":[{"i":[[-9.771,-62.915],[-79.097,87.639],[268.714,-227.214]],"o":[[7.647,49.242],[207.317,68.611],[-69.286,-105.214]],"v":[[284.424,-562.912],[518.683,-533.898],[450.286,-610.073]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":138,"s":[{"i":[[-9.96,-64.133],[-27.776,53.533],[87.401,-42.782]],"o":[[7.647,49.242],[56.143,-17.885],[-50.137,-33.455]],"v":[[285.06,-645.746],[446.622,-629.561],[390.89,-710.929]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":167,"s":[{"i":[[-0.076,-0.492],[-0.1,0.334],[0.27,0.078]],"o":[[0.058,0.376],[0.097,-0.326],[-0.341,-0.098]],"v":[[351.49,-743.717],[352.564,-743.622],[352.167,-744.254]],"c":true}]},{"t":168,"s":[{"i":[[-0.076,-0.492],[-0.1,0.334],[0.27,0.078]],"o":[[0.058,0.376],[0.097,-0.326],[-0.341,-0.098]],"v":[[343.49,-1075.717],[344.564,-1075.622],[344.167,-1076.254]],"c":true}]}]},"nm":"Path 3","hd":false},{"ind":13,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[-1.266,-4.784],[-0.496,1.775]],"o":[[0.615,2.322],[1.11,-3.968]],"v":[[456.064,-919.834],[460.091,-919.764]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":125,"s":[{"i":[[-1.266,-4.784],[-0.496,1.775]],"o":[[0.615,2.322],[1.11,-3.968]],"v":[[456.064,-923.834],[460.091,-923.764]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":126,"s":[{"i":[[-1.015,-3.834],[-0.398,1.422]],"o":[[0.493,1.861],[0.89,-3.18]],"v":[[457.428,-614.453],[460.656,-614.398]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":137,"s":[{"i":[[-23.882,-90.222],[-9.362,33.471]],"o":[[11.59,43.787],[20.932,-74.836]],"v":[[422.077,-648.318],[498.03,-647.002]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":169,"s":[{"i":[[-0.455,-1.719],[-0.178,0.638]],"o":[[0.221,0.834],[0.399,-1.425]],"v":[[455.358,-734.122],[456.805,-734.097]],"c":true}]},{"t":170,"s":[{"i":[[-0.455,-1.719],[-0.178,0.638]],"o":[[0.221,0.834],[0.399,-1.425]],"v":[[447.358,-1066.122],[448.805,-1066.097]],"c":true}]}]},"nm":"Path 4","hd":false},{"ind":14,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[0.929,-6.009],[-0.89,3.766]],"o":[[-0.943,6.104],[1.317,-5.57]],"v":[[523.547,-912.234],[530.691,-909.895]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":125,"s":[{"i":[[0.929,-6.009],[-0.89,3.766]],"o":[[-0.943,6.104],[1.317,-5.57]],"v":[[523.547,-916.234],[530.691,-913.895]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":126,"s":[{"i":[[0.863,-5.584],[-0.827,3.5]],"o":[[-0.877,5.673],[1.224,-5.176]],"v":[[524.877,-606.867],[531.515,-604.694]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":137,"s":[{"i":[[6.839,-44.252],[-6.556,27.732]],"o":[[-6.948,44.953],[9.696,-41.016]],"v":[[504.615,-646.984],[557.224,-629.763]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":168,"s":[{"i":[[0.101,-0.651],[-0.096,0.408]],"o":[[-0.102,0.661],[0.143,-0.603]],"v":[[533.63,-697.949],[534.404,-697.695]],"c":true}]},{"t":169,"s":[{"i":[[0.101,-0.651],[-0.096,0.408]],"o":[[-0.102,0.661],[0.143,-0.603]],"v":[[525.63,-1029.949],[526.404,-1029.696]],"c":true}]}]},"nm":"Path 5","hd":false},{"ind":15,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[{"i":[[-0.994,-4.026],[0.15,3.139]],"o":[[0.58,2.348],[-0.173,-3.625]],"v":[[597.52,-970.395],[602.541,-971.428]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":125,"s":[{"i":[[-0.994,-4.026],[0.15,3.139]],"o":[[0.58,2.348],[-0.173,-3.625]],"v":[[597.52,-974.395],[602.541,-975.428]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":126,"s":[{"i":[[-0.914,-3.7],[0.137,2.885]],"o":[[0.533,2.158],[-0.159,-3.331]],"v":[[598.914,-665.492],[603.528,-666.441]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":137,"s":[{"i":[[-8.229,-33.327],[1.238,25.984]],"o":[[4.799,19.435],[-1.429,-30.007]],"v":[[586.078,-691.225],[627.639,-699.773]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":166,"s":[{"i":[[-0.343,-1.389],[0.052,1.083]],"o":[[0.2,0.81],[-0.06,-1.25]],"v":[[611.212,-749.097],[612.944,-749.454]],"c":true}]},{"t":167,"s":[{"i":[[-0.343,-1.389],[0.052,1.083]],"o":[[0.2,0.81],[-0.06,-1.25]],"v":[[603.212,-1081.097],[604.944,-1081.454]],"c":true}]}]},"nm":"Path 17","hd":false},{"ty":"fl","c":{"a":0,"k":[0.945098099054,0.90588241278,0.792156922583,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.917647058824,0.847058823529,0.721568627451,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":16},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":39,"op":170,"st":-6,"bm":0}]},{"id":"comp_32","layers":[{"ddd":0,"ind":1,"ty":0,"nm":"ground","refId":"comp_33","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":49,"s":[0]},{"t":72,"s":[100]}]},"p":{"a":0,"k":[1140.139,1466.602,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":52,"s":[56,20.16,100]},{"t":181,"s":[100,36,100]}]}},"ao":0,"w":512,"h":512,"ip":49,"op":208,"st":49,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"ground","refId":"comp_33","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":27,"s":[0]},{"t":50,"s":[100]}]},"p":{"a":0,"k":[639.139,473.658,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":31,"s":[59,21.24,100]},{"t":176,"s":[196,70.56,100]}]}},"ao":0,"w":512,"h":512,"ip":27,"op":186,"st":27,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"ground","refId":"comp_33","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[0]},{"t":68,"s":[100]}]},"p":{"a":0,"k":[891.139,353.658,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":49,"s":[72,25.92,100]},{"t":180,"s":[148,53.28,100]}]}},"ao":0,"w":512,"h":512,"ip":45,"op":180,"st":45,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"ground 16","refId":"comp_33","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":67,"s":[0]},{"t":90,"s":[100]}]},"p":{"a":0,"k":[739.139,1293.658,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":73,"s":[80,28.8,100]},{"t":178,"s":[166,59.76,100]}]}},"ao":0,"w":512,"h":512,"ip":67,"op":246,"st":66,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"ground","refId":"comp_33","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[0]},{"t":51,"s":[100]}]},"p":{"a":0,"k":[273.139,653.658,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":33,"s":[67,24.12,100]},{"t":183,"s":[143,51.48,100]}]}},"ao":0,"w":512,"h":512,"ip":28,"op":187,"st":28,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"ground 15","refId":"comp_33","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0]},{"t":43,"s":[100]}]},"p":{"a":0,"k":[365.139,1405.658,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":27,"s":[108,38.88,100]},{"t":184,"s":[175,63,100]}]}},"ao":0,"w":512,"h":512,"ip":20,"op":199,"st":19,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"ground","refId":"comp_33","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0]},{"t":43,"s":[100]}]},"p":{"a":0,"k":[1125.139,535.658,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":29,"s":[76,27.36,100]},{"t":181,"s":[166,59.76,100]}]}},"ao":0,"w":512,"h":512,"ip":20,"op":180,"st":19,"bm":0},{"ddd":0,"ind":8,"ty":0,"nm":"ground","refId":"comp_33","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":34,"s":[0]},{"t":57,"s":[100]}]},"p":{"a":0,"k":[231.139,1039.658,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":42,"s":[41,14.76,100]},{"t":179,"s":[130,46.8,100]}]}},"ao":0,"w":512,"h":512,"ip":34,"op":214,"st":34,"bm":0},{"ddd":0,"ind":9,"ty":0,"nm":"ground","refId":"comp_33","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[0]},{"t":48,"s":[100]}]},"p":{"a":0,"k":[777.139,1125.658,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":32,"s":[69.299,24.948,100]},{"t":176,"s":[279,100.44,100]}]}},"ao":0,"w":512,"h":512,"ip":24,"op":180,"st":24,"bm":0},{"ddd":0,"ind":10,"ty":0,"nm":"ground","refId":"comp_33","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":59,"s":[0]},{"t":82,"s":[100]}]},"p":{"a":0,"k":[573.139,803.658,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":66,"s":[115,41.4,100]},{"t":181,"s":[264,95.04,100]}]}},"ao":0,"w":512,"h":512,"ip":59,"op":180,"st":59,"bm":0},{"ddd":0,"ind":11,"ty":0,"nm":"ground","refId":"comp_33","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0]},{"t":43,"s":[100]}]},"p":{"a":0,"k":[565.139,909.658,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":20,"s":[50,18,100]},{"t":184,"s":[161,57.96,100]}]}},"ao":0,"w":512,"h":512,"ip":19,"op":185,"st":19,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Shape Layer 30","parent":20,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":43,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":108,"s":[100]},{"t":151,"s":[0]}]},"p":{"a":0,"k":[-65.113,-69.948,0]},"a":{"a":0,"k":[-92,-36,0]},"s":{"a":0,"k":[82,82,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[840,840]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,1,0,0.5,1,1,0,1,1,1,0,0,1,0.5,0.5,1,0]}},"s":{"a":0,"k":[0,0]},"e":{"a":0,"k":[320,0]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 22","hd":false},{"ty":"tr","p":{"a":0,"k":[-92,-36]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":20,"op":180,"st":0,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"Shape Layer 33","parent":21,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":43,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":108,"s":[100]},{"t":162,"s":[0]}]},"p":{"a":0,"k":[-65.113,-69.948,0]},"a":{"a":0,"k":[-92,-36,0]},"s":{"a":0,"k":[82,82,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[840,840]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,1,0,0.5,1,1,0,1,1,1,0,0,1,0.5,0.5,1,0]}},"s":{"a":0,"k":[0,0]},"e":{"a":0,"k":[320,0]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 22","hd":false},{"ty":"tr","p":{"a":0,"k":[-92,-36]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":20,"op":180,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"Shape Layer 39","parent":22,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":78,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":143,"s":[100]},{"t":170,"s":[0]}]},"p":{"a":0,"k":[-65.113,-69.948,0]},"a":{"a":0,"k":[-92,-36,0]},"s":{"a":0,"k":[82,82,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[840,840]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,1,0,0.5,1,1,0,1,1,1,0,0,1,0.5,0.5,1,0]}},"s":{"a":0,"k":[0,0]},"e":{"a":0,"k":[320,0]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 22","hd":false},{"ty":"tr","p":{"a":0,"k":[-92,-36]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":20,"op":180,"st":0,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"Shape Layer 36","parent":23,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":85,"s":[100]},{"t":156,"s":[0]}]},"p":{"a":0,"k":[-65.113,-69.948,0]},"a":{"a":0,"k":[-92,-36,0]},"s":{"a":0,"k":[82,82,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[840,840]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,1,0,0.5,1,1,0,1,1,1,0,0,1,0.5,0.5,1,0]}},"s":{"a":0,"k":[0,0]},"e":{"a":0,"k":[320,0]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 22","hd":false},{"ty":"tr","p":{"a":0,"k":[-92,-36]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":20,"op":180,"st":0,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"Shape Layer 29","parent":20,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":43,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":108,"s":[100]},{"t":151,"s":[0]}]},"p":{"a":0,"k":[-109.11,-8.841,0]},"a":{"a":0,"k":[-92,-36,0]},"s":{"a":0,"k":[88,88,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[840,840]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,0,0,0.5,1,0,0,1,1,0,0,0,1,0.5,0.5,1,0]}},"s":{"a":0,"k":[0,0]},"e":{"a":0,"k":[397,0]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 23","hd":false},{"ty":"tr","p":{"a":0,"k":[-92,-36]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":20,"op":180,"st":0,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"Shape Layer 32","parent":21,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":43,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":108,"s":[100]},{"t":162,"s":[0]}]},"p":{"a":0,"k":[-109.11,-8.841,0]},"a":{"a":0,"k":[-92,-36,0]},"s":{"a":0,"k":[88,88,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[840,840]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,0,0,0.5,1,0,0,1,1,0,0,0,1,0.5,0.5,1,0]}},"s":{"a":0,"k":[0,0]},"e":{"a":0,"k":[397,0]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 23","hd":false},{"ty":"tr","p":{"a":0,"k":[-92,-36]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":20,"op":180,"st":0,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"Shape Layer 38","parent":22,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":78,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":143,"s":[100]},{"t":170,"s":[0]}]},"p":{"a":0,"k":[-109.11,-8.841,0]},"a":{"a":0,"k":[-92,-36,0]},"s":{"a":0,"k":[88,88,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[840,840]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,0,0,0.5,1,0,0,1,1,0,0,0,1,0.5,0.5,1,0]}},"s":{"a":0,"k":[0,0]},"e":{"a":0,"k":[397,0]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 23","hd":false},{"ty":"tr","p":{"a":0,"k":[-92,-36]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":20,"op":180,"st":0,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":"Shape Layer 35","parent":23,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":85,"s":[100]},{"t":156,"s":[0]}]},"p":{"a":0,"k":[-109.11,-8.841,0]},"a":{"a":0,"k":[-92,-36,0]},"s":{"a":0,"k":[88,88,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[840,840]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,0,0,0.5,1,0,0,1,1,0,0,0,1,0.5,0.5,1,0]}},"s":{"a":0,"k":[0,0]},"e":{"a":0,"k":[397,0]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 23","hd":false},{"ty":"tr","p":{"a":0,"k":[-92,-36]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":20,"op":180,"st":0,"bm":0},{"ddd":0,"ind":20,"ty":4,"nm":"Shape Layer 25","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":43,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":108,"s":[100]},{"t":151,"s":[0]}]},"p":{"a":0,"k":[776,1244,0]},"a":{"a":0,"k":[-92,-36,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":43,"s":[146,52.56,100]},{"t":179,"s":[178,64.08,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[840,840]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":40},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.161,0,0,0.5,0.161,0,0,1,0.161,0,0,0,1,0.5,0.5,1,0]}},"s":{"a":0,"k":[0,0]},"e":{"a":0,"k":[397,0]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 24","hd":false},{"ty":"tr","p":{"a":0,"k":[-92,-36]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":20,"op":180,"st":0,"bm":0},{"ddd":0,"ind":21,"ty":4,"nm":"Shape Layer 31","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":43,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":108,"s":[100]},{"t":162,"s":[0]}]},"p":{"a":0,"k":[564,1004,0]},"a":{"a":0,"k":[-92,-36,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":20,"s":[106,38.16,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":43,"s":[106,38.16,100]},{"t":179,"s":[136,48.96,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[840,840]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":40},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.161,0,0,0.5,0.161,0,0,1,0.161,0,0,0,1,0.5,0.5,1,0]}},"s":{"a":0,"k":[0,0]},"e":{"a":0,"k":[397,0]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 24","hd":false},{"ty":"tr","p":{"a":0,"k":[-92,-36]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":20,"op":180,"st":0,"bm":0},{"ddd":0,"ind":22,"ty":4,"nm":"Shape Layer 37","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":78,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":143,"s":[100]},{"t":170,"s":[0]}]},"p":{"a":0,"k":[636,816,0]},"a":{"a":0,"k":[-92,-36,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":20,"s":[105,37.8,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":43,"s":[105,37.8,100]},{"t":179,"s":[126,45.36,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[840,840]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":40},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.161,0,0,0.5,0.161,0,0,1,0.161,0,0,0,1,0.5,0.5,1,0]}},"s":{"a":0,"k":[0,0]},"e":{"a":0,"k":[397,0]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 24","hd":false},{"ty":"tr","p":{"a":0,"k":[-92,-36]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":20,"op":180,"st":0,"bm":0},{"ddd":0,"ind":23,"ty":4,"nm":"Shape Layer 34","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":85,"s":[100]},{"t":156,"s":[0]}]},"p":{"a":0,"k":[580,504,0]},"a":{"a":0,"k":[-92,-36,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":20,"s":[70,25.2,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":43,"s":[70,25.2,100]},{"t":179,"s":[149,53.64,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[840,840]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":40},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.161,0,0,0.5,0.161,0,0,1,0.161,0,0,0,1,0.5,0.5,1,0]}},"s":{"a":0,"k":[0,0]},"e":{"a":0,"k":[397,0]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 24","hd":false},{"ty":"tr","p":{"a":0,"k":[-92,-36]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Ellipse 1","bm":0,"hd":false}],"ip":20,"op":180,"st":0,"bm":0}]},{"id":"comp_33","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Layer 10","sr":1,"ks":{"r":{"a":0,"k":-198},"p":{"a":0,"k":[268.825,243.194,0]},"a":{"a":0,"k":[10,10,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[59,59,100]},{"t":175,"s":[107,107,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,-104.934],[104.934,0],[0,104.934],[-104.934,0]],"o":[[0,104.934],[-104.934,0],[0,-104.934],[104.934,0]],"v":[[190,0],[0,190],[-190,0],[0,-190]],"c":true}},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[0,1,1,0,0.5,1,1,0,1,1,1,0,0.007,0,0.056,0,0.095,0,0.462,0.5,0.83,1,0.915,0.5,1,0]},{"t":150,"s":[0,0.937,0.22,0,0.5,0.937,0.22,0,1,0.937,0.221,0,0,0,0.336,0,0.673,0,0.817,0.5,0.935,1,0.968,0.5,1,0]}]}},"s":{"a":0,"k":[0,0]},"e":{"a":0,"k":[185,0]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 31","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[77,77]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Layer 8","sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-1]},{"t":179,"s":[-132]}]},"p":{"a":0,"k":[250,261,0]},"a":{"a":0,"k":[-10,-10,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[75,75,100]},{"t":179,"s":[104,104,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,-104.934],[104.934,0],[0,104.934],[-104.934,0]],"o":[[0,104.934],[-104.934,0],[0,-104.934],[104.934,0]],"v":[[190,0],[0,190],[-190,0],[0,-190]],"c":true}},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,0,0,0.5,1,0,0,1,1,0,0,0,1,0.5,0.5,1,0]}},"s":{"a":0,"k":[0,0]},"e":{"a":0,"k":[185,0]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 32","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Layer 7","sr":1,"ks":{"o":{"a":0,"k":40},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[1080]}]},"p":{"a":0,"k":[256,256,0]},"a":{"a":0,"k":[10,10,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":10,"s":[95,95,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":20,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":30,"s":[95,95,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":40,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":50,"s":[95,95,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":60,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":70,"s":[95,95,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":80,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":90,"s":[95,95,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":100,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":110,"s":[95,95,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":120,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":130,"s":[95,95,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":140,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":150,"s":[95,95,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":160,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":170,"s":[95,95,100]},{"t":180,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,-104.934],[104.934,0],[0,104.934],[-104.934,0]],"o":[[0,104.934],[-104.934,0],[0,-104.934],[104.934,0]],"v":[[190,0],[0,190],[-190,0],[0,-190]],"c":true}},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.161,0,0,0.5,0.161,0,0,1,0.161,0,0,0,1,0.5,0.5,1,0]}},"s":{"a":0,"k":[0,0]},"e":{"a":0,"k":[185,0]},"t":2,"h":{"a":0,"k":0},"a":{"a":0,"k":0},"nm":"Gradient Fill 1","hd":false},{"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},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"fire1536","refId":"comp_0","sr":1,"ks":{"p":{"a":0,"k":[256,256,0]},"a":{"a":0,"k":[768,768,0]},"s":{"a":0,"k":[33,33,100]}},"ao":0,"w":1536,"h":1536,"ip":0,"op":180,"st":0,"bm":0}]} \ No newline at end of file diff --git a/Tests/LottieMesh/Sources/ViewController.swift b/Tests/LottieMesh/Sources/ViewController.swift deleted file mode 100644 index 83251bb646..0000000000 --- a/Tests/LottieMesh/Sources/ViewController.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation -import UIKit - -import LottieMeshSwift -import Postbox - -public final class ViewController: UIViewController { - override public func viewDidLoad() { - super.viewDidLoad() - - TempBox.initializeShared(basePath: NSTemporaryDirectory(), processType: "test", launchSpecificId: Int64.random(in: Int64.min ..< Int64.max)) - - self.view.backgroundColor = .black - - //let path = Bundle.main.path(forResource: "SUPER Fire", ofType: "json")! - let path = Bundle.main.path(forResource: "Fireworks", ofType: "json")! - //let path = Bundle.main.path(forResource: "Cat", ofType: "json")! - /*for _ in 0 ..< 100 { - let _ = generateMeshAnimation(data: try! Data(contentsOf: URL(fileURLWithPath: path)))! - }*/ - - if #available(iOS 13.0, *) { - let startTime = CFAbsoluteTimeGetCurrent() - let animationFile = generateMeshAnimation(data: try! Data(contentsOf: URL(fileURLWithPath: path)))! - print("Time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0)") - let buffer = MeshReadBuffer(data: try! Data(contentsOf: URL(fileURLWithPath: animationFile.path))) - let animation = MeshAnimation.read(buffer: buffer) - - let renderer = MeshRenderer(wireframe: false)! - - renderer.frame = CGRect(origin: CGPoint(x: 0.0, y: 50.0), size: CGSize(width: 300.0, height: 300.0)) - self.view.addSubview(renderer) - - renderer.add(mesh: animation, offset: CGPoint(), loop: true) - } - } -} diff --git a/Tests/LottieMetalTest/.gitignore b/Tests/LottieMetalTest/.gitignore new file mode 100644 index 0000000000..31360466c1 --- /dev/null +++ b/Tests/LottieMetalTest/.gitignore @@ -0,0 +1,2 @@ +TestData/*.json + diff --git a/Tests/LottieMesh/BUILD b/Tests/LottieMetalTest/BUILD similarity index 58% rename from Tests/LottieMesh/BUILD rename to Tests/LottieMetalTest/BUILD index 40da9bebbd..1e9987af9f 100644 --- a/Tests/LottieMesh/BUILD +++ b/Tests/LottieMetalTest/BUILD @@ -10,6 +10,27 @@ load("//build-system/bazel-utils:plist_fragment.bzl", "plist_fragment", ) +load( + "@build_bazel_rules_apple//apple:resources.bzl", + "apple_resource_bundle", + "apple_resource_group", +) + +load( + "@rules_xcodeproj//xcodeproj:defs.bzl", + "top_level_target", + "top_level_targets", + "xcodeproj", + "xcode_provisioning_profile", +) + +load("@build_bazel_rules_apple//apple:apple.bzl", "local_provisioning_profile") + +load( + "@build_configuration//:variables.bzl", + "telegram_bazel_path", +) + filegroup( name = "AppResources", srcs = glob([ @@ -26,7 +47,14 @@ swift_library( ":AppResources", ], deps = [ - "//submodules/LottieMeshSwift:LottieMeshSwift", + "//submodules/Display", + "//submodules/MetalEngine", + "//submodules/TelegramUI/Components/LottieCpp", + "//submodules/TelegramUI/Components/LottieMetal", + "//submodules/rlottie:RLottieBinding", + "//Tests/LottieMetalTest/QOILoader", + "//Tests/LottieMetalTest/SoftwareLottieRenderer", + "//Tests/LottieMetalTest/LottieSwift", ], ) @@ -72,7 +100,7 @@ plist_fragment( CFBundleDisplayName Test CFBundleIdentifier - org.telegram.LottieMesh + ph.telegra.Telegraph CFBundleName Telegram CFBundlePackageType @@ -127,12 +155,44 @@ plist_fragment( """ ) +filegroup( + name = "TestDataBundleFiles", + srcs = glob([ + "TestData/*.json", + ]), + visibility = ["//visibility:public"], +) + +plist_fragment( + name = "TestDataBundleInfoPlist", + extension = "plist", + template = + """ + CFBundleIdentifier + org.telegram.TestDataBundle + CFBundleDevelopmentRegion + en + CFBundleName + TestDataBundle + """ +) + +apple_resource_bundle( + name = "TestDataBundle", + infoplists = [ + ":TestDataBundleInfoPlist", + ], + resources = [ + ":TestDataBundleFiles", + ], +) + ios_application( - name = "LottieMesh", - bundle_id = "org.telegram.LottieMesh", + name = "LottieMetalTest", + bundle_id = "ph.telegra.Telegraph", families = ["iphone", "ipad"], - minimum_os_version = "9.0", - provisioning_profile = "@build_configuration//provisioning:Wildcard.mobileprovision", + minimum_os_version = "12.0", + provisioning_profile = "@build_configuration//provisioning:Telegram.mobileprovision", infoplists = [ ":AppInfoPlist", ":BuildNumberInfoPlist", @@ -140,6 +200,7 @@ ios_application( ], resources = [ "//Tests/Common:LaunchScreen", + ":TestDataBundle", ], frameworks = [ ], @@ -147,4 +208,28 @@ ios_application( "//Tests/Common:Main", ":Lib", ], + visibility = ["//visibility:public"], +) + +xcodeproj( + name = "LottieMetalTest_xcodeproj", + build_mode = "bazel", + bazel_path = telegram_bazel_path, + project_name = "LottieMetalTest", + tags = ["manual"], + top_level_targets = top_level_targets( + labels = [ + ":LottieMetalTest", + ], + target_environments = ["device", "simulator"], + ), + xcode_configurations = { + "Debug": { + "//command_line_option:compilation_mode": "dbg", + }, + "Release": { + "//command_line_option:compilation_mode": "opt", + }, + }, + default_xcode_configuration = "Debug" ) diff --git a/Tests/LottieMetalTest/LottieSwift/BUILD b/Tests/LottieMetalTest/LottieSwift/BUILD new file mode 100644 index 0000000000..01dac11776 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/BUILD @@ -0,0 +1,14 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", + "swift_library", +) + +swift_library( + name = "LottieSwift", + module_name = "LottieSwift", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + ], + visibility = ["//visibility:public"], +) diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/CAAnimation+TimingConfiguration.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/CAAnimation+TimingConfiguration.swift new file mode 100644 index 0000000000..66e221ff22 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/CAAnimation+TimingConfiguration.swift @@ -0,0 +1,76 @@ +// Created by Cal Stephens on 1/6/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +import QuartzCore + +extension CAAnimation { + /// Creates a `CAAnimation` that wraps this animation, + /// applying timing-related configuration from the given `LayerAnimationContext` + @nonobjc + func timed(with context: LayerAnimationContext, for layer: CALayer) -> CAAnimation { + + // The base animation always has the duration of the full animation, + // since that's the time space where keyframing and interpolating happens. + // So we start with a simple animation timeline from 0% to 100%: + // + // ┌──────────────────────────────────┐ + // │ baseAnimation │ + // └──────────────────────────────────┘ + // 0% 100% + // + let baseAnimation = self + baseAnimation.duration = context.animation.duration + baseAnimation.speed = (context.endFrame < context.startFrame) ? -1 : 1 + + // To select the subrange of the `baseAnimation` that should be played, + // we create a parent animation with the duration of that subrange + // to clip the `baseAnimation`. This parent animation can then loop + // and/or autoreverse over the clipped subrange. + // + // ┌────────────────────┬───────► + // │ clippingParent │ ... + // └────────────────────┴───────► + // 25% 75% + // ┌──────────────────────────────────┐ + // │ baseAnimation │ + // └──────────────────────────────────┘ + // 0% 100% + // + let clippingParent = CAAnimationGroup() + clippingParent.animations = [baseAnimation] + + clippingParent.duration = Double(abs(context.endFrame - context.startFrame)) / context.animation.framerate + baseAnimation.timeOffset = context.animation.time(forFrame: context.startFrame) + + clippingParent.autoreverses = context.timingConfiguration.autoreverses + clippingParent.repeatCount = context.timingConfiguration.repeatCount + clippingParent.timeOffset = context.timingConfiguration.timeOffset + + // Once the animation ends, it should pause on the final frame + clippingParent.fillMode = .both + clippingParent.isRemovedOnCompletion = false + + // We can pause the animation on a specific frame by setting the root layer's + // `speed` to 0, and then setting the `timeOffset` for the given frame. + // - For that setup to work properly, we have to set the `beginTime` + // of this animation to a time slightly before the current time. + // - It's not really clear why this is necessary, but `timeOffset` + // is not applied correctly without this configuration. + // - We can't do this when playing the animation in real time, + // because it can cause keyframe timings to be incorrect. + if context.timingConfiguration.speed == 0 { + let currentTime = layer.convertTime(CACurrentMediaTime(), from: nil) + clippingParent.beginTime = currentTime - .leastNonzeroMagnitude + } + + return clippingParent + } +} + +extension CALayer { + /// Adds the given animation to this layer, timed with the given timing configuration + @nonobjc + func add(_ animation: CAPropertyAnimation, timedWith context: LayerAnimationContext) { + add(animation.timed(with: context, for: self), forKey: animation.keyPath) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/CALayer+addAnimation.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/CALayer+addAnimation.swift new file mode 100644 index 0000000000..5a4d1b1214 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/CALayer+addAnimation.swift @@ -0,0 +1,315 @@ +// Created by Cal Stephens on 12/14/21. +// Copyright © 2021 Airbnb Inc. All rights reserved. + +import QuartzCore + +extension CALayer { + + // MARK: Internal + + /// Constructs a `CAKeyframeAnimation` that reflects the given keyframes, + /// and adds it to this `CALayer`. + @nonobjc + func addAnimation( + for property: LayerProperty, + keyframes: ContiguousArray>, + value keyframeValueMapping: (KeyframeValue) throws -> ValueRepresentation, + context: LayerAnimationContext) + throws + { + if let customAnimation = try customizedAnimation(for: property, context: context) { + add(customAnimation, timedWith: context) + } + + else if + let defaultAnimation = try defaultAnimation( + for: property, + keyframes: keyframes, + value: keyframeValueMapping, + context: context) + { + add(defaultAnimation, timedWith: context) + } + } + + // MARK: Private + + /// Constructs a `CAAnimation` that reflects the given keyframes + /// - If the value can be applied directly to the CALayer using KVC, + /// then no `CAAnimation` will be created and the value will be applied directly. + @nonobjc + private func defaultAnimation( + for property: LayerProperty, + keyframes: ContiguousArray>, + value keyframeValueMapping: (KeyframeValue) throws -> ValueRepresentation, + context: LayerAnimationContext) + throws + -> CAPropertyAnimation? + { + guard !keyframes.isEmpty else { return nil } + + // If there is exactly one keyframe value, we can improve performance + // by applying that value directly to the layer instead of creating + // a relatively expensive `CAKeyframeAnimation`. + if keyframes.count == 1 { + let keyframeValue = try keyframeValueMapping(keyframes[0].value) + + // If the keyframe value is the same as the layer's default value for this property, + // then we can just ignore this set of keyframes. + if keyframeValue == property.defaultValue { + return nil + } + + // If the property on the CALayer being animated hasn't been modified from the default yet, + // then we can apply the keyframe value directly to the layer using KVC instead + // of creating a `CAAnimation`. + if + let defaultValue = property.defaultValue, + defaultValue == value(forKey: property.caLayerKeypath) as? ValueRepresentation + { + setValue(keyframeValue, forKeyPath: property.caLayerKeypath) + return nil + } + + // Otherwise, we still need to create a `CAAnimation`, but we can + // create a simple `CABasicAnimation` that is still less expensive + // than computing a `CAKeyframeAnimation`. + let animation = CABasicAnimation(keyPath: property.caLayerKeypath) + animation.fromValue = keyframeValue + animation.toValue = keyframeValue + return animation + } + + return try keyframeAnimation( + for: property, + keyframes: keyframes, + value: keyframeValueMapping, + context: context) + } + + /// A `CAAnimation` that applies the custom value from the `AnyValueProvider` + /// registered for this specific property's `AnimationKeypath`, + /// if one has been registered using `AnimationView.setValueProvider(_:keypath:)`. + @nonobjc + private func customizedAnimation( + for property: LayerProperty, + context: LayerAnimationContext) + throws + -> CAPropertyAnimation? + { + guard + let customizableProperty = property.customizableProperty, + let customKeyframes = try context.valueProviderStore.customKeyframes( + of: customizableProperty, + for: AnimationKeypath(keys: context.currentKeypath.keys + customizableProperty.name.map { $0.rawValue }), + context: context) + else { return nil } + + // Since custom animations are overriding an existing animation, + // we always have to create a CAKeyframeAnimation for these instead of + // letting `defaultAnimation(...)` try to apply the value using KVC. + return try keyframeAnimation( + for: property, + keyframes: customKeyframes.keyframes, + value: { $0 }, + context: context) + } + + /// Creates a `CAKeyframeAnimation` for the given keyframes + private func keyframeAnimation( + for property: LayerProperty, + keyframes: ContiguousArray>, + value keyframeValueMapping: (KeyframeValue) throws -> ValueRepresentation, + context: LayerAnimationContext) + throws + -> CAKeyframeAnimation + { + // Convert the list of `Keyframe` into + // the representation used by `CAKeyframeAnimation` + var keyTimes = keyframes.map { keyframeModel -> NSNumber in + NSNumber(value: Float(context.progressTime(for: keyframeModel.time))) + } + + var timingFunctions = self.timingFunctions(for: keyframes) + let calculationMode = try self.calculationMode(for: keyframes, context: context) + + let animation = CAKeyframeAnimation(keyPath: property.caLayerKeypath) + + // Position animations define a `CGPath` curve that should be followed, + // instead of animating directly between keyframe point values. + if property.caLayerKeypath == LayerProperty.position.caLayerKeypath { + animation.path = try path(keyframes: keyframes, value: { value in + guard let point = try keyframeValueMapping(value) as? CGPoint else { + LottieLogger.shared.assertionFailure("Cannot create point from keyframe with value \(value)") + return .zero + } + + return point + }) + } + + // All other types of keyframes provide individual values that are interpolated by Core Animation + else { + var values = try keyframes.map { keyframeModel in + try keyframeValueMapping(keyframeModel.value) + } + + validate(values: &values, keyTimes: &keyTimes, timingFunctions: &timingFunctions, for: calculationMode) + animation.values = values + } + + animation.calculationMode = calculationMode + animation.keyTimes = keyTimes + animation.timingFunctions = timingFunctions + return animation + } + + /// The `CAAnimationCalculationMode` that should be used for a `CAKeyframeAnimation` + /// animating the given keyframes + private func calculationMode( + for keyframes: ContiguousArray>, + context: LayerAnimationContext) + throws + -> CAAnimationCalculationMode + { + // Animations using `isHold` should use `CAAnimationCalculationMode.discrete` + // + // - Since we currently only create a single `CAKeyframeAnimation`, + // we can currently only correctly support animations where + // `isHold` is either always `true` or always `false` + // (this requirement doesn't apply to the first/last keyframes). + // + // - We should be able to support this in the future by creating multiple + // `CAKeyframeAnimation`s with different `calculationMode`s and + // playing them sequentially. + // + let intermediateKeyframes = keyframes.dropFirst().dropLast() + if intermediateKeyframes.contains(where: \.isHold) { + if intermediateKeyframes.allSatisfy(\.isHold) { + return .discrete + } else { + try context.logCompatibilityIssue("Mixed `isHold` / `!isHold` keyframes are currently unsupported") + } + } + + return .linear + } + + /// `timingFunctions` to apply to a `CAKeyframeAnimation` animating the given keyframes + private func timingFunctions( + for keyframes: ContiguousArray>) + -> [CAMediaTimingFunction] + { + // Compute the timing function between each keyframe and the subsequent keyframe + var timingFunctions: [CAMediaTimingFunction] = [] + + for (index, keyframe) in keyframes.enumerated() + where index != keyframes.indices.last + { + let nextKeyframe = keyframes[index + 1] + + let controlPoint1 = keyframe.outTangent?.pointValue ?? .zero + let controlPoint2 = nextKeyframe.inTangent?.pointValue ?? CGPoint(x: 1, y: 1) + + timingFunctions.append(CAMediaTimingFunction( + controlPoints: + Float(controlPoint1.x), + Float(controlPoint1.y), + Float(controlPoint2.x), + Float(controlPoint2.y))) + } + + return timingFunctions + } + + /// Creates a `CGPath` for the given `position` keyframes, + /// which accounts for `spatialInTangent`s and `spatialOutTangents` + private func path( + keyframes positionKeyframes: ContiguousArray>, + value keyframeValueMapping: (KeyframeValue) throws -> CGPoint) rethrows + -> CGPath { + let path = CGMutablePath() + + for (index, keyframe) in positionKeyframes.enumerated() { + if index == positionKeyframes.indices.first { + path.move(to: try keyframeValueMapping(keyframe.value)) + } + + if index != positionKeyframes.indices.last { + let nextKeyframe = positionKeyframes[index + 1] + + if + let controlPoint1 = keyframe.spatialOutTangent?.pointValue, + let controlPoint2 = nextKeyframe.spatialInTangent?.pointValue, + controlPoint1 != .zero, + controlPoint2 != .zero + { + path.addCurve( + to: try keyframeValueMapping(nextKeyframe.value), + control1: try keyframeValueMapping(keyframe.value) + controlPoint1, + control2: try keyframeValueMapping(nextKeyframe.value) + controlPoint2) + } + + else { + path.addLine(to: try keyframeValueMapping(nextKeyframe.value)) + } + } + } + + path.closeSubpath() + return path + } + + /// Validates that the requirements of the `CAKeyframeAnimation` API are met correctly + private func validate( + values: inout [ValueRepresentation], + keyTimes: inout [NSNumber], + timingFunctions: inout [CAMediaTimingFunction], + for calculationMode: CAAnimationCalculationMode) + { + // Validate that we have correct start (0.0) and end (1.0) keyframes. + // From the documentation of `CAKeyframeAnimation.keyTimes`: + // - The first value in the `keyTimes` array must be 0.0 and the last value must be 1.0. + if keyTimes.first != 0.0 { + keyTimes.insert(0.0, at: 0) + values.insert(values[0], at: 0) + timingFunctions.insert(CAMediaTimingFunction(name: .linear), at: 0) + } + + if keyTimes.last != 1.0 { + keyTimes.append(1.0) + values.append(values.last!) + timingFunctions.append(CAMediaTimingFunction(name: .linear)) + } + + switch calculationMode { + case .linear, .cubic: + // From the documentation of `CAKeyframeAnimation.keyTimes`: + // - The number of elements in the keyTimes array + // should match the number of elements in the values property + LottieLogger.shared.assert( + values.count == keyTimes.count, + "`values.count` must exactly equal `keyTimes.count`") + + LottieLogger.shared.assert( + timingFunctions.count == (values.count - 1), + "`timingFunctions.count` must exactly equal `values.count - 1`") + + case .discrete: + // From the documentation of `CAKeyframeAnimation.keyTimes`: + // - If the calculationMode is set to discrete... the keyTimes array + // should have one more entry than appears in the values array. + values.removeLast() + + LottieLogger.shared.assert( + keyTimes.count == values.count + 1, + "`keyTimes.count` must exactly equal `values.count + 1`") + + default: + LottieLogger.shared.assertionFailure(""" + Unexpected keyframe calculation mode \(calculationMode) + """) + } + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/CombinedShapeAnimation.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/CombinedShapeAnimation.swift new file mode 100644 index 0000000000..9aadf86163 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/CombinedShapeAnimation.swift @@ -0,0 +1,52 @@ +// Created by Cal Stephens on 1/28/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +import QuartzCore + +extension CAShapeLayer { + /// Adds animations for the given `CombinedShapeItem` to this `CALayer` + @nonobjc + func addAnimations( + for combinedShapes: CombinedShapeItem, + context: LayerAnimationContext) + throws + { + try addAnimation( + for: .path, + keyframes: combinedShapes.shapes.keyframes, + value: { paths in + let combinedPath = CGMutablePath() + for path in paths { + combinedPath.addPath(path.cgPath()) + } + return combinedPath + }, + context: context) + } +} + +// MARK: - CombinedShapeItem + +/// A custom `ShapeItem` subclass that combines multiple `Shape`s into a single `KeyframeGroup` +final class CombinedShapeItem: ShapeItem { + + // MARK: Lifecycle + + init(shapes: KeyframeGroup<[BezierPath]>, name: String) { + self.shapes = shapes + super.init(name: name, type: .shape, hidden: false) + } + + required init(from _: Decoder) throws { + fatalError("init(from:) has not been implemented") + } + + required init(dictionary _: [String: Any]) throws { + fatalError("init(dictionary:) has not been implemented") + } + + // MARK: Internal + + let shapes: KeyframeGroup<[BezierPath]> + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/CustomPathAnimation.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/CustomPathAnimation.swift new file mode 100644 index 0000000000..28262e98a8 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/CustomPathAnimation.swift @@ -0,0 +1,22 @@ +// Created by Cal Stephens on 12/21/21. +// Copyright © 2021 Airbnb Inc. All rights reserved. + +import QuartzCore + +extension CAShapeLayer { + /// Adds animations for the given `BezierPath` keyframes to this `CALayer` + @nonobjc + func addAnimations( + for customPath: KeyframeGroup, + context: LayerAnimationContext) + throws + { + try addAnimation( + for: .path, + keyframes: customPath.keyframes, + value: { pathKeyframe in + pathKeyframe.cgPath() + }, + context: context) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/EllipseAnimation.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/EllipseAnimation.swift new file mode 100644 index 0000000000..383f3ae451 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/EllipseAnimation.swift @@ -0,0 +1,26 @@ +// Created by Cal Stephens on 12/21/21. +// Copyright © 2021 Airbnb Inc. All rights reserved. + +import QuartzCore + +extension CAShapeLayer { + /// Adds animations for the given `Ellipse` to this `CALayer` + @nonobjc + func addAnimations( + for ellipse: Ellipse, + context: LayerAnimationContext) + throws + { + try addAnimation( + for: .path, + keyframes: ellipse.size.keyframes, + value: { sizeKeyframe in + BezierPath.ellipse( + size: sizeKeyframe.sizeValue, + center: try ellipse.position.exactlyOneKeyframe(context: context, description: "ellipse position").value.pointValue, + direction: ellipse.direction) + .cgPath() + }, + context: context) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/GradientAnimations.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/GradientAnimations.swift new file mode 100644 index 0000000000..d02bda6b8f --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/GradientAnimations.swift @@ -0,0 +1,146 @@ +// Created by Cal Stephens on 1/7/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +import QuartzCore + +// MARK: - GradientShapeItem + +/// A `ShapeItem` that represents a gradient +protocol GradientShapeItem: OpacityAnimationModel { + var startPoint: KeyframeGroup { get } + var endPoint: KeyframeGroup { get } + var gradientType: GradientType { get } + var numberOfColors: Int { get } + var colors: KeyframeGroup<[Double]> { get } +} + +// MARK: - GradientFill + GradientShapeItem + +extension GradientFill: GradientShapeItem { } + +// MARK: - GradientStroke + GradientShapeItem + +extension GradientStroke: GradientShapeItem { } + +// MARK: - GradientRenderLayer + GradientShapeItem + +extension GradientRenderLayer { + + // MARK: Internal + + /// Adds gradient-related animations to this layer, from the given `GradientFill` + func addGradientAnimations(for gradient: GradientShapeItem, context: LayerAnimationContext) throws { + // We have to set `colors` to a non-nil value with some valid number of colors + // for the color animation below to have any effect + colors = .init( + repeating: CGColor.rgb(0, 0, 0), + count: gradient.numberOfColors) + + try addAnimation( + for: .colors, + keyframes: gradient.colors.keyframes, + value: { colorComponents in + gradient.colorConfiguration(from: colorComponents).map { $0.color } + }, + context: context) + + try addAnimation( + for: .locations, + keyframes: gradient.colors.keyframes, + value: { colorComponents in + gradient.colorConfiguration(from: colorComponents).map { $0.location } + }, + context: context) + + try addOpacityAnimation(for: gradient, context: context) + + switch gradient.gradientType { + case .linear: + try addLinearGradientAnimations(for: gradient, context: context) + case .radial: + try addRadialGradientAnimations(for: gradient, context: context) + case .none: + break + } + } + + // MARK: Private + + private func addLinearGradientAnimations( + for gradient: GradientShapeItem, + context: LayerAnimationContext) + throws + { + type = .axial + + try addAnimation( + for: .startPoint, + keyframes: gradient.startPoint.keyframes, + value: { absoluteStartPoint in + percentBasedPointInBounds(from: absoluteStartPoint.pointValue) + }, + context: context) + + try addAnimation( + for: .endPoint, + keyframes: gradient.endPoint.keyframes, + value: { absoluteEndPoint in + percentBasedPointInBounds(from: absoluteEndPoint.pointValue) + }, + context: context) + } + + private func addRadialGradientAnimations(for gradient: GradientShapeItem, context: LayerAnimationContext) throws { + type = .radial + + // To draw the correct gradients, we have to derive a custom `endPoint` + // relative to the `startPoint` value. Since calculating the `endPoint` + // at any given time requires knowing the current `startPoint`, + // we can't allow them to animate separately. + let absoluteStartPoint = try gradient.startPoint + .exactlyOneKeyframe(context: context, description: "gradient startPoint").value.pointValue + + let absoluteEndPoint = try gradient.endPoint + .exactlyOneKeyframe(context: context, description: "gradient endPoint").value.pointValue + + startPoint = percentBasedPointInBounds(from: absoluteStartPoint) + + let radius = absoluteStartPoint.distanceTo(absoluteEndPoint) + endPoint = percentBasedPointInBounds( + from: CGPoint( + x: absoluteStartPoint.x + radius, + y: absoluteStartPoint.y + radius)) + } +} + +extension GradientShapeItem { + /// Converts the compact `[Double]` color components representation + /// into an array of `CGColor`s and the location of those colors within the gradient + fileprivate func colorConfiguration( + from colorComponents: [Double]) + -> [(color: CGColor, location: CGFloat)] + { + precondition( + colorComponents.count >= numberOfColors * 4, + "Each color must have RGB components and a location component") + + var cgColors = [(color: CGColor, location: CGFloat)]() + + // Each group of four `Double` values represents a single `CGColor`, + // and its relative location within the gradient. + for colorIndex in 0.. { + /// The `CALayer` KVC key path that this value should be assigned to + let caLayerKeypath: String + + /// The default value of this property on a `CALayer` + /// - If the keyframe values are just equal to the default value, + /// then we can improve performance a bit by just not creating + /// a CAAnimation (since it would be redundant). + let defaultValue: ValueRepresentation? + + /// A description of how this property can be customized dynamically + /// at runtime using `AnimationView.setValueProvider(_:keypath:)` + let customizableProperty: CustomizableProperty? +} + +// MARK: - CustomizableProperty + +/// A description of how a `CALayer` property can be customized dynamically +/// at runtime using `AnimationView.setValueProvider(_:keypath:)` +struct CustomizableProperty { + /// The name that `AnimationKeypath`s can use to refer to this property + /// - When building an animation for this property that will be applied + /// to a specific layer, this `name` is appended to the end of that + /// layer's `AnimationKeypath`. The combined keypath is used to query + /// the `ValueProviderStore`. + let name: [PropertyName] + + /// A closure that coverts the type-erased value of an `AnyValueProvider` + /// to the strongly-typed representation used by this property, if possible. + let conversion: (Any) -> ValueRepresentation? +} + +// MARK: - PropertyName + +/// The name of a customizable property that can be used in an `AnimationKeypath` +/// - These values should be shared between the two rendering engines, +/// since they form the public API of the `AnimationKeypath` system. +enum PropertyName: String { + case color = "Color" +} + +// MARK: CALayer properties + +extension LayerProperty { + static var position: LayerProperty { + .init( + caLayerKeypath: "transform.translation", + defaultValue: CGPoint(x: 0, y: 0), + customizableProperty: nil /* currently unsupported */) + } + + static var positionX: LayerProperty { + .init( + caLayerKeypath: "transform.translation.x", + defaultValue: 0, + customizableProperty: nil /* currently unsupported */) + } + + static var positionY: LayerProperty { + .init( + caLayerKeypath: "transform.translation.y", + defaultValue: 0, + customizableProperty: nil /* currently unsupported */) + } + + static var scale: LayerProperty { + .init( + caLayerKeypath: "transform.scale", + defaultValue: 1, + customizableProperty: nil /* currently unsupported */) + } + + static var scaleX: LayerProperty { + .init( + caLayerKeypath: "transform.scale.x", + defaultValue: 1, + customizableProperty: nil /* currently unsupported */) + } + + static var scaleY: LayerProperty { + .init( + caLayerKeypath: "transform.scale.y", + defaultValue: 1, + customizableProperty: nil /* currently unsupported */) + } + + static var rotation: LayerProperty { + .init( + caLayerKeypath: "transform.rotation", + defaultValue: 0, + customizableProperty: nil /* currently unsupported */) + } + + static var rotationY: LayerProperty { + .init( + caLayerKeypath: "transform.rotation.y", + defaultValue: 0, + customizableProperty: nil /* currently unsupported */) + } + + static var anchorPoint: LayerProperty { + .init( + caLayerKeypath: #keyPath(CALayer.anchorPoint), + // This is intentionally not `GGPoint(x: 0.5, y: 0.5)` (the actual default) + // to opt `anchorPoint` out of the KVC `setValue` flow, which causes issues. + defaultValue: nil, + customizableProperty: nil /* currently unsupported */) + } + + static var opacity: LayerProperty { + .init( + caLayerKeypath: #keyPath(CALayer.opacity), + defaultValue: 1, + customizableProperty: nil /* currently unsupported */) + } +} + +// MARK: CAShapeLayer properties + +extension LayerProperty { + static var path: LayerProperty { + .init( + caLayerKeypath: #keyPath(CAShapeLayer.path), + defaultValue: nil, + customizableProperty: nil /* currently unsupported */) + } + + static var fillColor: LayerProperty { + .init( + caLayerKeypath: #keyPath(CAShapeLayer.fillColor), + defaultValue: nil, + customizableProperty: .color) + } + + static var lineWidth: LayerProperty { + .init( + caLayerKeypath: #keyPath(CAShapeLayer.lineWidth), + defaultValue: 1, + customizableProperty: nil /* currently unsupported */) + } + + static var lineDashPhase: LayerProperty { + .init( + caLayerKeypath: #keyPath(CAShapeLayer.lineDashPhase), + defaultValue: 0, + customizableProperty: nil /* currently unsupported */) + } + + static var strokeColor: LayerProperty { + .init( + caLayerKeypath: #keyPath(CAShapeLayer.strokeColor), + defaultValue: nil, + customizableProperty: .color) + } + + static var strokeStart: LayerProperty { + .init( + caLayerKeypath: #keyPath(CAShapeLayer.strokeStart), + defaultValue: 0, + customizableProperty: nil /* currently unsupported */) + } + + static var strokeEnd: LayerProperty { + .init( + caLayerKeypath: #keyPath(CAShapeLayer.strokeEnd), + defaultValue: 1, + customizableProperty: nil /* currently unsupported */) + } +} + +// MARK: CAGradientLayer properties + +extension LayerProperty { + static var colors: LayerProperty<[CGColor]> { + .init( + caLayerKeypath: #keyPath(CAGradientLayer.colors), + defaultValue: nil, + customizableProperty: nil /* currently unsupported */) + } + + static var locations: LayerProperty<[CGFloat]> { + .init( + caLayerKeypath: #keyPath(CAGradientLayer.locations), + defaultValue: nil, + customizableProperty: nil /* currently unsupported */) + } + + static var startPoint: LayerProperty { + .init( + caLayerKeypath: #keyPath(CAGradientLayer.startPoint), + defaultValue: nil, + customizableProperty: nil /* currently unsupported */) + } + + static var endPoint: LayerProperty { + .init( + caLayerKeypath: #keyPath(CAGradientLayer.endPoint), + defaultValue: nil, + customizableProperty: nil /* currently unsupported */) + } +} + +// MARK: - CustomizableProperty types + +extension CustomizableProperty { + static var color: CustomizableProperty { + .init( + name: [.color], + conversion: { typeErasedValue in + guard let color = typeErasedValue as? Color else { + return nil + } + + return .rgba(CGFloat(color.r), CGFloat(color.g), CGFloat(color.b), CGFloat(color.a)) + }) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/OpacityAnimation.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/OpacityAnimation.swift new file mode 100644 index 0000000000..d3e81d07df --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/OpacityAnimation.swift @@ -0,0 +1,52 @@ +// Created by Cal Stephens on 5/17/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +import QuartzCore + +// MARK: - OpacityAnimationModel + +protocol OpacityAnimationModel { + /// The opacity animation to apply to a `CALayer` + var opacity: KeyframeGroup { get } +} + +// MARK: - Transform + OpacityAnimationModel + +extension Transform: OpacityAnimationModel { } + +// MARK: - ShapeTransform + OpacityAnimationModel + +extension ShapeTransform: OpacityAnimationModel { } + +// MARK: - Fill + OpacityAnimationModel + +extension Fill: OpacityAnimationModel { } + +// MARK: - GradientFill + OpacityAnimationModel + +extension GradientFill: OpacityAnimationModel { } + +// MARK: - Stroke + OpacityAnimationModel + +extension Stroke: OpacityAnimationModel { } + +// MARK: - GradientStroke + OpacityAnimationModel + +extension GradientStroke: OpacityAnimationModel { } + +extension CALayer { + /// Adds the opacity animation from the given `OpacityAnimationModel` to this layer + @nonobjc + func addOpacityAnimation(for opacity: OpacityAnimationModel, context: LayerAnimationContext) throws { + try addAnimation( + for: .opacity, + keyframes: opacity.opacity.keyframes, + value: { + // Lottie animation files express opacity as a numerical percentage value + // (e.g. 0%, 50%, 100%) so we divide by 100 to get the decimal values + // expected by Core Animation (e.g. 0.0, 0.5, 1.0). + $0.cgFloatValue / 100 + }, + context: context) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/RectangleAnimation.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/RectangleAnimation.swift new file mode 100644 index 0000000000..2dd3ff5277 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/RectangleAnimation.swift @@ -0,0 +1,29 @@ +// Created by Cal Stephens on 12/21/21. +// Copyright © 2021 Airbnb Inc. All rights reserved. + +import QuartzCore + +extension CAShapeLayer { + /// Adds animations for the given `Rectangle` to this `CALayer` + @nonobjc + func addAnimations( + for rectangle: Rectangle, + context: LayerAnimationContext) + throws + { + try addAnimation( + for: .path, + keyframes: rectangle.size.keyframes, + value: { sizeKeyframe in + BezierPath.rectangle( + position: try rectangle.position + .exactlyOneKeyframe(context: context, description: "rectangle position").value.pointValue, + size: sizeKeyframe.sizeValue, + cornerRadius: try rectangle.cornerRadius + .exactlyOneKeyframe(context: context, description: "rectangle cornerRadius").value.cgFloatValue, + direction: rectangle.direction) + .cgPath() + }, + context: context) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/ShapeAnimation.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/ShapeAnimation.swift new file mode 100644 index 0000000000..f439320c73 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/ShapeAnimation.swift @@ -0,0 +1,123 @@ +// Created by Cal Stephens on 1/7/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +import QuartzCore + +extension CAShapeLayer { + /// Adds a `path` animation for the given `ShapeItem` + @nonobjc + func addAnimations(for shape: ShapeItem, context: LayerAnimationContext) throws { + switch shape { + case let customShape as Shape: + try addAnimations(for: customShape.path, context: context) + + case let combinedShape as CombinedShapeItem: + try addAnimations(for: combinedShape, context: context) + + case let ellipse as Ellipse: + try addAnimations(for: ellipse, context: context) + + case let rectangle as Rectangle: + try addAnimations(for: rectangle, context: context) + + case let star as Star: + try addAnimations(for: star, context: context) + + default: + // None of the other `ShapeItem` subclasses draw a `path` + try context.logCompatibilityIssue("Unexpected shape type \(type(of: shape))") + return + } + } + + /// Adds a `fillColor` animation for the given `Fill` object + @nonobjc + func addAnimations(for fill: Fill, context: LayerAnimationContext) throws { + fillRule = fill.fillRule.caFillRule + + try addAnimation( + for: .fillColor, + keyframes: fill.color.keyframes, + value: \.cgColorValue, + context: context) + + try addOpacityAnimation(for: fill, context: context) + } + + /// Adds animations for `strokeStart` and `strokeEnd` from the given `Trim` object + @nonobjc + func addAnimations(for trim: Trim, context: LayerAnimationContext) throws { + let (strokeStartKeyframes, strokeEndKeyframes) = trim.caShapeLayerKeyframes() + + if trim.offset.keyframes.contains(where: { $0.value.cgFloatValue != 0 }) { + try context.logCompatibilityIssue(""" + The CoreAnimation rendering engine doesn't support Trim offsets + """) + } + + try addAnimation( + for: .strokeStart, + keyframes: strokeStartKeyframes.keyframes, + value: { strokeStart in + // Lottie animation files express stoke trims as a numerical percentage value + // (e.g. 25%, 50%, 100%) so we divide by 100 to get the decimal values + // expected by Core Animation (e.g. 0.25, 0.5, 1.0). + CGFloat(strokeStart.cgFloatValue) / 100 + }, context: context) + + try addAnimation( + for: .strokeEnd, + keyframes: strokeEndKeyframes.keyframes, + value: { strokeEnd in + // Lottie animation files express stoke trims as a numerical percentage value + // (e.g. 25%, 50%, 100%) so we divide by 100 to get the decimal values + // expected by Core Animation (e.g. 0.25, 0.5, 1.0). + CGFloat(strokeEnd.cgFloatValue) / 100 + }, context: context) + } +} + +extension Trim { + + // MARK: Fileprivate + + /// The `strokeStart` and `strokeEnd` keyframes to apply to a `CAShapeLayer` + /// - `CAShapeLayer` requires that `strokeStart` be less than `strokeEnd`. + /// - Since this isn't a requirement in the Lottie schema, there are + /// some animations that have `strokeStart` and `strokeEnd` flipped. + /// - If we detect that this is the case for this specific `Trim`, then + /// we swap the start/end keyframes to match what `CAShapeLayer` expects. + fileprivate func caShapeLayerKeyframes() + -> (strokeStart: KeyframeGroup, strokeEnd: KeyframeGroup) + { + if startValueIsAlwaysGreaterThanEndValue() { + return (strokeStart: end, strokeEnd: start) + } else { + return (strokeStart: start, strokeEnd: end) + } + } + + // MARK: Private + + /// Checks whether or not the value for `trim.start` is greater + /// than the value for every `trim.end` at every keyframe. + private func startValueIsAlwaysGreaterThanEndValue() -> Bool { + let keyframeTimes = Set(start.keyframes.map { $0.time } + end.keyframes.map { $0.time }) + + let startInterpolator = KeyframeInterpolator(keyframes: start.keyframes) + let endInterpolator = KeyframeInterpolator(keyframes: end.keyframes) + + for keyframeTime in keyframeTimes { + guard + let startAtTime = startInterpolator.value(frame: keyframeTime) as? Vector1D, + let endAtTime = endInterpolator.value(frame: keyframeTime) as? Vector1D + else { continue } + + if startAtTime.cgFloatValue < endAtTime.cgFloatValue { + return false + } + } + + return true + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/StarAnimation.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/StarAnimation.swift new file mode 100644 index 0000000000..b1c50ca21f --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/StarAnimation.swift @@ -0,0 +1,90 @@ +// Created by Cal Stephens on 1/10/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +import QuartzCore + +extension CAShapeLayer { + + // MARK: Internal + + /// Adds animations for the given `Rectangle` to this `CALayer` + @nonobjc + func addAnimations( + for star: Star, + context: LayerAnimationContext) + throws + { + switch star.starType { + case .star: + try addStarAnimation(for: star, context: context) + case .polygon: + try addPolygonAnimation(for: star, context: context) + case .none: + break + } + } + + // MARK: Private + + @nonobjc + private func addStarAnimation( + for star: Star, + context: LayerAnimationContext) + throws + { + try addAnimation( + for: .path, + keyframes: star.position.keyframes, + value: { position in + // We can only use one set of keyframes to animate a given CALayer keypath, + // so we currently animate `position` and ignore any other keyframes. + // TODO: Is there a way to support this properly? + BezierPath.star( + position: position.pointValue, + outerRadius: try star.outerRadius + .exactlyOneKeyframe(context: context, description: "outerRadius").value.cgFloatValue, + innerRadius: try star.innerRadius? + .exactlyOneKeyframe(context: context, description: "innerRadius").value.cgFloatValue ?? 0, + outerRoundedness: try star.outerRoundness + .exactlyOneKeyframe(context: context, description: "outerRoundness").value.cgFloatValue, + innerRoundedness: try star.innerRoundness? + .exactlyOneKeyframe(context: context, description: "innerRoundness").value.cgFloatValue ?? 0, + numberOfPoints: try star.points + .exactlyOneKeyframe(context: context, description: "points").value.cgFloatValue, + rotation: try star.rotation + .exactlyOneKeyframe(context: context, description: "rotation").value.cgFloatValue, + direction: star.direction) + .cgPath() + }, + context: context) + } + + @nonobjc + private func addPolygonAnimation( + for star: Star, + context: LayerAnimationContext) + throws + { + try addAnimation( + for: .path, + keyframes: star.position.keyframes, + value: { position in + // We can only use one set of keyframes to animate a given CALayer keypath, + // so we currently animate `position` and ignore any other keyframes. + // TODO: Is there a way to support this properly? + BezierPath.polygon( + position: position.pointValue, + numberOfPoints: try star.points + .exactlyOneKeyframe(context: context, description: "numberOfPoints").value.cgFloatValue, + outerRadius: try star.outerRadius + .exactlyOneKeyframe(context: context, description: "outerRadius").value.cgFloatValue, + outerRoundedness: try star.outerRoundness + .exactlyOneKeyframe(context: context, description: "outerRoundedness").value.cgFloatValue, + rotation: try star.rotation + .exactlyOneKeyframe(context: context, description: "rotation").value.cgFloatValue, + direction: star.direction) + .cgPath() + }, + context: context) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/StrokeAnimation.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/StrokeAnimation.swift new file mode 100644 index 0000000000..290b06e96d --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/StrokeAnimation.swift @@ -0,0 +1,70 @@ +// Created by Cal Stephens on 2/10/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +import Foundation +import QuartzCore + +// MARK: - StrokeShapeItem + +/// A `ShapeItem` that represents a stroke +protocol StrokeShapeItem: OpacityAnimationModel { + var strokeColor: KeyframeGroup? { get } + var width: KeyframeGroup { get } + var lineCap: LineCap { get } + var lineJoin: LineJoin { get } + var miterLimit: Double { get } + var dashPattern: [DashElement]? { get } +} + +// MARK: - Stroke + StrokeShapeItem + +extension Stroke: StrokeShapeItem { + var strokeColor: KeyframeGroup? { color } +} + +// MARK: - GradientStroke + StrokeShapeItem + +extension GradientStroke: StrokeShapeItem { + var strokeColor: KeyframeGroup? { nil } +} + +// MARK: - CAShapeLayer + StrokeShapeItem + +extension CAShapeLayer { + /// Adds animations for properties related to the given `Stroke` object (`strokeColor`, `lineWidth`, etc) + @nonobjc + func addStrokeAnimations(for stroke: StrokeShapeItem, context: LayerAnimationContext) throws { + lineJoin = stroke.lineJoin.caLineJoin + lineCap = stroke.lineCap.caLineCap + miterLimit = CGFloat(stroke.miterLimit) + + if let strokeColor = stroke.strokeColor { + try addAnimation( + for: .strokeColor, + keyframes: strokeColor.keyframes, + value: \.cgColorValue, + context: context) + } + + try addAnimation( + for: .lineWidth, + keyframes: stroke.width.keyframes, + value: \.cgFloatValue, + context: context) + + try addOpacityAnimation(for: stroke, context: context) + + if let (dashPattern, dashPhase) = stroke.dashPattern?.shapeLayerConfiguration { + lineDashPattern = try dashPattern.map { + try KeyframeGroup(keyframes: $0) + .exactlyOneKeyframe(context: context, description: "stroke dashPattern").value.cgFloatValue as NSNumber + } + + try addAnimation( + for: .lineDashPhase, + keyframes: dashPhase, + value: \.cgFloatValue, + context: context) + } + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/TransformAnimations.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/TransformAnimations.swift new file mode 100644 index 0000000000..11a5a28ddd --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/TransformAnimations.swift @@ -0,0 +1,205 @@ +// Created by Cal Stephens on 12/17/21. +// Copyright © 2021 Airbnb Inc. All rights reserved. + +import QuartzCore + +// MARK: - TransformModel + +/// This protocol mirrors the interface of `Transform`, +/// but it also implemented by `ShapeTransform` to allow +/// both transform types to share the same animation implementation. +protocol TransformModel { + /// The anchor point of the transform. + var anchorPoint: KeyframeGroup { get } + + /// The position of the transform. This is nil if the position data was split. + var _position: KeyframeGroup? { get } + + /// The positionX of the transform. This is nil if the position property is set. + var _positionX: KeyframeGroup? { get } + + /// The positionY of the transform. This is nil if the position property is set. + var _positionY: KeyframeGroup? { get } + + /// The scale of the transform + var scale: KeyframeGroup { get } + + /// The rotation of the transform. Note: This is single dimensional rotation. + var rotation: KeyframeGroup { get } +} + +// MARK: - Transform + TransformModel + +extension Transform: TransformModel { + var _position: KeyframeGroup? { position } + var _positionX: KeyframeGroup? { positionX } + var _positionY: KeyframeGroup? { positionY } +} + +// MARK: - ShapeTransform + TransformModel + +extension ShapeTransform: TransformModel { + var anchorPoint: KeyframeGroup { anchor } + var _position: KeyframeGroup? { position } + var _positionX: KeyframeGroup? { nil } + var _positionY: KeyframeGroup? { nil } +} + +// MARK: - CALayer + TransformModel + +extension CALayer { + + // MARK: Internal + + /// Adds transform-related animations from the given `TransformModel` to this layer + /// - This _doesn't_ apply `transform.opacity`, which has to be handled separately + /// since child layers don't inherit the `opacity` of their parent. + @nonobjc + func addTransformAnimations(for transformModel: TransformModel, context: LayerAnimationContext) throws { + try addPositionAnimations(from: transformModel, context: context) + try addAnchorPointAnimation(from: transformModel, context: context) + try addScaleAnimations(from: transformModel, context: context) + try addRotationAnimation(from: transformModel, context: context) + } + + // MARK: Private + + @nonobjc + private func addPositionAnimations( + from transformModel: TransformModel, + context: LayerAnimationContext) + throws + { + if let positionKeyframes = transformModel._position?.keyframes { + try addAnimation( + for: .position, + keyframes: positionKeyframes, + value: \.pointValue, + context: context) + } else if + let xKeyframes = transformModel._positionX?.keyframes, + let yKeyframes = transformModel._positionY?.keyframes + { + try addAnimation( + for: .positionX, + keyframes: xKeyframes, + value: \.cgFloatValue, + context: context) + + try addAnimation( + for: .positionY, + keyframes: yKeyframes, + value: \.cgFloatValue, + context: context) + } else { + try context.logCompatibilityIssue(""" + `Transform` values must provide either `position` or `positionX` / `positionY` keyframes + """) + } + } + + @nonobjc + private func addAnchorPointAnimation( + from transformModel: TransformModel, + context: LayerAnimationContext) + throws + { + try addAnimation( + for: .anchorPoint, + keyframes: transformModel.anchorPoint.keyframes, + value: { absoluteAnchorPoint in + guard bounds.width > 0, bounds.height > 0 else { + LottieLogger.shared.assertionFailure("Size must be non-zero before an animation can be played") + return .zero + } + + // Lottie animation files express anchorPoint as an absolute point value, + // so we have to divide by the width/height of this layer to get the + // relative decimal values expected by Core Animation. + return CGPoint( + x: CGFloat(absoluteAnchorPoint.x) / bounds.width, + y: CGFloat(absoluteAnchorPoint.y) / bounds.height) + }, + context: context) + } + + @nonobjc + private func addScaleAnimations( + from transformModel: TransformModel, + context: LayerAnimationContext) + throws + { + try addAnimation( + for: .scaleX, + keyframes: transformModel.scale.keyframes, + value: { scale in + // Lottie animation files express scale as a numerical percentage value + // (e.g. 50%, 100%, 200%) so we divide by 100 to get the decimal values + // expected by Core Animation (e.g. 0.5, 1.0, 2.0). + // - Negative `scale.x` values aren't applied correctly by Core Animation. + // This appears to be because we animate `transform.scale.x` and `transform.scale.y` + // as separate `CAKeyframeAnimation`s instead of using a single animation of `transform` itself. + // https://openradar.appspot.com/FB9862872 + // - To work around this, we set up a `rotationY` animation below + // to flip the view horizontally, which gives us the desired effect. + abs(CGFloat(scale.x) / 100) + }, + context: context) + + // When `scale.x` is negative, we have to rotate the view + // half way around the y axis to flip it horizontally. + // - We don't do this in snapshot tests because it breaks the tests + // in surprising ways that don't happen at runtime. Definitely not ideal. + if TestHelpers.snapshotTestsAreRunning { + if transformModel.scale.keyframes.contains(where: { $0.value.x < 0 }) { + LottieLogger.shared.warn(""" + Negative `scale.x` values are not displayed correctly in snapshot tests + """) + } + } else { + try addAnimation( + for: .rotationY, + keyframes: transformModel.scale.keyframes, + value: { scale in + if scale.x < 0 { + return .pi + } else { + return 0 + } + }, + context: context) + } + + try addAnimation( + for: .scaleY, + keyframes: transformModel.scale.keyframes, + value: { scale in + // Lottie animation files express scale as a numerical percentage value + // (e.g. 50%, 100%, 200%) so we divide by 100 to get the decimal values + // expected by Core Animation (e.g. 0.5, 1.0, 2.0). + // - Negative `scaleY` values are correctly applied (they flip the view + // vertically), so we don't have to apply an additional rotation animation + // like we do for `scaleX`. + CGFloat(scale.y) / 100 + }, + context: context) + } + + private func addRotationAnimation( + from transformModel: TransformModel, + context: LayerAnimationContext) + throws + { + try addAnimation( + for: .rotation, + keyframes: transformModel.rotation.keyframes, + value: { rotationDegrees in + // Lottie animation files express rotation in degrees + // (e.g. 90º, 180º, 360º) so we covert to radians to get the + // values expected by Core Animation (e.g. π/2, π, 2π) + rotationDegrees.cgFloatValue * .pi / 180 + }, + context: context) + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/VisibilityAnimation.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/VisibilityAnimation.swift new file mode 100644 index 0000000000..8d12356f0a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Animations/VisibilityAnimation.swift @@ -0,0 +1,37 @@ +// Created by Cal Stephens on 12/21/21. +// Copyright © 2021 Airbnb Inc. All rights reserved. + +import QuartzCore + +extension CALayer { + /// Adds an animation for the given `inTime` and `outTime` to this `CALayer` + @nonobjc + func addVisibilityAnimation( + inFrame: AnimationFrameTime, + outFrame: AnimationFrameTime, + context: LayerAnimationContext) + { + let animation = CAKeyframeAnimation(keyPath: #keyPath(isHidden)) + animation.calculationMode = .discrete + + animation.values = [ + true, // hidden, before `inFrame` + false, // visible + true, // hidden, after `outFrame` + ] + + // From the documentation of `keyTimes`: + // - If the calculationMode is set to discrete, the first value in the array + // must be 0.0 and the last value must be 1.0. The array should have one more + // entry than appears in the values array. For example, if there are two values, + // there should be three key times. + animation.keyTimes = [ + NSNumber(value: 0.0), + NSNumber(value: max(Double(context.progressTime(for: inFrame)), 0)), + NSNumber(value: min(Double(context.progressTime(for: outFrame)), 1)), + NSNumber(value: 1.0), + ] + + add(animation, timedWith: context) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/CompatibilityTracker.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/CompatibilityTracker.swift new file mode 100644 index 0000000000..f08e05a9d8 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/CompatibilityTracker.swift @@ -0,0 +1,128 @@ +// Created by Cal Stephens on 5/4/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +// MARK: - CompatibilityIssue + +/// A compatibility issue that was encountered while setting up an animation with the Core Animation engine +struct CompatibilityIssue: CustomStringConvertible { + let message: String + let context: String + + var description: String { + "[\(context)] \(message)" + } +} + +// MARK: - CompatibilityTracker + +/// A type that tracks whether or not an animation is compatible with the Core Animation engine +final class CompatibilityTracker { + + // MARK: Lifecycle + + init(mode: Mode) { + self.mode = mode + } + + // MARK: Internal + + /// How compatibility issues should be handled + enum Mode { + /// When a compatibility issue is encountered, an error will be thrown immediately, + /// aborting the animation setup process as soon as possible. + case abort + + /// When a compatibility issue is encountered, it is stored in `CompatibilityTracker.issues` + case track + } + + enum Error: Swift.Error { + case encounteredCompatibilityIssue(CompatibilityIssue) + } + + /// Records a compatibility issue that will be reported according to `CompatibilityTracker.Mode` + func logIssue(message: String, context: String) throws { + LottieLogger.shared.assert(!context.isEmpty, "Compatibility issue context is unexpectedly empty") + + let issue = CompatibilityIssue( + // Compatibility messages are usually written in source files using multi-line strings, + // but converting them to be a single line makes it easier to read the ultimate log output. + message: message.replacingOccurrences(of: "\n", with: " "), + context: context) + + switch mode { + case .abort: + throw CompatibilityTracker.Error.encounteredCompatibilityIssue(issue) + case .track: + issues.append(issue) + } + } + + /// Asserts that a condition is true, otherwise logs a compatibility issue that will be reported + /// according to `CompatibilityTracker.Mode` + func assert( + _ condition: Bool, + _ message: @autoclosure () -> String, + context: @autoclosure () -> String) + throws + { + if !condition { + try logIssue(message: message(), context: context()) + } + } + + /// Reports the compatibility issues that were recorded when setting up the animation, + /// and clears the set of tracked issues. + func reportCompatibilityIssues(_ handler: ([CompatibilityIssue]) -> Void) { + handler(issues) + issues = [] + } + + // MARK: Private + + private let mode: Mode + + /// Compatibility issues encountered while setting up the animation + private var issues = [CompatibilityIssue]() + +} + +// MARK: - CompatibilityTrackerProviding + +protocol CompatibilityTrackerProviding { + var compatibilityTracker: CompatibilityTracker { get } + var compatibilityIssueContext: String { get } +} + +extension CompatibilityTrackerProviding { + /// Records a compatibility issue that will be reported according to `CompatibilityTracker.Mode` + func logCompatibilityIssue(_ message: String) throws { + try compatibilityTracker.logIssue(message: message, context: compatibilityIssueContext) + } + + /// Asserts that a condition is true, otherwise logs a compatibility issue that will be reported + /// according to `CompatibilityTracker.Mode` + func compatibilityAssert( + _ condition: Bool, + _ message: @autoclosure () -> String) + throws + { + try compatibilityTracker.assert(condition, message(), context: compatibilityIssueContext) + } +} + +// MARK: - LayerContext + CompatibilityTrackerProviding + +extension LayerContext: CompatibilityTrackerProviding { + var compatibilityIssueContext: String { + layerName + } +} + +// MARK: - LayerAnimationContext + CompatibilityTrackerProviding + +extension LayerAnimationContext: CompatibilityTrackerProviding { + var compatibilityIssueContext: String { + currentKeypath.fullPath + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/CoreAnimationLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/CoreAnimationLayer.swift new file mode 100644 index 0000000000..05f140e59f --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/CoreAnimationLayer.swift @@ -0,0 +1,456 @@ +// Created by Cal Stephens on 12/13/21. +// Copyright © 2021 Airbnb Inc. All rights reserved. + +import Foundation +import QuartzCore + +// MARK: - CoreAnimationLayer + +/// The root `CALayer` of the Core Animation rendering engine +final class CoreAnimationLayer: BaseAnimationLayer { + + // MARK: Lifecycle + + /// Initializes a `CALayer` that renders the given animation using `CAAnimation`s. + /// - This initializer is throwing, but will only throw when using + /// `CompatibilityTracker.Mode.abort`. + init( + animation: Animation, + imageProvider: AnimationImageProvider, + fontProvider: AnimationFontProvider, + compatibilityTrackerMode: CompatibilityTracker.Mode) + throws + { + self.animation = animation + self.imageProvider = imageProvider + self.fontProvider = fontProvider + compatibilityTracker = CompatibilityTracker(mode: compatibilityTrackerMode) + super.init() + + setup() + try setupChildLayers() + } + + /// Called by CoreAnimation to create a shadow copy of this layer + /// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init + override init(layer: Any) { + guard let typedLayer = layer as? Self else { + fatalError("init(layer:) incorrectly called with \(type(of: layer))") + } + + animation = typedLayer.animation + currentAnimationConfiguration = typedLayer.currentAnimationConfiguration + imageProvider = typedLayer.imageProvider + fontProvider = typedLayer.fontProvider + didSetUpAnimation = typedLayer.didSetUpAnimation + compatibilityTracker = typedLayer.compatibilityTracker + super.init(layer: typedLayer) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + + /// Timing-related configuration to apply to this layer's child `CAAnimation`s + /// - This is effectively a configurable subset of `CAMediaTiming` + struct CAMediaTimingConfiguration: Equatable { + var autoreverses = false + var repeatCount: Float = 0 + var speed: Float = 1 + var timeOffset: TimeInterval = 0 + } + + enum PlaybackState: Equatable { + /// The animation is playing in real-time + case playing + /// The animation is statically displaying a specific frame + case paused(frame: AnimationFrameTime) + } + + /// A closure that is called after this layer sets up its animation. + /// If the animation setup was unsuccessful and encountered compatibility issues, + /// those issues are included in this call. + var didSetUpAnimation: (([CompatibilityIssue]) -> Void)? + + /// The `AnimationImageProvider` that `ImageLayer`s use to retrieve images, + /// referenced by name in the animation json. + var imageProvider: AnimationImageProvider { + didSet { reloadImages() } + } + + /// The `FontProvider` that `TextLayer`s use to retrieve the `CTFont` + /// that they should use to render their text content + var fontProvider: AnimationFontProvider { + didSet { reloadFonts() } + } + + /// Queues the animation with the given timing configuration + /// to begin playing at the next `display()` call. + /// - This batches together animations so that even if `playAnimation` + /// is called multiple times in the same run loop cycle, the animation + /// will only be set up a single time. + func playAnimation( + context: AnimationContext, + timingConfiguration: CAMediaTimingConfiguration, + playbackState: PlaybackState = .playing) + { + pendingAnimationConfiguration = ( + animationConfiguration: .init(animationContext: context, timingConfiguration: timingConfiguration), + playbackState: playbackState) + + setNeedsDisplay() + } + + override func layoutSublayers() { + super.layoutSublayers() + + // If no animation has been set up yet, display the first frame + // now that the layer hierarchy has been setup and laid out + if + pendingAnimationConfiguration == nil, + currentAnimationConfiguration == nil, + bounds.size != .zero + { + currentFrame = animation.frameTime(forProgress: animationProgress) + } + } + + override func display() { + // We intentionally don't call `super.display()`, since this layer + // doesn't directly render any content. + // - This fixes an issue where certain animations would unexpectedly + // allocate a very large amount of memory (400mb+). + // - Alternatively this layer could subclass `CATransformLayer`, + // but this causes Core Animation to emit unnecessary logs. + + if let pendingAnimationConfiguration = pendingAnimationConfiguration { + self.pendingAnimationConfiguration = nil + + do { + try setupAnimation(for: pendingAnimationConfiguration.animationConfiguration) + } catch { + if case CompatibilityTracker.Error.encounteredCompatibilityIssue(let compatibilityIssue) = error { + // Even though the animation setup failed, we still update the layer's playback state + // so it can be read by the parent `AnimationView` when handling this error + currentPlaybackState = pendingAnimationConfiguration.playbackState + + didSetUpAnimation?([compatibilityIssue]) + return + } + } + + currentPlaybackState = pendingAnimationConfiguration.playbackState + + compatibilityTracker.reportCompatibilityIssues { compatibilityIssues in + didSetUpAnimation?(compatibilityIssues) + } + } + } + + // MARK: Private + + private struct AnimationConfiguration: Equatable { + let animationContext: AnimationContext + let timingConfiguration: CAMediaTimingConfiguration + } + + /// The configuration for the most recent animation which has been + /// queued by calling `playAnimation` but not yet actually set up + private var pendingAnimationConfiguration: ( + animationConfiguration: AnimationConfiguration, + playbackState: PlaybackState)? + + /// Configuration for the animation that is currently setup in this layer + private var currentAnimationConfiguration: AnimationConfiguration? + + /// The current progress of the placeholder `CAAnimation`, + /// which is also the realtime animation progress of this layer's animation + @objc private var animationProgress: CGFloat = 0 + + private let animation: Animation + private let valueProviderStore = ValueProviderStore() + private let compatibilityTracker: CompatibilityTracker + + /// The current playback state of the animation that is displayed in this layer + private var currentPlaybackState: PlaybackState? { + didSet { + guard playbackState != oldValue else { return } + + switch playbackState { + case .playing, nil: + timeOffset = 0 + case .paused(let frame): + timeOffset = animation.time(forFrame: frame) + } + } + } + + /// The current or pending playback state of the animation displayed in this layer + private var playbackState: PlaybackState? { + pendingAnimationConfiguration?.playbackState ?? currentPlaybackState + } + + /// Context used when setting up and configuring sublayers + private var layerContext: LayerContext { + LayerContext( + animation: animation, + imageProvider: imageProvider, + fontProvider: fontProvider, + compatibilityTracker: compatibilityTracker, + layerName: "root layer") + } + + private func setup() { + bounds = animation.bounds + } + + private func setupChildLayers() throws { + try setupLayerHierarchy( + for: animation.layers, + context: layerContext) + } + + /// Immediately builds and begins playing `CAAnimation`s for each sublayer + private func setupAnimation(for configuration: AnimationConfiguration) throws { + // Remove any existing animations from the layer hierarchy + removeAnimations() + + currentAnimationConfiguration = configuration + + let layerContext = LayerAnimationContext( + animation: animation, + timingConfiguration: configuration.timingConfiguration, + startFrame: configuration.animationContext.playFrom, + endFrame: configuration.animationContext.playTo, + valueProviderStore: valueProviderStore, + compatibilityTracker: compatibilityTracker, + currentKeypath: AnimationKeypath(keys: [])) + + // Perform a layout pass if necessary so all of the sublayers + // have the most up-to-date sizing information + layoutIfNeeded() + + // Set the speed of this layer, which will be inherited + // by all sublayers and their animations. + // - This is required to support scrubbing with a speed of 0 + speed = configuration.timingConfiguration.speed + + // Setup a placeholder animation to let us track the realtime animation progress + setupPlaceholderAnimation(context: layerContext) + + // Set up the new animations with the current `TimingConfiguration` + for animationLayer in sublayers ?? [] { + try (animationLayer as? AnimationLayer)?.setupAnimations(context: layerContext) + } + } + + /// Sets up a placeholder `CABasicAnimation` that tracks the current + /// progress of this animation (between 0 and 1). This lets us provide + /// realtime animation progress via `self.currentFrame`. + private func setupPlaceholderAnimation(context: LayerAnimationContext) { + let animationProgressTracker = CABasicAnimation(keyPath: #keyPath(animationProgress)) + animationProgressTracker.fromValue = 0 + animationProgressTracker.toValue = 1 + + let timedProgressAnimation = animationProgressTracker.timed(with: context, for: self) + timedProgressAnimation.delegate = currentAnimationConfiguration?.animationContext.closure + add(timedProgressAnimation, forKey: #keyPath(animationProgress)) + } + + // Removes the current `CAAnimation`s, and rebuilds new animations + // using the same configuration as the previous animations. + private func rebuildCurrentAnimation() { + guard + let currentConfiguration = currentAnimationConfiguration, + let playbackState = playbackState, + // Don't replace any pending animations that are queued to begin + // on the next run loop cycle, since an existing pending animation + // will cause the animation to be rebuilt anyway. + pendingAnimationConfiguration == nil + else { return } + + removeAnimations() + + switch playbackState { + case .paused(let frame): + currentFrame = frame + + case .playing: + playAnimation( + context: currentConfiguration.animationContext, + timingConfiguration: currentConfiguration.timingConfiguration) + } + } + +} + +// MARK: RootAnimationLayer + +extension CoreAnimationLayer: RootAnimationLayer { + + var primaryAnimationKey: AnimationKey { + .specific(#keyPath(animationProgress)) + } + + var isAnimationPlaying: Bool? { + switch playbackState { + case .playing: + return true + case nil, .paused: + return false + } + } + + var currentFrame: AnimationFrameTime { + get { + switch playbackState { + case .playing, nil: + return animation.frameTime(forProgress: (presentation() ?? self).animationProgress) + case .paused(let frame): + return frame + } + } + set { + // We can display a specific frame of the animation by setting + // `timeOffset` of this layer. This requires setting up the layer hierarchy + // with a specific configuration (speed=0, etc) at least once. But if + // the layer hierarchy is already set up correctly, we can update the + // `timeOffset` very cheaply. + let requiredAnimationConfiguration = AnimationConfiguration( + animationContext: AnimationContext( + playFrom: animation.startFrame, + playTo: animation.endFrame, + closure: nil), + timingConfiguration: CAMediaTimingConfiguration(speed: 0)) + + if + pendingAnimationConfiguration == nil, + currentAnimationConfiguration == requiredAnimationConfiguration + { + currentPlaybackState = .paused(frame: newValue) + } + + else { + playAnimation( + context: requiredAnimationConfiguration.animationContext, + timingConfiguration: requiredAnimationConfiguration.timingConfiguration, + playbackState: .paused(frame: newValue)) + } + } + } + + var renderScale: CGFloat { + get { contentsScale } + set { + contentsScale = newValue + + for sublayer in allSublayers { + sublayer.contentsScale = newValue + } + } + } + + var respectAnimationFrameRate: Bool { + get { false } + set { LottieLogger.shared.assertionFailure("`respectAnimationFrameRate` is currently unsupported") } + } + + var _animationLayers: [CALayer] { + (sublayers ?? []).filter { $0 is AnimationLayer } + } + + var textProvider: AnimationTextProvider { + get { DictionaryTextProvider([:]) } + set { LottieLogger.shared.assertionFailure("`textProvider` is currently unsupported") } + } + + func reloadImages() { + // When the image provider changes, we have to update all `ImageLayer`s + // so they can query the most up-to-date image from the new image provider. + for sublayer in allSublayers { + if let imageLayer = sublayer as? ImageLayer { + imageLayer.setupImage(context: layerContext) + } + } + } + + func reloadFonts() { + // When the text provider changes, we have to update all `TextLayer`s + // so they can query the most up-to-date font from the new font provider. + for sublayer in allSublayers { + if let textLayer = sublayer as? TextLayer { + try? textLayer.configureRenderLayer(with: layerContext) + } + } + } + + func forceDisplayUpdate() { + // Unimplemented / unused + } + + func logHierarchyKeypaths() { + // Unimplemented / unused + } + + func setValueProvider(_ valueProvider: AnyValueProvider, keypath: AnimationKeypath) { + valueProviderStore.setValueProvider(valueProvider, keypath: keypath) + + // We need to rebuild the current animation after registering a value provider, + // since any existing `CAAnimation`s could now be out of date. + rebuildCurrentAnimation() + } + + func getValue(for _: AnimationKeypath, atFrame _: AnimationFrameTime?) -> Any? { + LottieLogger.shared.assertionFailure(""" + The Core Animation rendering engine doesn't support querying values for individual frames + """) + return nil + } + + func getOriginalValue(for _: AnimationKeypath, atFrame _: AnimationFrameTime?) -> Any? { + LottieLogger.shared.assertionFailure(""" + The Core Animation rendering engine doesn't support querying values for individual frames + """) + return nil + } + + func layer(for _: AnimationKeypath) -> CALayer? { + LottieLogger.shared.assertionFailure("`AnimationKeypath`s are currently unsupported") + return nil + } + + func animatorNodes(for _: AnimationKeypath) -> [AnimatorNode]? { + LottieLogger.shared.assertionFailure("`AnimatorNode`s are not used in this rendering implementation") + return nil + } + + func removeAnimations() { + currentAnimationConfiguration = nil + currentPlaybackState = nil + removeAllAnimations() + + for sublayer in allSublayers { + sublayer.removeAllAnimations() + } + } + +} + +// MARK: - CALayer + allSublayers + +extension CALayer { + /// All of the layers in the layer tree that are descendants from this later + @nonobjc + var allSublayers: [CALayer] { + var allSublayers: [CALayer] = [] + + for sublayer in sublayers ?? [] { + allSublayers.append(sublayer) + allSublayers.append(contentsOf: sublayer.allSublayers) + } + + return allSublayers + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Extensions/CALayer+fillBounds.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Extensions/CALayer+fillBounds.swift new file mode 100644 index 0000000000..5b0baf16d1 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Extensions/CALayer+fillBounds.swift @@ -0,0 +1,35 @@ +// Created by Cal Stephens on 12/15/21. +// Copyright © 2021 Airbnb Inc. All rights reserved. + +import QuartzCore + +// MARK: - CALayer + fillBoundsOfSuperlayer + +extension CALayer { + /// Updates the `bounds` of this layer to fill the bounds of its `superlayer` + /// without setting `frame` (which is not permitted if the layer can rotate) + @nonobjc + func fillBoundsOfSuperlayer() { + guard let superlayer = superlayer else { return } + + if let customLayerLayer = self as? CustomLayoutLayer { + customLayerLayer.layout(superlayerBounds: superlayer.bounds) + } + + else { + // By default the `anchorPoint` of a layer is `CGPoint(x: 0.5, y: 0.5)`. + // Setting it to `.zero` makes the layer have the same coordinate space + // as its superlayer, which lets use use `superlayer.bounds` directly. + anchorPoint = .zero + + bounds = superlayer.bounds + } + } +} + +// MARK: - CustomLayoutLayer + +/// A `CALayer` that sets a custom `bounds` and `anchorPoint` relative to its superlayer +protocol CustomLayoutLayer: CALayer { + func layout(superlayerBounds: CGRect) +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Extensions/KeyframeGroup+exactlyOneKeyframe.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Extensions/KeyframeGroup+exactlyOneKeyframe.swift new file mode 100644 index 0000000000..728a8937c7 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Extensions/KeyframeGroup+exactlyOneKeyframe.swift @@ -0,0 +1,37 @@ +// Created by Cal Stephens on 1/11/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +// MARK: - KeyframeGroup + exactlyOneKeyframe + +extension KeyframeGroup { + /// Retrieves the first `Keyframe` from this group, + /// and asserts that there are not any extra keyframes that would be ignored + /// + /// - There are several places in Lottie animation definitions where multiple + /// sets of keyframe timings can be provided for properties that have to + /// be applied to a single `CALayer` property (for example, the definition for a + /// `Rectangle` technically lets you animate `size`, `position`, and `cornerRadius` + /// separately, but these all have to be combined into a single `CAKeyframeAnimation` + /// on the `CAShapeLayer.path` property. + /// + /// - In those sorts of cases, we currently choose one one `KeyframeGroup` to provide the + /// timing information, and disallow simultaneous animations on the other properties. + /// + func exactlyOneKeyframe( + context: CompatibilityTrackerProviding, + description: String, + fileID _: StaticString = #fileID, + line _: UInt = #line) + throws + -> Keyframe + { + try context.compatibilityAssert( + keyframes.count == 1, + """ + The Core Animation rendering engine does not support animating multiple keyframes + for \(description) values (due to limitations of Core Animation `CAKeyframeAnimation`s). + """) + + return keyframes[0] + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Extensions/Keyframes+combinedIfPossible.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Extensions/Keyframes+combinedIfPossible.swift new file mode 100644 index 0000000000..04237b5a28 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Extensions/Keyframes+combinedIfPossible.swift @@ -0,0 +1,61 @@ +// Created by Cal Stephens on 1/28/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +// MARK: - Keyframes + +enum Keyframes { + /// Combines the given `[KeyframeGroup]` of `Keyframe`s + /// into a single `KeyframeGroup` of `Keyframe<[T]>`s + /// if all of the `KeyframeGroup`s have the exact same animation timing + static func combinedIfPossible(_ groups: [KeyframeGroup]) -> KeyframeGroup<[T]>? { + guard + !groups.isEmpty, + groups.allSatisfy({ $0.hasSameTimingParameters(as: groups[0]) }) + else { return nil } + + var combinedKeyframes = ContiguousArray>() + + for index in groups[0].keyframes.indices { + let baseKeyframe = groups[0].keyframes[index] + let combinedValues = groups.map { $0.keyframes[index].value } + combinedKeyframes.append(baseKeyframe.withValue(combinedValues)) + } + + return KeyframeGroup(keyframes: combinedKeyframes) + } + + /// Combines the given `[KeyframeGroup?]` of `Keyframe`s + /// into a single `KeyframeGroup` of `Keyframe<[T]>`s + /// if all of the `KeyframeGroup`s have the exact same animation timing + static func combinedIfPossible(_ groups: [KeyframeGroup?]) -> KeyframeGroup<[T]>? { + let nonOptionalGroups = groups.compactMap { $0 } + guard nonOptionalGroups.count == groups.count else { return nil } + return combinedIfPossible(nonOptionalGroups) + } +} + +extension KeyframeGroup { + /// Whether or not all of the keyframes in this `KeyframeGroup` have the same + /// timing parameters as the corresponding keyframe in the other given `KeyframeGroup` + func hasSameTimingParameters(as other: KeyframeGroup) -> Bool { + guard keyframes.count == other.keyframes.count else { + return false + } + + return zip(keyframes, other.keyframes).allSatisfy { + $0.hasSameTimingParameters(as: $1) + } + } +} + +extension Keyframe { + /// Whether or not this keyframe has the same timing parameters as the given keyframe + func hasSameTimingParameters(as other: Keyframe) -> Bool { + time == other.time + && isHold == other.isHold + && inTangent == other.inTangent + && outTangent == other.outTangent + && spatialInTangent == other.spatialInTangent + && spatialOutTangent == other.spatialOutTangent + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/AnimationLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/AnimationLayer.swift new file mode 100644 index 0000000000..37ae2531da --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/AnimationLayer.swift @@ -0,0 +1,70 @@ +// Created by Cal Stephens on 12/14/21. +// Copyright © 2021 Airbnb Inc. All rights reserved. + +import QuartzCore + +// MARK: - AnimationLayer + +/// A type of `CALayer` that can be used in a Lottie animation +/// - Layers backed by a `LayerModel` subclass should subclass `BaseCompositionLayer` +protocol AnimationLayer: CALayer { + /// Instructs this layer to setup its `CAAnimation`s + /// using the given `LayerAnimationContext` + func setupAnimations(context: LayerAnimationContext) throws +} + +// MARK: - LayerAnimationContext + +// Context describing the timing parameters of the current animation +struct LayerAnimationContext { + /// The animation being played + let animation: Animation + + /// The timing configuration that should be applied to `CAAnimation`s + let timingConfiguration: CoreAnimationLayer.CAMediaTimingConfiguration + + /// The absolute frame number that this animation begins at + let startFrame: AnimationFrameTime + + /// The absolute frame number that this animation ends at + let endFrame: AnimationFrameTime + + /// The set of custom Value Providers applied to this animation + let valueProviderStore: ValueProviderStore + + /// Information about whether or not an animation is compatible with the Core Animation engine + let compatibilityTracker: CompatibilityTracker + + /// The AnimationKeypath represented by the current layer + var currentKeypath: AnimationKeypath + + /// A closure that remaps the given frame in the child layer's local time to a frame + /// in the animation's overall global time + private(set) var timeRemapping: ((AnimationFrameTime) -> AnimationFrameTime) = { $0 } + + /// Adds the given component string to the `AnimationKeypath` stored + /// that describes the current path being configured by this context value + func addingKeypathComponent(_ component: String) -> LayerAnimationContext { + var context = self + context.currentKeypath.keys.append(component) + return context + } + + /// The `AnimationProgressTime` for the given `AnimationFrameTime` within this layer, + /// accounting for the `timeRemapping` applied to this layer + func progressTime(for frame: AnimationFrameTime) -> AnimationProgressTime { + animation.progressTime(forFrame: timeRemapping(frame), clamped: false) + } + + /// Chains an additional `timeRemapping` closure onto this layer context + func withTimeRemapping( + _ additionalTimeRemapping: @escaping (AnimationFrameTime) -> AnimationFrameTime) + -> LayerAnimationContext + { + var copy = self + copy.timeRemapping = { [existingTimeRemapping = timeRemapping] time in + existingTimeRemapping(additionalTimeRemapping(time)) + } + return copy + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/BaseAnimationLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/BaseAnimationLayer.swift new file mode 100644 index 0000000000..06248b20f8 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/BaseAnimationLayer.swift @@ -0,0 +1,33 @@ +// Created by Cal Stephens on 1/27/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +import QuartzCore + +/// A base `CALayer` that manages the frame and animations +/// of its `sublayers` and `mask` +class BaseAnimationLayer: CALayer, AnimationLayer { + + // MARK: Internal + + override func layoutSublayers() { + super.layoutSublayers() + + for sublayer in managedSublayers { + sublayer.fillBoundsOfSuperlayer() + } + } + + func setupAnimations(context: LayerAnimationContext) throws { + for childAnimationLayer in managedSublayers { + try (childAnimationLayer as? AnimationLayer)?.setupAnimations(context: context) + } + } + + // MARK: Private + + /// All of the sublayers managed by this container + private var managedSublayers: [CALayer] { + (sublayers ?? []) + [mask].compactMap { $0 } + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/BaseCompositionLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/BaseCompositionLayer.swift new file mode 100644 index 0000000000..67b6bab151 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/BaseCompositionLayer.swift @@ -0,0 +1,87 @@ +// Created by Cal Stephens on 12/20/21. +// Copyright © 2021 Airbnb Inc. All rights reserved. + +import QuartzCore + +// MARK: - BaseCompositionLayer + +/// The base type of `AnimationLayer` that can contain other `AnimationLayer`s +class BaseCompositionLayer: BaseAnimationLayer { + + // MARK: Lifecycle + + init(layerModel: LayerModel) { + baseLayerModel = layerModel + super.init() + + setupSublayers() + compositingFilter = layerModel.blendMode.filterName + name = layerModel.name + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// Called by CoreAnimation to create a shadow copy of this layer + /// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init + override init(layer: Any) { + guard let typedLayer = layer as? Self else { + fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))") + } + + baseLayerModel = typedLayer.baseLayerModel + super.init(layer: typedLayer) + } + + // MARK: Internal + + /// Whether or not this layer render should render any visible content + var renderLayerContents: Bool { true } + + /// Sets up the base `LayerModel` animations for this layer, + /// and all child `AnimationLayer`s. + /// - Can be overridden by subclasses, which much call `super`. + override func setupAnimations(context: LayerAnimationContext) throws { + var context = context + if renderLayerContents { + context = context.addingKeypathComponent(baseLayerModel.name) + } + + try setupLayerAnimations(context: context) + try setupChildAnimations(context: context) + } + + func setupLayerAnimations(context: LayerAnimationContext) throws { + let context = context.addingKeypathComponent(baseLayerModel.name) + + try addTransformAnimations(for: baseLayerModel.transform, context: context) + + if renderLayerContents { + try addOpacityAnimation(for: baseLayerModel.transform, context: context) + + addVisibilityAnimation( + inFrame: CGFloat(baseLayerModel.inFrame), + outFrame: CGFloat(baseLayerModel.outFrame), + context: context) + } + } + + func setupChildAnimations(context: LayerAnimationContext) throws { + try super.setupAnimations(context: context) + } + + // MARK: Private + + private let baseLayerModel: LayerModel + + private func setupSublayers() { + if + renderLayerContents, + let masks = baseLayerModel.masks + { + mask = MaskCompositionLayer(masks: masks) + } + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/CALayer+setupLayerHierarchy.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/CALayer+setupLayerHierarchy.swift new file mode 100644 index 0000000000..571605b854 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/CALayer+setupLayerHierarchy.swift @@ -0,0 +1,131 @@ +// Created by Cal Stephens on 1/11/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +import QuartzCore + +extension CALayer { + /// Sets up an `AnimationLayer` / `CALayer` hierarchy in this layer, + /// using the given list of layers. + @nonobjc + func setupLayerHierarchy( + for layers: [LayerModel], + context: LayerContext) + throws + { + // An `Animation`'s `LayerModel`s are listed from front to back, + // but `CALayer.sublayers` are listed from back to front. + // We reverse the layer ordering to match what Core Animation expects. + // The final view hierarchy must display the layers in this exact order. + let layersInZAxisOrder = layers.reversed() + + let layersByIndex = Dictionary(grouping: layersInZAxisOrder, by: \.index) + .compactMapValues(\.first) + + /// Layers specify a `parent` layer. Child layers inherit the `transform` of their parent. + /// - We can't add the child as a sublayer of the parent `CALayer`, since that would + /// break the ordering specified in `layersInZAxisOrder`. + /// - Instead, we create an invisible `TransformLayer` to handle the parent + /// transform animations, and add the child layer to that `TransformLayer`. + func makeParentTransformLayer( + childLayerModel: LayerModel, + childLayer: CALayer, + name: (LayerModel) -> String) + -> CALayer + { + guard + let parentIndex = childLayerModel.parent, + let parentLayerModel = layersByIndex[parentIndex] + else { return childLayer } + + let parentLayer = TransformLayer(layerModel: parentLayerModel) + parentLayer.name = name(parentLayerModel) + parentLayer.addSublayer(childLayer) + + return makeParentTransformLayer( + childLayerModel: parentLayerModel, + childLayer: parentLayer, + name: name) + } + + // Create an `AnimationLayer` for each `LayerModel` + for (layerModel, maskLayerModel) in try layersInZAxisOrder.pairedLayersAndMasks(context: context) { + guard let layer = try layerModel.makeAnimationLayer(context: context) else { + continue + } + + // If this layer has a `parent`, we create an invisible `TransformLayer` + // to handle displaying / animating the parent transform. + let parentTransformLayer = makeParentTransformLayer( + childLayerModel: layerModel, + childLayer: layer, + name: { parentLayerModel in + "\(layerModel.name) (parent, \(parentLayerModel.name))" + }) + + // Create the `mask` layer for this layer, if it has a `MatteType` + if + let maskLayerModel = maskLayerModel, + let maskLayer = try maskLayerModel.makeAnimationLayer(context: context) + { + let maskParentTransformLayer = makeParentTransformLayer( + childLayerModel: maskLayerModel, + childLayer: maskLayer, + name: { parentLayerModel in + "\(maskLayerModel.name) (mask of \(layerModel.name)) (parent, \(parentLayerModel.name))" + }) + + // Set up a parent container to host both the layer + // and its mask in the same coordinate space + let maskContainer = BaseAnimationLayer() + maskContainer.name = "\(layerModel.name) (parent, masked)" + maskContainer.addSublayer(parentTransformLayer) + + // Core Animation will silently fail to apply a mask if a `mask` layer + // itself _also_ has a `mask`. As a workaround, we can wrap this layer's + // mask in an additional container layer which never has its own `mask`. + let additionalMaskParent = BaseAnimationLayer() + additionalMaskParent.addSublayer(maskParentTransformLayer) + maskContainer.mask = additionalMaskParent + + addSublayer(maskContainer) + } + + else { + addSublayer(parentTransformLayer) + } + } + } + +} + +extension Collection where Element == LayerModel { + /// Pairs each `LayerModel` within this array with + /// a `LayerModel` to use as its mask, if applicable + /// based on the layer's `MatteType` configuration. + /// - Assumes the layers are sorted in z-axis order. + fileprivate func pairedLayersAndMasks(context: LayerContext) throws -> [(layer: LayerModel, mask: LayerModel?)] { + var layersAndMasks = [(layer: LayerModel, mask: LayerModel?)]() + var unprocessedLayers = reversed() + + while let layer = unprocessedLayers.popLast() { + /// If a layer has a `MatteType`, then the next layer will be used as its `mask` + if + let matteType = layer.matte, + matteType != .none, + let maskLayer = unprocessedLayers.popLast() + { + try context.compatibilityAssert( + matteType == .add, + "The Core Animation rendering engine currently only supports `MatteMode.add`.") + + layersAndMasks.append((layer: layer, mask: maskLayer)) + } + + else { + layersAndMasks.append((layer: layer, mask: nil)) + } + } + + return layersAndMasks + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/GradientRenderLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/GradientRenderLayer.swift new file mode 100644 index 0000000000..0238811ba5 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/GradientRenderLayer.swift @@ -0,0 +1,87 @@ +// Created by Cal Stephens on 1/10/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +import QuartzCore + +// MARK: - GradientRenderLayer + +/// A `CAGradientLayer` subclass used to render a gradient _outside_ the normal layer bounds +/// +/// - `GradientFill.startPoint` and `GradientFill.endPoint` are expressed +/// with respect to the `bounds` of the `ShapeItemLayer`. +/// +/// - The gradient itself is supposed to be rendered infinitely in all directions +/// (e.g. including outside of `bounds`). This is because `ShapeItemLayer` paths +/// don't necessarily sit within the layer's `bounds`. +/// +/// - To support this, `GradientRenderLayer` tracks a `gradientReferenceBounds` +/// that `startPoint` / `endPoint` are calculated relative to. +/// The _actual_ `bounds` of this layer is padded by a large amount so that +/// the gradient can be drawn outside of the `gradientReferenceBounds`. +/// +final class GradientRenderLayer: CAGradientLayer { + + // MARK: Internal + + /// The reference bounds within this layer that the gradient's + /// `startPoint` and `endPoint` should be calculated relative to + var gradientReferenceBounds: CGRect = .zero { + didSet { + if oldValue != gradientReferenceBounds { + updateLayout() + } + } + } + + /// Converts the given `CGPoint` within `gradientReferenceBounds` + /// to a percentage value relative to the full `bounds` of this layer + /// - This converts absolute `startPoint` and `endPoint` values into + /// the percent-based values expected by Core Animation, + /// with respect to the custom bounds geometry used by this layer type. + func percentBasedPointInBounds(from referencePoint: CGPoint) -> CGPoint { + guard bounds.width > 0, bounds.height > 0 else { + LottieLogger.shared.assertionFailure("Size must be non-zero before an animation can be played") + return .zero + } + + let pointInBounds = CGPoint( + x: referencePoint.x + gradientPadding, + y: referencePoint.y + gradientPadding) + + return CGPoint( + x: CGFloat(pointInBounds.x) / bounds.width, + y: CGFloat(pointInBounds.y) / bounds.height) + } + + // MARK: Private + + /// Extra padding around the `gradientReferenceBounds` where the gradient is also rendered + /// - This specific value is arbitrary and can be increased if necessary. + /// Theoretically this should be "infinite", to match the behavior of + /// `CGContext.drawLinearGradient` with `[.drawsAfterEndLocation, .drawsBeforeStartLocation]`. + private let gradientPadding: CGFloat = 2_000 + + private func updateLayout() { + anchorPoint = .zero + + bounds = CGRect( + x: gradientReferenceBounds.origin.x, + y: gradientReferenceBounds.origin.y, + width: gradientPadding + gradientReferenceBounds.width + gradientPadding, + height: gradientPadding + gradientReferenceBounds.height + gradientPadding) + + transform = CATransform3DMakeTranslation( + -gradientPadding, + -gradientPadding, + 0) + } + +} + +// MARK: CustomLayoutLayer + +extension GradientRenderLayer: CustomLayoutLayer { + func layout(superlayerBounds: CGRect) { + gradientReferenceBounds = superlayerBounds + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/ImageLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/ImageLayer.swift new file mode 100644 index 0000000000..98350f0454 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/ImageLayer.swift @@ -0,0 +1,79 @@ +// Created by Cal Stephens on 1/10/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +import QuartzCore + +// MARK: - ImageLayer + +/// The `CALayer` type responsible for rendering `ImageLayerModel`s +final class ImageLayer: BaseCompositionLayer { + + // MARK: Lifecycle + + init( + imageLayer: ImageLayerModel, + context: LayerContext) + { + self.imageLayer = imageLayer + super.init(layerModel: imageLayer) + setupImage(context: context) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// Called by CoreAnimation to create a shadow copy of this layer + /// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init + override init(layer: Any) { + guard let typedLayer = layer as? Self else { + fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))") + } + + imageLayer = typedLayer.imageLayer + super.init(layer: typedLayer) + } + + // MARK: Internal + + func setupImage(context: LayerContext) { + guard + let imageAsset = context.animation.assetLibrary?.imageAssets[imageLayer.referenceID], + let image = context.imageProvider.imageForAsset(asset: imageAsset) + else { + self.imageAsset = nil + contents = nil + return + } + + self.imageAsset = imageAsset + contents = image + setNeedsLayout() + } + + // MARK: Private + + private let imageLayer: ImageLayerModel + private var imageAsset: ImageAsset? + +} + +// MARK: CustomLayoutLayer + +extension ImageLayer: CustomLayoutLayer { + func layout(superlayerBounds: CGRect) { + anchorPoint = .zero + + guard let imageAsset = imageAsset else { + bounds = superlayerBounds + return + } + + // Image layers specifically need to use the size of the image itself + bounds = CGRect( + x: superlayerBounds.origin.x, + y: superlayerBounds.origin.y, + width: CGFloat(imageAsset.width), + height: CGFloat(imageAsset.height)) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/LayerModel+makeAnimationLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/LayerModel+makeAnimationLayer.swift new file mode 100644 index 0000000000..7a2b47e15e --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/LayerModel+makeAnimationLayer.swift @@ -0,0 +1,60 @@ +// Created by Cal Stephens on 12/20/21. +// Copyright © 2021 Airbnb Inc. All rights reserved. + +import QuartzCore + +// MARK: - LayerContext + +/// Context available when constructing an `AnimationLayer` +struct LayerContext { + let animation: Animation + let imageProvider: AnimationImageProvider + let fontProvider: AnimationFontProvider + let compatibilityTracker: CompatibilityTracker + var layerName: String + + func forLayer(_ layer: LayerModel) -> LayerContext { + var context = self + context.layerName = layer.name + return context + } +} + +// MARK: - LayerModel + makeAnimationLayer + +extension LayerModel { + /// Constructs an `AnimationLayer` / `CALayer` that represents this `LayerModel` + func makeAnimationLayer(context: LayerContext) throws -> BaseCompositionLayer? { + let context = context.forLayer(self) + + switch (type, self) { + case (.precomp, let preCompLayerModel as PreCompLayerModel): + let preCompLayer = PreCompLayer(preCompLayer: preCompLayerModel) + try preCompLayer.setup(context: context) + return preCompLayer + + case (.solid, let solidLayerModel as SolidLayerModel): + return SolidLayer(solidLayerModel) + + case (.shape, let shapeLayerModel as ShapeLayerModel): + return try ShapeLayer(shapeLayer: shapeLayerModel, context: context) + + case (.image, let imageLayerModel as ImageLayerModel): + return ImageLayer(imageLayer: imageLayerModel, context: context) + + case (.text, let textLayerModel as TextLayerModel): + return try TextLayer(textLayerModel: textLayerModel, context: context) + + case (.null, _): + return TransformLayer(layerModel: self) + + default: + try context.logCompatibilityIssue(""" + Unexpected layer type combination ("\(type)" and "\(Swift.type(of: self))") + """) + + return nil + } + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/MaskCompositionLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/MaskCompositionLayer.swift new file mode 100644 index 0000000000..8663bd5eae --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/MaskCompositionLayer.swift @@ -0,0 +1,104 @@ +// Created by Cal Stephens on 1/6/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +import QuartzCore + +// MARK: - MaskCompositionLayer + +/// The CALayer type responsible for rendering the `Mask` of a `BaseCompositionLayer` +final class MaskCompositionLayer: CALayer { + + // MARK: Lifecycle + + init(masks: [Mask]) { + maskLayers = masks.map(MaskLayer.init(mask:)) + super.init() + + for maskLayer in maskLayers { + addSublayer(maskLayer) + } + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// Called by CoreAnimation to create a shadow copy of this layer + /// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init + override init(layer: Any) { + guard let typedLayer = layer as? Self else { + fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))") + } + + maskLayers = typedLayer.maskLayers + super.init(layer: typedLayer) + } + + // MARK: Internal + + override func layoutSublayers() { + super.layoutSublayers() + + for sublayer in sublayers ?? [] { + sublayer.fillBoundsOfSuperlayer() + } + } + + // MARK: Private + + private let maskLayers: [MaskLayer] + +} + +// MARK: AnimationLayer + +extension MaskCompositionLayer: AnimationLayer { + func setupAnimations(context: LayerAnimationContext) throws { + for maskLayer in maskLayers { + try maskLayer.setupAnimations(context: context) + } + } +} + +// MARK: - MaskLayer + +extension MaskCompositionLayer { + final class MaskLayer: CAShapeLayer { + + // MARK: Lifecycle + + init(mask: Mask) { + maskModel = mask + super.init() + fillColor = .rgb(0, 0, 0) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// Called by CoreAnimation to create a shadow copy of this layer + /// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init + override init(layer: Any) { + guard let typedLayer = layer as? Self else { + fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))") + } + + maskModel = typedLayer.maskModel + super.init(layer: typedLayer) + } + + // MARK: Private + + private let maskModel: Mask + + } +} + +// MARK: - MaskCompositionLayer.MaskLayer + AnimationLayer + +extension MaskCompositionLayer.MaskLayer: AnimationLayer { + func setupAnimations(context: LayerAnimationContext) throws { + try addAnimations(for: maskModel.shape, context: context) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/PreCompLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/PreCompLayer.swift new file mode 100644 index 0000000000..ee10d9f807 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/PreCompLayer.swift @@ -0,0 +1,140 @@ +// Created by Cal Stephens on 12/14/21. +// Copyright © 2021 Airbnb Inc. All rights reserved. + +import QuartzCore + +// MARK: - PreCompLayer + +/// The `CALayer` type responsible for rendering `PreCompLayerModel`s +final class PreCompLayer: BaseCompositionLayer { + + // MARK: Lifecycle + + init(preCompLayer: PreCompLayerModel) { + self.preCompLayer = preCompLayer + super.init(layerModel: preCompLayer) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// Called by CoreAnimation to create a shadow copy of this layer + /// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init + override init(layer: Any) { + guard let typedLayer = layer as? Self else { + fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))") + } + + preCompLayer = typedLayer.preCompLayer + timeRemappingInterpolator = typedLayer.timeRemappingInterpolator + super.init(layer: typedLayer) + } + + // MARK: Internal + + /// Post-init setup for `PreCompLayer`s. + /// Should always be called after `PreCompLayer.init(preCompLayer:)`. + /// + /// This is a workaround for a hard-to-reproduce crash that was + /// triggered when `PreCompLayer.init` was called reentantly. We didn't + /// have any consistent repro steps for this crash (it happened 100% of + /// the time for some testers, and 0% of the time for other testers), + /// but moving this code out of `PreCompLayer.init` does seem to fix it. + /// + /// The stack trace looked like: + /// - `_os_unfair_lock_recursive_abort` + /// - `-[CALayerAccessibility__UIKit__QuartzCore dealloc]` + /// - `PreCompLayer.__allocating_init(preCompLayer:context:)` <- reentrant init call + /// - ... + /// - `CALayer.setupLayerHierarchy(for:context:)` + /// - `PreCompLayer.init(preCompLayer:context:)` + /// + func setup(context: LayerContext) throws { + if let timeRemappingKeyframes = preCompLayer.timeRemapping { + timeRemappingInterpolator = try .timeRemapping(keyframes: timeRemappingKeyframes, context: context) + } else { + timeRemappingInterpolator = nil + } + + try setupLayerHierarchy( + for: context.animation.assetLibrary?.precompAssets[preCompLayer.referenceID]?.layers ?? [], + context: context) + } + + override func setupAnimations(context: LayerAnimationContext) throws { + var context = context + context = context.addingKeypathComponent(preCompLayer.name) + try setupLayerAnimations(context: context) + + // Precomp layers can adjust the local time of their child layers (relative to the + // animation's global time) via `timeRemapping` or a custom `startTime` + let contextForChildren = context.withTimeRemapping { [preCompLayer, timeRemappingInterpolator] layerLocalFrame in + if let timeRemappingInterpolator = timeRemappingInterpolator { + return timeRemappingInterpolator.value(frame: layerLocalFrame) as? AnimationFrameTime ?? layerLocalFrame + } else { + return layerLocalFrame + AnimationFrameTime(preCompLayer.startTime) + } + } + + try setupChildAnimations(context: contextForChildren) + } + + // MARK: Private + + private let preCompLayer: PreCompLayerModel + private var timeRemappingInterpolator: KeyframeInterpolator? + +} + +// MARK: CustomLayoutLayer + +extension PreCompLayer: CustomLayoutLayer { + func layout(superlayerBounds: CGRect) { + anchorPoint = .zero + + // Pre-comp layers use a size specified in the layer model, + // and clip the composition to that bounds + bounds = CGRect( + x: superlayerBounds.origin.x, + y: superlayerBounds.origin.y, + width: CGFloat(preCompLayer.width), + height: CGFloat(preCompLayer.height)) + + masksToBounds = true + } +} + +extension KeyframeInterpolator where ValueType == AnimationFrameTime { + /// A `KeyframeInterpolator` for the given `timeRemapping` keyframes + static func timeRemapping( + keyframes timeRemappingKeyframes: KeyframeGroup, + context: LayerContext) + throws + -> KeyframeInterpolator + { + try context.logCompatibilityIssue(""" + The Core Animation rendering engine partially supports time remapping keyframes, + but this is somewhat experimental and has some known issues. Since it doesn't work + in all cases, we have to fall back to using the main thread engine when using + `RenderingEngineOption.automatic`. + """) + + // `timeRemapping` is a mapping from the animation's global time to the layer's local time. + // In the Core Animation engine, we need to perform the opposite calculation -- convert + // the layer's local time into the animation's global time. We can get this by inverting + // the time remapping, swapping the x axis (global time) and the y axis (local time). + let localTimeToGlobalTimeMapping = timeRemappingKeyframes.keyframes.map { keyframe in + Keyframe( + value: keyframe.time, + time: keyframe.value.cgFloatValue * CGFloat(context.animation.framerate), + isHold: keyframe.isHold, + inTangent: keyframe.inTangent, + outTangent: keyframe.outTangent, + spatialInTangent: keyframe.spatialInTangent, + spatialOutTangent: keyframe.spatialOutTangent) + } + + return KeyframeInterpolator(keyframes: .init(localTimeToGlobalTimeMapping)) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/ShapeItemLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/ShapeItemLayer.swift new file mode 100644 index 0000000000..9c3d54415e --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/ShapeItemLayer.swift @@ -0,0 +1,257 @@ +// Created by Cal Stephens on 12/13/21. +// Copyright © 2021 Airbnb Inc. All rights reserved. + +import QuartzCore + +// MARK: - ShapeItemLayer + +/// A CALayer type that renders an array of `[ShapeItem]`s, +/// from a `Group` in a `ShapeLayerModel`. +final class ShapeItemLayer: BaseAnimationLayer { + + // MARK: Lifecycle + + /// Initializes a `ShapeItemLayer` that renders a `Group` from a `ShapeLayerModel` + /// - Parameters: + /// - shape: The `ShapeItem` in this group that renders a `GGPath` + /// - otherItems: Other items in this group that affect the appearance of the shape + init(shape: Item, otherItems: [Item], context: LayerContext) throws { + self.shape = shape + self.otherItems = otherItems + + try context.compatibilityAssert( + shape.item.drawsCGPath, + "`ShapeItemLayer` must contain exactly one `ShapeItem` that draws a `GPPath`") + + try context.compatibilityAssert( + !otherItems.contains(where: { $0.item.drawsCGPath }), + "`ShapeItemLayer` must contain exactly one `ShapeItem` that draws a `GPPath`") + + super.init() + + setupLayerHierarchy() + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// Called by CoreAnimation to create a shadow copy of this layer + /// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init + override init(layer: Any) { + guard let typedLayer = layer as? Self else { + fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))") + } + + shape = typedLayer.shape + otherItems = typedLayer.otherItems + super.init(layer: typedLayer) + } + + // MARK: Internal + + /// An item that can be displayed by this layer + struct Item { + /// A `ShapeItem` that should be rendered by this layer + let item: ShapeItem + + /// The group that contains this `ShapeItem`, if applicable + let parentGroup: Group? + } + + override func setupAnimations(context: LayerAnimationContext) throws { + try super.setupAnimations(context: context) + + guard let sublayerConfiguration = sublayerConfiguration else { return } + + switch sublayerConfiguration.fill { + case .solidFill(let shapeLayer): + try setupSolidFillAnimations(shapeLayer: shapeLayer, context: context) + + case .gradientFill(let gradientLayers): + try setupGradientFillAnimations( + gradientLayer: gradientLayers.gradientLayer, + maskLayer: gradientLayers.maskLayer, + context: context) + } + + if let gradientStrokeConfiguration = sublayerConfiguration.gradientStroke { + try setupGradientStrokeAnimations( + gradientLayer: gradientStrokeConfiguration.gradientLayer, + maskLayer: gradientStrokeConfiguration.maskLayer, + context: context) + } + } + + // MARK: Private + + private struct GradientLayers { + /// The `CALayer` that renders the actual gradient + let gradientLayer: GradientRenderLayer + /// The `CAShapeLayer` that clips the gradient layer to the expected shape + let maskLayer: CAShapeLayer + } + + /// The configuration of this layer's `fill` sublayers + private enum FillLayerConfiguration { + /// This layer displays a single `CAShapeLayer` + case solidFill(CAShapeLayer) + + /// This layer displays a `GradientRenderLayer` masked by a `CAShapeLayer`. + case gradientFill(GradientLayers) + } + + /// The `ShapeItem` in this group that renders a `GGPath` + private let shape: Item + + /// Other items in this group that affect the appearance of the shape + private let otherItems: [Item] + + /// The current configuration of this layer's sublayer(s) + private var sublayerConfiguration: (fill: FillLayerConfiguration, gradientStroke: GradientLayers?)? + + private func setupLayerHierarchy() { + // We have to build a different layer hierarchy depending on if + // we're rendering a gradient (a `CAGradientLayer` masked by a `CAShapeLayer`) + // or a solid shape (a simple `CAShapeLayer`). + let fillLayerConfiguration: FillLayerConfiguration + if otherItems.contains(where: { $0.item is GradientFill }) { + fillLayerConfiguration = setupGradientFillLayerHierarchy() + } else { + fillLayerConfiguration = setupSolidFillLayerHierarchy() + } + + let gradientStrokeConfiguration: GradientLayers? + if otherItems.contains(where: { $0.item is GradientStroke }) { + gradientStrokeConfiguration = setupGradientStrokeLayerHierarchy() + } else { + gradientStrokeConfiguration = nil + } + + sublayerConfiguration = (fillLayerConfiguration, gradientStrokeConfiguration) + } + + private func setupSolidFillLayerHierarchy() -> FillLayerConfiguration { + let shapeLayer = LottieCAShapeLayer() + addSublayer(shapeLayer) + + // `CAShapeLayer.fillColor` defaults to black, so we have to + // nil out the background color if there isn't an expected fill color + if !otherItems.contains(where: { $0.item is Fill }) { + shapeLayer.fillColor = nil + } + + return .solidFill(shapeLayer) + } + + private func setupGradientFillLayerHierarchy() -> FillLayerConfiguration { + let pathMask = LottieCAShapeLayer() + pathMask.fillColor = .rgb(0, 0, 0) + mask = pathMask + + let gradientLayer = GradientRenderLayer() + addSublayer(gradientLayer) + + return .gradientFill(.init(gradientLayer: gradientLayer, maskLayer: pathMask)) + } + + private func setupGradientStrokeLayerHierarchy() -> GradientLayers { + let container = BaseAnimationLayer() + + let pathMask = LottieCAShapeLayer() + pathMask.fillColor = nil + pathMask.strokeColor = .rgb(0, 0, 0) + container.mask = pathMask + + let gradientLayer = GradientRenderLayer() + container.addSublayer(gradientLayer) + addSublayer(container) + + return .init(gradientLayer: gradientLayer, maskLayer: pathMask) + } + + private func setupSolidFillAnimations( + shapeLayer: CAShapeLayer, + context: LayerAnimationContext) + throws + { + try shapeLayer.addAnimations(for: shape.item, context: context.for(shape)) + + if let (fill, context) = otherItems.first(Fill.self, context: context) { + try shapeLayer.addAnimations(for: fill, context: context) + } + + if let (stroke, context) = otherItems.first(Stroke.self, context: context) { + try shapeLayer.addStrokeAnimations(for: stroke, context: context) + } + + if let (trim, context) = otherItems.first(Trim.self, context: context) { + try shapeLayer.addAnimations(for: trim, context: context) + } + } + + private func setupGradientFillAnimations( + gradientLayer: GradientRenderLayer, + maskLayer: CAShapeLayer, + context: LayerAnimationContext) + throws + { + try maskLayer.addAnimations(for: shape.item, context: context.for(shape)) + + if let (gradientFill, context) = otherItems.first(GradientFill.self, context: context) { + try gradientLayer.addGradientAnimations(for: gradientFill, context: context) + } + } + + private func setupGradientStrokeAnimations( + gradientLayer: GradientRenderLayer, + maskLayer: CAShapeLayer, + context: LayerAnimationContext) + throws + { + try maskLayer.addAnimations(for: shape.item, context: context.for(shape)) + + if let (gradientStroke, context) = otherItems.first(GradientStroke.self, context: context) { + try gradientLayer.addGradientAnimations(for: gradientStroke, context: context) + try maskLayer.addStrokeAnimations(for: gradientStroke, context: context) + } + + if let (trim, context) = otherItems.first(Trim.self, context: context) { + try maskLayer.addAnimations(for: trim, context: context) + } + } + +} + +// MARK: - [ShapeItem] helpers + +extension Array where Element == ShapeItemLayer.Item { + /// The first `ShapeItem` in this array of the given type + func first( + _: ItemType.Type, context: LayerAnimationContext) + -> (item: ItemType, context: LayerAnimationContext)? + { + for item in self { + if let match = item.item as? ItemType { + return (match, context.for(item)) + } + } + + return nil + } +} + +extension LayerAnimationContext { + /// An updated `LayerAnimationContext` with the`AnimationKeypath` + /// that refers to this specific `ShapeItem`. + func `for`(_ item: ShapeItemLayer.Item) -> LayerAnimationContext { + var context = self + + if let group = item.parentGroup { + context.currentKeypath.keys.append(group.name) + } + + context.currentKeypath.keys.append(item.item.name) + return context + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/ShapeLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/ShapeLayer.swift new file mode 100644 index 0000000000..a997ea85cc --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/ShapeLayer.swift @@ -0,0 +1,305 @@ +// Created by Cal Stephens on 12/14/21. +// Copyright © 2021 Airbnb Inc. All rights reserved. + +import QuartzCore + +// MARK: - ShapeLayer + +/// The CALayer type responsible for rendering `ShapeLayerModel`s +final class ShapeLayer: BaseCompositionLayer { + + // MARK: Lifecycle + + init(shapeLayer: ShapeLayerModel, context: LayerContext) throws { + self.shapeLayer = shapeLayer + super.init(layerModel: shapeLayer) + try setupGroups(from: shapeLayer.items, parentGroup: nil, context: context) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// Called by CoreAnimation to create a shadow copy of this layer + /// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init + override init(layer: Any) { + guard let typedLayer = layer as? Self else { + fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))") + } + + shapeLayer = typedLayer.shapeLayer + super.init(layer: typedLayer) + } + + // MARK: Private + + private let shapeLayer: ShapeLayerModel + +} + +// MARK: - GroupLayer + +/// The CALayer type responsible for rendering `Group`s +final class GroupLayer: BaseAnimationLayer { + + // MARK: Lifecycle + + init(group: Group, inheritedItems: [ShapeItemLayer.Item], context: LayerContext) throws { + self.group = group + self.inheritedItems = inheritedItems + super.init() + try setupLayerHierarchy(context: context) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// Called by CoreAnimation to create a shadow copy of this layer + /// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init + override init(layer: Any) { + guard let typedLayer = layer as? Self else { + fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))") + } + + group = typedLayer.group + inheritedItems = typedLayer.inheritedItems + super.init(layer: typedLayer) + } + + // MARK: Internal + + override func setupAnimations(context: LayerAnimationContext) throws { + try super.setupAnimations(context: context) + + if let (shapeTransform, context) = nonGroupItems.first(ShapeTransform.self, context: context) { + try addTransformAnimations(for: shapeTransform, context: context) + try addOpacityAnimation(for: shapeTransform, context: context) + } + } + + // MARK: Private + + private let group: Group + + /// `ShapeItem`s that were listed in the parent's `items: [ShapeItem]` array + /// - This layer's parent is either the root `ShapeLayerModel` or some other `Group` + private let inheritedItems: [ShapeItemLayer.Item] + + /// `ShapeItem`s (other than nested `Group`s) that are included in this group + private lazy var nonGroupItems = group.items + .filter { !($0 is Group) } + .map { ShapeItemLayer.Item(item: $0, parentGroup: group) } + + inheritedItems + + private func setupLayerHierarchy(context: LayerContext) throws { + // Groups can contain other groups, so we may have to continue + // recursively creating more `GroupLayer`s + try setupGroups(from: group.items, parentGroup: group, context: context) + + // Create `ShapeItemLayer`s for each subgroup of shapes that should be rendered as a single unit + // - These groups are listed from front-to-back, so we have to add the sublayers in reverse order + for shapeRenderGroup in nonGroupItems.shapeRenderGroups.reversed() { + // If all of the path-drawing `ShapeItem`s have keyframes with the same timing information, + // we can combine the `[KeyframeGroup]` (which have to animate in separate layers) + // into a single `KeyframeGroup<[BezierPath]>`, which can be combined into a single CGPath animation. + // + // This is how Groups with multiple path-drawing items are supposed to be rendered, + // because combining multiple paths into a single `CGPath` (instead of rendering them in separate layers) + // allows `CAShapeLayerFillRule.evenOdd` to be applied if the paths overlap. We just can't do this + // in all cases, due to limitations of Core Animation. + if + shapeRenderGroup.pathItems.count > 1, + let combinedShapeKeyframes = Keyframes.combinedIfPossible( + shapeRenderGroup.pathItems.map { ($0.item as? Shape)?.path }), + // `Trim`s are currently only applied correctly using individual `ShapeItemLayer`s, + // because each path has to be trimmed separately. + !shapeRenderGroup.otherItems.contains(where: { $0.item is Trim }) + { + let combinedShape = CombinedShapeItem( + shapes: combinedShapeKeyframes, + name: group.name) + + let sublayer = try ShapeItemLayer( + shape: ShapeItemLayer.Item(item: combinedShape, parentGroup: group), + otherItems: shapeRenderGroup.otherItems, + context: context) + + addSublayer(sublayer) + } + + // Otherwise, if each `ShapeItem` that draws a `GGPath` animates independently, + // we have to create a separate `ShapeItemLayer` for each one. + else { + for pathDrawingItem in shapeRenderGroup.pathItems { + let sublayer = try ShapeItemLayer( + shape: pathDrawingItem, + otherItems: shapeRenderGroup.otherItems, + context: context) + + addSublayer(sublayer) + } + } + } + } + +} + +extension CALayer { + /// Sets up `GroupLayer`s for each `Group` in the given list of `ShapeItem`s + /// - Each `Group` item becomes its own `GroupLayer` sublayer. + /// - Other `ShapeItem` are applied to all sublayers + fileprivate func setupGroups(from items: [ShapeItem], parentGroup: Group?, context: LayerContext) throws { + let (groupItems, otherItems) = items.grouped(by: { $0 is Group }) + + // Groups are listed from front to back, + // but `CALayer.sublayers` are listed from back to front. + let groupsInZAxisOrder = groupItems.reversed() + + for group in groupsInZAxisOrder { + guard let group = group as? Group else { continue } + + // `ShapeItem`s either draw a path, or modify how a path is rendered. + // - If this group doesn't have any items that draw a path, then its + // items are applied to all of this groups children. + let inheritedItems: [ShapeItemLayer.Item] + if !otherItems.contains(where: { $0.drawsCGPath }) { + inheritedItems = otherItems.map { + ShapeItemLayer.Item(item: $0, parentGroup: parentGroup) + } + } else { + inheritedItems = [] + } + + let groupLayer = try GroupLayer( + group: group, + inheritedItems: inheritedItems, + context: context) + + addSublayer(groupLayer) + } + } +} + +extension ShapeItem { + /// Whether or not this `ShapeItem` is responsible for rendering a `CGPath` + var drawsCGPath: Bool { + switch type { + case .ellipse, .rectangle, .shape, .star: + return true + + case .fill, .gradientFill, .group, .gradientStroke, .merge, + .repeater, .round, .stroke, .trim, .transform, .unknown: + return false + } + } + + /// Whether or not this `ShapeItem` provides a fill for a set of shapes + var isFill: Bool { + switch type { + case .fill, .gradientFill: + return true + + case .ellipse, .rectangle, .shape, .star, .group, .gradientStroke, + .merge, .repeater, .round, .stroke, .trim, .transform, .unknown: + return false + } + } + + /// Whether or not this `ShapeItem` provides a stroke for a set of shapes + var isStroke: Bool { + switch type { + case .stroke, .gradientStroke: + return true + + case .ellipse, .rectangle, .shape, .star, .group, .gradientFill, + .merge, .repeater, .round, .fill, .trim, .transform, .unknown: + return false + } + } +} + +extension Collection { + /// Splits this collection into two groups, based on the given predicate + func grouped(by predicate: (Element) -> Bool) -> (trueElements: [Element], falseElements: [Element]) { + var trueElements = [Element]() + var falseElements = [Element]() + + for element in self { + if predicate(element) { + trueElements.append(element) + } else { + falseElements.append(element) + } + } + + return (trueElements, falseElements) + } +} + +// MARK: - ShapeRenderGroup + +/// A group of `ShapeItem`s that should be rendered together as a single unit +struct ShapeRenderGroup { + /// The items in this group that render `CGPath`s + var pathItems: [ShapeItemLayer.Item] = [] + /// Shape items that modify the appearance of the shapes rendered by this group + var otherItems: [ShapeItemLayer.Item] = [] +} + +extension Array where Element == ShapeItemLayer.Item { + /// Splits this list of `ShapeItem`s into groups that should be rendered together as individual units + var shapeRenderGroups: [ShapeRenderGroup] { + var renderGroups = [ShapeRenderGroup()] + + for item in self { + // `renderGroups` is non-empty, so is guaranteed to have a valid end index + let lastIndex = renderGroups.indices.last! + + if item.item.drawsCGPath { + renderGroups[lastIndex].pathItems.append(item) + } + + // `Fill` items are unique, because they specifically only apply to _previous_ shapes in a `Group` + // - For example, with [Rectangle, Fill(Red), Circle, Fill(Blue)], the Rectangle should be Red + // but the Circle should be Blue. + // - To handle this, we create a new `ShapeRenderGroup` when we encounter a `Fill` item + else if item.item.isFill { + renderGroups[lastIndex].otherItems.append(item) + renderGroups.append(ShapeRenderGroup()) + } + + // Other items in the list are applied to all subgroups + else { + for index in renderGroups.indices { + renderGroups[index].otherItems.append(item) + } + } + } + + // `Fill` and `Stroke` items have an `alpha` property that can be animated separately, + // but each layer only has a single `opacity` property, so we have to create + // separate layers / render groups for each of these if necessary. + return renderGroups.flatMap { group -> [ShapeRenderGroup] in + let (strokesAndFills, otherItems) = group.otherItems.grouped(by: { $0.item.isFill || $0.item.isStroke }) + + // However, if all of the strokes / fills have the exact same opacity animation configuration, + // then we can continue using a single layer / render group. + let allAlphaAnimationsAreIdentical = strokesAndFills.allSatisfy { item in + (item.item as? OpacityAnimationModel)?.opacity + == (strokesAndFills.first?.item as? OpacityAnimationModel)?.opacity + } + + if allAlphaAnimationsAreIdentical { + return [group] + } + + // Create a new group for each stroke / fill + return strokesAndFills.map { strokeOrFill in + ShapeRenderGroup( + pathItems: group.pathItems, + otherItems: [strokeOrFill] + otherItems) + } + } + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/SolidLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/SolidLayer.swift new file mode 100644 index 0000000000..c2be2550e8 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/SolidLayer.swift @@ -0,0 +1,47 @@ +// Created by Cal Stephens on 12/13/21. +// Copyright © 2021 Airbnb Inc. All rights reserved. + +import QuartzCore + +// MARK: - SolidLayer + +final class SolidLayer: BaseCompositionLayer { + + // MARK: Lifecycle + + init(_ solidLayer: SolidLayerModel) { + self.solidLayer = solidLayer + super.init(layerModel: solidLayer) + setupContentLayer() + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// Called by CoreAnimation to create a shadow copy of this layer + /// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init + override init(layer: Any) { + guard let typedLayer = layer as? Self else { + fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))") + } + + solidLayer = typedLayer.solidLayer + super.init(layer: typedLayer) + } + + // MARK: Private + + private let solidLayer: SolidLayerModel + + private func setupContentLayer() { + // Render the fill color in a child `CAShapeLayer` + // - Using a `CAShapeLayer` specifically, instead of a `CALayer` with a `backgroundColor`, + // allows the size of the fill shape to be different from `contentsLayer.size`. + let shapeLayer = LottieCAShapeLayer() + shapeLayer.fillColor = solidLayer.colorHex.cgColor + shapeLayer.path = CGPath(rect: .init(x: 0, y: 0, width: solidLayer.width, height: solidLayer.height), transform: nil) + addSublayer(shapeLayer) + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/TextLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/TextLayer.swift new file mode 100644 index 0000000000..28fecbbb45 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/TextLayer.swift @@ -0,0 +1,91 @@ +// Created by Cal Stephens on 2/9/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +import QuartzCore + +/// The `CALayer` type responsible for rendering `TextLayer`s +final class TextLayer: BaseCompositionLayer { + + // MARK: Lifecycle + + init( + textLayerModel: TextLayerModel, + context: LayerContext) + throws + { + self.textLayerModel = textLayerModel + super.init(layerModel: textLayerModel) + setupSublayers() + try configureRenderLayer(with: context) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// Called by CoreAnimation to create a shadow copy of this layer + /// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init + override init(layer: Any) { + guard let typedLayer = layer as? Self else { + fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))") + } + + textLayerModel = typedLayer.textLayerModel + super.init(layer: typedLayer) + } + + // MARK: Internal + + func configureRenderLayer(with context: LayerContext) throws { + // We can't use `CATextLayer`, because it doesn't support enough features we use. + // Instead, we use the same `CoreTextRenderLayer` (with a custom `draw` implementation) + // used by the Main Thread rendering engine. This means the Core Animation engine can't + // _animate_ text properties, but it can display static text without any issues. + let text = try textLayerModel.text.exactlyOneKeyframe(context: context, description: "text layer text").value + + // The Core Animation engine doesn't currently support `TextAnimator`s. + // - We could add support for animating the transform-related properties without much trouble. + // - We may be able to support animating `fillColor` by getting clever with layer blend modes + // or masks (e.g. use `CoreTextRenderLayer` to draw black glyphs, and then fill them in + // using a `CAShapeLayer`). + if !textLayerModel.animators.isEmpty { + try context.logCompatibilityIssue(""" + The Core Animation rendering engine currently doesn't support text animators. + """) + } + + renderLayer.text = text.text + renderLayer.font = context.fontProvider.fontFor(family: text.fontFamily, size: CGFloat(text.fontSize)) + + renderLayer.alignment = text.justification.textAlignment + renderLayer.lineHeight = CGFloat(text.lineHeight) + renderLayer.tracking = (CGFloat(text.fontSize) * CGFloat(text.tracking)) / 1000 + + renderLayer.fillColor = text.fillColorData?.cgColorValue + renderLayer.strokeColor = text.strokeColorData?.cgColorValue + renderLayer.strokeWidth = CGFloat(text.strokeWidth ?? 0) + renderLayer.strokeOnTop = text.strokeOverFill ?? false + + renderLayer.preferredSize = text.textFrameSize?.sizeValue + renderLayer.sizeToFit() + + renderLayer.transform = CATransform3DIdentity + renderLayer.position = text.textFramePosition?.pointValue ?? .zero + } + + // MARK: Private + + private let textLayerModel: TextLayerModel + private let renderLayer = CoreTextRenderLayer() + + private func setupSublayers() { + // Place the text render layer in an additional container + // - Direct sublayers of a `BaseCompositionLayer` always fill the bounds + // of their superlayer -- so this container will be the bounds of self, + // and the text render layer can be positioned anywhere. + let textContainerLayer = CALayer() + textContainerLayer.addSublayer(renderLayer) + addSublayer(textContainerLayer) + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/TransformLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/TransformLayer.swift new file mode 100644 index 0000000000..027739a447 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/Layers/TransformLayer.swift @@ -0,0 +1,11 @@ +// Created by Cal Stephens on 12/21/21. +// Copyright © 2021 Airbnb Inc. All rights reserved. + +/// The CALayer type responsible for only rendering the `transform` of a `LayerModel` +final class TransformLayer: BaseCompositionLayer { + + /// `TransformLayer`s don't render any visible content, + /// they just `transform` their sublayers + override var renderLayerContents: Bool { false } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/ValueProviderStore.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/ValueProviderStore.swift new file mode 100644 index 0000000000..a3d4aecf90 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/CoreAnimation/ValueProviderStore.swift @@ -0,0 +1,127 @@ +// Created by Cal Stephens on 1/13/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +import QuartzCore + +// MARK: - ValueProviderStore + +/// Registration and storage for `AnyValueProvider`s that can dynamically +/// provide custom values for `AnimationKeypath`s within an `Animation`. +final class ValueProviderStore { + + // MARK: Internal + + /// Registers an `AnyValueProvider` for the given `AnimationKeypath` + func setValueProvider(_ valueProvider: AnyValueProvider, keypath: AnimationKeypath) { + LottieLogger.shared.assert( + valueProvider.typeErasedStorage.isSupportedByCoreAnimationRenderingEngine, + """ + The Core Animation rendering engine doesn't support Value Providers that vend a closure, + because that would require calling the closure on the main thread once per frame. + """) + + // TODO: Support more value types + LottieLogger.shared.assert( + keypath.keys.last == PropertyName.color.rawValue, + "The Core Animation rendering engine currently only supports customizing color values") + + valueProviders.append((keypath: keypath, valueProvider: valueProvider)) + } + + // Retrieves the custom value keyframes for the given property, + // if an `AnyValueProvider` was registered for the given keypath. + func customKeyframes( + of customizableProperty: CustomizableProperty, + for keypath: AnimationKeypath, + context: LayerAnimationContext) + throws + -> KeyframeGroup? + { + guard let anyValueProvider = valueProvider(for: keypath) else { + return nil + } + + // Retrieve the type-erased keyframes from the custom `ValueProvider` + let typeErasedKeyframes: [Keyframe] + switch anyValueProvider.typeErasedStorage { + case .singleValue(let typeErasedValue): + typeErasedKeyframes = [Keyframe(typeErasedValue)] + + case .keyframes(let keyframes, _): + typeErasedKeyframes = keyframes + + case .closure: + try context.logCompatibilityIssue(""" + The Core Animation rendering engine doesn't support Value Providers that vend a closure, + because that would require calling the closure on the main thread once per frame. + """) + return nil + } + + // Convert the type-erased keyframe values using this `CustomizableProperty`'s conversion closure + let typedKeyframes = typeErasedKeyframes.compactMap { typeErasedKeyframe -> Keyframe? in + guard let convertedValue = customizableProperty.conversion(typeErasedKeyframe.value) else { + LottieLogger.shared.assertionFailure(""" + Could not convert value of type \(type(of: typeErasedKeyframe.value)) to expected type \(Value.self) + """) + return nil + } + + return typeErasedKeyframe.withValue(convertedValue) + } + + // Verify that all of the keyframes were successfully converted to the expected type + guard typedKeyframes.count == typeErasedKeyframes.count else { + return nil + } + + return KeyframeGroup(keyframes: ContiguousArray(typedKeyframes)) + } + + // MARK: Private + + private var valueProviders = [(keypath: AnimationKeypath, valueProvider: AnyValueProvider)]() + + /// Retrieves the most-recently-registered Value Provider that matches the given keypat + private func valueProvider(for keypath: AnimationKeypath) -> AnyValueProvider? { + // Find the last keypath matching the given keypath, + // so we return the value provider that was registered most-recently + valueProviders.last(where: { registeredKeypath, _ in + keypath.matches(registeredKeypath) + })?.valueProvider + } + +} + +extension AnyValueProviderStorage { + /// Whether or not this type of value provider is supported + /// by the new Core Animation rendering engine + var isSupportedByCoreAnimationRenderingEngine: Bool { + switch self { + case .singleValue, .keyframes: + return true + case .closure: + return false + } + } +} + +extension AnimationKeypath { + /// Whether or not this keypath from the animation hierarchy + /// matches the given keypath (which may contain wildcards) + func matches(_ keypath: AnimationKeypath) -> Bool { + var regex = "^" // match the start of the string + + keypath.keys.joined(separator: "\\.") // match this keypath, escaping "." characters + + "$" // match the end of the string + + // ** wildcards match anything + // - "**.Color" matches both "Layer 1.Color" and "Layer 1.Layer 2.Color" + regex = regex.replacingOccurrences(of: "**", with: ".+") + + // * wildcards match any individual path component + // - "*.Color" matches "Layer 1.Color" but not "Layer 1.Layer 2.Color" + regex = regex.replacingOccurrences(of: "*", with: "[^.]+") + + return fullPath.range(of: regex, options: .regularExpression) != nil + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.cpp new file mode 100644 index 0000000000..e4fca1e328 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.cpp @@ -0,0 +1,29 @@ +#include "CompositionLayer.hpp" + +#include "Lottie/Public/Primitives/RenderTree.hpp" + +namespace lottie { + +InvertedMatteLayer::InvertedMatteLayer(std::shared_ptr inputMatte) : +_inputMatte(inputMatte) { + setBounds(inputMatte->bounds()); + setNeedsDisplay(true); + + addSublayer(_inputMatte); +} + +void InvertedMatteLayer::setup() { + _inputMatte->setLayerDelegate(shared_from_base()); +} + +void InvertedMatteLayer::frameUpdated(double frame) { + setNeedsDisplay(true); +} + +std::shared_ptr makeInvertedMatteLayer(std::shared_ptr compositionLayer) { + auto result = std::make_shared(compositionLayer); + result->setup(); + return result; +} + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp new file mode 100644 index 0000000000..df28991e4e --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp @@ -0,0 +1,217 @@ +#ifndef CompositionLayer_hpp +#define CompositionLayer_hpp + +#include "Lottie/Public/Primitives/Vectors.hpp" +#include "Lottie/Public/Primitives/CALayer.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.hpp" +#include "Lottie/Private/Model/Layers/LayerModel.hpp" +#include "Lottie/Private/MainThread/LayerContainers/Utility/LayerTransformNode.hpp" +#include "Lottie/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.hpp" +#include "Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayerDelegate.hpp" + +#include + +namespace lottie { + +class CompositionLayer; +class InvertedMatteLayer; + +/// A layer that inverses the alpha output of its input layer. +class InvertedMatteLayer: public CALayer, public CompositionLayerDelegate { +public: + InvertedMatteLayer(std::shared_ptr inputMatte); + + void setup(); + + std::shared_ptr _inputMatte; + //let wrapperLayer = CALayer() + + virtual void frameUpdated(double frame) override; + /*virtual bool implementsDraw() const override; + virtual void draw(std::shared_ptr const &context) override;*/ + //virtual std::shared_ptr renderableItem() override; + + virtual bool isInvertedMatte() const override { + return true; + } +}; + +std::shared_ptr makeInvertedMatteLayer(std::shared_ptr compositionLayer); + +/// The base class for a child layer of CompositionContainer +class CompositionLayer: public CALayer, public KeypathSearchable { +public: + CompositionLayer(std::shared_ptr const &layer, Vector2D size) { + _contentsLayer = std::make_shared(); + + _transformNode = std::make_shared(layer->transform); + + if (layer->masks.has_value()) { + _maskLayer = std::make_shared(layer->masks.value()); + } else { + _maskLayer = nullptr; + } + + _matteType = layer->matte; + + _inFrame = layer->inFrame; + _outFrame = layer->outFrame; + _timeStretch = layer->timeStretch(); + _startFrame = layer->startTime; + if (layer->name.has_value()) { + _keypathName = layer->name.value(); + } else { + _keypathName = "Layer"; + } + + _childKeypaths.push_back(_transformNode->transformProperties()); + + _contentsLayer->setBounds(CGRect(0.0, 0.0, size.x, size.y)); + + if (layer->blendMode.has_value() && layer->blendMode.value() != BlendMode::Normal) { + setCompositingFilter(layer->blendMode); + } + + addSublayer(_contentsLayer); + + if (_maskLayer) { + _contentsLayer->setMask(_maskLayer); + } + } + + virtual std::string keypathName() const override { + return _keypathName; + } + + virtual std::map> keypathProperties() const override { + return {}; + } + + virtual std::shared_ptr keypathLayer() const override { + return _contentsLayer; + } + + void displayWithFrame(double frame, bool forceUpdates) { + _transformNode->updateTree(frame, forceUpdates); + bool layerVisible = isInRangeOrEqual(frame, _inFrame, _outFrame); + /// Only update contents if current time is within the layers time bounds. + if (layerVisible) { + displayContentsWithFrame(frame, forceUpdates); + if (_maskLayer) { + _maskLayer->updateWithFrame(frame, forceUpdates); + } + } + _contentsLayer->setTransform(_transformNode->globalTransform()); + _contentsLayer->setOpacity(_transformNode->opacity()); + _contentsLayer->setIsHidden(!layerVisible); + + if (const auto delegate = _layerDelegate.lock()) { + delegate->frameUpdated(frame); + } + } + + virtual void displayContentsWithFrame(double frame, bool forceUpdates) { + /// To be overridden by subclass + } + + + virtual std::vector> const &childKeypaths() const override { + return _childKeypaths; + } + + std::shared_ptr _matteLayer; + void setMatteLayer(std::shared_ptr matteLayer) { + _matteLayer = matteLayer; + if (matteLayer) { + if (_matteType.has_value() && _matteType.value() == MatteType::Invert) { + setMask(makeInvertedMatteLayer(matteLayer)); + } else { + setMask(matteLayer); + } + } else { + setMask(nullptr); + } + } + + std::weak_ptr const &layerDelegate() const { + return _layerDelegate; + } + void setLayerDelegate(std::weak_ptr const &layerDelegate) { + _layerDelegate = layerDelegate; + } + + std::shared_ptr const &contentsLayer() const { + return _contentsLayer; + } + + std::shared_ptr const &maskLayer() const { + return _maskLayer; + } + void setMaskLayer(std::shared_ptr const &maskLayer) { + _maskLayer = maskLayer; + } + + std::optional const &matteType() const { + return _matteType; + } + + double inFrame() const { + return _inFrame; + } + double outFrame() const { + return _outFrame; + } + double startFrame() const { + return _startFrame; + } + double timeStretch() const { + return _timeStretch; + } + + virtual std::shared_ptr renderTreeNode() { + return nullptr; + } + +public: + std::shared_ptr const transformNode() const { + return _transformNode; + } + +protected: + std::shared_ptr _contentsLayer; + std::optional _matteType; + +private: + std::weak_ptr _layerDelegate; + + std::shared_ptr _transformNode; + + std::shared_ptr _maskLayer; + + double _inFrame = 0.0; + double _outFrame = 0.0; + double _startFrame = 0.0; + double _timeStretch = 0.0; + + // MARK: Keypath Searchable + + std::string _keypathName; + + //std::shared_ptr _renderTree; + +public: + virtual bool isImageCompositionLayer() const { + return false; + } + + virtual bool isTextCompositionLayer() const { + return false; + } + +protected: + std::vector> _childKeypaths; +}; + +} + +#endif /* CompositionLayer_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.swift new file mode 100644 index 0000000000..0c647bf907 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.swift @@ -0,0 +1,164 @@ +// +// LayerContainer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/22/19. +// + +import Foundation +import QuartzCore + +protocol LottieDrawingLayer: CALayer { +} + +// MARK: - CompositionLayer + +/// The base class for a child layer of CompositionContainer +class CompositionLayer: CALayer, KeypathSearchable { + + // MARK: Lifecycle + + init(layer: LayerModel, size: CGSize) { + transformNode = LayerTransformNode(transform: layer.transform) + if let masks = layer.masks { + maskLayer = MaskContainerLayer(masks: masks) + } else { + maskLayer = nil + } + matteType = layer.matte + inFrame = layer.inFrame.cgFloat + outFrame = layer.outFrame.cgFloat + timeStretch = layer.timeStretch.cgFloat + startFrame = layer.startTime.cgFloat + keypathName = layer.name + childKeypaths = [transformNode.transformProperties] + super.init() + anchorPoint = .zero + actions = [ + "opacity" : NSNull(), + "transform" : NSNull(), + "bounds" : NSNull(), + "anchorPoint" : NSNull(), + "sublayerTransform" : NSNull(), + ] + + contentsLayer.anchorPoint = .zero + contentsLayer.bounds = CGRect(origin: .zero, size: size) + contentsLayer.actions = [ + "opacity" : NSNull(), + "transform" : NSNull(), + "bounds" : NSNull(), + "anchorPoint" : NSNull(), + "sublayerTransform" : NSNull(), + "hidden" : NSNull(), + ] + compositingFilter = layer.blendMode.filterName + addSublayer(contentsLayer) + + if let maskLayer = maskLayer { + contentsLayer.mask = maskLayer + } + + name = layer.name + } + + override init(layer: Any) { + /// Used for creating shadow model layers. Read More here: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init + guard let layer = layer as? CompositionLayer else { + fatalError("Wrong Layer Class") + } + transformNode = layer.transformNode + matteType = layer.matteType + inFrame = layer.inFrame + outFrame = layer.outFrame + timeStretch = layer.timeStretch + startFrame = layer.startFrame + keypathName = layer.keypathName + childKeypaths = [transformNode.transformProperties] + maskLayer = nil + super.init(layer: layer) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + + weak var layerDelegate: CompositionLayerDelegate? + + let transformNode: LayerTransformNode + + let contentsLayer = CALayer() + + let maskLayer: MaskContainerLayer? + + let matteType: MatteType? + + let inFrame: CGFloat + let outFrame: CGFloat + let startFrame: CGFloat + let timeStretch: CGFloat + + // MARK: Keypath Searchable + + let keypathName: String + + final var childKeypaths: [KeypathSearchable] + + var renderScale: CGFloat = 1 { + didSet { + updateRenderScale() + } + } + + var matteLayer: CompositionLayer? { + didSet { + if let matte = matteLayer { + if let type = matteType, type == .invert { + mask = InvertedMatteLayer(inputMatte: matte) + } else { + mask = matte + } + } else { + mask = nil + } + } + } + + var keypathProperties: [String: AnyNodeProperty] { + [:] + } + + var keypathLayer: CALayer? { + contentsLayer + } + + final func displayWithFrame(frame: CGFloat, forceUpdates: Bool) { + transformNode.updateTree(frame, forceUpdates: forceUpdates) + let layerVisible = frame.isInRangeOrEqual(inFrame, outFrame) + /// Only update contents if current time is within the layers time bounds. + if layerVisible { + displayContentsWithFrame(frame: frame, forceUpdates: forceUpdates) + maskLayer?.updateWithFrame(frame: frame, forceUpdates: forceUpdates) + } + contentsLayer.transform = transformNode.globalTransform + contentsLayer.opacity = transformNode.opacity + contentsLayer.isHidden = !layerVisible + layerDelegate?.frameUpdated(frame: frame) + } + + func displayContentsWithFrame(frame _: CGFloat, forceUpdates _: Bool) { + /// To be overridden by subclass + } + + func updateRenderScale() { + contentsScale = renderScale + } +} + +// MARK: - CompositionLayerDelegate + +protocol CompositionLayerDelegate: AnyObject { + func frameUpdated(frame: CGFloat) +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/CompositionLayerDelegate.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/CompositionLayerDelegate.hpp new file mode 100644 index 0000000000..59b05a3426 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/CompositionLayerDelegate.hpp @@ -0,0 +1,13 @@ +#ifndef CompositionLayerDelegate_hpp +#define CompositionLayerDelegate_hpp + +namespace lottie { + +class CompositionLayerDelegate { +public: + virtual void frameUpdated(double frame) = 0; +}; + +} + +#endif /* CompositionLayerDelegate_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.cpp new file mode 100644 index 0000000000..ad3b669b24 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.cpp @@ -0,0 +1,5 @@ +#include "ImageCompositionLayer.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.hpp new file mode 100644 index 0000000000..8b9d709245 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.hpp @@ -0,0 +1,42 @@ +#ifndef ImageCompositionLayer_hpp +#define ImageCompositionLayer_hpp + +#include "Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp" +#include "Lottie/Private/Model/Layers/ImageLayerModel.hpp" + +namespace lottie { + +class ImageCompositionLayer: public CompositionLayer { +public: + ImageCompositionLayer(std::shared_ptr const &imageLayer, Vector2D const &size) : + CompositionLayer(imageLayer, size) { + _imageReferenceID = imageLayer->referenceID; + + contentsLayer()->setMasksToBounds(true); + } + + std::shared_ptr image() { + return _image; + } + void setImage(std::shared_ptr image) { + _image = image; + contentsLayer()->setContents(image); + } + + std::string const &imageReferenceID() { + return _imageReferenceID; + } + +public: + virtual bool isImageCompositionLayer() const override { + return true; + } + +private: + std::string _imageReferenceID; + std::shared_ptr _image; +}; + +} + +#endif /* ImageCompositionLayer_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.swift new file mode 100644 index 0000000000..b1be98001d --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.swift @@ -0,0 +1,50 @@ +// +// ImageCompositionLayer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/25/19. +// + +import CoreGraphics +import Foundation +import QuartzCore + +final class ImageCompositionLayer: CompositionLayer { + + // MARK: Lifecycle + + init(imageLayer: ImageLayerModel, size: CGSize) { + imageReferenceID = imageLayer.referenceID + super.init(layer: imageLayer, size: size) + contentsLayer.masksToBounds = true + contentsLayer.contentsGravity = CALayerContentsGravity.resize + } + + override init(layer: Any) { + /// Used for creating shadow model layers. Read More here: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init + guard let layer = layer as? ImageCompositionLayer else { + fatalError("init(layer:) Wrong Layer Class") + } + imageReferenceID = layer.imageReferenceID + image = nil + super.init(layer: layer) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + + let imageReferenceID: String + + var image: CGImage? = nil { + didSet { + if let image = image { + contentsLayer.contents = image + } else { + contentsLayer.contents = nil + } + } + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.cpp new file mode 100644 index 0000000000..398d52d57c --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.cpp @@ -0,0 +1,5 @@ +#include "MaskContainerLayer.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.hpp new file mode 100644 index 0000000000..069bf5c63b --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.hpp @@ -0,0 +1,176 @@ +#ifndef MaskContainerLayer_hpp +#define MaskContainerLayer_hpp + +#include "Lottie/Private/Model/Objects/Mask.hpp" +#include "Lottie/Public/Primitives/CALayer.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp" + +namespace lottie { + +inline MaskMode usableMaskMode(MaskMode mode) { + switch (mode) { + case MaskMode::Add: + return MaskMode::Add; + case MaskMode::Subtract: + return MaskMode::Subtract; + case MaskMode::Intersect: + return MaskMode::Intersect; + case MaskMode::Lighten: + return MaskMode::Add; + case MaskMode::Darken: + return MaskMode::Darken; + case MaskMode::Difference: + return MaskMode::Intersect; + case MaskMode::None: + return MaskMode::None; + } +} + +class MaskNodeProperties: public NodePropertyMap { +public: + MaskNodeProperties(std::shared_ptr const &mask) : + _mode(mask->mode()), + _inverted(mask->inverted) { + _opacity = std::make_shared>(std::make_shared>(mask->opacity->keyframes)); + _shape = std::make_shared>(std::make_shared>(mask->shape.keyframes)); + _expansion = std::make_shared>(std::make_shared>(mask->expansion->keyframes)); + + _propertyMap.insert(std::make_pair("Opacity", _opacity)); + _propertyMap.insert(std::make_pair("Shape", _shape)); + _propertyMap.insert(std::make_pair("Expansion", _expansion)); + + for (const auto &it : _propertyMap) { + _properties.push_back(it.second); + } + } + + virtual std::vector> &properties() override { + return _properties; + } + + virtual std::vector> const &childKeypaths() const override { + return _childKeypaths; + } + + std::shared_ptr> const &opacity() const { + return _opacity; + } + + std::shared_ptr> const &shape() const { + return _shape; + } + + std::shared_ptr> const &expansion() const { + return _expansion; + } + + MaskMode mode() const { + return _mode; + } + + bool inverted() const { + return _inverted; + } + +private: + std::map> _propertyMap; + std::vector> _childKeypaths; + + std::vector> _properties; + + MaskMode _mode = MaskMode::Add; + bool _inverted = false; + + std::shared_ptr> _opacity; + std::shared_ptr> _shape; + std::shared_ptr> _expansion; +}; + +class MaskLayer: public CALayer { +public: + MaskLayer(std::shared_ptr const &mask) : + _properties(mask) { + _maskLayer = std::make_shared(); + + addSublayer(_maskLayer); + + if (mask->mode() == MaskMode::Add) { + _maskLayer->setFillColor(Color(1.0, 0.0, 0.0, 1.0)); + } else { + _maskLayer->setFillColor(Color(0.0, 1.0, 0.0, 1.0)); + } + _maskLayer->setFillRule(FillRule::EvenOdd); + } + + void updateWithFrame(double frame, bool forceUpdates) { + if (_properties.opacity()->needsUpdate(frame) || forceUpdates) { + _properties.opacity()->update(frame); + setOpacity(_properties.opacity()->value().value); + } + + if (_properties.shape()->needsUpdate(frame) || forceUpdates) { + _properties.shape()->update(frame); + _properties.expansion()->update(frame); + + auto path = _properties.shape()->value().cgPath(); + auto usableMode = usableMaskMode(_properties.mode()); + if ((usableMode == MaskMode::Subtract && !_properties.inverted()) || + (usableMode == MaskMode::Add && _properties.inverted())) { + /// Add a bounds rect to invert the mask + auto newPath = CGPath::makePath(); + newPath->addRect(CGRect::veryLarge()); + newPath->addPath(path); + path = std::static_pointer_cast(newPath); + } + _maskLayer->setPath(path); + } + } + +private: + MaskNodeProperties _properties; + + std::shared_ptr _maskLayer; +}; + +class MaskContainerLayer: public CALayer { +public: + MaskContainerLayer(std::vector> const &masks) { + auto containerLayer = std::make_shared(); + bool firstObject = true; + for (const auto &mask : masks) { + auto maskLayer = std::make_shared(mask); + _maskLayers.push_back(maskLayer); + + auto usableMode = usableMaskMode(mask->mode()); + if (usableMode == MaskMode::None) { + continue; + } else if (usableMode == MaskMode::Add || firstObject) { + firstObject = false; + containerLayer->addSublayer(maskLayer); + } else { + containerLayer->setMask(maskLayer); + auto newContainer = std::make_shared(); + newContainer->addSublayer(containerLayer); + containerLayer = newContainer; + } + } + addSublayer(containerLayer); + } + + // MARK: Internal + + void updateWithFrame(double frame, bool forceUpdates) { + for (const auto &maskLayer : _maskLayers) { + maskLayer->updateWithFrame(frame, forceUpdates); + } + } + +private: + std::vector> _maskLayers; +}; + +} + +#endif /* MaskContainerLayer_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.swift new file mode 100644 index 0000000000..f98d321212 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.swift @@ -0,0 +1,191 @@ +// +// MaskContainerLayer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/25/19. +// + +import Foundation +import QuartzCore + +extension MaskMode { + var usableMode: MaskMode { + switch self { + case .add: + return .add + case .subtract: + return .subtract + case .intersect: + return .intersect + case .lighten: + return .add + case .darken: + return .darken + case .difference: + return .intersect + case .none: + return .none + } + } +} + +// MARK: - MaskContainerLayer + +final class MaskContainerLayer: CALayer { + + // MARK: Lifecycle + + init(masks: [Mask]) { + super.init() + anchorPoint = .zero + var containerLayer = CALayer() + var firstObject = true + for mask in masks { + let maskLayer = MaskLayer(mask: mask) + maskLayers.append(maskLayer) + if mask.mode.usableMode == .none { + continue + } else if mask.mode.usableMode == .add || firstObject { + firstObject = false + containerLayer.addSublayer(maskLayer) + } else { + containerLayer.mask = maskLayer + let newContainer = CALayer() + newContainer.addSublayer(containerLayer) + containerLayer = newContainer + } + } + addSublayer(containerLayer) + } + + override init(layer: Any) { + /// Used for creating shadow model layers. Read More here: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init + guard let layer = layer as? MaskContainerLayer else { + fatalError("init(layer:) Wrong Layer Class") + } + super.init(layer: layer) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + + func updateWithFrame(frame: CGFloat, forceUpdates: Bool) { + maskLayers.forEach({ $0.updateWithFrame(frame: frame, forceUpdates: forceUpdates) }) + } + + // MARK: Fileprivate + + fileprivate var maskLayers: [MaskLayer] = [] +} + +extension CGRect { + static var veryLargeRect: CGRect { + CGRect( + x: -100_000_000, + y: -100_000_000, + width: 200_000_000, + height: 200_000_000) + } +} + +// MARK: - MaskLayer + +private class MaskLayer: CALayer { + + // MARK: Lifecycle + + init(mask: Mask) { + properties = MaskNodeProperties(mask: mask) + super.init() + addSublayer(maskLayer) + anchorPoint = .zero + maskLayer.fillColor = mask.mode == .add + ? CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1, 0, 0, 1]) + : CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0, 1, 0, 1]) + maskLayer.fillRule = CAShapeLayerFillRule.evenOdd + actions = [ + "opacity" : NSNull(), + ] + + } + + override init(layer: Any) { + properties = nil + super.init(layer: layer) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + + let properties: MaskNodeProperties? + + let maskLayer = LottieCAShapeLayer() + + func updateWithFrame(frame: CGFloat, forceUpdates: Bool) { + guard let properties = properties else { return } + if properties.opacity.needsUpdate(frame: frame) || forceUpdates { + properties.opacity.update(frame: frame) + opacity = Float(properties.opacity.value.cgFloatValue) + } + + if properties.shape.needsUpdate(frame: frame) || forceUpdates { + properties.shape.update(frame: frame) + properties.expansion.update(frame: frame) + + let shapePath = properties.shape.value.cgPath() + var path = shapePath + if + properties.mode.usableMode == .subtract && !properties.inverted || + (properties.mode.usableMode == .add && properties.inverted) + { + /// Add a bounds rect to invert the mask + let newPath = CGMutablePath() + newPath.addRect(CGRect.veryLargeRect) + newPath.addPath(shapePath) + path = newPath + } + maskLayer.path = path + } + + } +} + +// MARK: - MaskNodeProperties + +private class MaskNodeProperties: NodePropertyMap { + + // MARK: Lifecycle + + init(mask: Mask) { + mode = mask.mode + inverted = mask.inverted + opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: mask.opacity.keyframes)) + shape = NodeProperty(provider: KeyframeInterpolator(keyframes: mask.shape.keyframes)) + expansion = NodeProperty(provider: KeyframeInterpolator(keyframes: mask.expansion.keyframes)) + propertyMap = [ + "Opacity" : opacity, + "Shape" : shape, + "Expansion" : expansion, + ] + properties = Array(propertyMap.values) + } + + // MARK: Internal + + var propertyMap: [String: AnyNodeProperty] + + var properties: [AnyNodeProperty] + + let mode: MaskMode + let inverted: Bool + + let opacity: NodeProperty + let shape: NodeProperty + let expansion: NodeProperty +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/NullCompositionLayer.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/NullCompositionLayer.cpp new file mode 100644 index 0000000000..6c5345cfd6 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/NullCompositionLayer.cpp @@ -0,0 +1,5 @@ +#include "NullCompositionLayer.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/NullCompositionLayer.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/NullCompositionLayer.hpp new file mode 100644 index 0000000000..c3d3dea5cc --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/NullCompositionLayer.hpp @@ -0,0 +1,17 @@ +#ifndef NullCompositionLayer_hpp +#define NullCompositionLayer_hpp + +#include "Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp" + +namespace lottie { + +class NullCompositionLayer: public CompositionLayer { +public: + NullCompositionLayer(std::shared_ptr const &layer) : + CompositionLayer(layer, Vector2D::Zero()) { + } +}; + +} + +#endif /* NullCompositionLayer_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/NullCompositionLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/NullCompositionLayer.swift new file mode 100644 index 0000000000..3fdf163760 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/NullCompositionLayer.swift @@ -0,0 +1,28 @@ +// +// NullCompositionLayer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/25/19. +// + +import Foundation + +final class NullCompositionLayer: CompositionLayer { + + init(layer: LayerModel) { + super.init(layer: layer, size: .zero) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override init(layer: Any) { + /// Used for creating shadow model layers. Read More here: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init + guard let layer = layer as? NullCompositionLayer else { + fatalError("init(layer:) Wrong Layer Class") + } + super.init(layer: layer) + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.cpp new file mode 100644 index 0000000000..d3428a9b1b --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.cpp @@ -0,0 +1,5 @@ +#include "PreCompositionLayer.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.hpp new file mode 100644 index 0000000000..384d96d645 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.hpp @@ -0,0 +1,200 @@ +#ifndef PreCompositionLayer_hpp +#define PreCompositionLayer_hpp + +#include "Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp" +#include "Lottie/Private/Model/Layers/PreCompLayerModel.hpp" +#include "Lottie/Private/Model/Assets/PrecompAsset.hpp" +#include "Lottie/Private/MainThread/LayerContainers/Utility/LayerImageProvider.hpp" +#include "Lottie/Public/TextProvider/AnimationTextProvider.hpp" +#include "Lottie/Public/FontProvider/AnimationFontProvider.hpp" +#include "Lottie/Private/Model/Assets/AssetLibrary.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp" +#include "Lottie/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.hpp" + +namespace lottie { + +class PreCompositionLayer: public CompositionLayer { +public: + PreCompositionLayer( + std::shared_ptr const &precomp, + PrecompAsset const &asset, + std::shared_ptr const &layerImageProvider, + std::shared_ptr const &textProvider, + std::shared_ptr const &fontProvider, + std::shared_ptr const &assetLibrary, + double frameRate + ) : CompositionLayer(precomp, Vector2D(precomp->width, precomp->height)) { + if (precomp->timeRemapping) { + _remappingNode = std::make_shared>(std::make_shared>(precomp->timeRemapping->keyframes)); + } + _frameRate = frameRate; + + setBounds(CGRect(0.0, 0.0, precomp->width, precomp->height)); + contentsLayer()->setMasksToBounds(true); + contentsLayer()->setBounds(bounds()); + + auto layers = initializeCompositionLayers( + asset.layers, + assetLibrary, + layerImageProvider, + textProvider, + fontProvider, + frameRate + ); + + std::vector> imageLayers; + + std::shared_ptr mattedLayer; + + for (auto layerIt = layers.rbegin(); layerIt != layers.rend(); layerIt++) { + std::shared_ptr layer = *layerIt; + layer->setBounds(bounds()); + _animationLayers.push_back(layer); + + if (layer->isImageCompositionLayer()) { + imageLayers.push_back(std::static_pointer_cast(layer)); + } + if (mattedLayer) { + /// The previous layer requires this layer to be its matte + mattedLayer->setMatteLayer(layer); + mattedLayer = nullptr; + continue; + } + if (layer->matteType().has_value() && (layer->matteType().value() == MatteType::Add || layer->matteType().value() == MatteType::Invert)) { + /// We have a layer that requires a matte. + mattedLayer = layer; + } + contentsLayer()->addSublayer(layer); + } + + for (const auto &layer : layers) { + _childKeypaths.push_back(layer); + } + + layerImageProvider->addImageLayers(imageLayers); + } + + virtual std::map> keypathProperties() const override { + if (!_remappingNode) { + return {}; + } + + std::map> result; + result.insert(std::make_pair("Time Remap", _remappingNode)); + + return result; + } + + virtual void displayContentsWithFrame(double frame, bool forceUpdates) override { + double localFrame = 0.0; + if (_remappingNode) { + _remappingNode->update(frame); + localFrame = _remappingNode->value().value * _frameRate; + } else { + localFrame = (frame - startFrame()) / timeStretch(); + } + + for (const auto &animationLayer : _animationLayers) { + animationLayer->displayWithFrame(localFrame, forceUpdates); + } + } + + virtual std::shared_ptr renderTreeNode() override { + if (_contentsLayer->isHidden()) { + return nullptr; + } + + std::shared_ptr maskNode; + bool invertMask = false; + if (_matteLayer) { + maskNode = _matteLayer->renderTreeNode(); + if (maskNode && _matteType.has_value() && _matteType.value() == MatteType::Invert) { + invertMask = true; + } + } + + std::vector> renderTreeValue; + auto renderTreeContentItem = renderTree(); + if (renderTreeContentItem) { + renderTreeValue.push_back(renderTreeContentItem); + } + + std::vector> subnodes; + subnodes.push_back(std::make_shared( + _contentsLayer->bounds(), + _contentsLayer->position(), + _contentsLayer->transform(), + _contentsLayer->opacity(), + _contentsLayer->masksToBounds(), + _contentsLayer->isHidden(), + nullptr, + renderTreeValue, + nullptr, + false + )); + + assert(opacity() == 1.0); + assert(!isHidden()); + assert(!masksToBounds()); + assert(transform().isIdentity()); + assert(position() == Vector2D::Zero()); + + return std::make_shared( + bounds(), + position(), + transform(), + opacity(), + masksToBounds(), + isHidden(), + nullptr, + subnodes, + maskNode, + invertMask + ); + } + + std::shared_ptr renderTree() { + std::vector> result; + + for (const auto &animationLayer : _animationLayers) { + bool found = false; + for (const auto &sublayer : contentsLayer()->sublayers()) { + if (animationLayer == sublayer) { + found = true; + break; + } + } + if (found) { + auto node = animationLayer->renderTreeNode(); + if (node) { + result.push_back(node); + } + } + } + + std::vector> subnodes; + return std::make_shared( + CGRect(0.0, 0.0, 0.0, 0.0), + Vector2D(0.0, 0.0), + CATransform3D::identity(), + 1.0, + false, + false, + nullptr, + result, + nullptr, + false + ); + } + +private: + double _frameRate = 0.0; + std::shared_ptr> _remappingNode; + + std::vector> _animationLayers; +}; + +} + +#endif /* PreCompositionLayer_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.swift new file mode 100644 index 0000000000..d0722deb78 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.swift @@ -0,0 +1,121 @@ +// +// PreCompositionLayer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/25/19. +// + +import Foundation +import QuartzCore + +final class PreCompositionLayer: CompositionLayer { + + // MARK: Lifecycle + + init( + precomp: PreCompLayerModel, + asset: PrecompAsset, + layerImageProvider: LayerImageProvider, + textProvider: AnimationTextProvider, + fontProvider: AnimationFontProvider, + assetLibrary: AssetLibrary?, + frameRate: CGFloat) + { + animationLayers = [] + if let keyframes = precomp.timeRemapping?.keyframes { + remappingNode = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframes)) + } else { + remappingNode = nil + } + self.frameRate = frameRate + super.init(layer: precomp, size: CGSize(width: precomp.width, height: precomp.height)) + bounds = CGRect(origin: .zero, size: CGSize(width: precomp.width, height: precomp.height)) + contentsLayer.masksToBounds = true + contentsLayer.bounds = bounds + + let layers = asset.layers.initializeCompositionLayers( + assetLibrary: assetLibrary, + layerImageProvider: layerImageProvider, + textProvider: textProvider, + fontProvider: fontProvider, + frameRate: frameRate) + + var imageLayers = [ImageCompositionLayer]() + + var mattedLayer: CompositionLayer? = nil + + for layer in layers.reversed() { + layer.bounds = bounds + animationLayers.append(layer) + if let imageLayer = layer as? ImageCompositionLayer { + imageLayers.append(imageLayer) + } + if let matte = mattedLayer { + /// The previous layer requires this layer to be its matte + matte.matteLayer = layer + mattedLayer = nil + continue + } + if + let matte = layer.matteType, + matte == .add || matte == .invert + { + /// We have a layer that requires a matte. + mattedLayer = layer + } + contentsLayer.addSublayer(layer) + } + + childKeypaths.append(contentsOf: layers) + + layerImageProvider.addImageLayers(imageLayers) + } + + override init(layer: Any) { + /// Used for creating shadow model layers. Read More here: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init + guard let layer = layer as? PreCompositionLayer else { + fatalError("init(layer:) Wrong Layer Class") + } + frameRate = layer.frameRate + remappingNode = nil + animationLayers = [] + + super.init(layer: layer) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + + let frameRate: CGFloat + let remappingNode: NodeProperty? + + override var keypathProperties: [String: AnyNodeProperty] { + guard let remappingNode = remappingNode else { + return super.keypathProperties + } + return ["Time Remap" : remappingNode] + } + + override func displayContentsWithFrame(frame: CGFloat, forceUpdates: Bool) { + let localFrame: CGFloat + if let remappingNode = remappingNode { + remappingNode.update(frame: frame) + localFrame = remappingNode.value.cgFloatValue * frameRate + } else { + localFrame = (frame - startFrame) / timeStretch + } + animationLayers.forEach( { $0.displayWithFrame(frame: localFrame, forceUpdates: forceUpdates) }) + } + + override func updateRenderScale() { + super.updateRenderScale() + animationLayers.forEach( { $0.renderScale = renderScale } ) + } + + // MARK: Fileprivate + + fileprivate var animationLayers: [CompositionLayer] +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp new file mode 100644 index 0000000000..fd1f61ecdc --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp @@ -0,0 +1,1354 @@ +#include "ShapeCompositionLayer.hpp" + +#include "Lottie/Private/Model/ShapeItems/Group.hpp" +#include "Lottie/Private/Model/ShapeItems/Ellipse.hpp" +#include "Lottie/Private/Model/ShapeItems/Rectangle.hpp" +#include "Lottie/Private/Model/ShapeItems/Star.hpp" +#include "Lottie/Private/Model/ShapeItems/Shape.hpp" +#include "Lottie/Private/Model/ShapeItems/Trim.hpp" +#include "Lottie/Private/Model/ShapeItems/Stroke.hpp" +#include "Lottie/Private/Model/ShapeItems/GradientStroke.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/RenderLayers/GetGradientParameters.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/DashPatternInterpolator.hpp" +#include "Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeUtils/BezierPathUtils.hpp" +#include "Lottie/Private/Model/ShapeItems/ShapeTransform.hpp" + +namespace lottie { + +class ShapeLayerPresentationTree { +public: + class FillOutput { + public: + FillOutput() { + } + ~FillOutput() = default; + + virtual void update(AnimationFrameTime frameTime) = 0; + virtual std::shared_ptr fill() = 0; + }; + + class SolidFillOutput : public FillOutput { + public: + explicit SolidFillOutput(Fill const &fill) : + rule(fill.fillRule.value_or(FillRule::NonZeroWinding)), + color(fill.color.keyframes), + opacity(fill.opacity.keyframes) { + } + + virtual void update(AnimationFrameTime frameTime) override { + bool hasUpdates = false; + + if (color.hasUpdate(frameTime)) { + hasUpdates = true; + colorValue = color.value(frameTime); + } + + if (opacity.hasUpdate(frameTime)) { + hasUpdates = true; + opacityValue = opacity.value(frameTime).value; + } + + if (!_fill || hasUpdates) { + auto solid = std::make_shared(colorValue, opacityValue * 0.01); + _fill = std::make_shared( + solid, + rule + ); + } + } + + virtual std::shared_ptr fill() override { + return _fill; + } + + private: + FillRule rule; + + KeyframeInterpolator color; + Color colorValue = Color(0.0, 0.0, 0.0, 0.0); + + KeyframeInterpolator opacity; + double opacityValue = 0.0; + + std::shared_ptr _fill; + }; + + class GradientFillOutput : public FillOutput { + public: + explicit GradientFillOutput(GradientFill const &gradientFill) : + rule(FillRule::NonZeroWinding), + numberOfColors(gradientFill.numberOfColors), + gradientType(gradientFill.gradientType), + colors(gradientFill.colors.keyframes), + startPoint(gradientFill.startPoint.keyframes), + endPoint(gradientFill.endPoint.keyframes), + opacity(gradientFill.opacity.keyframes) { + } + + virtual void update(AnimationFrameTime frameTime) override { + bool hasUpdates = false; + + if (colors.hasUpdate(frameTime)) { + hasUpdates = true; + colorsValue = colors.value(frameTime); + } + + if (startPoint.hasUpdate(frameTime)) { + hasUpdates = true; + startPointValue = startPoint.value(frameTime); + } + + if (endPoint.hasUpdate(frameTime)) { + hasUpdates = true; + endPointValue = endPoint.value(frameTime); + } + + if (opacity.hasUpdate(frameTime)) { + hasUpdates = true; + opacityValue = opacity.value(frameTime).value; + } + + if (!_fill || hasUpdates) { + std::vector colors; + std::vector locations; + getGradientParameters(numberOfColors, colorsValue, colors, locations); + + auto gradient = std::make_shared( + opacityValue * 0.01, + gradientType, + colors, + locations, + Vector2D(startPointValue.x, startPointValue.y), + Vector2D(endPointValue.x, endPointValue.y) + ); + _fill = std::make_shared( + gradient, + rule + ); + } + } + + virtual std::shared_ptr fill() override { + return _fill; + } + + private: + FillRule rule; + int numberOfColors = 0; + GradientType gradientType; + + KeyframeInterpolator colors; + GradientColorSet colorsValue; + + KeyframeInterpolator startPoint; + Vector3D startPointValue = Vector3D(0.0, 0.0, 0.0); + + KeyframeInterpolator endPoint; + Vector3D endPointValue = Vector3D(0.0, 0.0, 0.0); + + KeyframeInterpolator opacity; + double opacityValue = 0.0; + + std::shared_ptr _fill; + }; + + class StrokeOutput { + public: + StrokeOutput() { + } + ~StrokeOutput() = default; + + virtual void update(AnimationFrameTime frameTime) = 0; + virtual std::shared_ptr stroke() = 0; + }; + + class SolidStrokeOutput : public StrokeOutput { + public: + SolidStrokeOutput(Stroke const &stroke) : + lineJoin(stroke.lineJoin), + lineCap(stroke.lineCap), + miterLimit(stroke.miterLimit.value_or(4.0)), + color(stroke.color.keyframes), + opacity(stroke.opacity.keyframes), + width(stroke.width.keyframes) { + if (stroke.dashPattern.has_value()) { + StrokeShapeDashConfiguration dashConfiguration(stroke.dashPattern.value()); + dashPattern = std::make_unique(dashConfiguration.dashPatterns); + + if (!dashConfiguration.dashPhase.empty()) { + dashPhase = std::make_unique>(dashConfiguration.dashPhase); + } + } + } + + virtual void update(AnimationFrameTime frameTime) override { + bool hasUpdates = false; + + if (color.hasUpdate(frameTime)) { + hasUpdates = true; + colorValue = color.value(frameTime); + } + + if (opacity.hasUpdate(frameTime)) { + hasUpdates = true; + opacityValue = opacity.value(frameTime).value; + } + + if (width.hasUpdate(frameTime)) { + hasUpdates = true; + widthValue = width.value(frameTime).value; + } + + if (dashPattern) { + if (dashPattern->hasUpdate(frameTime)) { + hasUpdates = true; + dashPatternValue = dashPattern->value(frameTime); + } + } + + if (dashPhase) { + if (dashPhase->hasUpdate(frameTime)) { + hasUpdates = true; + dashPhaseValue = dashPhase->value(frameTime).value; + } + } + + if (!_stroke || hasUpdates) { + bool hasNonZeroDashes = false; + if (!dashPatternValue.values.empty()) { + for (const auto &value : dashPatternValue.values) { + if (value != 0) { + hasNonZeroDashes = true; + break; + } + } + } + + auto solid = std::make_shared(colorValue, opacityValue * 0.01); + _stroke = std::make_shared( + solid, + widthValue, + lineJoin, + lineCap, + miterLimit, + hasNonZeroDashes ? dashPhaseValue : 0.0, + hasNonZeroDashes ? dashPatternValue.values : std::vector() + ); + } + } + + virtual std::shared_ptr stroke() override { + return _stroke; + } + + private: + LineJoin lineJoin; + LineCap lineCap; + double miterLimit = 4.0; + + KeyframeInterpolator color; + Color colorValue = Color(0.0, 0.0, 0.0, 0.0); + + KeyframeInterpolator opacity; + double opacityValue = 0.0; + + KeyframeInterpolator width; + double widthValue = 0.0; + + std::unique_ptr dashPattern; + DashPattern dashPatternValue = DashPattern({}); + + std::unique_ptr> dashPhase; + double dashPhaseValue = 0.0; + + std::shared_ptr _stroke; + }; + + class GradientStrokeOutput : public StrokeOutput { + public: + GradientStrokeOutput(GradientStroke const &gradientStroke) : + lineJoin(gradientStroke.lineJoin), + lineCap(gradientStroke.lineCap), + miterLimit(gradientStroke.miterLimit.value_or(4.0)), + numberOfColors(gradientStroke.numberOfColors), + gradientType(gradientStroke.gradientType), + colors(gradientStroke.colors.keyframes), + startPoint(gradientStroke.startPoint.keyframes), + endPoint(gradientStroke.endPoint.keyframes), + opacity(gradientStroke.opacity.keyframes), + width(gradientStroke.width.keyframes) { + if (gradientStroke.dashPattern.has_value()) { + StrokeShapeDashConfiguration dashConfiguration(gradientStroke.dashPattern.value()); + dashPattern = std::make_unique(dashConfiguration.dashPatterns); + + if (!dashConfiguration.dashPhase.empty()) { + dashPhase = std::make_unique>(dashConfiguration.dashPhase); + } + } + } + + virtual void update(AnimationFrameTime frameTime) override { + bool hasUpdates = false; + + if (colors.hasUpdate(frameTime)) { + hasUpdates = true; + colorsValue = colors.value(frameTime); + } + + if (startPoint.hasUpdate(frameTime)) { + hasUpdates = true; + startPointValue = startPoint.value(frameTime); + } + + if (endPoint.hasUpdate(frameTime)) { + hasUpdates = true; + endPointValue = endPoint.value(frameTime); + } + + if (opacity.hasUpdate(frameTime)) { + hasUpdates = true; + opacityValue = opacity.value(frameTime).value; + } + + if (width.hasUpdate(frameTime)) { + hasUpdates = true; + widthValue = width.value(frameTime).value; + } + + if (dashPattern) { + if (dashPattern->hasUpdate(frameTime)) { + hasUpdates = true; + dashPatternValue = dashPattern->value(frameTime); + } + } + + if (dashPhase) { + if (dashPhase->hasUpdate(frameTime)) { + hasUpdates = true; + dashPhaseValue = dashPhase->value(frameTime).value; + } + } + + if (!_stroke || hasUpdates) { + bool hasNonZeroDashes = false; + if (!dashPatternValue.values.empty()) { + for (const auto &value : dashPatternValue.values) { + if (value != 0) { + hasNonZeroDashes = true; + break; + } + } + } + + std::vector colors; + std::vector locations; + getGradientParameters(numberOfColors, colorsValue, colors, locations); + + auto gradient = std::make_shared( + opacityValue * 0.01, + gradientType, + colors, + locations, + Vector2D(startPointValue.x, startPointValue.y), + Vector2D(endPointValue.x, endPointValue.y) + ); + _stroke = std::make_shared( + gradient, + widthValue, + lineJoin, + lineCap, + miterLimit, + hasNonZeroDashes ? dashPhaseValue : 0.0, + hasNonZeroDashes ? dashPatternValue.values : std::vector() + ); + } + } + + virtual std::shared_ptr stroke() override { + return _stroke; + } + + private: + LineJoin lineJoin; + LineCap lineCap; + double miterLimit = 4.0; + + int numberOfColors = 0; + GradientType gradientType; + + KeyframeInterpolator colors; + GradientColorSet colorsValue; + + KeyframeInterpolator startPoint; + Vector3D startPointValue = Vector3D(0.0, 0.0, 0.0); + + KeyframeInterpolator endPoint; + Vector3D endPointValue = Vector3D(0.0, 0.0, 0.0); + + KeyframeInterpolator opacity; + double opacityValue = 0.0; + + KeyframeInterpolator width; + double widthValue = 0.0; + + std::unique_ptr dashPattern; + DashPattern dashPatternValue = DashPattern({}); + + std::unique_ptr> dashPhase; + double dashPhaseValue = 0.0; + + std::shared_ptr _stroke; + }; + + struct TrimParams { + double start = 0.0; + double end = 0.0; + double offset = 0.0; + TrimType type = TrimType::Simultaneously; + size_t subItemLimit = 0; + + TrimParams(double start_, double end_, double offset_, TrimType type_, size_t subItemLimit_) : + start(start_), + end(end_), + offset(offset_), + type(type_), + subItemLimit(subItemLimit_) { + } + }; + + class TrimParamsOutput { + public: + TrimParamsOutput(Trim const &trim, size_t subItemLimit) : + type(trim.trimType), + subItemLimit(subItemLimit), + start(trim.start.keyframes), + end(trim.end.keyframes), + offset(trim.offset.keyframes) { + } + + void update(AnimationFrameTime frameTime) { + if (start.hasUpdate(frameTime)) { + startValue = start.value(frameTime).value; + } + + if (end.hasUpdate(frameTime)) { + endValue = end.value(frameTime).value; + } + + if (offset.hasUpdate(frameTime)) { + offsetValue = offset.value(frameTime).value; + } + } + + TrimParams trimParams() { + double resolvedStartValue = startValue * 0.01; + double resolvedEndValue = endValue * 0.01; + double resolvedStart = std::min(resolvedStartValue, resolvedEndValue); + double resolvedEnd = std::max(resolvedStartValue, resolvedEndValue); + + double resolvedOffset = fmod(offsetValue, 360.0) / 360.0; + + return TrimParams(resolvedStart, resolvedEnd, resolvedOffset, type, subItemLimit); + } + + private: + TrimType type; + size_t subItemLimit = 0; + + KeyframeInterpolator start; + double startValue = 0.0; + + KeyframeInterpolator end; + double endValue = 0.0; + + KeyframeInterpolator offset; + double offsetValue = 0.0; + }; + + struct ShadingVariant { + std::shared_ptr fill; + std::shared_ptr stroke; + size_t subItemLimit = 0; + + std::shared_ptr renderTree; + }; + + struct TransformedPath { + BezierPath path; + CATransform3D transform; + + TransformedPath(BezierPath const &path_, CATransform3D const &transform_) : + path(path_), + transform(transform_) { + } + }; + + class PathOutput { + public: + PathOutput() { + } + virtual ~PathOutput() = default; + + virtual void update(AnimationFrameTime frameTime) = 0; + virtual BezierPath const *currentPath() = 0; + }; + + class StaticPathOutput : public PathOutput { + public: + explicit StaticPathOutput(BezierPath const &path) : + resolvedPath(path) { + } + + virtual void update(AnimationFrameTime frameTime) override { + } + + virtual BezierPath const *currentPath() override { + return &resolvedPath; + } + + private: + BezierPath resolvedPath; + }; + + class ShapePathOutput : public PathOutput { + public: + explicit ShapePathOutput(Shape const &shape) : + path(shape.path.keyframes) { + } + + virtual void update(AnimationFrameTime frameTime) override { + if (!hasValidData || path.hasUpdate(frameTime)) { + path.update(frameTime, resolvedPath); + } + + hasValidData = true; + } + + virtual BezierPath const *currentPath() override { + return &resolvedPath; + } + + private: + bool hasValidData = false; + + BezierPathKeyframeInterpolator path; + + BezierPath resolvedPath; + }; + + class RectanglePathOutput : public PathOutput { + public: + explicit RectanglePathOutput(Rectangle const &rectangle) : + direction(rectangle.direction.value_or(PathDirection::Clockwise)), + position(rectangle.position.keyframes), + size(rectangle.size.keyframes), + cornerRadius(rectangle.cornerRadius.keyframes) { + } + + virtual void update(AnimationFrameTime frameTime) override { + bool hasUpdates = false; + + if (!hasValidData || position.hasUpdate(frameTime)) { + hasUpdates = true; + positionValue = position.value(frameTime); + } + if (!hasValidData || size.hasUpdate(frameTime)) { + hasUpdates = true; + sizeValue = size.value(frameTime); + } + if (!hasValidData || cornerRadius.hasUpdate(frameTime)) { + hasUpdates = true; + cornerRadiusValue = cornerRadius.value(frameTime).value; + } + + if (hasUpdates) { + resolvedPath = makeRectangleBezierPath(Vector2D(positionValue.x, positionValue.y), Vector2D(sizeValue.x, sizeValue.y), cornerRadiusValue, direction); + } + + hasValidData = true; + } + + virtual BezierPath const *currentPath() override { + return &resolvedPath; + } + + private: + bool hasValidData = false; + + PathDirection direction; + + KeyframeInterpolator position; + Vector3D positionValue = Vector3D(0.0, 0.0, 0.0); + + KeyframeInterpolator size; + Vector3D sizeValue = Vector3D(0.0, 0.0, 0.0); + + KeyframeInterpolator cornerRadius; + double cornerRadiusValue = 0.0; + + BezierPath resolvedPath; + }; + + class EllipsePathOutput : public PathOutput { + public: + explicit EllipsePathOutput(Ellipse const &ellipse) : + direction(ellipse.direction.value_or(PathDirection::Clockwise)), + position(ellipse.position.keyframes), + size(ellipse.size.keyframes) { + } + + virtual void update(AnimationFrameTime frameTime) override { + bool hasUpdates = false; + + if (!hasValidData || position.hasUpdate(frameTime)) { + hasUpdates = true; + positionValue = position.value(frameTime); + } + if (!hasValidData || size.hasUpdate(frameTime)) { + hasUpdates = true; + sizeValue = size.value(frameTime); + } + + if (hasUpdates) { + resolvedPath = makeEllipseBezierPath(Vector2D(sizeValue.x, sizeValue.y), Vector2D(positionValue.x, positionValue.y), direction); + } + + hasValidData = true; + } + + virtual BezierPath const *currentPath() override { + return &resolvedPath; + } + + private: + bool hasValidData = false; + + PathDirection direction; + + KeyframeInterpolator position; + Vector3D positionValue = Vector3D(0.0, 0.0, 0.0); + + KeyframeInterpolator size; + Vector3D sizeValue = Vector3D(0.0, 0.0, 0.0); + + BezierPath resolvedPath; + }; + + class StarPathOutput : public PathOutput { + public: + explicit StarPathOutput(Star const &star) : + direction(star.direction.value_or(PathDirection::Clockwise)), + position(star.position.keyframes), + outerRadius(star.outerRadius.keyframes), + outerRoundedness(star.outerRoundness.keyframes), + rotation(star.rotation.keyframes), + points(star.points.keyframes) { + if (star.innerRadius.has_value()) { + innerRadius = std::make_unique>(std::make_shared>(star.innerRadius->keyframes)); + } else { + innerRadius = std::make_unique>(std::make_shared>(Vector1D(0.0))); + } + + if (star.innerRoundness.has_value()) { + innerRoundedness = std::make_unique>(std::make_shared>(star.innerRoundness->keyframes)); + } else { + innerRoundedness = std::make_unique>(std::make_shared>(Vector1D(0.0))); + } + } + + virtual void update(AnimationFrameTime frameTime) override { + bool hasUpdates = false; + + if (!hasValidData || position.hasUpdate(frameTime)) { + hasUpdates = true; + positionValue = position.value(frameTime); + } + + if (!hasValidData || outerRadius.hasUpdate(frameTime)) { + hasUpdates = true; + outerRadiusValue = outerRadius.value(frameTime).value; + } + + innerRadius->update(frameTime); + if (!hasValidData || innerRadiusValue != innerRadius->value().value) { + hasUpdates = true; + innerRadiusValue = innerRadius->value().value; + } + + if (!hasValidData || outerRoundedness.hasUpdate(frameTime)) { + hasUpdates = true; + outerRoundednessValue = outerRoundedness.value(frameTime).value; + } + + innerRoundedness->update(frameTime); + if (!hasValidData || innerRoundednessValue != innerRoundedness->value().value) { + hasUpdates = true; + innerRoundednessValue = innerRoundedness->value().value; + } + + if (!hasValidData || points.hasUpdate(frameTime)) { + hasUpdates = true; + pointsValue = points.value(frameTime).value; + } + + if (!hasValidData || rotation.hasUpdate(frameTime)) { + hasUpdates = true; + rotationValue = rotation.value(frameTime).value; + } + + if (hasUpdates) { + resolvedPath = makeStarBezierPath(Vector2D(positionValue.x, positionValue.y), outerRadiusValue, innerRadiusValue, outerRoundednessValue, innerRoundednessValue, pointsValue, rotationValue, direction); + } + + hasValidData = true; + } + + virtual BezierPath const *currentPath() override { + return &resolvedPath; + } + + private: + bool hasValidData = false; + + PathDirection direction; + + KeyframeInterpolator position; + Vector3D positionValue = Vector3D(0.0, 0.0, 0.0); + + KeyframeInterpolator outerRadius; + double outerRadiusValue = 0.0; + + KeyframeInterpolator outerRoundedness; + double outerRoundednessValue = 0.0; + + std::unique_ptr> innerRadius; + double innerRadiusValue = 0.0; + + std::unique_ptr> innerRoundedness; + double innerRoundednessValue = 0.0; + + KeyframeInterpolator rotation; + double rotationValue = 0.0; + + KeyframeInterpolator points; + double pointsValue = 0.0; + + BezierPath resolvedPath; + }; + + class TransformOutput { + public: + TransformOutput(std::shared_ptr shapeTransform) { + if (shapeTransform->anchor) { + _anchor = std::make_unique>(shapeTransform->anchor->keyframes); + } + if (shapeTransform->position) { + _position = std::make_unique>(shapeTransform->position->keyframes); + } + if (shapeTransform->scale) { + _scale = std::make_unique>(shapeTransform->scale->keyframes); + } + if (shapeTransform->rotation) { + _rotation = std::make_unique>(shapeTransform->rotation->keyframes); + } + if (shapeTransform->skew) { + _skew = std::make_unique>(shapeTransform->skew->keyframes); + } + if (shapeTransform->skewAxis) { + _skewAxis = std::make_unique>(shapeTransform->skewAxis->keyframes); + } + if (shapeTransform->opacity) { + _opacity = std::make_unique>(shapeTransform->opacity->keyframes); + } + } + + void update(AnimationFrameTime frameTime) { + bool hasUpdates = false; + + if (!hasValidData) { + hasUpdates = true; + } + if (_anchor && _anchor->hasUpdate(frameTime)) { + hasUpdates = true; + } + if (_position && _position->hasUpdate(frameTime)) { + hasUpdates = true; + } + if (_scale && _scale->hasUpdate(frameTime)) { + hasUpdates = true; + } + if (_rotation && _rotation->hasUpdate(frameTime)) { + hasUpdates = true; + } + if (_skew && _skew->hasUpdate(frameTime)) { + hasUpdates = true; + } + if (_skewAxis && _skewAxis->hasUpdate(frameTime)) { + hasUpdates = true; + } + if (_opacity && _opacity->hasUpdate(frameTime)) { + hasUpdates = true; + } + + if (hasUpdates) { + //TODO:optimize by storing components + + Vector3D anchorValue(0.0, 0.0, 0.0); + if (_anchor) { + anchorValue = _anchor->value(frameTime); + } + + Vector3D positionValue(0.0, 0.0, 0.0); + if (_position) { + positionValue = _position->value(frameTime); + } + + Vector3D scaleValue(100.0, 100.0, 100.0); + if (_scale) { + scaleValue = _scale->value(frameTime); + } + + double rotationValue = 0.0; + if (_rotation) { + rotationValue = _rotation->value(frameTime).value; + } + + double skewValue = 0.0; + if (_skew) { + skewValue = _skew->value(frameTime).value; + } + + double skewAxisValue = 0.0; + if (_skewAxis) { + skewAxisValue = _skewAxis->value(frameTime).value; + } + + if (_opacity) { + _opacityValue = _opacity->value(frameTime).value * 0.01; + } else { + _opacityValue = 1.0; + } + + _transformValue = CATransform3D::identity().translated(Vector2D(positionValue.x, positionValue.y)).rotated(rotationValue).skewed(-skewValue, skewAxisValue).scaled(Vector2D(scaleValue.x * 0.01, scaleValue.y * 0.01)).translated(Vector2D(-anchorValue.x, -anchorValue.y)); + + hasValidData = true; + } + } + + CATransform3D const &transform() { + return _transformValue; + } + + double opacity() { + return _opacityValue; + } + + private: + bool hasValidData = false; + + std::unique_ptr> _anchor; + std::unique_ptr> _position; + std::unique_ptr> _scale; + std::unique_ptr> _rotation; + std::unique_ptr> _skew; + std::unique_ptr> _skewAxis; + std::unique_ptr> _opacity; + + CATransform3D _transformValue = CATransform3D::identity(); + double _opacityValue = 1.0; + }; + + class ContentItem { + public: + ContentItem() { + } + + public: + bool isGroup = false; + + void setPath(std::unique_ptr &&path_) { + path = std::move(path_); + } + + void setTransform(std::unique_ptr &&transform_) { + transform = std::move(transform_); + } + + std::shared_ptr const &renderTree() const { + return _renderTree; + } + + private: + std::unique_ptr path; + std::unique_ptr transform; + + std::vector shadings; + std::vector> trims; + + std::vector> subItems; + + std::shared_ptr _renderTree; + + private: + std::vector collectPaths(AnimationFrameTime frameTime, size_t subItemLimit, CATransform3D parentTransform) { + std::vector mappedPaths; + + CATransform3D effectiveTransform = parentTransform; + CATransform3D effectiveChildTransform = parentTransform; + + size_t maxSubitem = std::min(subItems.size(), subItemLimit); + + if (path) { + path->update(frameTime); + mappedPaths.emplace_back(*(path->currentPath()), effectiveTransform); + } + + for (size_t i = 0; i < maxSubitem; i++) { + auto &subItem = subItems[i]; + CATransform3D subItemTransform = effectiveChildTransform; + + if (subItem->isGroup && subItem->transform) { + subItem->transform->update(frameTime); + subItemTransform = subItem->transform->transform() * subItemTransform; + } + + std::optional currentTrim; + if (!trims.empty()) { + trims[0]->update(frameTime); + currentTrim = trims[0]->trimParams(); + } + + auto subItemPaths = subItem->collectPaths(frameTime, INT32_MAX, subItemTransform); + + if (currentTrim) { + CompoundBezierPath tempPath; + for (auto &path : subItemPaths) { + tempPath.appendPath(path.path.copyUsingTransform(path.transform)); + } + CompoundBezierPath trimmedPath = trimCompoundPath(tempPath, currentTrim->start, currentTrim->end, currentTrim->offset, currentTrim->type); + for (auto &path : trimmedPath.paths) { + mappedPaths.emplace_back(path, CATransform3D::identity()); + } + } else { + for (auto &path : subItemPaths) { + mappedPaths.emplace_back(path.path, path.transform); + } + } + } + + return mappedPaths; + } + + public: + void addSubItem(std::shared_ptr const &subItem) { + subItems.push_back(subItem); + } + + void addFill(std::shared_ptr fill) { + ShadingVariant shading; + shading.subItemLimit = subItems.size(); + shading.fill = fill; + shadings.insert(shadings.begin(), shading); + } + + void addStroke(std::shared_ptr stroke) { + ShadingVariant shading; + shading.subItemLimit = subItems.size(); + shading.stroke = stroke; + shadings.insert(shadings.begin(), shading); + } + + void addTrim(Trim const &trim) { + trims.push_back(std::make_shared(trim, subItems.size())); + } + + public: + void initializeRenderChildren() { + _renderTree = std::make_shared( + CGRect(0.0, 0.0, 0.0, 0.0), + Vector2D(0.0, 0.0), + CATransform3D::identity(), + 1.0, + false, + false, + nullptr, + std::vector>(), + nullptr, + false + ); + + if (!shadings.empty()) { + for (int i = 0; i < shadings.size(); i++) { + auto &shadingVariant = shadings[i]; + + if (!(shadingVariant.fill || shadingVariant.stroke)) { + continue; + } + + auto shadingRenderTree = std::make_shared( + CGRect(0.0, 0.0, 0.0, 0.0), + Vector2D(0.0, 0.0), + CATransform3D::identity(), + 1.0, + false, + false, + nullptr, + std::vector>(), + nullptr, + false + ); + shadingVariant.renderTree = shadingRenderTree; + _renderTree->_subnodes.push_back(shadingRenderTree); + } + } + + if (isGroup && !subItems.empty()) { + std::vector> subItemNodes; + for (int i = (int)subItems.size() - 1; i >= 0; i--) { + subItems[i]->initializeRenderChildren(); + subItemNodes.push_back(subItems[i]->_renderTree); + } + + if (!subItemNodes.empty()) { + _renderTree->_subnodes.push_back(std::make_shared( + CGRect(0.0, 0.0, 0.0, 0.0), + Vector2D(0.0, 0.0), + CATransform3D::identity(), + 1.0, + false, + false, + nullptr, + subItemNodes, + nullptr, + false + )); + } + } + } + + public: + void renderChildren(AnimationFrameTime frameTime, std::optional parentTrim) { + CATransform3D containerTransform = CATransform3D::identity(); + double containerOpacity = 1.0; + if (transform) { + transform->update(frameTime); + containerTransform = transform->transform(); + containerOpacity = transform->opacity(); + } + _renderTree->_transform = containerTransform; + _renderTree->_alpha = containerOpacity; + + for (int i = 0; i < shadings.size(); i++) { + const auto &shadingVariant = shadings[i]; + + if (!(shadingVariant.fill || shadingVariant.stroke)) { + continue; + } + + CompoundBezierPath compoundPath; + auto paths = collectPaths(frameTime, shadingVariant.subItemLimit, CATransform3D::identity()); + for (const auto &path : paths) { + compoundPath.appendPath(path.path.copyUsingTransform(path.transform)); + } + + //std::optional currentTrim = parentTrim; + //TODO:investigate + /*if (!trims.empty()) { + currentTrim = trims[0]; + }*/ + + if (parentTrim) { + compoundPath = trimCompoundPath(compoundPath, parentTrim->start, parentTrim->end, parentTrim->offset, parentTrim->type); + } + + std::vector resultPaths; + for (const auto &path : compoundPath.paths) { + resultPaths.push_back(path); + } + + std::shared_ptr content; + + std::shared_ptr fill; + if (shadingVariant.fill) { + shadingVariant.fill->update(frameTime); + fill = shadingVariant.fill->fill(); + } + + std::shared_ptr stroke; + if (shadingVariant.stroke) { + shadingVariant.stroke->update(frameTime); + stroke = shadingVariant.stroke->stroke(); + } + + content = std::make_shared( + resultPaths, + stroke, + fill + ); + + shadingVariant.renderTree->_content = content; + } + + if (isGroup && !subItems.empty()) { + for (int i = (int)subItems.size() - 1; i >= 0; i--) { + std::optional childTrim = parentTrim; + for (const auto &trim : trims) { + trim->update(frameTime); + + if (i < (int)trim->trimParams().subItemLimit) { + //TODO:allow combination + //assert(!parentTrim); + childTrim = trim->trimParams(); + } + } + + subItems[i]->renderChildren(frameTime, childTrim); + } + } + } + }; + +public: + ShapeLayerPresentationTree(std::vector> const &items) { + itemTree = std::make_shared(); + itemTree->isGroup = true; + ShapeLayerPresentationTree::renderTreeContent(items, itemTree); + } + + ShapeLayerPresentationTree(std::shared_ptr const &solidLayer) { + itemTree = std::make_shared(); + itemTree->isGroup = true; + + std::vector> items; + items.push_back(std::make_shared( + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + solidLayer->hidden, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + KeyframeGroup(Vector3D(0.0, 0.0, 0.0)), + KeyframeGroup(Vector3D(solidLayer->width, solidLayer->height, 0.0)), + KeyframeGroup(Vector1D(0.0)) + )); + ShapeLayerPresentationTree::renderTreeContent(items, itemTree); + } + +private: + static void renderTreeContent(std::vector> const &items, std::shared_ptr &itemTree) { + for (const auto &item : items) { + if (item->hidden()) { + continue; + } + + switch (item->type) { + case ShapeType::Fill: { + Fill const &fill = *((Fill *)item.get()); + + itemTree->addFill(std::make_shared(fill)); + + break; + } + case ShapeType::GradientFill: { + GradientFill const &gradientFill = *((GradientFill *)item.get()); + + itemTree->addFill(std::make_shared(gradientFill)); + + break; + } + case ShapeType::Stroke: { + Stroke const &stroke = *((Stroke *)item.get()); + + itemTree->addStroke(std::make_shared(stroke)); + + break; + } + case ShapeType::GradientStroke: { + GradientStroke const &gradientStroke = *((GradientStroke *)item.get()); + + itemTree->addStroke(std::make_shared(gradientStroke)); + + break; + } + case ShapeType::Group: { + Group const &group = *((Group *)item.get()); + + auto groupItem = std::make_shared(); + groupItem->isGroup = true; + + ShapeLayerPresentationTree::renderTreeContent(group.items, groupItem); + + itemTree->addSubItem(groupItem); + + break; + } + case ShapeType::Shape: { + Shape const &shape = *((Shape *)item.get()); + + auto shapeItem = std::make_shared(); + shapeItem->setPath(std::make_unique(shape)); + itemTree->addSubItem(shapeItem); + + break; + } + case ShapeType::Trim: { + Trim const &trim = *((Trim *)item.get()); + + itemTree->addTrim(trim); + + break; + } + case ShapeType::Transform: { + auto transform = std::static_pointer_cast(item); + + itemTree->setTransform(std::make_unique(transform)); + + break; + } + case ShapeType::Ellipse: { + Ellipse const &ellipse = *((Ellipse *)item.get()); + + auto shapeItem = std::make_shared(); + shapeItem->setPath(std::make_unique(ellipse)); + itemTree->addSubItem(shapeItem); + + break; + } + case ShapeType::Merge: { + //assert(false); + break; + } + case ShapeType::Rectangle: { + Rectangle const &rectangle = *((Rectangle *)item.get()); + + auto shapeItem = std::make_shared(); + shapeItem->setPath(std::make_unique(rectangle)); + itemTree->addSubItem(shapeItem); + + break; + } + case ShapeType::Repeater: { + assert(false); + break; + } + case ShapeType::Star: { + Star const &star = *((Star *)item.get()); + + auto shapeItem = std::make_shared(); + shapeItem->setPath(std::make_unique(star)); + itemTree->addSubItem(shapeItem); + + break; + } + case ShapeType::RoundedRectangle: { + //TODO:restore + break; + } + default: { + break; + } + } + } + + itemTree->initializeRenderChildren(); + } + +public: + std::shared_ptr itemTree; +}; + +ShapeCompositionLayer::ShapeCompositionLayer(std::shared_ptr const &shapeLayer) : +CompositionLayer(shapeLayer, Vector2D::Zero()) { + _contentTree = std::make_shared(shapeLayer->items); +} + +ShapeCompositionLayer::ShapeCompositionLayer(std::shared_ptr const &solidLayer) : +CompositionLayer(solidLayer, Vector2D::Zero()) { + _contentTree = std::make_shared(solidLayer); +} + +void ShapeCompositionLayer::displayContentsWithFrame(double frame, bool forceUpdates) { + _frameTime = frame; + _frameTimeInitialized = true; + + _contentTree->itemTree->renderChildren(_frameTime, std::nullopt); +} + +std::shared_ptr ShapeCompositionLayer::renderTreeNode() { + if (_contentsLayer->isHidden()) { + return nullptr; + } + + assert(_frameTimeInitialized); + + std::shared_ptr maskNode; + bool invertMask = false; + if (_matteLayer) { + maskNode = _matteLayer->renderTreeNode(); + if (maskNode && _matteType.has_value() && _matteType.value() == MatteType::Invert) { + invertMask = true; + } + } + + std::vector> renderTreeValue; + renderTreeValue.push_back(_contentTree->itemTree->renderTree()); + + //printf("Name: %s\n", keypathName().c_str()); + /*if (!maskNode && keypathName().find("Shape Layer 3") != -1) { + return std::make_shared( + bounds(), + _contentsLayer->position(), + _contentsLayer->transform(), + _contentsLayer->opacity(), + _contentsLayer->masksToBounds(), + _contentsLayer->isHidden(), + nullptr, + renderTreeValue, + nullptr, + false + ); + }*/ + + std::vector> subnodes; + subnodes.push_back(std::make_shared( + _contentsLayer->bounds(), + _contentsLayer->position(), + _contentsLayer->transform(), + _contentsLayer->opacity(), + _contentsLayer->masksToBounds(), + _contentsLayer->isHidden(), + nullptr, + renderTreeValue, + nullptr, + false + )); + + assert(position() == Vector2D::Zero()); + assert(transform().isIdentity()); + assert(opacity() == 1.0); + assert(!masksToBounds()); + assert(!isHidden()); + + assert(_contentsLayer->bounds() == CGRect(0.0, 0.0, 0.0, 0.0)); + assert(_contentsLayer->position() == Vector2D::Zero()); + assert(!_contentsLayer->masksToBounds()); + + return std::make_shared( + bounds(), + position(), + transform(), + opacity(), + masksToBounds(), + isHidden(), + nullptr, + subnodes, + maskNode, + invertMask + ); +} + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.hpp new file mode 100644 index 0000000000..ea88c5d0ac --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.hpp @@ -0,0 +1,31 @@ +#ifndef ShapeCompositionLayer_hpp +#define ShapeCompositionLayer_hpp + +#include "Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp" +#include "Lottie/Private/Model/Layers/ShapeLayerModel.hpp" +#include "Lottie/Private/Model/Layers/SolidLayerModel.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.hpp" + +namespace lottie { + +class ShapeLayerPresentationTree; + +/// A CompositionLayer responsible for initializing and rendering shapes +class ShapeCompositionLayer: public CompositionLayer { +public: + ShapeCompositionLayer(std::shared_ptr const &shapeLayer); + ShapeCompositionLayer(std::shared_ptr const &solidLayer); + + virtual void displayContentsWithFrame(double frame, bool forceUpdates) override; + virtual std::shared_ptr renderTreeNode() override; + +private: + std::shared_ptr _contentTree; + + AnimationFrameTime _frameTime = 0.0; + bool _frameTimeInitialized = false; +}; + +} + +#endif /* ShapeCompositionLayer_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.swift new file mode 100644 index 0000000000..a001ca790d --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.swift @@ -0,0 +1,57 @@ +// +// ShapeLayerContainer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/22/19. +// + +import CoreGraphics +import Foundation + +/// A CompositionLayer responsible for initializing and rendering shapes +final class ShapeCompositionLayer: CompositionLayer { + + // MARK: Lifecycle + + init(shapeLayer: ShapeLayerModel) { + let results = shapeLayer.items.initializeNodeTree() + let renderContainer = ShapeContainerLayer() + self.renderContainer = renderContainer + rootNode = results.rootNode + super.init(layer: shapeLayer, size: .zero) + contentsLayer.addSublayer(renderContainer) + for container in results.renderContainers { + renderContainer.insertRenderLayer(container) + } + rootNode?.updateTree(0, forceUpdates: true) + childKeypaths.append(contentsOf: results.childrenNodes) + } + + override init(layer: Any) { + guard let layer = layer as? ShapeCompositionLayer else { + fatalError("init(layer:) wrong class.") + } + rootNode = nil + renderContainer = nil + super.init(layer: layer) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + + let rootNode: AnimatorNode? + let renderContainer: ShapeContainerLayer? + + override func displayContentsWithFrame(frame: CGFloat, forceUpdates: Bool) { + rootNode?.updateTree(frame, forceUpdates: forceUpdates) + renderContainer?.markRenderUpdates(forFrame: frame) + } + + override func updateRenderScale() { + super.updateRenderScale() + renderContainer?.renderScale = renderScale + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ShapeUtils/BezierPathUtils.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ShapeUtils/BezierPathUtils.cpp new file mode 100644 index 0000000000..cd129d2606 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ShapeUtils/BezierPathUtils.cpp @@ -0,0 +1,487 @@ +#include "BezierPathUtils.hpp" + +namespace lottie { + +BezierPath makeEllipseBezierPath( + Vector2D const &size, + Vector2D const ¢er, + PathDirection direction +) { + const double ControlPointConstant = 0.55228; + + Vector2D half = size * 0.5; + if (direction == PathDirection::CounterClockwise) { + half.x = half.x * -1.0; + } + + Vector2D q1(center.x, center.y - half.y); + Vector2D q2(center.x + half.x, center.y); + Vector2D q3(center.x, center.y + half.y); + Vector2D q4(center.x - half.x, center.y); + + Vector2D cp = half * ControlPointConstant; + + BezierPath path(CurveVertex::relative( + q1, + Vector2D(-cp.x, 0), + Vector2D(cp.x, 0))); + path.addVertex(CurveVertex::relative( + q2, + Vector2D(0, -cp.y), + Vector2D(0, cp.y))); + + path.addVertex(CurveVertex::relative( + q3, + Vector2D(cp.x, 0), + Vector2D(-cp.x, 0))); + + path.addVertex(CurveVertex::relative( + q4, + Vector2D(0, cp.y), + Vector2D(0, -cp.y))); + + path.addVertex(CurveVertex::relative( + q1, + Vector2D(-cp.x, 0), + Vector2D(cp.x, 0))); + path.close(); + return path; +} + +BezierPath makeRectangleBezierPath( + Vector2D const &position, + Vector2D const &inputSize, + double cornerRadius, + PathDirection direction +) { + const double ControlPointConstant = 0.55228; + + Vector2D size = inputSize * 0.5; + double radius = std::min(std::min(cornerRadius, size.x), size.y); + + BezierPath bezierPath; + std::vector points; + + if (radius <= 0.0) { + /// No Corners + points = { + /// Lead In + CurveVertex::relative( + Vector2D(size.x, -size.y), + Vector2D::Zero(), + Vector2D::Zero()) + .translated(position), + /// Corner 1 + CurveVertex::relative( + Vector2D(size.x, size.y), + Vector2D::Zero(), + Vector2D::Zero()) + .translated(position), + /// Corner 2 + CurveVertex::relative( + Vector2D(-size.x, size.y), + Vector2D::Zero(), + Vector2D::Zero()) + .translated(position), + /// Corner 3 + CurveVertex::relative( + Vector2D(-size.x, -size.y), + Vector2D::Zero(), + Vector2D::Zero()) + .translated(position), + /// Corner 4 + CurveVertex::relative( + Vector2D(size.x, -size.y), + Vector2D::Zero(), + Vector2D::Zero()) + .translated(position) + }; + } else { + double controlPoint = radius * ControlPointConstant; + points = { + /// Lead In + CurveVertex::absolute( + Vector2D(radius, 0), + Vector2D(radius, 0), + Vector2D(radius, 0)) + .translated(Vector2D(-radius, radius)) + .translated(Vector2D(size.x, -size.y)) + .translated(position), + /// Corner 1 + CurveVertex::absolute( + Vector2D(radius, 0), // Point + Vector2D(radius, 0), // In tangent + Vector2D(radius, controlPoint)) + .translated(Vector2D(-radius, -radius)) + .translated(Vector2D(size.x, size.y)) + .translated(position), + CurveVertex::absolute( + Vector2D(0, radius), // Point + Vector2D(controlPoint, radius), // In tangent + Vector2D(0, radius)) // Out Tangent + .translated(Vector2D(-radius, -radius)) + .translated(Vector2D(size.x, size.y)) + .translated(position), + /// Corner 2 + CurveVertex::absolute( + Vector2D(0, radius), // Point + Vector2D(0, radius), // In tangent + Vector2D(-controlPoint, radius))// Out tangent + .translated(Vector2D(radius, -radius)) + .translated(Vector2D(-size.x, size.y)) + .translated(position), + CurveVertex::absolute( + Vector2D(-radius, 0), // Point + Vector2D(-radius, controlPoint), // In tangent + Vector2D(-radius, 0)) // Out tangent + .translated(Vector2D(radius, -radius)) + .translated(Vector2D(-size.x, size.y)) + .translated(position), + /// Corner 3 + CurveVertex::absolute( + Vector2D(-radius, 0), // Point + Vector2D(-radius, 0), // In tangent + Vector2D(-radius, -controlPoint)) // Out tangent + .translated(Vector2D(radius, radius)) + .translated(Vector2D(-size.x, -size.y)) + .translated(position), + CurveVertex::absolute( + Vector2D(0, -radius), // Point + Vector2D(-controlPoint, -radius), // In tangent + Vector2D(0, -radius)) // Out tangent + .translated(Vector2D(radius, radius)) + .translated(Vector2D(-size.x, -size.y)) + .translated(position), + /// Corner 4 + CurveVertex::absolute( + Vector2D(0, -radius), // Point + Vector2D(0, -radius), // In tangent + Vector2D(controlPoint, -radius)) // Out tangent + .translated(Vector2D(-radius, radius)) + .translated(Vector2D(size.x, -size.y)) + .translated(position), + CurveVertex::absolute( + Vector2D(radius, 0), // Point + Vector2D(radius, -controlPoint), // In tangent + Vector2D(radius, 0)) // Out tangent + .translated(Vector2D(-radius, radius)) + .translated(Vector2D(size.x, -size.y)) + .translated(position) + }; + } + bool reversed = direction == PathDirection::CounterClockwise; + if (reversed) { + for (auto vertexIt = points.rbegin(); vertexIt != points.rend(); vertexIt++) { + bezierPath.addVertex((*vertexIt).reversed()); + } + } else { + for (auto vertexIt = points.begin(); vertexIt != points.end(); vertexIt++) { + bezierPath.addVertex(*vertexIt); + } + } + bezierPath.close(); + return bezierPath; +} + +/// Magic number needed for building path data +static constexpr double StarNodePolystarConstant = 0.47829; + +BezierPath makeStarBezierPath( + Vector2D const &position, + double outerRadius, + double innerRadius, + double inputOuterRoundedness, + double inputInnerRoundedness, + double numberOfPoints, + double rotation, + PathDirection direction +) { + double currentAngle = degreesToRadians(rotation - 90.0); + double anglePerPoint = (2.0 * M_PI) / numberOfPoints; + double halfAnglePerPoint = anglePerPoint / 2.0; + double partialPointAmount = numberOfPoints - floor(numberOfPoints); + double outerRoundedness = inputOuterRoundedness * 0.01; + double innerRoundedness = inputInnerRoundedness * 0.01; + + Vector2D point = Vector2D::Zero(); + + double partialPointRadius = 0.0; + if (partialPointAmount != 0.0) { + currentAngle += halfAnglePerPoint * (1 - partialPointAmount); + partialPointRadius = innerRadius + partialPointAmount * (outerRadius - innerRadius); + point.x = (partialPointRadius * cos(currentAngle)); + point.y = (partialPointRadius * sin(currentAngle)); + currentAngle += anglePerPoint * partialPointAmount / 2; + } else { + point.x = (outerRadius * cos(currentAngle)); + point.y = (outerRadius * sin(currentAngle)); + currentAngle += halfAnglePerPoint; + } + + std::vector vertices; + vertices.push_back(CurveVertex::relative(point + position, Vector2D::Zero(), Vector2D::Zero())); + + Vector2D previousPoint = point; + bool longSegment = false; + int numPoints = (int)(ceil(numberOfPoints) * 2.0); + for (int i = 0; i < numPoints; i++) { + double radius = longSegment ? outerRadius : innerRadius; + double dTheta = halfAnglePerPoint; + if (partialPointRadius != 0.0 && i == numPoints - 2) { + dTheta = anglePerPoint * partialPointAmount / 2; + } + if (partialPointRadius != 0.0 && i == numPoints - 1) { + radius = partialPointRadius; + } + previousPoint = point; + point.x = (radius * cos(currentAngle)); + point.y = (radius * sin(currentAngle)); + + if (innerRoundedness == 0.0 && outerRoundedness == 0.0) { + vertices.push_back(CurveVertex::relative(point + position, Vector2D::Zero(), Vector2D::Zero())); + } else { + double cp1Theta = (atan2(previousPoint.y, previousPoint.x) - M_PI / 2.0); + double cp1Dx = cos(cp1Theta); + double cp1Dy = sin(cp1Theta); + + double cp2Theta = (atan2(point.y, point.x) - M_PI / 2.0); + double cp2Dx = cos(cp2Theta); + double cp2Dy = sin(cp2Theta); + + double cp1Roundedness = longSegment ? innerRoundedness : outerRoundedness; + double cp2Roundedness = longSegment ? outerRoundedness : innerRoundedness; + double cp1Radius = longSegment ? innerRadius : outerRadius; + double cp2Radius = longSegment ? outerRadius : innerRadius; + + Vector2D cp1( + cp1Radius * cp1Roundedness * StarNodePolystarConstant * cp1Dx, + cp1Radius * cp1Roundedness * StarNodePolystarConstant * cp1Dy + ); + Vector2D cp2( + cp2Radius * cp2Roundedness * StarNodePolystarConstant * cp2Dx, + cp2Radius * cp2Roundedness * StarNodePolystarConstant * cp2Dy + ); + if (partialPointAmount != 0.0) { + if (i == 0) { + cp1 = cp1 * partialPointAmount; + } else if (i == numPoints - 1) { + cp2 = cp2 * partialPointAmount; + } + } + auto previousVertex = vertices[vertices.size() - 1]; + vertices[vertices.size() - 1] = CurveVertex::absolute( + previousVertex.point, + previousVertex.inTangent, + previousVertex.point - cp1 + ); + vertices.push_back(CurveVertex::relative(point + position, cp2, Vector2D::Zero())); + } + currentAngle += dTheta; + longSegment = !longSegment; + } + + bool reverse = direction == PathDirection::CounterClockwise; + BezierPath path; + if (reverse) { + for (auto vertexIt = vertices.rbegin(); vertexIt != vertices.rend(); vertexIt++) { + path.addVertex((*vertexIt).reversed()); + } + } else { + for (auto vertexIt = vertices.begin(); vertexIt != vertices.end(); vertexIt++) { + path.addVertex(*vertexIt); + } + } + path.close(); + return path; +} + +CompoundBezierPath trimCompoundPath(CompoundBezierPath sourcePath, double start, double end, double offset, TrimType type) { + /// No need to trim, it's a full path + if (start == 0.0 && end == 1.0) { + return sourcePath; + } + + /// All paths are empty. + if (start == end) { + return CompoundBezierPath(); + } + + if (type == TrimType::Simultaneously) { + CompoundBezierPath result; + + for (BezierPath &path : sourcePath.paths) { + CompoundBezierPath tempPath; + tempPath.appendPath(path); + + auto subPaths = tempPath.trim(start, end, offset); + + for (const auto &subPath : subPaths->paths) { + result.appendPath(subPath); + } + } + + return result; + } + + /// Individual path trimming. + + /// Brace yourself for the below code. + + /// Normalize lengths with offset. + double startPosition = fmod(start + offset, 1.0); + double endPosition = fmod(end + offset, 1.0); + + if (startPosition < 0.0) { + startPosition = 1.0 + startPosition; + } + + if (endPosition < 0.0) { + endPosition = 1.0 + endPosition; + } + if (startPosition == 1.0) { + startPosition = 0.0; + } + if (endPosition == 0.0) { + endPosition = 1.0; + } + + /// First get the total length of all paths. + double totalLength = 0.0; + for (auto &upstreamPath : sourcePath.paths) { + totalLength += upstreamPath.length(); + } + + /// Now determine the start and end cut lengths + double startLength = startPosition * totalLength; + double endLength = endPosition * totalLength; + double pathStart = 0.0; + + CompoundBezierPath result; + + /// Now loop through all path containers + for (auto &pathContainer : sourcePath.paths) { + auto pathEnd = pathStart + pathContainer.length(); + + if (!isInRange(startLength, pathStart, pathEnd) && + isInRange(endLength, pathStart, pathEnd)) { + // pathStart|=======E----------------------|pathEnd + // Cut path components, removing after end. + + double pathCutLength = endLength - pathStart; + double subpathStart = 0.0; + double subpathEnd = subpathStart + pathContainer.length(); + if (pathCutLength < subpathEnd) { + /// This is the subpath that needs to be cut. + double cutLength = pathCutLength - subpathStart; + + CompoundBezierPath tempPath; + tempPath.appendPath(pathContainer); + auto newPaths = tempPath.trim(0, cutLength / pathContainer.length(), 0); + for (const auto &newPath : newPaths->paths) { + result.appendPath(newPath); + } + } else { + /// Add to container and move on + result.appendPath(pathContainer); + } + /*if (pathCutLength == subpathEnd) { + /// Right on the end. The next subpath is not included. Break. + break; + } + subpathStart = subpathEnd;*/ + } else if (!isInRange(endLength, pathStart, pathEnd) && + isInRange(startLength, pathStart, pathEnd)) { + // pathStart|-------S======================|pathEnd + // + + // Cut path components, removing before beginning. + double pathCutLength = startLength - pathStart; + // Clear paths from container + double subpathStart = 0.0; + double subpathEnd = subpathStart + pathContainer.length(); + + if (subpathStart < pathCutLength && pathCutLength < subpathEnd) { + /// This is the subpath that needs to be cut. + double cutLength = pathCutLength - subpathStart; + CompoundBezierPath tempPath; + tempPath.appendPath(pathContainer); + auto newPaths = tempPath.trim(cutLength / pathContainer.length(), 1, 0); + for (const auto &newPath : newPaths->paths) { + result.appendPath(newPath); + } + } else if (pathCutLength <= subpathStart) { + result.appendPath(pathContainer); + } + //subpathStart = subpathEnd; + } else if (isInRange(endLength, pathStart, pathEnd) && + isInRange(startLength, pathStart, pathEnd)) { + // pathStart|-------S============E---------|endLength + // pathStart|=====E----------------S=======|endLength + // trim from path beginning to endLength. + + // Cut path components, removing before beginnings. + double startCutLength = startLength - pathStart; + double endCutLength = endLength - pathStart; + + double subpathStart = 0.0; + + double subpathEnd = subpathStart + pathContainer.length(); + + if (!isInRange(startCutLength, subpathStart, subpathEnd) && + !isInRange(endCutLength, subpathStart, subpathEnd)) + { + // The whole path is included. Add + // S|==============================|E + result.appendPath(pathContainer); + } else if (isInRange(startCutLength, subpathStart, subpathEnd) && + !isInRange(endCutLength, subpathStart, subpathEnd)) { + /// The start of the path needs to be trimmed + // |-------S======================|E + double cutLength = startCutLength - subpathStart; + CompoundBezierPath tempPath; + tempPath.appendPath(pathContainer); + auto newPaths = tempPath.trim(cutLength / pathContainer.length(), 1, 0); + for (const auto &newPath : newPaths->paths) { + result.appendPath(newPath); + } + } else if (!isInRange(startCutLength, subpathStart, subpathEnd) && + isInRange(endCutLength, subpathStart, subpathEnd)) { + // S|=======E----------------------| + double cutLength = endCutLength - subpathStart; + CompoundBezierPath tempPath; + tempPath.appendPath(pathContainer); + auto newPaths = tempPath.trim(0, cutLength / pathContainer.length(), 0); + for (const auto &newPath : newPaths->paths) { + result.appendPath(newPath); + } + } else if (isInRange(startCutLength, subpathStart, subpathEnd) && + isInRange(endCutLength, subpathStart, subpathEnd)) { + // |-------S============E---------| + double cutFromLength = startCutLength - subpathStart; + double cutToLength = endCutLength - subpathStart; + CompoundBezierPath tempPath; + tempPath.appendPath(pathContainer); + auto newPaths = tempPath.trim( + cutFromLength / pathContainer.length(), + cutToLength / pathContainer.length(), + 0 + ); + for (const auto &newPath : newPaths->paths) { + result.appendPath(newPath); + } + } + } else if ((endLength <= pathStart && pathEnd <= startLength) || + (startLength <= pathStart && endLength <= pathStart) || + (pathEnd <= startLength && pathEnd <= endLength)) { + /// The Path needs to be cleared + } else { + result.appendPath(pathContainer); + } + + pathStart = pathEnd; + } + + return result; +} + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ShapeUtils/BezierPathUtils.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ShapeUtils/BezierPathUtils.hpp new file mode 100644 index 0000000000..9d9d27b612 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/ShapeUtils/BezierPathUtils.hpp @@ -0,0 +1,39 @@ +#ifndef BezierPaths_h +#define BezierPaths_h + +#include "Lottie/Private/Model/ShapeItems/Ellipse.hpp" +#include "Lottie/Private/Utility/Primitives/BezierPath.hpp" +#include "Lottie/Private/Utility/Primitives/CompoundBezierPath.hpp" +#include "Lottie/Private/Model/ShapeItems/Trim.hpp" + +namespace lottie { + +BezierPath makeEllipseBezierPath( + Vector2D const &size, + Vector2D const ¢er, + PathDirection direction +); + +BezierPath makeRectangleBezierPath( + Vector2D const &position, + Vector2D const &inputSize, + double cornerRadius, + PathDirection direction +); + +BezierPath makeStarBezierPath( + Vector2D const &position, + double outerRadius, + double innerRadius, + double inputOuterRoundedness, + double inputInnerRoundedness, + double numberOfPoints, + double rotation, + PathDirection direction +); + +CompoundBezierPath trimCompoundPath(CompoundBezierPath sourcePath, double start, double end, double offset, TrimType type); + +} + +#endif /* BezierPaths_h */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/SolidCompositionLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/SolidCompositionLayer.swift new file mode 100644 index 0000000000..f1972d14d0 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/SolidCompositionLayer.swift @@ -0,0 +1,60 @@ +// +// SolidCompositionLayer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/25/19. +// + +import Foundation +import QuartzCore + +final class LottieCAShapeLayer: CAShapeLayer { +} + +final class SolidCompositionLayer: CompositionLayer { + + // MARK: Lifecycle + + init(solid: SolidLayerModel) { + let components = solid.colorHex.hexColorComponents() + colorProperty = + NodeProperty(provider: SingleValueProvider(Color( + r: Double(components.red), + g: Double(components.green), + b: Double(components.blue), + a: 1))) + + super.init(layer: solid, size: .zero) + solidShape.path = CGPath(rect: CGRect(x: 0, y: 0, width: solid.width, height: solid.height), transform: nil) + contentsLayer.addSublayer(solidShape) + } + + override init(layer: Any) { + /// Used for creating shadow model layers. Read More here: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init + guard let layer = layer as? SolidCompositionLayer else { + fatalError("init(layer:) Wrong Layer Class") + } + colorProperty = layer.colorProperty + super.init(layer: layer) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + + let colorProperty: NodeProperty? + let solidShape = LottieCAShapeLayer() + + override var keypathProperties: [String: AnyNodeProperty] { + guard let colorProperty = colorProperty else { return super.keypathProperties } + return [PropertyName.color.rawValue : colorProperty] + } + + override func displayContentsWithFrame(frame: CGFloat, forceUpdates _: Bool) { + guard let colorProperty = colorProperty else { return } + colorProperty.update(frame: frame) + solidShape.fillColor = colorProperty.value.cgColorValue + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.cpp new file mode 100644 index 0000000000..5e773d361b --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.cpp @@ -0,0 +1,5 @@ +#include "TextCompositionLayer.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.hpp new file mode 100644 index 0000000000..57e0545345 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.hpp @@ -0,0 +1,124 @@ +#ifndef TextCompositionLayer_hpp +#define TextCompositionLayer_hpp + +#include "Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp" +#include "Lottie/Private/Model/Layers/TextLayerModel.hpp" +#include "Lottie/Public/TextProvider/AnimationTextProvider.hpp" +#include "Lottie/Public/FontProvider/AnimationFontProvider.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.hpp" + +namespace lottie { + +class TextCompositionLayer: public CompositionLayer { +public: + TextCompositionLayer(std::shared_ptr const &textLayer, std::shared_ptr textProvider, std::shared_ptr fontProvider) : + CompositionLayer(textLayer, Vector2D::Zero()) { + std::shared_ptr rootNode; + for (const auto &animator : textLayer->animators) { + rootNode = std::make_shared(rootNode, animator); + } + _rootNode = rootNode; + _textDocument = std::make_shared>(textLayer->text.keyframes); + + _textProvider = textProvider; + _fontProvider = fontProvider; + + //_contentsLayer->addSublayer(_textLayer); + + assert(false); + //self.textLayer.masksToBounds = false + //self.textLayer.isGeometryFlipped = true + + if (_rootNode) { + _childKeypaths.push_back(rootNode); + } + } + + std::shared_ptr const &textProvider() const { + return _textProvider; + } + void setTextProvider(std::shared_ptr const &textProvider) { + _textProvider = textProvider; + } + + std::shared_ptr const &fontProvider() const { + return _fontProvider; + } + void setFontProvider(std::shared_ptr const &fontProvider) { + _fontProvider = fontProvider; + } + + virtual void displayContentsWithFrame(double frame, bool forceUpdates) override { + if (!_textDocument) { + return; + } + + bool documentUpdate = _textDocument->hasUpdate(frame); + + bool animatorUpdate = false; + if (_rootNode) { + animatorUpdate = _rootNode->updateContents(frame, forceUpdates); + } + + if (!(documentUpdate || animatorUpdate)) { + return; + } + + if (_rootNode) { + _rootNode->rebuildOutputs(frame); + } + + assert(false); + /*// Get Text Attributes + let text = textDocument.value(frame: frame) as! TextDocument + let strokeColor = rootNode?.textOutputNode.strokeColor ?? text.strokeColorData?.cgColorValue + let strokeWidth = rootNode?.textOutputNode.strokeWidth ?? CGFloat(text.strokeWidth ?? 0) + let tracking = (CGFloat(text.fontSize) * (rootNode?.textOutputNode.tracking ?? CGFloat(text.tracking))) / 1000.0 + let matrix = rootNode?.textOutputNode.xform ?? CATransform3DIdentity + let textString = textProvider.textFor(keypathName: keypathName, sourceText: text.text) + let ctFont = fontProvider.fontFor(family: text.fontFamily, size: CGFloat(text.fontSize)) + + // Set all of the text layer options + textLayer.text = textString + textLayer.font = ctFont + textLayer.alignment = text.justification.textAlignment + textLayer.lineHeight = CGFloat(text.lineHeight) + textLayer.tracking = tracking + + if let fillColor = rootNode?.textOutputNode.fillColor { + textLayer.fillColor = fillColor + } else if let fillColor = text.fillColorData?.cgColorValue { + textLayer.fillColor = fillColor + } else { + textLayer.fillColor = nil + } + + textLayer.preferredSize = text.textFrameSize?.sizeValue + textLayer.strokeOnTop = text.strokeOverFill ?? false + textLayer.strokeWidth = strokeWidth + textLayer.strokeColor = strokeColor + textLayer.sizeToFit() + + textLayer.opacity = Float(rootNode?.textOutputNode.opacity ?? 1) + textLayer.transform = CATransform3DIdentity + textLayer.position = text.textFramePosition?.pointValue ?? CGPoint.zero + textLayer.transform = matrix*/ + } + +public: + virtual bool isTextCompositionLayer() const override { + return true; + } + +private: + std::shared_ptr _rootNode; + std::shared_ptr> _textDocument; + + //std::shared_ptr _textLayer; + std::shared_ptr _textProvider; + std::shared_ptr _fontProvider; +}; + +} + +#endif /* TextCompositionLayer_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.swift new file mode 100644 index 0000000000..0c219958b8 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.swift @@ -0,0 +1,149 @@ +// +// TextCompositionLayer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/25/19. +// + +import CoreGraphics +import CoreText +import Foundation +import QuartzCore + +/// Needed for NSMutableParagraphStyle... +#if os(OSX) +import AppKit +#else +import UIKit +#endif + +extension TextJustification { + var textAlignment: NSTextAlignment { + switch self { + case .left: + return .left + case .right: + return .right + case .center: + return .center + } + } + + var caTextAlignement: CATextLayerAlignmentMode { + switch self { + case .left: + return .left + case .right: + return .right + case .center: + return .center + } + } +} + +// MARK: - TextCompositionLayer + +final class TextCompositionLayer: CompositionLayer { + + // MARK: Lifecycle + + init(textLayer: TextLayerModel, textProvider: AnimationTextProvider, fontProvider: AnimationFontProvider) { + var rootNode: TextAnimatorNode? + for animator in textLayer.animators { + rootNode = TextAnimatorNode(parentNode: rootNode, textAnimator: animator) + } + self.rootNode = rootNode + textDocument = KeyframeInterpolator(keyframes: textLayer.text.keyframes) + + self.textProvider = textProvider + self.fontProvider = fontProvider + + super.init(layer: textLayer, size: .zero) + contentsLayer.addSublayer(self.textLayer) + self.textLayer.masksToBounds = false + self.textLayer.isGeometryFlipped = true + + if let rootNode = rootNode { + childKeypaths.append(rootNode) + } + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override init(layer: Any) { + /// Used for creating shadow model layers. Read More here: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init + guard let layer = layer as? TextCompositionLayer else { + fatalError("init(layer:) Wrong Layer Class") + } + rootNode = nil + textDocument = nil + + textProvider = DefaultTextProvider() + fontProvider = DefaultFontProvider() + + super.init(layer: layer) + } + + // MARK: Internal + + let rootNode: TextAnimatorNode? + let textDocument: KeyframeInterpolator? + + let textLayer = CoreTextRenderLayer() + var textProvider: AnimationTextProvider + var fontProvider: AnimationFontProvider + + override func displayContentsWithFrame(frame: CGFloat, forceUpdates: Bool) { + guard let textDocument = textDocument else { return } + + textLayer.contentsScale = renderScale + + let documentUpdate = textDocument.hasUpdate(frame: frame) + let animatorUpdate = rootNode?.updateContents(frame, forceLocalUpdate: forceUpdates) ?? false + guard documentUpdate == true || animatorUpdate == true else { return } + + rootNode?.rebuildOutputs(frame: frame) + + // Get Text Attributes + let text = textDocument.value(frame: frame) as! TextDocument + let strokeColor = rootNode?.textOutputNode.strokeColor ?? text.strokeColorData?.cgColorValue + let strokeWidth = rootNode?.textOutputNode.strokeWidth ?? CGFloat(text.strokeWidth ?? 0) + let tracking = (CGFloat(text.fontSize) * (rootNode?.textOutputNode.tracking ?? CGFloat(text.tracking))) / 1000.0 + let matrix = rootNode?.textOutputNode.xform ?? CATransform3DIdentity + let textString = textProvider.textFor(keypathName: keypathName, sourceText: text.text) + let ctFont = fontProvider.fontFor(family: text.fontFamily, size: CGFloat(text.fontSize)) + + // Set all of the text layer options + textLayer.text = textString + textLayer.font = ctFont + textLayer.alignment = text.justification.textAlignment + textLayer.lineHeight = CGFloat(text.lineHeight) + textLayer.tracking = tracking + + if let fillColor = rootNode?.textOutputNode.fillColor { + textLayer.fillColor = fillColor + } else if let fillColor = text.fillColorData?.cgColorValue { + textLayer.fillColor = fillColor + } else { + textLayer.fillColor = nil + } + + textLayer.preferredSize = text.textFrameSize?.sizeValue + textLayer.strokeOnTop = text.strokeOverFill ?? false + textLayer.strokeWidth = strokeWidth + textLayer.strokeColor = strokeColor + textLayer.sizeToFit() + + textLayer.opacity = Float(rootNode?.textOutputNode.opacity ?? 1) + textLayer.transform = CATransform3DIdentity + textLayer.position = text.textFramePosition?.pointValue ?? CGPoint.zero + textLayer.transform = matrix + } + + override func updateRenderScale() { + super.updateRenderScale() + textLayer.contentsScale = renderScale + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.cpp new file mode 100644 index 0000000000..a205f16eba --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.cpp @@ -0,0 +1,5 @@ +#include "MainThreadAnimationLayer.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.hpp new file mode 100644 index 0000000000..f1e512c4d7 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.hpp @@ -0,0 +1,272 @@ +#ifndef MainThreadAnimationLayer_hpp +#define MainThreadAnimationLayer_hpp + +#include "Lottie/Public/Primitives/CALayer.hpp" +#include "Lottie/Public/ImageProvider/AnimationImageProvider.hpp" +#include "Lottie/Private/Model/Animation.hpp" +#include "Lottie/Public/TextProvider/AnimationTextProvider.hpp" +#include "Lottie/Public/FontProvider/AnimationFontProvider.hpp" +#include "Lottie/Private/MainThread/LayerContainers/Utility/LayerImageProvider.hpp" +#include "Lottie/Private/MainThread/LayerContainers/Utility/LayerTextProvider.hpp" +#include "Lottie/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.hpp" +#include "Lottie/Private/MainThread/LayerContainers/Utility/LayerFontProvider.hpp" +#include "Lottie/Public/DynamicProperties/AnyValueProvider.hpp" +#include "Lottie/Public/DynamicProperties/AnimationKeypath.hpp" + +namespace lottie { + +class BlankImageProvider: public AnimationImageProvider { +public: + std::shared_ptr imageForAsset(ImageAsset const &asset) { + return nullptr; + } +}; + +class MainThreadAnimationLayer: public CALayer { +public: + MainThreadAnimationLayer( + Animation const &animation, + std::shared_ptr const &imageProvider, + std::shared_ptr const &textProvider, + std::shared_ptr const &fontProvider + ) { + if (animation.assetLibrary) { + _layerImageProvider = std::make_shared(imageProvider, animation.assetLibrary->imageAssets); + } else { + std::map> imageAssets; + _layerImageProvider = std::make_shared(imageProvider, imageAssets); + } + + _layerTextProvider = std::make_shared(textProvider); + _layerFontProvider = std::make_shared(fontProvider); + + setBounds(CGRect(0.0, 0.0, animation.width, animation.height)); + + auto layers = initializeCompositionLayers( + animation.layers, + animation.assetLibrary, + _layerImageProvider, + textProvider, + fontProvider, + animation.framerate + ); + + std::vector> imageLayers; + std::vector> textLayers; + + std::shared_ptr mattedLayer; + + for (auto layerIt = layers.rbegin(); layerIt != layers.rend(); layerIt++) { + std::shared_ptr const &layer = *layerIt; + layer->setBounds(bounds()); + _animationLayers.push_back(layer); + + if (layer->isImageCompositionLayer()) { + imageLayers.push_back(std::static_pointer_cast(layer)); + } + if (layer->isTextCompositionLayer()) { + textLayers.push_back(std::static_pointer_cast(layer)); + } + + if (mattedLayer) { + /// The previous layer requires this layer to be its matte + mattedLayer->setMatteLayer(layer); + mattedLayer = nullptr; + continue; + } + if (layer->matteType().has_value() && (layer->matteType() == MatteType::Add || layer->matteType() == MatteType::Invert)) { + /// We have a layer that requires a matte. + mattedLayer = layer; + } + addSublayer(layer); + } + + _layerImageProvider->addImageLayers(imageLayers); + _layerImageProvider->reloadImages(); + _layerTextProvider->addTextLayers(textLayers); + _layerTextProvider->reloadTexts(); + _layerFontProvider->addTextLayers(textLayers); + _layerFontProvider->reloadTexts(); + + setNeedsDisplay(true); + } + + void setRespectAnimationFrameRate(bool respectAnimationFrameRate) { + _respectAnimationFrameRate = respectAnimationFrameRate; + } + + void display() { + double newFrame = currentFrame(); + if (_respectAnimationFrameRate) { + newFrame = floor(newFrame); + } + for (const auto &layer : _animationLayers) { + layer->displayWithFrame(newFrame, false); + } + } + + std::vector> const &animationLayers() const { + return _animationLayers; + } + + void reloadImages() { + _layerImageProvider->reloadImages(); + } + + /// Forces the view to update its drawing. + void forceDisplayUpdate() { + for (const auto &layer : _animationLayers) { + layer->displayWithFrame(currentFrame(), true); + } + } + + void logHierarchyKeypaths() { + printf("Lottie: Logging Animation Keypaths\n"); + assert(false); + //animationLayers.forEach({ $0.logKeypaths(for: nil) }) + } + + void setValueProvider(std::shared_ptr const &valueProvider, AnimationKeypath const &keypath) { + for (const auto &layer : _animationLayers) { + assert(false); + /*if let foundProperties = layer.nodeProperties(for: keypath) { + for property in foundProperties { + property.setProvider(provider: valueProvider) + } + layer.displayWithFrame(frame: presentation()?.currentFrame ?? currentFrame, forceUpdates: true) + }*/ + } + } + + std::optional getValue(AnimationKeypath const &keypath, std::optional atFrame) { + for (const auto &layer : _animationLayers) { + assert(false); + /*if + let foundProperties = layer.nodeProperties(for: keypath), + let first = foundProperties.first + { + return first.valueProvider.value(frame: atFrame ?? currentFrame) + }*/ + } + return std::nullopt; + } + + std::optional getOriginalValue(AnimationKeypath const &keypath, std::optional atFrame) { + for (const auto &layer : _animationLayers) { + assert(false); + /*if + let foundProperties = layer.nodeProperties(for: keypath), + let first = foundProperties.first + { + return first.originalValueProvider.value(frame: atFrame ?? currentFrame) + }*/ + } + return std::nullopt; + } + + std::shared_ptr layerForKeypath(AnimationKeypath const &keyPath) { + assert(false); + /*for layer in animationLayers { + if let foundLayer = layer.layer(for: keypath) { + return foundLayer + } + }*/ + return nullptr; + } + + std::vector> animatorNodesForKeypath(AnimationKeypath const &keypath) { + std::vector> results; + /*for (const auto &layer : _animationLayers) { + if let nodes = layer.animatorNodes(for: keypath) { + results.append(contentsOf: nodes) + } + }*/ + return results; + } + + double currentFrame() const { + return _currentFrame; + } + void setCurrentFrame(double currentFrame) { + _currentFrame = currentFrame; + + for (size_t i = 0; i < _animationLayers.size(); i++) { + _animationLayers[i]->displayWithFrame(_currentFrame, false); + } + } + + std::shared_ptr imageProvider() const { + return _layerImageProvider->imageProvider(); + } + void setImageProvider(std::shared_ptr const &imageProvider) { + _layerImageProvider->setImageProvider(imageProvider); + } + + std::shared_ptr textProvider() const { + return _layerTextProvider->textProvider(); + } + void setTextProvider(std::shared_ptr const &textProvider) { + _layerTextProvider->setTextProvider(textProvider); + } + + std::shared_ptr fontProvider() const { + return _layerFontProvider->fontProvider(); + } + void setFontProvider(std::shared_ptr const &fontProvider) { + _layerFontProvider->setFontProvider(fontProvider); + } + + virtual std::shared_ptr renderTreeNode() { + std::vector> subnodes; + for (const auto &animationLayer : _animationLayers) { + bool found = false; + for (const auto &sublayer : sublayers()) { + if (animationLayer == sublayer) { + found = true; + break; + } + } + if (found) { + auto node = animationLayer->renderTreeNode(); + if (node) { + subnodes.push_back(node); + } + } + } + + return std::make_shared( + bounds(), + position(), + CATransform3D::identity(), + 1.0, + false, + false, + nullptr, + subnodes, + nullptr, + false + ); + } + +private: + // MARK: Internal + + /// The animatable Current Frame Property + double _currentFrame = 0.0; + + std::shared_ptr _imageProvider; + std::shared_ptr _textProvider; + std::shared_ptr _fontProvider; + + bool _respectAnimationFrameRate = true; + + std::vector> _animationLayers; + + std::shared_ptr _layerImageProvider; + std::shared_ptr _layerTextProvider; + std::shared_ptr _layerFontProvider; +}; + +} + +#endif /* MainThreadAnimationLayer_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift new file mode 100644 index 0000000000..32a133f846 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift @@ -0,0 +1,279 @@ +// +// MainThreadAnimationLayer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/24/19. +// + +import Foundation +import QuartzCore + +// MARK: - MainThreadAnimationLayer + +/// The base `CALayer` for the Main Thread rendering engine +/// +/// This layer holds a single composition container and allows for animation of +/// the currentFrame property. +public final class MainThreadAnimationLayer: CALayer, RootAnimationLayer { + + // MARK: Lifecycle + +public init( + animation: Animation, + imageProvider: AnimationImageProvider, + textProvider: AnimationTextProvider, + fontProvider: AnimationFontProvider) + { + layerImageProvider = LayerImageProvider(imageProvider: imageProvider, assets: animation.assetLibrary?.imageAssets) + layerTextProvider = LayerTextProvider(textProvider: textProvider) + layerFontProvider = LayerFontProvider(fontProvider: fontProvider) + animationLayers = [] + super.init() + bounds = animation.bounds + let layers = animation.layers.initializeCompositionLayers( + assetLibrary: animation.assetLibrary, + layerImageProvider: layerImageProvider, + textProvider: textProvider, + fontProvider: fontProvider, + frameRate: CGFloat(animation.framerate)) + + var imageLayers = [ImageCompositionLayer]() + var textLayers = [TextCompositionLayer]() + + var mattedLayer: CompositionLayer? = nil + + for layer in layers.reversed() { + layer.bounds = bounds + animationLayers.append(layer) + if let imageLayer = layer as? ImageCompositionLayer { + imageLayers.append(imageLayer) + } + if let textLayer = layer as? TextCompositionLayer { + textLayers.append(textLayer) + } + if let matte = mattedLayer { + /// The previous layer requires this layer to be its matte + matte.matteLayer = layer + mattedLayer = nil + continue + } + if + let matte = layer.matteType, + matte == .add || matte == .invert + { + /// We have a layer that requires a matte. + mattedLayer = layer + } + addSublayer(layer) + } + + layerImageProvider.addImageLayers(imageLayers) + layerImageProvider.reloadImages() + layerTextProvider.addTextLayers(textLayers) + layerTextProvider.reloadTexts() + layerFontProvider.addTextLayers(textLayers) + layerFontProvider.reloadTexts() + setNeedsDisplay() + } + + /// For CAAnimation Use + public override init(layer: Any) { + animationLayers = [] + layerImageProvider = LayerImageProvider(imageProvider: BlankImageProvider(), assets: nil) + layerTextProvider = LayerTextProvider(textProvider: DefaultTextProvider()) + layerFontProvider = LayerFontProvider(fontProvider: DefaultFontProvider()) + super.init(layer: layer) + + guard let animationLayer = layer as? MainThreadAnimationLayer else { return } + + currentFrame = animationLayer.currentFrame + + } + + required public init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Public + + public var respectAnimationFrameRate = false + + // MARK: CALayer Animations + + override public class func needsDisplay(forKey key: String) -> Bool { + if key == "currentFrame" { + return true + } + return super.needsDisplay(forKey: key) + } + + override public func action(forKey event: String) -> CAAction? { + if event == "currentFrame" { + let animation = CABasicAnimation(keyPath: event) + animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) + animation.fromValue = presentation()?.currentFrame + return animation + } + return super.action(forKey: event) + } + + public override func display() { + guard Thread.isMainThread else { return } + var newFrame: CGFloat + if + let animationKeys = animationKeys(), + !animationKeys.isEmpty + { + newFrame = presentation()?.currentFrame ?? currentFrame + } else { + // We ignore the presentation's frame if there's no animation in the layer. + newFrame = currentFrame + } + if respectAnimationFrameRate { + newFrame = floor(newFrame) + } + animationLayers.forEach { $0.displayWithFrame(frame: newFrame, forceUpdates: false) } + } + + // MARK: Internal + + /// The animatable Current Frame Property + @NSManaged public var currentFrame: CGFloat + + var animationLayers: ContiguousArray + + var primaryAnimationKey: AnimationKey { + .managed + } + + var isAnimationPlaying: Bool? { + nil // this state is managed by `AnimationView` + } + + var _animationLayers: [CALayer] { + Array(animationLayers) + } + + var imageProvider: AnimationImageProvider { + get { + layerImageProvider.imageProvider + } + set { + layerImageProvider.imageProvider = newValue + } + } + + var renderScale: CGFloat = 1 { + didSet { + animationLayers.forEach({ $0.renderScale = renderScale }) + } + } + + var textProvider: AnimationTextProvider { + get { layerTextProvider.textProvider } + set { layerTextProvider.textProvider = newValue } + } + + var fontProvider: AnimationFontProvider { + get { layerFontProvider.fontProvider } + set { layerFontProvider.fontProvider = newValue } + } + + func reloadImages() { + layerImageProvider.reloadImages() + } + + func removeAnimations() { + // no-op, since the primary animation is managed by the `AnimationView`. + } + + /// Forces the view to update its drawing. + func forceDisplayUpdate() { + animationLayers.forEach( { $0.displayWithFrame(frame: currentFrame, forceUpdates: true) }) + } + + public func displayUpdate() { + for i in 0 ..< animationLayers.count { + animationLayers[i].displayWithFrame(frame: currentFrame, forceUpdates: false) + } + } + + func logHierarchyKeypaths() { + print("Lottie: Logging Animation Keypaths") + animationLayers.forEach({ $0.logKeypaths(for: nil) }) + } + + func setValueProvider(_ valueProvider: AnyValueProvider, keypath: AnimationKeypath) { + for layer in animationLayers { + if let foundProperties = layer.nodeProperties(for: keypath) { + for property in foundProperties { + property.setProvider(provider: valueProvider) + } + layer.displayWithFrame(frame: presentation()?.currentFrame ?? currentFrame, forceUpdates: true) + } + } + } + + func getValue(for keypath: AnimationKeypath, atFrame: CGFloat?) -> Any? { + for layer in animationLayers { + if + let foundProperties = layer.nodeProperties(for: keypath), + let first = foundProperties.first + { + return first.valueProvider.value(frame: atFrame ?? currentFrame) + } + } + return nil + } + + func getOriginalValue(for keypath: AnimationKeypath, atFrame: AnimationFrameTime?) -> Any? { + for layer in animationLayers { + if + let foundProperties = layer.nodeProperties(for: keypath), + let first = foundProperties.first + { + return first.originalValueProvider.value(frame: atFrame ?? currentFrame) + } + } + return nil + } + + func layer(for keypath: AnimationKeypath) -> CALayer? { + for layer in animationLayers { + if let foundLayer = layer.layer(for: keypath) { + return foundLayer + } + } + return nil + } + + func animatorNodes(for keypath: AnimationKeypath) -> [AnimatorNode]? { + var results = [AnimatorNode]() + for layer in animationLayers { + if let nodes = layer.animatorNodes(for: keypath) { + results.append(contentsOf: nodes) + } + } + if results.count == 0 { + return nil + } + return results + } + + // MARK: Fileprivate + + fileprivate let layerImageProvider: LayerImageProvider + fileprivate let layerTextProvider: LayerTextProvider + fileprivate let layerFontProvider: LayerFontProvider +} + +// MARK: - BlankImageProvider + +public class BlankImageProvider: AnimationImageProvider { + public init() { + } + + public func imageForAsset(asset _: ImageAsset) -> CGImage? { + nil + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/CachedImageProvider.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/CachedImageProvider.swift new file mode 100644 index 0000000000..d2fa02cd97 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/CachedImageProvider.swift @@ -0,0 +1,47 @@ +// Created by Jianjun Wu on 2022/5/12. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +import CoreGraphics +import Foundation + +// MARK: - CachedImageProvider + +private final class CachedImageProvider: AnimationImageProvider { + + // MARK: Lifecycle + + /// Initializes an image provider with an image provider + /// + /// - Parameter imageProvider: The provider to load image from asset + /// + public init(imageProvider: AnimationImageProvider) { + self.imageProvider = imageProvider + } + + // MARK: Public + + public func imageForAsset(asset: ImageAsset) -> CGImage? { + if let image = imageCache.object(forKey: asset.id as NSString) { + return image + } + if let image = imageProvider.imageForAsset(asset: asset) { + imageCache.setObject(image, forKey: asset.id as NSString) + return image + } + return nil + } + + // MARK: Internal + + let imageCache: NSCache = .init() + let imageProvider: AnimationImageProvider +} + +extension AnimationImageProvider { + /// Create a cache enabled image provider which will reuse the asset image with the same asset id + /// It wraps the current provider as image loader, and uses `NSCache` to cache the images for resue. + /// The cache will be reset when the `animation` is reset. + var cachedImageProvider: AnimationImageProvider { + CachedImageProvider(imageProvider: self) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.cpp new file mode 100644 index 0000000000..bec737cb0a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.cpp @@ -0,0 +1,112 @@ +#include "CompositionLayersInitializer.hpp" + +#include "Lottie/Private/MainThread/LayerContainers/CompLayers/NullCompositionLayer.hpp" +#include "Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.hpp" +#include "Lottie/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.hpp" +#include "Lottie/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.hpp" +#include "Lottie/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.hpp" + +namespace lottie { + +std::vector> initializeCompositionLayers( + std::vector> const &layers, + std::shared_ptr const &assetLibrary, + std::shared_ptr const &layerImageProvider, + std::shared_ptr const &textProvider, + std::shared_ptr const &fontProvider, + double frameRate +) { + std::vector> compositionLayers; + std::map> layerMap; + + /// Organize the assets into a dictionary of [ID : ImageAsset] + std::vector> childLayers; + + for (const auto &layer : layers) { + if (layer->hidden) { + auto genericLayer = std::make_shared(layer); + compositionLayers.push_back(genericLayer); + if (layer->index) { + layerMap.insert(std::make_pair(layer->index.value(), genericLayer)); + } + } else if (layer->type == LayerType::Shape) { + auto shapeContainer = std::make_shared(std::static_pointer_cast(layer)); + compositionLayers.push_back(shapeContainer); + if (layer->index) { + layerMap.insert(std::make_pair(layer->index.value(), shapeContainer)); + } + } else if (layer->type == LayerType::Solid) { + auto shapeContainer = std::make_shared(std::static_pointer_cast(layer)); + compositionLayers.push_back(shapeContainer); + if (layer->index) { + layerMap.insert(std::make_pair(layer->index.value(), shapeContainer)); + } + } else if (layer->type == LayerType::Precomp && assetLibrary) { + auto precompLayer = std::static_pointer_cast(layer); + auto precompAssetIt = assetLibrary->precompAssets.find(precompLayer->referenceID); + if (precompAssetIt != assetLibrary->precompAssets.end()) { + auto precompContainer = std::make_shared( + precompLayer, + *(precompAssetIt->second), + layerImageProvider, + textProvider, + fontProvider, + assetLibrary, + frameRate + ); + compositionLayers.push_back(precompContainer); + if (layer->index) { + layerMap.insert(std::make_pair(layer->index.value(), precompContainer)); + } + } + } else if (layer->type == LayerType::Image && assetLibrary) { + auto imageLayer = std::static_pointer_cast(layer); + auto imageAssetIt = assetLibrary->imageAssets.find(imageLayer->referenceID); + if (imageAssetIt != assetLibrary->imageAssets.end()) { + auto imageContainer = std::make_shared( + imageLayer, + Vector2D((*imageAssetIt->second).width, (*imageAssetIt->second).height) + ); + compositionLayers.push_back(imageContainer); + if (layer->index) { + layerMap.insert(std::make_pair(layer->index.value(), imageContainer)); + } + } + } else if (layer->type == LayerType::Text) { + auto textContainer = std::make_shared(std::static_pointer_cast(layer), textProvider, fontProvider); + compositionLayers.push_back(textContainer); + if (layer->index) { + layerMap.insert(std::make_pair(layer->index.value(), textContainer)); + } + } else { + auto genericLayer = std::make_shared(layer); + compositionLayers.push_back(genericLayer); + if (layer->index) { + layerMap.insert(std::make_pair(layer->index.value(), genericLayer)); + } + } + if (layer->parent) { + childLayers.push_back(layer); + } + } + + /// Now link children with their parents + for (const auto &layerModel : childLayers) { + if (!layerModel->index.has_value()) { + continue; + } + if (const auto parentID = layerModel->parent) { + auto childLayerIt = layerMap.find(layerModel->index.value()); + if (childLayerIt != layerMap.end()) { + auto parentLayerIt = layerMap.find(parentID.value()); + if (parentLayerIt != layerMap.end()) { + childLayerIt->second->transformNode()->setParentNode(parentLayerIt->second->transformNode()); + } + } + } + } + + return compositionLayers; +} + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.hpp new file mode 100644 index 0000000000..247d8d5631 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.hpp @@ -0,0 +1,23 @@ +#ifndef CompositionLayersInitializer_hpp +#define CompositionLayersInitializer_hpp + +#include "Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp" +#include "Lottie/Private/Model/Assets/AssetLibrary.hpp" +#include "Lottie/Private/MainThread/LayerContainers/Utility/LayerImageProvider.hpp" +#include "Lottie/Public/TextProvider/AnimationTextProvider.hpp" +#include "Lottie/Public/FontProvider/AnimationFontProvider.hpp" + +namespace lottie { + +std::vector> initializeCompositionLayers( + std::vector> const &layers, + std::shared_ptr const &assetLibrary, + std::shared_ptr const &layerImageProvider, + std::shared_ptr const &textProvider, + std::shared_ptr const &fontProvider, + double frameRate +); + +} + +#endif /* CompositionLayersInitializer_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.swift new file mode 100644 index 0000000000..fb4aa2b387 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.swift @@ -0,0 +1,90 @@ +// +// CompositionLayersInitializer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/25/19. +// + +import CoreGraphics +import Foundation + +extension Array where Element == LayerModel { + + func initializeCompositionLayers( + assetLibrary: AssetLibrary?, + layerImageProvider: LayerImageProvider, + textProvider: AnimationTextProvider, + fontProvider: AnimationFontProvider, + frameRate: CGFloat) -> [CompositionLayer] + { + var compositionLayers = [CompositionLayer]() + var layerMap = [Int : CompositionLayer]() + + /// Organize the assets into a dictionary of [ID : ImageAsset] + var childLayers = [LayerModel]() + + for layer in self { + if layer.hidden == true { + let genericLayer = NullCompositionLayer(layer: layer) + compositionLayers.append(genericLayer) + layerMap[layer.index] = genericLayer + } else if let shapeLayer = layer as? ShapeLayerModel { + let shapeContainer = ShapeCompositionLayer(shapeLayer: shapeLayer) + compositionLayers.append(shapeContainer) + layerMap[layer.index] = shapeContainer + } else if let solidLayer = layer as? SolidLayerModel { + let solidContainer = SolidCompositionLayer(solid: solidLayer) + compositionLayers.append(solidContainer) + layerMap[layer.index] = solidContainer + } else if + let precompLayer = layer as? PreCompLayerModel, + let assetLibrary = assetLibrary, + let precompAsset = assetLibrary.precompAssets[precompLayer.referenceID] + { + let precompContainer = PreCompositionLayer( + precomp: precompLayer, + asset: precompAsset, + layerImageProvider: layerImageProvider, + textProvider: textProvider, + fontProvider: fontProvider, + assetLibrary: assetLibrary, + frameRate: frameRate) + compositionLayers.append(precompContainer) + layerMap[layer.index] = precompContainer + } else if + let imageLayer = layer as? ImageLayerModel, + let assetLibrary = assetLibrary, + let imageAsset = assetLibrary.imageAssets[imageLayer.referenceID] + { + let imageContainer = ImageCompositionLayer( + imageLayer: imageLayer, + size: CGSize(width: imageAsset.width, height: imageAsset.height)) + compositionLayers.append(imageContainer) + layerMap[layer.index] = imageContainer + } else if let textLayer = layer as? TextLayerModel { + let textContainer = TextCompositionLayer(textLayer: textLayer, textProvider: textProvider, fontProvider: fontProvider) + compositionLayers.append(textContainer) + layerMap[layer.index] = textContainer + } else { + let genericLayer = NullCompositionLayer(layer: layer) + compositionLayers.append(genericLayer) + layerMap[layer.index] = genericLayer + } + if layer.parent != nil { + childLayers.append(layer) + } + } + + /// Now link children with their parents + for layerModel in childLayers { + if let parentID = layerModel.parent { + let childLayer = layerMap[layerModel.index] + let parentLayer = layerMap[parentID] + childLayer?.transformNode.parentNode = parentLayer?.transformNode + } + } + + return compositionLayers + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/CoreTextRenderLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/CoreTextRenderLayer.swift new file mode 100644 index 0000000000..63a3a6b81c --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/CoreTextRenderLayer.swift @@ -0,0 +1,320 @@ +// +// TextLayer.swift +// Pods +// +// Created by Brandon Withrow on 8/3/20. +// + +import CoreGraphics +import CoreText +import Foundation +import QuartzCore +/// Needed for NSMutableParagraphStyle... +#if os(OSX) +import AppKit +#else +import UIKit +#endif + +// MARK: - CoreTextRenderLayer + +/// A CALayer subclass that renders text content using CoreText +final class CoreTextRenderLayer: CALayer, LottieDrawingLayer { + + // MARK: Public + + public var text: String? { + didSet { + needsContentUpdate = true + setNeedsLayout() + setNeedsDisplay() + } + } + + public var font: CTFont? { + didSet { + needsContentUpdate = true + setNeedsLayout() + setNeedsDisplay() + } + } + + public var alignment: NSTextAlignment = .left { + didSet { + needsContentUpdate = true + setNeedsLayout() + setNeedsDisplay() + } + } + + public var lineHeight: CGFloat = 0 { + didSet { + needsContentUpdate = true + setNeedsLayout() + setNeedsDisplay() + } + } + + public var tracking: CGFloat = 0 { + didSet { + needsContentUpdate = true + setNeedsLayout() + setNeedsDisplay() + } + } + + public var fillColor: CGColor? { + didSet { + needsContentUpdate = true + setNeedsLayout() + setNeedsDisplay() + } + } + + public var strokeColor: CGColor? { + didSet { + needsContentUpdate = true + setNeedsLayout() + setNeedsDisplay() + } + } + + public var strokeWidth: CGFloat = 0 { + didSet { + needsContentUpdate = true + setNeedsLayout() + setNeedsDisplay() + } + } + + public var strokeOnTop = false { + didSet { + setNeedsLayout() + setNeedsDisplay() + } + } + + public var preferredSize: CGSize? { + didSet { + needsContentUpdate = true + setNeedsLayout() + setNeedsDisplay() + } + } + + public func sizeToFit() { + updateTextContent() + bounds = drawingRect + anchorPoint = drawingAnchor + setNeedsLayout() + setNeedsDisplay() + } + + // MARK: Internal + + override func action(forKey _: String) -> CAAction? { + nil + } + + override func draw(in ctx: CGContext) { + guard let attributedString = attributedString else { return } + updateTextContent() + guard fillFrameSetter != nil || strokeFrameSetter != nil else { return } + + ctx.textMatrix = .identity + ctx.setAllowsAntialiasing(true) + ctx.setAllowsFontSubpixelPositioning(true) + ctx.setAllowsFontSubpixelQuantization(true) + + ctx.setShouldAntialias(true) + ctx.setShouldSubpixelPositionFonts(true) + ctx.setShouldSubpixelQuantizeFonts(true) + + if contentsAreFlipped() { + ctx.translateBy(x: 0, y: drawingRect.height) + ctx.scaleBy(x: 1.0, y: -1.0) + } + + let drawingPath = CGPath(rect: drawingRect, transform: nil) + + let fillFrame: CTFrame? + if let setter = fillFrameSetter { + fillFrame = CTFramesetterCreateFrame(setter, CFRangeMake(0, attributedString.length), drawingPath, nil) + } else { + fillFrame = nil + } + + let strokeFrame: CTFrame? + if let setter = strokeFrameSetter { + strokeFrame = CTFramesetterCreateFrame(setter, CFRangeMake(0, attributedString.length), drawingPath, nil) + } else { + strokeFrame = nil + } + + if !strokeOnTop, let strokeFrame = strokeFrame { + CTFrameDraw(strokeFrame, ctx) + } + + if let fillFrame = fillFrame { + CTFrameDraw(fillFrame, ctx) + } + + if strokeOnTop, let strokeFrame = strokeFrame { + CTFrameDraw(strokeFrame, ctx) + } + } + + // MARK: Private + + private var drawingRect: CGRect = .zero + private var drawingAnchor: CGPoint = .zero + private var fillFrameSetter: CTFramesetter? + private var attributedString: NSAttributedString? + private var strokeFrameSetter: CTFramesetter? + private var needsContentUpdate = false + + // Draws Debug colors for the font alignment. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + private func drawDebug(_ ctx: CGContext) { + if let font = font { + let ascent = CTFontGetAscent(font) + let descent = CTFontGetDescent(font) + let capHeight = CTFontGetCapHeight(font) + let leading = CTFontGetLeading(font) + + // Ascent Red + ctx.setFillColor(CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 0.5)) + ctx.fill(CGRect(x: 0, y: 0, width: drawingRect.width, height: ascent)) + + // Descent Blue + ctx.setFillColor(CGColor(srgbRed: 0, green: 0, blue: 1, alpha: 0.5)) + ctx.fill(CGRect(x: 0, y: ascent, width: drawingRect.width, height: descent)) + + // Leading Yellow + ctx.setFillColor(CGColor(srgbRed: 1, green: 1, blue: 0, alpha: 0.5)) + ctx.fill(CGRect(x: 0, y: ascent + descent, width: drawingRect.width, height: leading)) + + // Cap height Green + ctx.setFillColor(CGColor(srgbRed: 0, green: 1, blue: 0, alpha: 0.5)) + ctx.fill(CGRect(x: 0, y: ascent - capHeight, width: drawingRect.width, height: capHeight)) + + if drawingRect.height - ascent + descent + leading > 0 { + // Remainder + ctx.setFillColor(CGColor(srgbRed: 0, green: 1, blue: 1, alpha: 0.5)) + ctx + .fill(CGRect( + x: 0, + y: ascent + descent + leading, + width: drawingRect.width, + height: drawingRect.height - ascent + descent + leading)) + } + } + } + + private func updateTextContent() { + guard needsContentUpdate else { return } + needsContentUpdate = false + guard let font = font, let text = text, text.count > 0, fillColor != nil || strokeColor != nil else { + drawingRect = .zero + drawingAnchor = .zero + attributedString = nil + fillFrameSetter = nil + strokeFrameSetter = nil + return + } + + // Get Font properties + let ascent = CTFontGetAscent(font) + let descent = CTFontGetDescent(font) + let capHeight = CTFontGetCapHeight(font) + let leading = CTFontGetLeading(font) + let minLineHeight = -(ascent + descent + leading) + + // Calculate line spacing + let lineSpacing = max(CGFloat(minLineHeight) + lineHeight, CGFloat(minLineHeight)) + // Build Attributes + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineSpacing = lineSpacing + paragraphStyle.lineHeightMultiple = 1 + paragraphStyle.maximumLineHeight = ascent + descent + leading + paragraphStyle.alignment = alignment + paragraphStyle.lineBreakMode = NSLineBreakMode.byWordWrapping + var attributes: [NSAttributedString.Key: Any] = [ + NSAttributedString.Key.ligature: 0, + NSAttributedString.Key.font: font, + NSAttributedString.Key.kern: tracking, + NSAttributedString.Key.paragraphStyle: paragraphStyle, + ] + + if let fillColor = fillColor { + attributes[NSAttributedString.Key.foregroundColor] = fillColor + } + + let attrString = NSAttributedString(string: text, attributes: attributes) + attributedString = attrString + + if fillColor != nil { + let setter = CTFramesetterCreateWithAttributedString(attrString as CFAttributedString) + fillFrameSetter = setter + } else { + fillFrameSetter = nil + } + + if let strokeColor = strokeColor { + attributes[NSAttributedString.Key.foregroundColor] = nil + attributes[NSAttributedString.Key.strokeWidth] = strokeWidth + attributes[NSAttributedString.Key.strokeColor] = strokeColor + let strokeAttributedString = NSAttributedString(string: text, attributes: attributes) + strokeFrameSetter = CTFramesetterCreateWithAttributedString(strokeAttributedString as CFAttributedString) + } else { + strokeFrameSetter = nil + strokeWidth = 0 + } + + guard let setter = fillFrameSetter ?? strokeFrameSetter else { + return + } + + // Calculate drawing size and anchor offset + let textAnchor: CGPoint + if let preferredSize = preferredSize { + drawingRect = CGRect(origin: .zero, size: preferredSize) + drawingRect.size.height += (ascent - capHeight) + drawingRect.size.height += descent + textAnchor = CGPoint(x: 0, y: ascent - capHeight) + } else { + let size = CTFramesetterSuggestFrameSizeWithConstraints( + setter, + CFRange(location: 0, length: attrString.length), + nil, + CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), + nil) + switch alignment { + case .left: + textAnchor = CGPoint(x: 0, y: ascent) + case .right: + textAnchor = CGPoint(x: size.width, y: ascent) + case .center: + textAnchor = CGPoint(x: size.width * 0.5, y: ascent) + default: + textAnchor = .zero + } + drawingRect = CGRect( + x: 0, + y: 0, + width: ceil(size.width), + height: ceil(size.height)) + } + + // Now Calculate Anchor + drawingAnchor = CGPoint( + x: textAnchor.x.remap(fromLow: 0, fromHigh: drawingRect.size.width, toLow: 0, toHigh: 1), + y: textAnchor.y.remap(fromLow: 0, fromHigh: drawingRect.size.height, toLow: 0, toHigh: 1)) + + if fillFrameSetter != nil && strokeFrameSetter != nil { + drawingRect.size.width += strokeWidth + drawingRect.size.height += strokeWidth + } + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/InvertedMatteLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/InvertedMatteLayer.swift new file mode 100644 index 0000000000..7e1bc387aa --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/InvertedMatteLayer.swift @@ -0,0 +1,59 @@ +// +// InvertedMatteLayer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/28/19. +// + +import Foundation +import QuartzCore + +/// A layer that inverses the alpha output of its input layer. +/// +/// WARNING: This is experimental and probably not very performant. +final class InvertedMatteLayer: CALayer, CompositionLayerDelegate, LottieDrawingLayer { + + // MARK: Lifecycle + + init(inputMatte: CompositionLayer) { + self.inputMatte = inputMatte + super.init() + inputMatte.layerDelegate = self + anchorPoint = .zero + bounds = inputMatte.bounds + setNeedsDisplay() + } + + override init(layer: Any) { + guard let layer = layer as? InvertedMatteLayer else { + fatalError("init(layer:) wrong class.") + } + inputMatte = nil + super.init(layer: layer) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + + let inputMatte: CompositionLayer? + let wrapperLayer = CALayer() + + func frameUpdated(frame _: CGFloat) { + setNeedsDisplay() + displayIfNeeded() + } + + override func draw(in ctx: CGContext) { + guard let inputMatte = inputMatte else { return } + guard let fillColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0, 0, 0, 1]) + else { return } + ctx.setFillColor(fillColor) + ctx.fill(bounds) + ctx.setBlendMode(.destinationOut) + inputMatte.render(in: ctx) + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerFontProvider.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerFontProvider.cpp new file mode 100644 index 0000000000..d6123ee219 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerFontProvider.cpp @@ -0,0 +1,5 @@ +#include "LayerFontProvider.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerFontProvider.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerFontProvider.hpp new file mode 100644 index 0000000000..ff9eaae19c --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerFontProvider.hpp @@ -0,0 +1,45 @@ +#ifndef LayerFontProvider_hpp +#define LayerFontProvider_hpp + +#include "Lottie/Public/FontProvider/AnimationFontProvider.hpp" +#include "Lottie/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.hpp" + +namespace lottie { + +/// Connects a LottieFontProvider to a group of text layers +class LayerFontProvider { +public: + LayerFontProvider(std::shared_ptr const &fontProvider) { + _fontProvider = fontProvider; + reloadTexts(); + } + + std::shared_ptr const &fontProvider() const { + return _fontProvider; + } + void setFontProvider(std::shared_ptr const &fontProvider) { + _fontProvider = fontProvider; + reloadTexts(); + } + + void addTextLayers(std::vector> const &layers) { + for (const auto &layer : layers) { + _textLayers.push_back(layer); + } + } + + void reloadTexts() { + for (const auto &layer : _textLayers) { + layer->setFontProvider(_fontProvider); + } + } + +private: + std::vector> _textLayers; + + std::shared_ptr _fontProvider; +}; + +} + +#endif /* LayerFontProvider_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerFontProvider.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerFontProvider.swift new file mode 100644 index 0000000000..902f438dc9 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerFontProvider.swift @@ -0,0 +1,41 @@ +// +// LayerFontProvider.swift +// Lottie +// +// Created by Brandon Withrow on 8/5/20. +// Copyright © 2020 YurtvilleProds. All rights reserved. +// + +import Foundation + +/// Connects a LottieFontProvider to a group of text layers +final class LayerFontProvider { + + // MARK: Lifecycle + + init(fontProvider: AnimationFontProvider) { + self.fontProvider = fontProvider + textLayers = [] + reloadTexts() + } + + // MARK: Internal + + private(set) var textLayers: [TextCompositionLayer] + + var fontProvider: AnimationFontProvider { + didSet { + reloadTexts() + } + } + + func addTextLayers(_ layers: [TextCompositionLayer]) { + textLayers += layers + } + + func reloadTexts() { + textLayers.forEach { + $0.fontProvider = fontProvider + } + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerImageProvider.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerImageProvider.cpp new file mode 100644 index 0000000000..536c043749 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerImageProvider.cpp @@ -0,0 +1,5 @@ +#include "LayerImageProvider.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerImageProvider.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerImageProvider.hpp new file mode 100644 index 0000000000..d9affbd1cf --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerImageProvider.hpp @@ -0,0 +1,58 @@ +#ifndef LayerImageProvider_hpp +#define LayerImageProvider_hpp + +#include "Lottie/Public/ImageProvider/AnimationImageProvider.hpp" +#include "Lottie/Private/Model/Assets/ImageAsset.hpp" +#include "Lottie/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.hpp" + +namespace lottie { + +/// Connects a LottieImageProvider to a group of image layers +class LayerImageProvider { +public: + LayerImageProvider(std::shared_ptr const &imageProvider, std::map> const &assets) : + _imageProvider(imageProvider), + _imageAssets(assets) { + reloadImages(); + } + + std::shared_ptr imageProvider() const { + return _imageProvider; + } + void setImageProvider(std::shared_ptr const &imageProvider) { + _imageProvider = imageProvider; + reloadImages(); + } + + std::vector> const &imageLayers() const { + return _imageLayers; + } + + void addImageLayers(std::vector> const &layers) { + for (const auto &layer : layers) { + auto it = _imageAssets.find(layer->imageReferenceID()); + if (it != _imageAssets.end()) { + _imageLayers.push_back(layer); + } + } + } + + void reloadImages() { + for (const auto &imageLayer : imageLayers()) { + auto it = _imageAssets.find(imageLayer->imageReferenceID()); + if (it != _imageAssets.end()) { + imageLayer->setImage(_imageProvider->imageForAsset(*it->second)); + } + } + } + +private: + std::shared_ptr _imageProvider; + std::vector> _imageLayers; + + std::map> _imageAssets; +}; + +} + +#endif /* LayerImageProvider_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerImageProvider.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerImageProvider.swift new file mode 100644 index 0000000000..d943ffeb4c --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerImageProvider.swift @@ -0,0 +1,53 @@ +// +// LayerImageProvider.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/25/19. +// + +import Foundation + +/// Connects a LottieImageProvider to a group of image layers +final class LayerImageProvider { + + // MARK: Lifecycle + + init(imageProvider: AnimationImageProvider, assets: [String: ImageAsset]?) { + self.imageProvider = imageProvider + imageLayers = [ImageCompositionLayer]() + if let assets = assets { + imageAssets = assets + } else { + imageAssets = [:] + } + reloadImages() + } + + // MARK: Internal + + private(set) var imageLayers: [ImageCompositionLayer] + let imageAssets: [String: ImageAsset] + + var imageProvider: AnimationImageProvider { + didSet { + reloadImages() + } + } + + func addImageLayers(_ layers: [ImageCompositionLayer]) { + for layer in layers { + if imageAssets[layer.imageReferenceID] != nil { + /// Found a linking asset in our asset library. Add layer + imageLayers.append(layer) + } + } + } + + func reloadImages() { + for imageLayer in imageLayers { + if let asset = imageAssets[imageLayer.imageReferenceID] { + imageLayer.image = imageProvider.imageForAsset(asset: asset) + } + } + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerTextProvider.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerTextProvider.cpp new file mode 100644 index 0000000000..22da907bd3 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerTextProvider.cpp @@ -0,0 +1,5 @@ +#include "LayerTextProvider.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerTextProvider.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerTextProvider.hpp new file mode 100644 index 0000000000..82c96ec8f3 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerTextProvider.hpp @@ -0,0 +1,45 @@ +#ifndef LayerTextProvider_hpp +#define LayerTextProvider_hpp + +#include "Lottie/Public/TextProvider/AnimationTextProvider.hpp" +#include "Lottie/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.hpp" + +namespace lottie { + +/// Connects a LottieTextProvider to a group of text layers +class LayerTextProvider { +public: + LayerTextProvider(std::shared_ptr const &textProvider) { + _textProvider = textProvider; + reloadTexts(); + } + + std::shared_ptr const &textProvider() const { + return _textProvider; + } + void setTextProvider(std::shared_ptr const &textProvider) { + _textProvider = textProvider; + reloadTexts(); + } + + void addTextLayers(std::vector> const &layers) { + for (const auto &layer : layers) { + _textLayers.push_back(layer); + } + } + + void reloadTexts() { + for (const auto &layer : _textLayers) { + layer->setTextProvider(_textProvider); + } + } + +private: + std::vector> _textLayers; + + std::shared_ptr _textProvider; +}; + +} + +#endif /* LayerTextProvider_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerTextProvider.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerTextProvider.swift new file mode 100644 index 0000000000..53806d57c0 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerTextProvider.swift @@ -0,0 +1,40 @@ +// +// LayerTextProvider.swift +// lottie-ios-iOS +// +// Created by Alexandr Goncharov on 07/06/2019. +// + +import Foundation + +/// Connects a LottieTextProvider to a group of text layers +final class LayerTextProvider { + + // MARK: Lifecycle + + init(textProvider: AnimationTextProvider) { + self.textProvider = textProvider + textLayers = [] + reloadTexts() + } + + // MARK: Internal + + private(set) var textLayers: [TextCompositionLayer] + + var textProvider: AnimationTextProvider { + didSet { + reloadTexts() + } + } + + func addTextLayers(_ layers: [TextCompositionLayer]) { + textLayers += layers + } + + func reloadTexts() { + textLayers.forEach { + $0.textProvider = textProvider + } + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerTransformNode.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerTransformNode.cpp new file mode 100644 index 0000000000..8f751a571f --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerTransformNode.cpp @@ -0,0 +1,5 @@ +#include "LayerTransformNode.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerTransformNode.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerTransformNode.hpp new file mode 100644 index 0000000000..b2d9ad7e19 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerTransformNode.hpp @@ -0,0 +1,201 @@ +#ifndef LayerTransformNode_hpp +#define LayerTransformNode_hpp + +#include "Lottie/Private/Model/Objects/Transform.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/PassThroughOutputNode.hpp" + +namespace lottie { + +class LayerTransformProperties: public KeypathSearchableNodePropertyMap { +public: + LayerTransformProperties(std::shared_ptr transform) { + _anchor = std::make_shared>(std::make_shared>(transform->anchorPoint().keyframes)); + _scale = std::make_shared>(std::make_shared>(transform->scale().keyframes)); + _rotation = std::make_shared>(std::make_shared>(transform->rotation().keyframes)); + _opacity = std::make_shared>(std::make_shared>(transform->opacity().keyframes)); + + std::map> propertyMap; + _keypathProperties.insert(std::make_pair("Anchor Point", _anchor)); + _keypathProperties.insert(std::make_pair("Scale", _scale)); + _keypathProperties.insert(std::make_pair("Rotation", _rotation)); + _keypathProperties.insert(std::make_pair("Opacity", _opacity)); + + if (transform->positionX().has_value() && transform->positionY().has_value()) { + auto xPosition = std::make_shared>(std::make_shared>(transform->positionX()->keyframes)); + auto yPosition = std::make_shared>(std::make_shared>(transform->positionY()->keyframes)); + _keypathProperties.insert(std::make_pair("X Position", xPosition)); + _keypathProperties.insert(std::make_pair("Y Position", yPosition)); + + _positionX = xPosition; + _positionY = yPosition; + _position = nullptr; + } else if (transform->position().has_value()) { + auto position = std::make_shared>(std::make_shared>(transform->position()->keyframes)); + _keypathProperties.insert(std::make_pair("Position", position)); + + _position = position; + _positionX = nullptr; + _positionY = nullptr; + } else { + _position = nullptr; + _positionX = nullptr; + _positionY = nullptr; + } + + for (const auto &it : _keypathProperties) { + _properties.push_back(it.second); + } + } + + virtual std::vector> &properties() override { + return _properties; + } + + virtual std::vector> const &childKeypaths() const override { + return _childKeypaths; + } + + virtual std::string keypathName() const override { + return "Transform"; + } + + virtual std::map> keypathProperties() const override { + return _keypathProperties; + } + + virtual std::shared_ptr keypathLayer() const override { + return nullptr; + } + + std::shared_ptr> const &anchor() { + return _anchor; + } + + std::shared_ptr> const &scale() { + return _scale; + } + + std::shared_ptr> const &rotation() { + return _rotation; + } + + std::shared_ptr> const &position() { + return _position; + } + + std::shared_ptr> const &positionX() { + return _positionX; + } + + std::shared_ptr> const &positionY() { + return _positionY; + } + + std::shared_ptr> const &opacity() { + return _opacity; + } + +private: + std::map> _keypathProperties; + std::vector> _childKeypaths; + + std::vector> _properties; + + std::shared_ptr> _anchor; + std::shared_ptr> _scale; + std::shared_ptr> _rotation; + std::shared_ptr> _position; + std::shared_ptr> _positionX; + std::shared_ptr> _positionY; + std::shared_ptr> _opacity; +}; + +class LayerTransformNode: public AnimatorNode { +public: + LayerTransformNode(std::shared_ptr transform) : + AnimatorNode(nullptr), + _transformProperties(std::make_shared(transform)) { + _outputNode = std::make_shared(nullptr); + } + + virtual std::shared_ptr outputNode() override { + return _outputNode; + } + + virtual std::shared_ptr propertyMap() const override { + return _transformProperties; + } + + virtual bool shouldRebuildOutputs(double frame) override { + return hasLocalUpdates() || hasUpstreamUpdates(); + } + + virtual void rebuildOutputs(double frame) override { + _opacity = ((float)_transformProperties->opacity()->value().value) * 0.01f; + + Vector2D position(0.0, 0.0); + if (_transformProperties->position()) { + auto position3d = _transformProperties->position()->value(); + position.x = position3d.x; + position.y = position3d.y; + } else if (_transformProperties->positionX() && _transformProperties->positionY()) { + position = Vector2D( + _transformProperties->positionX()->value().value, + _transformProperties->positionY()->value().value + ); + } + + Vector3D anchor = _transformProperties->anchor()->value(); + Vector3D scale = _transformProperties->scale()->value(); + _localTransform = CATransform3D::makeTransform( + Vector2D(anchor.x, anchor.y), + position, + Vector2D(scale.x, scale.y), + _transformProperties->rotation()->value().value, + std::nullopt, + std::nullopt + ); + + if (parentNode() && parentNode()->asLayerTransformNode()) { + _globalTransform = _localTransform * parentNode()->asLayerTransformNode()->_globalTransform; + } else { + _globalTransform = _localTransform; + } + } + + std::shared_ptr const &transformProperties() { + return _transformProperties; + } + + float opacity() { + return _opacity; + } + + CATransform3D const &globalTransform() { + return _globalTransform; + } + +private: + std::shared_ptr _outputNode; + + std::shared_ptr _transformProperties; + + float _opacity = 1.0; + CATransform3D _localTransform = CATransform3D::identity(); + CATransform3D _globalTransform = CATransform3D::identity(); + +public: + virtual LayerTransformNode *asLayerTransformNode() override { + return this; + } +}; + +} + +#endif /* LayerTransformNode_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerTransformNode.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerTransformNode.swift new file mode 100644 index 0000000000..a2981f8e4d --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/LayerContainers/Utility/LayerTransformNode.swift @@ -0,0 +1,144 @@ +// +// LayerTransformPropertyMap.swift +// lottie-swift +// +// Created by Brandon Withrow on 2/4/19. +// + +import CoreGraphics +import Foundation +import QuartzCore + +// MARK: - LayerTransformProperties + +final class LayerTransformProperties: NodePropertyMap, KeypathSearchable { + + // MARK: Lifecycle + + init(transform: Transform) { + + anchor = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.anchorPoint.keyframes)) + scale = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.scale.keyframes)) + rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.rotation.keyframes)) + opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.opacity.keyframes)) + + var propertyMap: [String: AnyNodeProperty] = [ + "Anchor Point" : anchor, + "Scale" : scale, + "Rotation" : rotation, + "Opacity" : opacity, + ] + + if + let positionKeyframesX = transform.positionX?.keyframes, + let positionKeyframesY = transform.positionY?.keyframes + { + let xPosition: NodeProperty = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframesX)) + let yPosition: NodeProperty = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframesY)) + propertyMap["X Position"] = xPosition + propertyMap["Y Position"] = yPosition + positionX = xPosition + positionY = yPosition + position = nil + } else if let positionKeyframes = transform.position?.keyframes { + let position: NodeProperty = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframes)) + propertyMap["Position"] = position + self.position = position + positionX = nil + positionY = nil + } else { + position = nil + positionY = nil + positionX = nil + } + + keypathProperties = propertyMap + properties = Array(propertyMap.values) + } + + // MARK: Internal + + let keypathProperties: [String: AnyNodeProperty] + var keypathName = "Transform" + + let properties: [AnyNodeProperty] + + let anchor: NodeProperty + let scale: NodeProperty + let rotation: NodeProperty + let position: NodeProperty? + let positionX: NodeProperty? + let positionY: NodeProperty? + let opacity: NodeProperty + + var childKeypaths: [KeypathSearchable] { + [] + } +} + +// MARK: - LayerTransformNode + +class LayerTransformNode: AnimatorNode { + + // MARK: Lifecycle + + init(transform: Transform) { + transformProperties = LayerTransformProperties(transform: transform) + } + + // MARK: Internal + + let outputNode: NodeOutput = PassThroughOutputNode(parent: nil) + + let transformProperties: LayerTransformProperties + + var parentNode: AnimatorNode? + var hasLocalUpdates = false + var hasUpstreamUpdates = false + var lastUpdateFrame: CGFloat? = nil + var isEnabled = true + + var opacity: Float = 1 + var localTransform: CATransform3D = CATransform3DIdentity + var globalTransform: CATransform3D = CATransform3DIdentity + + // MARK: Animator Node Protocol + + var propertyMap: NodePropertyMap & KeypathSearchable { + transformProperties + } + + func shouldRebuildOutputs(frame _: CGFloat) -> Bool { + hasLocalUpdates || hasUpstreamUpdates + } + + func rebuildOutputs(frame _: CGFloat) { + opacity = Float(transformProperties.opacity.value.cgFloatValue) * 0.01 + + let position: CGPoint + if let point = transformProperties.position?.value.pointValue { + position = point + } else if + let xPos = transformProperties.positionX?.value.cgFloatValue, + let yPos = transformProperties.positionY?.value.cgFloatValue + { + position = CGPoint(x: xPos, y: yPos) + } else { + position = .zero + } + + localTransform = CATransform3D.makeTransform( + anchor: transformProperties.anchor.value.pointValue, + position: position, + scale: transformProperties.scale.value.sizeValue, + rotation: transformProperties.rotation.value.cgFloatValue, + skew: nil, + skewAxis: nil) + + if let parentNode = parentNode as? LayerTransformNode { + globalTransform = CATransform3DConcat(localTransform, parentNode.globalTransform) + } else { + globalTransform = localTransform + } + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Extensions/ItemsExtension.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Extensions/ItemsExtension.swift new file mode 100644 index 0000000000..2df5908203 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Extensions/ItemsExtension.swift @@ -0,0 +1,97 @@ +// +// ItemsExtension.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/18/19. +// + +import Foundation + +// MARK: - NodeTree + +final class NodeTree { + var rootNode: AnimatorNode? = nil + var transform: ShapeTransform? = nil + var renderContainers: [ShapeContainerLayer] = [] + var paths: [PathOutputNode] = [] + var childrenNodes: [AnimatorNode] = [] +} + +extension Array where Element == ShapeItem { + func initializeNodeTree() -> NodeTree { + + let nodeTree = NodeTree() + + for item in self { + guard item.hidden == false, item.type != .unknown else { continue } + if let fill = item as? Fill { + let node = FillNode(parentNode: nodeTree.rootNode, fill: fill) + nodeTree.rootNode = node + nodeTree.childrenNodes.append(node) + } else if let stroke = item as? Stroke { + let node = StrokeNode(parentNode: nodeTree.rootNode, stroke: stroke) + nodeTree.rootNode = node + nodeTree.childrenNodes.append(node) + } else if let gradientFill = item as? GradientFill { + let node = GradientFillNode(parentNode: nodeTree.rootNode, gradientFill: gradientFill) + nodeTree.rootNode = node + nodeTree.childrenNodes.append(node) + } else if let gradientStroke = item as? GradientStroke { + let node = GradientStrokeNode(parentNode: nodeTree.rootNode, gradientStroke: gradientStroke) + nodeTree.rootNode = node + nodeTree.childrenNodes.append(node) + } else if let ellipse = item as? Ellipse { + let node = EllipseNode(parentNode: nodeTree.rootNode, ellipse: ellipse) + nodeTree.rootNode = node + nodeTree.childrenNodes.append(node) + } else if let rect = item as? Rectangle { + let node = RectangleNode(parentNode: nodeTree.rootNode, rectangle: rect) + nodeTree.rootNode = node + nodeTree.childrenNodes.append(node) + } else if let star = item as? Star { + switch star.starType { + case .none: + continue + case .polygon: + let node = PolygonNode(parentNode: nodeTree.rootNode, star: star) + nodeTree.rootNode = node + nodeTree.childrenNodes.append(node) + case .star: + let node = StarNode(parentNode: nodeTree.rootNode, star: star) + nodeTree.rootNode = node + nodeTree.childrenNodes.append(node) + } + } else if let shape = item as? Shape { + let node = ShapeNode(parentNode: nodeTree.rootNode, shape: shape) + nodeTree.rootNode = node + nodeTree.childrenNodes.append(node) + } else if let trim = item as? Trim { + let node = TrimPathNode(parentNode: nodeTree.rootNode, trim: trim, upstreamPaths: nodeTree.paths) + nodeTree.rootNode = node + nodeTree.childrenNodes.append(node) + } else if let xform = item as? ShapeTransform { + nodeTree.transform = xform + continue + } else if let group = item as? Group { + + let tree = group.items.initializeNodeTree() + let node = GroupNode(name: group.name, parentNode: nodeTree.rootNode, tree: tree) + nodeTree.rootNode = node + nodeTree.childrenNodes.append(node) + /// Now add all child paths to current tree + nodeTree.paths.append(contentsOf: tree.paths) + nodeTree.renderContainers.append(node.container) + } + + if let pathNode = nodeTree.rootNode as? PathNode { + //// Add path container to the node tree + nodeTree.paths.append(pathNode.pathOutput) + } + + if let renderNode = nodeTree.rootNode as? RenderNode { + nodeTree.renderContainers.append(ShapeRenderLayer(renderer: renderNode.renderer)) + } + } + return nodeTree + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.cpp new file mode 100644 index 0000000000..eb9e3a5837 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.cpp @@ -0,0 +1,5 @@ +#include "NodeProperty.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.hpp new file mode 100644 index 0000000000..f7eece88e6 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.hpp @@ -0,0 +1,55 @@ +#ifndef NodeProperty_hpp +#define NodeProperty_hpp + +#include "Lottie/Public/Primitives/AnyValue.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.hpp" +#include "Lottie/Public/DynamicProperties/AnyValueProvider.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueContainer.hpp" + +namespace lottie { + +/// A node property that holds a reference to a T ValueProvider and a T ValueContainer. +template +class NodeProperty: public AnyNodeProperty { +public: + NodeProperty(std::shared_ptr> provider) : + _valueProvider(provider), + //_originalValueProvider(provider), + _typedContainer(provider->value(0.0)) { + _typedContainer.setNeedsUpdate(); + } + +public: + virtual AnyValue::Type valueType() const override { + return AnyValueType::type(); + } + + virtual T value() { + return _typedContainer.outputValue(); + } + + virtual bool needsUpdate(double frame) const override { + return _typedContainer.needsUpdate() || _valueProvider->hasUpdate(frame); + } + + virtual void setProvider(std::shared_ptr provider) override { + /*if (provider->valueType() != valueType()) { + return; + } + _valueProvider = provider; + _typedContainer.setNeedsUpdate();*/ + } + + virtual void update(double frame) override { + _typedContainer.setValue(_valueProvider->value(frame), frame); + } + +private: + ValueContainer _typedContainer; + std::shared_ptr> _valueProvider; + //std::shared_ptr _originalValueProvider; +}; + +} + +#endif /* NodeProperty_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.swift new file mode 100644 index 0000000000..8702f2c59c --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.swift @@ -0,0 +1,55 @@ +// +// NodeProperty.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/30/19. +// + +import CoreGraphics +import Foundation + +/// A node property that holds a reference to a T ValueProvider and a T ValueContainer. +class NodeProperty: AnyNodeProperty { + + // MARK: Lifecycle + + init(provider: AnyValueProvider) { + valueProvider = provider + originalValueProvider = valueProvider + typedContainer = ValueContainer(provider.value(frame: 0) as! T) + typedContainer.setNeedsUpdate() + } + + // MARK: Internal + + var valueProvider: AnyValueProvider + var originalValueProvider: AnyValueProvider + + var valueType: Any.Type { T.self } + + var value: T { + typedContainer.outputValue + } + + var valueContainer: AnyValueContainer { + typedContainer + } + + func needsUpdate(frame: CGFloat) -> Bool { + valueContainer.needsUpdate || valueProvider.hasUpdate(frame: frame) + } + + func setProvider(provider: AnyValueProvider) { + guard provider.valueType == valueType else { return } + valueProvider = provider + valueContainer.setNeedsUpdate() + } + + func update(frame: CGFloat) { + typedContainer.setValue(valueProvider.value(frame: frame), forFrame: frame) + } + + // MARK: Fileprivate + + fileprivate var typedContainer: ValueContainer +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.cpp new file mode 100644 index 0000000000..8609641d49 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.cpp @@ -0,0 +1,5 @@ +#include "AnyNodeProperty.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.hpp new file mode 100644 index 0000000000..f317e68b9f --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.hpp @@ -0,0 +1,33 @@ +#ifndef AnyNodeProperty_hpp +#define AnyNodeProperty_hpp + +#include "Lottie/Public/Primitives/AnyValue.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.hpp" +#include "Lottie/Public/DynamicProperties/AnyValueProvider.hpp" + +#include + +namespace lottie { + +/// A property of a node. The node property holds a provider and a container +class AnyNodeProperty { +public: + virtual ~AnyNodeProperty() = default; + +public: + /// Returns true if the property needs to recompute its stored value + virtual bool needsUpdate(double frame) const = 0; + + /// Updates the property for the frame + virtual void update(double frame) = 0; + + /// The Type of the value provider + virtual AnyValue::Type valueType() const = 0; + + /// Sets the value provider for the property. + virtual void setProvider(std::shared_ptr provider) = 0; +}; + +} + +#endif /* AnyNodeProperty_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.swift new file mode 100644 index 0000000000..132d96a894 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.swift @@ -0,0 +1,50 @@ +// +// AnyNodeProperty.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/30/19. +// + +import CoreGraphics +import Foundation + +// MARK: - AnyNodeProperty + +/// A property of a node. The node property holds a provider and a container +protocol AnyNodeProperty { + + /// Returns true if the property needs to recompute its stored value + func needsUpdate(frame: CGFloat) -> Bool + + /// Updates the property for the frame + func update(frame: CGFloat) + + /// The stored value container for the property + var valueContainer: AnyValueContainer { get } + + /// The value provider for the property + var valueProvider: AnyValueProvider { get } + + /// The original value provider for the property + var originalValueProvider: AnyValueProvider { get } + + /// The Type of the value provider + var valueType: Any.Type { get } + + /// Sets the value provider for the property. + func setProvider(provider: AnyValueProvider) +} + +extension AnyNodeProperty { + + /// Returns the most recently computed value for the keypath, returns nil if property wasn't found + func getValueOfType() -> T? { + valueContainer.value as? T + } + + /// Returns the most recently computed value for the keypath, returns nil if property wasn't found + func getValue() -> Any? { + valueContainer.value + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.cpp new file mode 100644 index 0000000000..b186f2e2f4 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.cpp @@ -0,0 +1,5 @@ +#include "AnyValueContainer.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.hpp new file mode 100644 index 0000000000..55e6dac2e2 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.hpp @@ -0,0 +1,25 @@ +#ifndef AnyValueContainer_hpp +#define AnyValueContainer_hpp + +#include "Lottie/Public/Primitives/AnyValue.hpp" + +namespace lottie { + +class AnyValueContainer { +public: + /// The stored value of the container + virtual AnyValue value() const = 0; + + /// Notifies the provider that it should update its container + virtual void setNeedsUpdate() = 0; + + /// When true the container needs to have its value updated by its provider + virtual bool needsUpdate() const = 0; + + /// The frame time of the last provided update + virtual double lastUpdateFrame() const = 0; +}; + +} + +#endif /* AnyValueContainer_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.swift new file mode 100644 index 0000000000..769d51cb1f --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.swift @@ -0,0 +1,26 @@ +// +// AnyValueContainer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/30/19. +// + +import CoreGraphics +import Foundation + +/// The container for the value of a property. +protocol AnyValueContainer: AnyObject { + + /// The stored value of the container + var value: Any { get } + + /// Notifies the provider that it should update its container + func setNeedsUpdate() + + /// When true the container needs to have its value updated by its provider + var needsUpdate: Bool { get } + + /// The frame time of the last provided update + var lastUpdateFrame: CGFloat { get } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasRenderUpdates.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasRenderUpdates.hpp new file mode 100644 index 0000000000..cf30fec769 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasRenderUpdates.hpp @@ -0,0 +1,13 @@ +#ifndef HasRenderUpdates_hpp +#define HasRenderUpdates_hpp + +namespace lottie { + +class HasRenderUpdates { +public: + virtual bool hasRenderUpdates(double forFrame) = 0; +}; + +} + +#endif /* HasRenderUpdates_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasUpdate.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasUpdate.hpp new file mode 100644 index 0000000000..f0c35b0530 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasUpdate.hpp @@ -0,0 +1,14 @@ +#ifndef HasUpdate_hpp +#define HasUpdate_hpp + +namespace lottie { + +class HasUpdate { +public: + /// The last frame in which this node was updated. + virtual bool hasUpdate() = 0; +}; + +} + +#endif /* HasUpdate_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.cpp new file mode 100644 index 0000000000..62c08facba --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.cpp @@ -0,0 +1,5 @@ +#include "KeypathSearchable.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.hpp new file mode 100644 index 0000000000..4586c7ddc2 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.hpp @@ -0,0 +1,36 @@ +#ifndef KeypathSearchable_hpp +#define KeypathSearchable_hpp + +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.hpp" +#include "Lottie/Public/Primitives/CALayer.hpp" + +#include +#include +#include +#include + +namespace lottie { + +class KeypathSearchable; + +class HasChildKeypaths { +public: + /// Children Keypaths + virtual std::vector> const &childKeypaths() const = 0; +}; + +/// Protocol that provides keypath search functionality. Returns all node properties associated with a keypath. +class KeypathSearchable: virtual public HasChildKeypaths { +public: + /// The name of the Keypath + virtual std::string keypathName() const = 0; + + /// A list of properties belonging to the keypath. + virtual std::map> keypathProperties() const = 0; + + virtual std::shared_ptr keypathLayer() const = 0; +}; + +} + +#endif /* KeypathSearchable_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.swift new file mode 100644 index 0000000000..b46f524f81 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.swift @@ -0,0 +1,24 @@ +// +// KeypathSettable.swift +// lottie-swift +// +// Created by Brandon Withrow on 2/4/19. +// + +import Foundation +import QuartzCore + +/// Protocol that provides keypath search functionality. Returns all node properties associated with a keypath. +protocol KeypathSearchable { + + /// The name of the Keypath + var keypathName: String { get } + + /// A list of properties belonging to the keypath. + var keypathProperties: [String: AnyNodeProperty] { get } + + /// Children Keypaths + var childKeypaths: [KeypathSearchable] { get } + + var keypathLayer: CALayer? { get } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.cpp new file mode 100644 index 0000000000..100edba8df --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.cpp @@ -0,0 +1,5 @@ +#include "NodePropertyMap.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.hpp new file mode 100644 index 0000000000..d02d02ff0a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.hpp @@ -0,0 +1,41 @@ +#ifndef NodePropertyMap_hpp +#define NodePropertyMap_hpp + +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.hpp" +#include "Lottie/Public/Primitives/CALayer.hpp" + +#include + +namespace lottie { + +class NodePropertyMap: virtual public HasChildKeypaths { +public: + virtual std::vector> &properties() = 0; + + bool needsLocalUpdate(double frame) { + for (auto &property : properties()) { + if (property->needsUpdate(frame)) { + return true; + } + } + return false; + } + + void updateNodeProperties(double frame) { + for (auto &property : properties()) { + property->update(frame); + } + } +}; + +class KeypathSearchableNodePropertyMap: virtual public NodePropertyMap, virtual public KeypathSearchable { +public: + virtual std::shared_ptr keypathLayer() { + return nullptr; + } +}; + +} + +#endif /* NodePropertyMap_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.swift new file mode 100644 index 0000000000..07b101f77d --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.swift @@ -0,0 +1,44 @@ +// +// NodePropertyMap.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/21/19. +// + +import Foundation +import QuartzCore + +// MARK: - NodePropertyMap + +protocol NodePropertyMap { + var properties: [AnyNodeProperty] { get } +} + +extension NodePropertyMap { + + var childKeypaths: [KeypathSearchable] { + [] + } + + var keypathLayer: CALayer? { + nil + } + + /// Checks if the node's local contents need to be rebuilt. + func needsLocalUpdate(frame: CGFloat) -> Bool { + for property in properties { + if property.needsUpdate(frame: frame) { + return true + } + } + return false + } + + /// Rebuilds only the local nodes that have an update for the frame + func updateNodeProperties(frame: CGFloat) { + properties.forEach { property in + property.update(frame: frame) + } + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueContainer.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueContainer.cpp new file mode 100644 index 0000000000..2ead9de1ed --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueContainer.cpp @@ -0,0 +1,5 @@ +#include "ValueContainer.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueContainer.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueContainer.hpp new file mode 100644 index 0000000000..54f1548b14 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueContainer.hpp @@ -0,0 +1,58 @@ +#ifndef ValueContainer_hpp +#define ValueContainer_hpp + +#include "Lottie/Public/Primitives/AnyValue.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.hpp" + +namespace lottie { + +/// A container for a node value that is Typed to T. +template +class ValueContainer: public AnyValueContainer { +public: + ValueContainer(T value) : + _outputValue(value) { + } + +public: + double _lastUpdateFrame = std::numeric_limits::infinity(); + bool _needsUpdate = true; + + virtual AnyValue value() const override { + return AnyValue(_outputValue); + } + + virtual bool needsUpdate() const override { + return _needsUpdate; + } + + virtual double lastUpdateFrame() const override { + return _lastUpdateFrame; + } + + T _outputValue; + + T outputValue() { + return _outputValue; + } + void setOutputValue(T value) { + _outputValue = value; + _needsUpdate = false; + } + + void setValue(AnyValue value, double forFrame) { + if (value.type() == AnyValueType::type()) { + _needsUpdate = false; + _lastUpdateFrame = forFrame; + _outputValue = value.get(); + } + } + + virtual void setNeedsUpdate() override { + _needsUpdate = true; + } +}; + +} + +#endif /* ValueContainer_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueContainer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueContainer.swift new file mode 100644 index 0000000000..7717090966 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueContainer.swift @@ -0,0 +1,47 @@ +// +// ValueContainer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/30/19. +// + +import CoreGraphics +import Foundation + +/// A container for a node value that is Typed to T. +class ValueContainer: AnyValueContainer { + + // MARK: Lifecycle + + init(_ value: T) { + outputValue = value + } + + // MARK: Internal + + private(set) var lastUpdateFrame = CGFloat.infinity + + fileprivate(set) var needsUpdate = true + + var value: Any { + outputValue as Any + } + + var outputValue: T { + didSet { + needsUpdate = false + } + } + + func setValue(_ value: Any, forFrame: CGFloat) { + if let typedValue = value as? T { + needsUpdate = false + lastUpdateFrame = forFrame + outputValue = typedValue + } + } + + func setNeedsUpdate() { + needsUpdate = true + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/DashPatternInterpolator.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/DashPatternInterpolator.cpp new file mode 100644 index 0000000000..b3f91f556b --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/DashPatternInterpolator.cpp @@ -0,0 +1,5 @@ +#include "DashPatternInterpolator.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/DashPatternInterpolator.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/DashPatternInterpolator.hpp new file mode 100644 index 0000000000..d714fc9044 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/DashPatternInterpolator.hpp @@ -0,0 +1,46 @@ +#ifndef DashPatternInterpolator_hpp +#define DashPatternInterpolator_hpp + +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp" +#include "Lottie/Public/Primitives/DashPattern.hpp" + +namespace lottie { + +/// A value provider that produces an array of values from an array of Keyframe Interpolators +class DashPatternInterpolator: public ValueProvider, public std::enable_shared_from_this { +public: + /// Initialize with an array of array of keyframes. + DashPatternInterpolator(std::vector>> const &keyframeGroups) { + for (const auto &keyframeGroup : keyframeGroups) { + _keyframeInterpolators.push_back(std::make_shared>(keyframeGroup)); + } + } + + virtual AnyValue::Type valueType() const override { + return AnyValueType::type(); + } + + virtual DashPattern value(AnimationFrameTime frame) override { + std::vector values; + for (const auto &interpolator : _keyframeInterpolators) { + values.push_back(interpolator->value(frame).value); + } + return DashPattern(std::move(values)); + } + + virtual bool hasUpdate(double frame) const override { + for (const auto &interpolator : _keyframeInterpolators) { + if (interpolator->hasUpdate(frame)) { + return true; + } + } + return false; + } + +private: + std::vector>> _keyframeInterpolators; +}; + +} + +#endif /* DashPatternInterpolator_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/GroupInterpolator.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/GroupInterpolator.swift new file mode 100644 index 0000000000..8be2dc2363 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/GroupInterpolator.swift @@ -0,0 +1,39 @@ +// +// KeyframeGroupInterpolator.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/22/19. +// + +import CoreGraphics +import Foundation + +/// A value provider that produces an array of values from an array of Keyframe Interpolators +final class GroupInterpolator: ValueProvider where ValueType: Interpolatable { + + // MARK: Lifecycle + + /// Initialize with an array of array of keyframes. + init(keyframeGroups: ContiguousArray>>) { + keyframeInterpolators = ContiguousArray(keyframeGroups.map({ KeyframeInterpolator(keyframes: $0) })) + } + + // MARK: Internal + + let keyframeInterpolators: ContiguousArray> + + var valueType: Any.Type { + [ValueType].self + } + + var storage: ValueProviderStorage<[ValueType]> { + .closure { frame in + self.keyframeInterpolators.map({ $0.value(frame: frame) as! ValueType }) + } + } + + func hasUpdate(frame: CGFloat) -> Bool { + let updated = keyframeInterpolators.first(where: { $0.hasUpdate(frame: frame) }) + return updated != nil + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.cpp new file mode 100644 index 0000000000..8ec2364762 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.cpp @@ -0,0 +1,5 @@ +#include "KeyframeInterpolator.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp new file mode 100644 index 0000000000..7e99ae3482 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp @@ -0,0 +1,449 @@ +#ifndef KeyframeInterpolator_hpp +#define KeyframeInterpolator_hpp + +#include "Lottie/Public/DynamicProperties/AnyValueProvider.hpp" + +namespace lottie { + +/// A value provider that produces a value at Time from a group of keyframes +template +class KeyframeInterpolator: public ValueProvider, public std::enable_shared_from_this> { +public: + KeyframeInterpolator(std::vector> const &keyframes_) : + keyframes(keyframes_) { + assert(!keyframes.empty()); + } + +public: + std::vector> keyframes; + + virtual AnyValue::Type valueType() const override { + return AnyValueType::type(); + } + + virtual T value(AnimationFrameTime frame) override { + // First set the keyframe span for the frame. + updateSpanIndices(frame); + lastUpdatedFrame = frame; + // If only one keyframe return its value + + if (leadingKeyframe.has_value() && + trailingKeyframe.has_value()) + { + /// We have leading and trailing keyframe. + auto progress = leadingKeyframe->interpolatedProgress(trailingKeyframe.value(), frame); + return leadingKeyframe->interpolate(trailingKeyframe.value(), progress); + } else if (leadingKeyframe.has_value()) { + return leadingKeyframe->value; + } else if (trailingKeyframe.has_value()) { + return trailingKeyframe->value; + } else { + /// Satisfy the compiler. + return keyframes[0].value; + } + } + + /// Returns true to trigger a frame update for this interpolator. + /// + /// An interpolator will be asked if it needs to update every frame. + /// If the interpolator needs updating it will be asked to compute its value for + /// the given frame. + /// + /// Cases a keyframe should not be updated: + /// - If time is in span and leading keyframe is hold + /// - If time is after the last keyframe. + /// - If time is before the first keyframe + /// + /// Cases for updating a keyframe: + /// - If time is in the span, and is not a hold + /// - If time is outside of the span, and there are more keyframes + /// - If a value delegate is set + /// - If leading and trailing are both nil. + virtual bool hasUpdate(double frame) const override { + if (!lastUpdatedFrame.has_value()) { + return true; + } + + if (leadingKeyframe.has_value() && + !trailingKeyframe.has_value() && + leadingKeyframe->time < frame) + { + /// Frame is after bounds of keyframes + return false; + } + if (trailingKeyframe.has_value() && + !leadingKeyframe.has_value() && + frame < trailingKeyframe->time) + { + /// Frame is before bounds of keyframes + return false; + } + if (leadingKeyframe.has_value() && + trailingKeyframe.has_value() && + leadingKeyframe->isHold && + leadingKeyframe->time < frame && + frame < trailingKeyframe->time) + { + return false; + } + return true; + } + + // MARK: Fileprivate + + std::optional lastUpdatedFrame; + + std::optional leadingIndex; + std::optional trailingIndex; + std::optional> leadingKeyframe; + std::optional> trailingKeyframe; + + /// Finds the appropriate Leading and Trailing keyframe index for the given time. + void updateSpanIndices(double frame) { + if (keyframes.empty()) { + leadingIndex = std::nullopt; + trailingIndex = std::nullopt; + leadingKeyframe = std::nullopt; + trailingKeyframe = std::nullopt; + return; + } + + // This function searches through the array to find the span of two keyframes + // that contain the current time. + // + // We could use Array.first(where:) but that would search through the entire array + // each frame. + // Instead we track the last used index and search either forwards or + // backwards from there. This reduces the iterations and complexity from + // + // O(n), where n is the length of the sequence to + // O(n), where n is the number of items after or before the last used index. + // + + if (keyframes.size() == 1) { + /// Only one keyframe. Set it as first and move on. + leadingIndex = 0; + trailingIndex = std::nullopt; + leadingKeyframe = keyframes[0]; + trailingKeyframe = std::nullopt; + return; + } + + /// Sets the initial keyframes. This is often only needed for the first check. + if + (!leadingIndex.has_value() && + !trailingIndex.has_value()) + { + if (frame < keyframes[0].time) { + /// Time is before the first keyframe. Set it as the trailing. + trailingIndex = 0; + } else { + /// Time is after the first keyframe. Set the keyframe and the trailing. + leadingIndex = 0; + trailingIndex = 1; + } + } + + if + (trailingIndex.has_value() && + keyframes[trailingIndex.value()].time <= frame) + { + /// Time is after the current span. Iterate forward. + auto newLeading = trailingIndex.value(); + bool keyframeFound = false; + while (!keyframeFound) { + leadingIndex = newLeading; + if (newLeading + 1 >= 0 && newLeading + 1 < keyframes.size()) { + trailingIndex = newLeading + 1; + } else { + trailingIndex = std::nullopt; + } + + if (!trailingIndex.has_value()) { + /// We have reached the end of our keyframes. Time is after the last keyframe. + keyframeFound = true; + continue; + } + + if (frame < keyframes[trailingIndex.value()].time) { + /// Keyframe in current span. + keyframeFound = true; + continue; + } + /// Advance the array. + newLeading = trailingIndex.value(); + } + + } else if + (leadingIndex.has_value() && + frame < keyframes[leadingIndex.value()].time) + { + + /// Time is before the current span. Iterate backwards + auto newTrailing = leadingIndex.value(); + + bool keyframeFound = false; + while (!keyframeFound) { + if (newTrailing - 1 >= 0 && newTrailing - 1 < keyframes.size()) { + leadingIndex = newTrailing - 1; + } else { + leadingIndex = std::nullopt; + } + trailingIndex = newTrailing; + + if (!leadingIndex.has_value()) { + /// We have reached the end of our keyframes. Time is after the last keyframe. + keyframeFound = true; + continue; + } + if (keyframes[leadingIndex.value()].time <= frame) { + /// Keyframe in current span. + keyframeFound = true; + continue; + } + /// Step back + newTrailing = leadingIndex.value(); + } + } + if (const auto keyFrame = leadingIndex) { + leadingKeyframe = keyframes[keyFrame.value()]; + } else { + leadingKeyframe = std::nullopt; + } + + if (const auto keyFrame = trailingIndex) { + trailingKeyframe = keyframes[keyFrame.value()]; + } else { + trailingKeyframe = std::nullopt; + } + } +}; + +class BezierPathKeyframeInterpolator { +public: + BezierPathKeyframeInterpolator(std::vector> const &keyframes_) : + keyframes(keyframes_) { + assert(!keyframes.empty()); + } + +public: + std::vector> keyframes; + + void update(AnimationFrameTime frame, BezierPath &outPath) { + // First set the keyframe span for the frame. + updateSpanIndices(frame); + lastUpdatedFrame = frame; + // If only one keyframe return its value + + if (leadingKeyframe.has_value() && + trailingKeyframe.has_value()) + { + /// We have leading and trailing keyframe. + auto progress = leadingKeyframe->interpolatedProgress(trailingKeyframe.value(), frame); + interpolateInplace(leadingKeyframe.value(), trailingKeyframe.value(), progress, outPath); + } else if (leadingKeyframe.has_value()) { + setInplace(leadingKeyframe.value(), outPath); + } else if (trailingKeyframe.has_value()) { + setInplace(trailingKeyframe.value(), outPath); + } else { + /// Satisfy the compiler. + setInplace(keyframes[0], outPath); + } + } + + /// Returns true to trigger a frame update for this interpolator. + /// + /// An interpolator will be asked if it needs to update every frame. + /// If the interpolator needs updating it will be asked to compute its value for + /// the given frame. + /// + /// Cases a keyframe should not be updated: + /// - If time is in span and leading keyframe is hold + /// - If time is after the last keyframe. + /// - If time is before the first keyframe + /// + /// Cases for updating a keyframe: + /// - If time is in the span, and is not a hold + /// - If time is outside of the span, and there are more keyframes + /// - If a value delegate is set + /// - If leading and trailing are both nil. + bool hasUpdate(double frame) const { + if (!lastUpdatedFrame.has_value()) { + return true; + } + + if (leadingKeyframe.has_value() && + !trailingKeyframe.has_value() && + leadingKeyframe->time < frame) + { + /// Frame is after bounds of keyframes + return false; + } + if (trailingKeyframe.has_value() && + !leadingKeyframe.has_value() && + frame < trailingKeyframe->time) + { + /// Frame is before bounds of keyframes + return false; + } + if (leadingKeyframe.has_value() && + trailingKeyframe.has_value() && + leadingKeyframe->isHold && + leadingKeyframe->time < frame && + frame < trailingKeyframe->time) + { + return false; + } + return true; + } + + // MARK: Fileprivate + + std::optional lastUpdatedFrame; + + std::optional leadingIndex; + std::optional trailingIndex; + std::optional> leadingKeyframe; + std::optional> trailingKeyframe; + + /// Finds the appropriate Leading and Trailing keyframe index for the given time. + void updateSpanIndices(double frame) { + if (keyframes.empty()) { + leadingIndex = std::nullopt; + trailingIndex = std::nullopt; + leadingKeyframe = std::nullopt; + trailingKeyframe = std::nullopt; + return; + } + + // This function searches through the array to find the span of two keyframes + // that contain the current time. + // + // We could use Array.first(where:) but that would search through the entire array + // each frame. + // Instead we track the last used index and search either forwards or + // backwards from there. This reduces the iterations and complexity from + // + // O(n), where n is the length of the sequence to + // O(n), where n is the number of items after or before the last used index. + // + + if (keyframes.size() == 1) { + /// Only one keyframe. Set it as first and move on. + leadingIndex = 0; + trailingIndex = std::nullopt; + leadingKeyframe = keyframes[0]; + trailingKeyframe = std::nullopt; + return; + } + + /// Sets the initial keyframes. This is often only needed for the first check. + if + (!leadingIndex.has_value() && + !trailingIndex.has_value()) + { + if (frame < keyframes[0].time) { + /// Time is before the first keyframe. Set it as the trailing. + trailingIndex = 0; + } else { + /// Time is after the first keyframe. Set the keyframe and the trailing. + leadingIndex = 0; + trailingIndex = 1; + } + } + + if + (trailingIndex.has_value() && + keyframes[trailingIndex.value()].time <= frame) + { + /// Time is after the current span. Iterate forward. + auto newLeading = trailingIndex.value(); + bool keyframeFound = false; + while (!keyframeFound) { + leadingIndex = newLeading; + if (newLeading + 1 >= 0 && newLeading + 1 < keyframes.size()) { + trailingIndex = newLeading + 1; + } else { + trailingIndex = std::nullopt; + } + + if (!trailingIndex.has_value()) { + /// We have reached the end of our keyframes. Time is after the last keyframe. + keyframeFound = true; + continue; + } + + if (frame < keyframes[trailingIndex.value()].time) { + /// Keyframe in current span. + keyframeFound = true; + continue; + } + /// Advance the array. + newLeading = trailingIndex.value(); + } + + } else if + (leadingIndex.has_value() && + frame < keyframes[leadingIndex.value()].time) + { + + /// Time is before the current span. Iterate backwards + auto newTrailing = leadingIndex.value(); + + bool keyframeFound = false; + while (!keyframeFound) { + if (newTrailing - 1 >= 0 && newTrailing - 1 < keyframes.size()) { + leadingIndex = newTrailing - 1; + } else { + leadingIndex = std::nullopt; + } + trailingIndex = newTrailing; + + if (!leadingIndex.has_value()) { + /// We have reached the end of our keyframes. Time is after the last keyframe. + keyframeFound = true; + continue; + } + if (keyframes[leadingIndex.value()].time <= frame) { + /// Keyframe in current span. + keyframeFound = true; + continue; + } + /// Step back + newTrailing = leadingIndex.value(); + } + } + if (const auto keyFrame = leadingIndex) { + leadingKeyframe = keyframes[keyFrame.value()]; + } else { + leadingKeyframe = std::nullopt; + } + + if (const auto keyFrame = trailingIndex) { + trailingKeyframe = keyframes[keyFrame.value()]; + } else { + trailingKeyframe = std::nullopt; + } + } + +private: + void setInplace(Keyframe const &from, BezierPath &outPath) { + ValueInterpolator::setInplace(from.value, outPath); + } + + void interpolateInplace(Keyframe const &from, Keyframe const &to, double progress, BezierPath &outPath) { + std::optional spatialOutTangent2d; + if (from.spatialOutTangent) { + spatialOutTangent2d = Vector2D(from.spatialOutTangent->x, from.spatialOutTangent->y); + } + std::optional spatialInTangent2d; + if (to.spatialInTangent) { + spatialInTangent2d = Vector2D(to.spatialInTangent->x, to.spatialInTangent->y); + } + ValueInterpolator::interpolateInplace(from.value, to.value, progress, spatialOutTangent2d, spatialInTangent2d, outPath); + } +}; + +} + +#endif /* KeyframeInterpolator_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.swift new file mode 100644 index 0000000000..f008b96f03 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.swift @@ -0,0 +1,253 @@ +// +// KeyframeInterpolator.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/15/19. +// + +import CoreGraphics +import Foundation + +// MARK: - KeyframeInterpolator + +/// A value provider that produces a value at Time from a group of keyframes +final class KeyframeInterpolator: ValueProvider where ValueType: AnyInterpolatable { + + // MARK: Lifecycle + + init(keyframes: ContiguousArray>) { + self.keyframes = keyframes + } + + // MARK: Internal + + let keyframes: ContiguousArray> + + var valueType: Any.Type { + ValueType.self + } + + var storage: ValueProviderStorage { + .closure { [self] frame in + // First set the keyframe span for the frame. + updateSpanIndices(frame: frame) + lastUpdatedFrame = frame + // If only one keyframe return its value + let progress: CGFloat + let value: ValueType + + if + let leading = leadingKeyframe, + let trailing = trailingKeyframe + { + /// We have leading and trailing keyframe. + progress = leading.interpolatedProgress(trailing, keyTime: frame) + value = leading.interpolate(to: trailing, progress: progress) + } else if let leading = leadingKeyframe { + progress = 0 + value = leading.value + } else if let trailing = trailingKeyframe { + progress = 1 + value = trailing.value + } else { + /// Satisfy the compiler. + progress = 0 + value = keyframes[0].value + } + return value + } + } + + /// Returns true to trigger a frame update for this interpolator. + /// + /// An interpolator will be asked if it needs to update every frame. + /// If the interpolator needs updating it will be asked to compute its value for + /// the given frame. + /// + /// Cases a keyframe should not be updated: + /// - If time is in span and leading keyframe is hold + /// - If time is after the last keyframe. + /// - If time is before the first keyframe + /// + /// Cases for updating a keyframe: + /// - If time is in the span, and is not a hold + /// - If time is outside of the span, and there are more keyframes + /// - If a value delegate is set + /// - If leading and trailing are both nil. + func hasUpdate(frame: CGFloat) -> Bool { + if lastUpdatedFrame == nil { + return true + } + + if + let leading = leadingKeyframe, + trailingKeyframe == nil, + leading.time < frame + { + /// Frame is after bounds of keyframes + return false + } + if + let trailing = trailingKeyframe, + leadingKeyframe == nil, + frame < trailing.time + { + /// Frame is before bounds of keyframes + return false + } + if + let leading = leadingKeyframe, + let trailing = trailingKeyframe, + leading.isHold, + leading.time < frame, + frame < trailing.time + { + return false + } + return true + } + + // MARK: Fileprivate + + fileprivate var lastUpdatedFrame: CGFloat? + + fileprivate var leadingIndex: Int? = nil + fileprivate var trailingIndex: Int? = nil + fileprivate var leadingKeyframe: Keyframe? = nil + fileprivate var trailingKeyframe: Keyframe? = nil + + /// Finds the appropriate Leading and Trailing keyframe index for the given time. + fileprivate func updateSpanIndices(frame: CGFloat) { + guard keyframes.count > 0 else { + leadingIndex = nil + trailingIndex = nil + leadingKeyframe = nil + trailingKeyframe = nil + return + } + + // This function searches through the array to find the span of two keyframes + // that contain the current time. + // + // We could use Array.first(where:) but that would search through the entire array + // each frame. + // Instead we track the last used index and search either forwards or + // backwards from there. This reduces the iterations and complexity from + // + // O(n), where n is the length of the sequence to + // O(n), where n is the number of items after or before the last used index. + // + + if keyframes.count == 1 { + /// Only one keyframe. Set it as first and move on. + leadingIndex = 0 + trailingIndex = nil + leadingKeyframe = keyframes[0] + trailingKeyframe = nil + return + } + + /// Sets the initial keyframes. This is often only needed for the first check. + if + leadingIndex == nil && + trailingIndex == nil + { + if frame < keyframes[0].time { + /// Time is before the first keyframe. Set it as the trailing. + trailingIndex = 0 + } else { + /// Time is after the first keyframe. Set the keyframe and the trailing. + leadingIndex = 0 + trailingIndex = 1 + } + } + + if + let currentTrailing = trailingIndex, + keyframes[currentTrailing].time <= frame + { + /// Time is after the current span. Iterate forward. + var newLeading = currentTrailing + var keyframeFound = false + while !keyframeFound { + + leadingIndex = newLeading + trailingIndex = keyframes.validIndex(newLeading + 1) + + guard let trailing = trailingIndex else { + /// We have reached the end of our keyframes. Time is after the last keyframe. + keyframeFound = true + continue + } + if frame < keyframes[trailing].time { + /// Keyframe in current span. + keyframeFound = true + continue + } + /// Advance the array. + newLeading = trailing + } + + } else if + let currentLeading = leadingIndex, + frame < keyframes[currentLeading].time + { + + /// Time is before the current span. Iterate backwards + var newTrailing = currentLeading + + var keyframeFound = false + while !keyframeFound { + + leadingIndex = keyframes.validIndex(newTrailing - 1) + trailingIndex = newTrailing + + guard let leading = leadingIndex else { + /// We have reached the end of our keyframes. Time is after the last keyframe. + keyframeFound = true + continue + } + if keyframes[leading].time <= frame { + /// Keyframe in current span. + keyframeFound = true + continue + } + /// Step back + newTrailing = leading + } + } + if let keyFrame = leadingIndex { + leadingKeyframe = keyframes[keyFrame] + } else { + leadingKeyframe = nil + } + + if let keyFrame = trailingIndex { + trailingKeyframe = keyframes[keyFrame] + } else { + trailingKeyframe = nil + } + } +} + +extension Array { + + fileprivate func validIndex(_ index: Int) -> Int? { + if 0 <= index, index < endIndex { + return index + } + return nil + } + +} + +extension ContiguousArray { + + fileprivate func validIndex(_ index: Int) -> Int? { + if 0 <= index, index < endIndex { + return index + } + return nil + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/SingleValueProvider.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/SingleValueProvider.cpp new file mode 100644 index 0000000000..face321499 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/SingleValueProvider.cpp @@ -0,0 +1,5 @@ +#include "SingleValueProvider.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/SingleValueProvider.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/SingleValueProvider.hpp new file mode 100644 index 0000000000..73456a595a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/SingleValueProvider.hpp @@ -0,0 +1,40 @@ +#ifndef SingleValueProvider_hpp +#define SingleValueProvider_hpp + +#include "Lottie/Public/DynamicProperties/AnyValueProvider.hpp" + +namespace lottie { + +/// Returns a value for every frame. +template +class SingleValueProvider: public ValueProvider { +public: + SingleValueProvider(T const &value) : + _value(value) { + } + + void setValue(T const &value) { + _value = value; + _hasUpdate = true; + } + + virtual T value(AnimationFrameTime frame) override { + return _value; + } + + virtual AnyValue::Type valueType() const override { + return AnyValueType::type(); + } + + virtual bool hasUpdate(double frame) const override { + return _hasUpdate; + } + +private: + T _value; + bool _hasUpdate = true; +}; + +} + +#endif /* SingleValueProvider_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/SingleValueProvider.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/SingleValueProvider.swift new file mode 100644 index 0000000000..b4bff3aac3 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/SingleValueProvider.swift @@ -0,0 +1,43 @@ +// +// SingleValueProvider.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/30/19. +// + +import Foundation +import QuartzCore + +/// Returns a value for every frame. +final class SingleValueProvider: ValueProvider { + + // MARK: Lifecycle + + init(_ value: ValueType) { + self.value = value + } + + // MARK: Internal + + var value: ValueType { + didSet { + hasUpdate = true + } + } + + var storage: ValueProviderStorage { + .singleValue(value) + } + + var valueType: Any.Type { + ValueType.self + } + + func hasUpdate(frame _: CGFloat) -> Bool { + hasUpdate + } + + // MARK: Private + + private var hasUpdate = true +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/ModifierNodes/TrimPathNode.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/ModifierNodes/TrimPathNode.swift new file mode 100644 index 0000000000..1278e4e3af --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/ModifierNodes/TrimPathNode.swift @@ -0,0 +1,281 @@ +// +// TrimPathNode.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/23/19. +// + +import Foundation +import QuartzCore + +// MARK: - TrimPathProperties + +final class TrimPathProperties: NodePropertyMap, KeypathSearchable { + + // MARK: Lifecycle + + init(trim: Trim) { + keypathName = trim.name + start = NodeProperty(provider: KeyframeInterpolator(keyframes: trim.start.keyframes)) + end = NodeProperty(provider: KeyframeInterpolator(keyframes: trim.end.keyframes)) + offset = NodeProperty(provider: KeyframeInterpolator(keyframes: trim.offset.keyframes)) + type = trim.trimType + keypathProperties = [ + "Start" : start, + "End" : end, + "Offset" : offset, + ] + properties = Array(keypathProperties.values) + } + + // MARK: Internal + + let keypathProperties: [String: AnyNodeProperty] + let properties: [AnyNodeProperty] + let keypathName: String + + let start: NodeProperty + let end: NodeProperty + let offset: NodeProperty + let type: TrimType +} + +// MARK: - TrimPathNode + +final class TrimPathNode: AnimatorNode { + + // MARK: Lifecycle + + init(parentNode: AnimatorNode?, trim: Trim, upstreamPaths: [PathOutputNode]) { + outputNode = PassThroughOutputNode(parent: parentNode?.outputNode) + self.parentNode = parentNode + properties = TrimPathProperties(trim: trim) + self.upstreamPaths = upstreamPaths + } + + // MARK: Internal + + let properties: TrimPathProperties + + let parentNode: AnimatorNode? + let outputNode: NodeOutput + var hasLocalUpdates = false + var hasUpstreamUpdates = false + var lastUpdateFrame: CGFloat? = nil + var isEnabled = true + + // MARK: Animator Node + var propertyMap: NodePropertyMap & KeypathSearchable { + properties + } + + func forceUpstreamOutputUpdates() -> Bool { + hasLocalUpdates || hasUpstreamUpdates + } + + func rebuildOutputs(frame: CGFloat) { + /// Make sure there is a trim. + let startValue = properties.start.value.cgFloatValue * 0.01 + let endValue = properties.end.value.cgFloatValue * 0.01 + let start = min(startValue, endValue) + let end = max(startValue, endValue) + + let offset = properties.offset.value.cgFloatValue.truncatingRemainder(dividingBy: 360) / 360 + + /// No need to trim, it's a full path + if start == 0, end == 1 { + return + } + + /// All paths are empty. + if start == end { + for pathContainer in upstreamPaths { + pathContainer.removePaths(updateFrame: frame) + } + return + } + + if properties.type == .simultaneously { + /// Just trim each path + for pathContainer in upstreamPaths { + let pathObjects = pathContainer.removePaths(updateFrame: frame) + for path in pathObjects { + // We are treating each compount path as an individual path. Its subpaths are treated as a whole. + pathContainer.appendPath( + path.trim(fromPosition: start, toPosition: end, offset: offset, trimSimultaneously: false), + updateFrame: frame) + } + } + return + } + + /// Individual path trimming. + + /// Brace yourself for the below code. + + /// Normalize lengths with offset. + var startPosition = (start + offset).truncatingRemainder(dividingBy: 1) + var endPosition = (end + offset).truncatingRemainder(dividingBy: 1) + + if startPosition < 0 { + startPosition = 1 + startPosition + } + + if endPosition < 0 { + endPosition = 1 + endPosition + } + if startPosition == 1 { + startPosition = 0 + } + if endPosition == 0 { + endPosition = 1 + } + + /// First get the total length of all paths. + var totalLength: CGFloat = 0 + upstreamPaths.forEach({ totalLength = totalLength + $0.totalLength }) + + /// Now determine the start and end cut lengths + let startLength = startPosition * totalLength + let endLength = endPosition * totalLength + var pathStart: CGFloat = 0 + + /// Now loop through all path containers + for pathContainer in upstreamPaths { + + let pathEnd = pathStart + pathContainer.totalLength + + if + !startLength.isInRange(pathStart, pathEnd) && + endLength.isInRange(pathStart, pathEnd) + { + // pathStart|=======E----------------------|pathEnd + // Cut path components, removing after end. + + let pathCutLength = endLength - pathStart + let subpaths = pathContainer.removePaths(updateFrame: frame) + var subpathStart: CGFloat = 0 + for path in subpaths { + let subpathEnd = subpathStart + path.length + if pathCutLength < subpathEnd { + /// This is the subpath that needs to be cut. + let cutLength = pathCutLength - subpathStart + let newPath = path.trim(fromPosition: 0, toPosition: cutLength / path.length, offset: 0, trimSimultaneously: false) + pathContainer.appendPath(newPath, updateFrame: frame) + break + } else { + /// Add to container and move on + pathContainer.appendPath(path, updateFrame: frame) + } + if pathCutLength == subpathEnd { + /// Right on the end. The next subpath is not included. Break. + break + } + subpathStart = subpathEnd + } + + } else if + !endLength.isInRange(pathStart, pathEnd) && + startLength.isInRange(pathStart, pathEnd) + { + // pathStart|-------S======================|pathEnd + // + + // Cut path components, removing before beginning. + let pathCutLength = startLength - pathStart + // Clear paths from container + let subpaths = pathContainer.removePaths(updateFrame: frame) + var subpathStart: CGFloat = 0 + for path in subpaths { + let subpathEnd = subpathStart + path.length + + if subpathStart < pathCutLength, pathCutLength < subpathEnd { + /// This is the subpath that needs to be cut. + let cutLength = pathCutLength - subpathStart + let newPath = path.trim(fromPosition: cutLength / path.length, toPosition: 1, offset: 0, trimSimultaneously: false) + pathContainer.appendPath(newPath, updateFrame: frame) + } else if pathCutLength <= subpathStart { + pathContainer.appendPath(path, updateFrame: frame) + } + subpathStart = subpathEnd + } + } else if + endLength.isInRange(pathStart, pathEnd) && + startLength.isInRange(pathStart, pathEnd) + { + // pathStart|-------S============E---------|endLength + // pathStart|=====E----------------S=======|endLength + // trim from path beginning to endLength. + + // Cut path components, removing before beginnings. + let startCutLength = startLength - pathStart + let endCutLength = endLength - pathStart + // Clear paths from container + let subpaths = pathContainer.removePaths(updateFrame: frame) + var subpathStart: CGFloat = 0 + for path in subpaths { + + let subpathEnd = subpathStart + path.length + + if + !startCutLength.isInRange(subpathStart, subpathEnd) && + !endCutLength.isInRange(subpathStart, subpathEnd) + { + // The whole path is included. Add + // S|==============================|E + pathContainer.appendPath(path, updateFrame: frame) + + } else if + startCutLength.isInRange(subpathStart, subpathEnd) && + !endCutLength.isInRange(subpathStart, subpathEnd) + { + /// The start of the path needs to be trimmed + // |-------S======================|E + let cutLength = startCutLength - subpathStart + let newPath = path.trim(fromPosition: cutLength / path.length, toPosition: 1, offset: 0, trimSimultaneously: false) + pathContainer.appendPath(newPath, updateFrame: frame) + } else if + !startCutLength.isInRange(subpathStart, subpathEnd) && + endCutLength.isInRange(subpathStart, subpathEnd) + { + // S|=======E----------------------| + let cutLength = endCutLength - subpathStart + let newPath = path.trim(fromPosition: 0, toPosition: cutLength / path.length, offset: 0, trimSimultaneously: false) + pathContainer.appendPath(newPath, updateFrame: frame) + break + } else if + startCutLength.isInRange(subpathStart, subpathEnd) && + endCutLength.isInRange(subpathStart, subpathEnd) + { + // |-------S============E---------| + let cutFromLength = startCutLength - subpathStart + let cutToLength = endCutLength - subpathStart + let newPath = path.trim( + fromPosition: cutFromLength / path.length, + toPosition: cutToLength / path.length, + offset: 0, + trimSimultaneously: false) + pathContainer.appendPath(newPath, updateFrame: frame) + break + } + + subpathStart = subpathEnd + } + } else if + (endLength <= pathStart && pathEnd <= startLength) || + (startLength <= pathStart && endLength <= pathStart) || + (pathEnd <= startLength && pathEnd <= endLength) + { + /// The Path needs to be cleared + pathContainer.removePaths(updateFrame: frame) + } + + pathStart = pathEnd + } + + } + + // MARK: Fileprivate + + fileprivate let upstreamPaths: [PathOutputNode] +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/GroupOutputNode.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/GroupOutputNode.swift new file mode 100644 index 0000000000..dc2b692516 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/GroupOutputNode.swift @@ -0,0 +1,77 @@ +// +// TransformNodeOutput.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/30/19. +// + +import CoreGraphics +import Foundation +import QuartzCore + +class GroupOutputNode: NodeOutput { + + // MARK: Lifecycle + + init(parent: NodeOutput?, rootNode: NodeOutput?) { + self.parent = parent + self.rootNode = rootNode + } + + // MARK: Internal + + let parent: NodeOutput? + let rootNode: NodeOutput? + var isEnabled = true + + private(set) var outputPath: CGPath? = nil + private(set) var transform: CATransform3D = CATransform3DIdentity + + func setTransform(_ xform: CATransform3D, forFrame _: CGFloat) { + transform = xform + outputPath = nil + } + + func hasOutputUpdates(_ forFrame: CGFloat) -> Bool { + guard isEnabled else { + let upstreamUpdates = parent?.hasOutputUpdates(forFrame) ?? false + outputPath = parent?.outputPath + return upstreamUpdates + } + + let upstreamUpdates = parent?.hasOutputUpdates(forFrame) ?? false + if upstreamUpdates { + outputPath = nil + } + let rootUpdates = rootNode?.hasOutputUpdates(forFrame) ?? false + if rootUpdates { + outputPath = nil + } + + var localUpdates = false + if outputPath == nil { + localUpdates = true + + let newPath = CGMutablePath() + if let parentNode = parent, let parentPath = parentNode.outputPath { + /// First add parent path. + newPath.addPath(parentPath) + } + var xform = CATransform3DGetAffineTransform(transform) + if + let rootNode = rootNode, + let rootPath = rootNode.outputPath + { + if let xformedPath = rootPath.copy(using: &xform) { + /// Now add root path. Note root path is transformed. + newPath.addPath(xformedPath) + } + } + + outputPath = newPath + } + + return upstreamUpdates || localUpdates + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/PassThroughOutputNode.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/PassThroughOutputNode.hpp new file mode 100644 index 0000000000..c909c7b790 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/PassThroughOutputNode.hpp @@ -0,0 +1,70 @@ +#ifndef PassThroughOutputNode_hpp +#define PassThroughOutputNode_hpp + +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasRenderUpdates.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasUpdate.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp" + +namespace lottie { + +class PassThroughOutputNode: virtual public NodeOutput, virtual public HasRenderUpdates, virtual public HasUpdate { +public: + PassThroughOutputNode(std::shared_ptr parent) : + _parent(parent) { + } + + virtual std::shared_ptr parent() override { + return _parent; + } + + virtual bool isEnabled() const override { + return _isEnabled; + } + virtual void setIsEnabled(bool isEnabled) override { + _isEnabled = isEnabled; + } + + virtual bool hasUpdate() override { + return _hasUpdate; + } + void setHasUpdate(bool hasUpdate) { + _hasUpdate = hasUpdate; + } + + virtual std::shared_ptr outputPath() override { + if (_parent) { + return _parent->outputPath(); + } + return nullptr; + } + + virtual bool hasOutputUpdates(double forFrame) override { + /// Changes to this node do not affect downstream nodes. + bool parentUpdate = false; + if (_parent) { + parentUpdate = _parent->hasOutputUpdates(forFrame); + } + /// Changes to upstream nodes do, however, affect this nodes state. + _hasUpdate = _hasUpdate || parentUpdate; + return parentUpdate; + } + + virtual bool hasRenderUpdates(double forFrame) override { + /// Return true if there are upstream updates or if this node has updates + bool upstreamUpdates = false; + if (_parent) { + upstreamUpdates = _parent->hasOutputUpdates(forFrame); + } + _hasUpdate = _hasUpdate || upstreamUpdates; + return _hasUpdate; + } + +private: + std::shared_ptr _parent; + bool _hasUpdate = false; + bool _isEnabled = true; +}; + +} + +#endif /* PassThroughOutputNode_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/PassThroughOutputNode.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/PassThroughOutputNode.swift new file mode 100644 index 0000000000..6518d9c32a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/PassThroughOutputNode.swift @@ -0,0 +1,47 @@ +// +// PassThroughOutputNode.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/30/19. +// + +import CoreGraphics +import Foundation + +class PassThroughOutputNode: NodeOutput { + + // MARK: Lifecycle + + init(parent: NodeOutput?) { + self.parent = parent + } + + // MARK: Internal + + let parent: NodeOutput? + + var hasUpdate = false + var isEnabled = true + + var outputPath: CGPath? { + if let parent = parent { + return parent.outputPath + } + return nil + } + + func hasOutputUpdates(_ forFrame: CGFloat) -> Bool { + /// Changes to this node do not affect downstream nodes. + let parentUpdate = parent?.hasOutputUpdates(forFrame) ?? false + /// Changes to upstream nodes do, however, affect this nodes state. + hasUpdate = hasUpdate || parentUpdate + return parentUpdate + } + + func hasRenderUpdates(_ forFrame: CGFloat) -> Bool { + /// Return true if there are upstream updates or if this node has updates + let upstreamUpdates = parent?.hasOutputUpdates(forFrame) ?? false + hasUpdate = hasUpdate || upstreamUpdates + return hasUpdate + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/PathOutputNode.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/PathOutputNode.swift new file mode 100644 index 0000000000..74d153f4af --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/PathOutputNode.swift @@ -0,0 +1,90 @@ +// +// PathNodeOutput.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/30/19. +// + +import CoreGraphics +import Foundation + +/// A node that has an output of a BezierPath +class PathOutputNode: NodeOutput { + + // MARK: Lifecycle + + init(parent: NodeOutput?) { + self.parent = parent + } + + // MARK: Internal + + let parent: NodeOutput? + + fileprivate(set) var outputPath: CGPath? = nil + + var lastUpdateFrame: CGFloat? = nil + var lastPathBuildFrame: CGFloat? = nil + var isEnabled = true + fileprivate(set) var totalLength: CGFloat = 0 + fileprivate(set) var pathObjects: [CompoundBezierPath] = [] + + func hasOutputUpdates(_ forFrame: CGFloat) -> Bool { + guard isEnabled else { + let upstreamUpdates = parent?.hasOutputUpdates(forFrame) ?? false + outputPath = parent?.outputPath + return upstreamUpdates + } + + /// Ask if parent was updated + let upstreamUpdates = parent?.hasOutputUpdates(forFrame) ?? false + + /// If parent was updated and the path hasn't been built for this frame, clear the path. + if upstreamUpdates && lastPathBuildFrame != forFrame { + outputPath = nil + } + + if outputPath == nil { + /// If the path is clear, build the new path. + lastPathBuildFrame = forFrame + let newPath = CGMutablePath() + if let parentNode = parent, let parentPath = parentNode.outputPath { + newPath.addPath(parentPath) + } + for path in pathObjects { + for subPath in path.paths { + newPath.addPath(subPath.cgPath()) + } + } + outputPath = newPath + } + + /// Return true if there were upstream updates or if this node was updated. + return upstreamUpdates || (lastUpdateFrame == forFrame) + } + + @discardableResult + func removePaths(updateFrame: CGFloat?) -> [CompoundBezierPath] { + lastUpdateFrame = updateFrame + let returnPaths = pathObjects + outputPath = nil + totalLength = 0 + pathObjects = [] + return returnPaths + } + + func setPath(_ path: BezierPath, updateFrame: CGFloat) { + lastUpdateFrame = updateFrame + outputPath = nil + totalLength = path.length + pathObjects = [CompoundBezierPath(path: path)] + } + + func appendPath(_ path: CompoundBezierPath, updateFrame: CGFloat) { + lastUpdateFrame = updateFrame + outputPath = nil + totalLength = totalLength + path.length + pathObjects.append(path) + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/FillRenderer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/FillRenderer.swift new file mode 100644 index 0000000000..1631ea9dd0 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/FillRenderer.swift @@ -0,0 +1,71 @@ +// +// FillRenderer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/30/19. +// + +import CoreGraphics +import Foundation +import QuartzCore + +extension FillRule { + var cgFillRule: CGPathFillRule { + switch self { + case .evenOdd: + return .evenOdd + default: + return .winding + } + } + + var caFillRule: CAShapeLayerFillRule { + switch self { + case .evenOdd: + return CAShapeLayerFillRule.evenOdd + default: + return CAShapeLayerFillRule.nonZero + } + } +} + +// MARK: - FillRenderer + +/// A rendered for a Path Fill +final class FillRenderer: PassThroughOutputNode, Renderable { + var shouldRenderInContext = false + + var color: CGColor? { + didSet { + hasUpdate = true + } + } + + var opacity: CGFloat = 0 { + didSet { + hasUpdate = true + } + } + + var fillRule: FillRule = .none { + didSet { + hasUpdate = true + } + } + + func render(_: CGContext) { + // do nothing + } + + func setupSublayers(layer _: CAShapeLayer) { + // do nothing + } + + func updateShapeLayer(layer: CAShapeLayer) { + layer.fillColor = color + layer.opacity = Float(opacity) + layer.fillRule = fillRule.caFillRule + hasUpdate = false + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/GradientFillRenderer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/GradientFillRenderer.swift new file mode 100644 index 0000000000..1986c11a69 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/GradientFillRenderer.swift @@ -0,0 +1,311 @@ +// +// GradientFillRenderer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/30/19. +// + +import Foundation +import QuartzCore + +public var lottieSwift_getPathNativeBoundingBox: ((CGPath) -> CGRect)? + +// MARK: - GradientFillLayer + +extension CGPath { + var stringRepresentation: String { + var result = "" + + var indent = 1 + self.applyWithBlock { element in + let indentString = Array(repeating: " ", count: indent * 2).joined(separator: "") + switch element.pointee.type { + case .moveToPoint: + let point = element.pointee.points.advanced(by: 0).pointee + result += indentString + (NSString(format: "moveto (%10.15f, %10.15f)\n", point.x, point.y) as String) + indent += 1 + case .addLineToPoint: + let point = element.pointee.points.advanced(by: 0).pointee + result += indentString + (NSString(format: "lineto (%10.15f, %10.15f)\n", point.x, point.y) as String) + case .addCurveToPoint: + let cp1 = element.pointee.points.advanced(by: 0).pointee + let cp2 = element.pointee.points.advanced(by: 1).pointee + let point = element.pointee.points.advanced(by: 2).pointee + result += indentString + (NSString(format: "curveto (%10.15f, %10.15f) (%10.15f, %10.15f) (%10.15f, %10.15f)\n", cp1.x, cp1.y, cp2.x, cp2.y, point.x, point.y) as String) + case .addQuadCurveToPoint: + let cp = element.pointee.points.advanced(by: 0).pointee + let point = element.pointee.points.advanced(by: 1).pointee + result += indentString + (NSString(format: "quadcurveto (%10.15f, %10.15f) (%10.15f, %10.15f)\n", cp.x, cp.y, point.x, point.y) as String) + case .closeSubpath: + result += indentString + "closepath\n" + indent -= 1 + @unknown default: + break + } + } + + return result + } +} + +private final class GradientFillLayer: CALayer, LottieDrawingLayer { + + var start: CGPoint = .zero { + didSet { + setNeedsDisplay() + } + } + + var numberOfColors = 0 { + didSet { + setNeedsDisplay() + } + } + + var colors: [CGFloat] = [] { + didSet { + setNeedsDisplay() + } + } + + var end: CGPoint = .zero { + didSet { + setNeedsDisplay() + } + } + + var type: GradientType = .none { + didSet { + setNeedsDisplay() + } + } + + override func draw(in ctx: CGContext) { + var alphaValues = [CGFloat]() + var alphaLocations = [CGFloat]() + + var gradientColors = [Color]() + var colorLocations = [CGFloat]() + let colorSpace = ctx.colorSpace ?? CGColorSpaceCreateDeviceRGB() + for i in 0.. ix { + gradientColors.append(Color(r: colors[ix + 1], g: colors[ix + 2], b: colors[ix + 3], a: 1.0)) + colorLocations.append(colors[ix]) + } + } + + var drawMask = false + for i in stride(from: numberOfColors * 4, to: colors.endIndex, by: 2) { + let alpha = colors[i + 1] + if alpha < 1 { + drawMask = true + } + alphaLocations.append(colors[i]) + alphaValues.append(alpha) + } + + if drawMask { + var locations: [CGFloat] = [] + for i in 0 ..< min(gradientColors.count, colorLocations.count) { + if !locations.contains(colorLocations[i]) { + locations.append(colorLocations[i]) + } + } + for i in 0 ..< min(alphaValues.count, alphaLocations.count) { + if !locations.contains(alphaLocations[i]) { + locations.append(alphaLocations[i]) + } + } + + locations.sort() + if locations[0] != 0.0 { + locations.insert(0.0, at: 0) + } + if locations[locations.count - 1] != 1.0 { + locations.append(1.0) + } + + var colors: [Color] = [] + + for location in locations { + var color: Color? + for i in 0 ..< min(gradientColors.count, colorLocations.count) - 1 { + if location >= colorLocations[i] && location <= colorLocations[i + 1] { + let localLocation: Double + if colorLocations[i] != colorLocations[i + 1] { + localLocation = location.remap(fromLow: colorLocations[i], fromHigh: colorLocations[i + 1], toLow: 0.0, toHigh: 1.0) + } else { + localLocation = 0.0 + } + let fromColor = gradientColors[i] + let toColor = gradientColors[i + 1] + color = fromColor.interpolate(to: toColor, amount: localLocation) + + break + } + } + + var alpha: CGFloat? + for i in 0 ..< min(alphaValues.count, alphaLocations.count) - 1 { + if location >= alphaLocations[i] && location <= alphaLocations[i + 1] { + let localLocation: Double + if alphaLocations[i] != alphaLocations[i + 1] { + localLocation = location.remap(fromLow: alphaLocations[i], fromHigh: alphaLocations[i + 1], toLow: 0.0, toHigh: 1.0) + } else { + localLocation = 0.0 + } + let fromAlpha = alphaValues[i] + let toAlpha = alphaValues[i + 1] + alpha = fromAlpha.interpolate(to: toAlpha, amount: localLocation) + + break + } + } + + var resultColor = color ?? gradientColors[0] + resultColor.a = alpha ?? 1.0 + + /*resultColor.r = 1.0 + resultColor.g = 0.0 + resultColor.b = 0.0 + resultColor.a = 1.0*/ + + colors.append(resultColor) + } + + gradientColors = colors + colorLocations = locations + } + + let cgGradientColors: [CGColor] = gradientColors.map { color -> CGColor in + return color.cgColorValue(colorSpace: colorSpace) + } + + guard let gradient = CGGradient(colorsSpace: colorSpace, colors: cgGradientColors as CFArray, locations: colorLocations) + else { return } + if type == .linear { + ctx.drawLinearGradient(gradient, start: start, end: end, options: [.drawsAfterEndLocation, .drawsBeforeStartLocation]) + } else { + ctx.drawRadialGradient( + gradient, + startCenter: start, + startRadius: 0, + endCenter: start, + endRadius: start.distanceTo(end), + options: [.drawsAfterEndLocation, .drawsBeforeStartLocation]) + } + } + +} + +// MARK: - GradientFillRenderer + +/// A rendered for a Path Fill +final class GradientFillRenderer: PassThroughOutputNode, Renderable { + + // MARK: Lifecycle + + override init(parent: NodeOutput?) { + super.init(parent: parent) + + maskLayer.fillColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1, 1, 1, 1]) + gradientLayer.mask = maskLayer + + maskLayer.actions = [ + "startPoint" : NSNull(), + "endPoint" : NSNull(), + "opacity" : NSNull(), + "locations" : NSNull(), + "colors" : NSNull(), + "bounds" : NSNull(), + "anchorPoint" : NSNull(), + "isRadial" : NSNull(), + "path" : NSNull(), + ] + gradientLayer.actions = maskLayer.actions + } + + // MARK: Internal + + var shouldRenderInContext = false + + var start: CGPoint = .zero { + didSet { + hasUpdate = true + } + } + + var numberOfColors = 0 { + didSet { + hasUpdate = true + } + } + + var colors: [CGFloat] = [] { + didSet { + hasUpdate = true + } + } + + var end: CGPoint = .zero { + didSet { + hasUpdate = true + } + } + + var opacity: CGFloat = 0 { + didSet { + hasUpdate = true + } + } + + var type: GradientType = .none { + didSet { + hasUpdate = true + } + } + + func render(_: CGContext) { + // do nothing + } + + func setupSublayers(layer: CAShapeLayer) { + layer.addSublayer(gradientLayer) + layer.fillColor = nil + } + + func updateShapeLayer(layer: CAShapeLayer) { + hasUpdate = false + + guard let path = layer.path else { + return + } + + let frame = lottieSwift_getPathNativeBoundingBox!(path) + + let anchor = (frame.size.width.isZero || frame.size.height.isZero) ? CGPoint() : CGPoint( + x: -frame.origin.x / frame.size.width, + y: -frame.origin.y / frame.size.height) + maskLayer.path = path + maskLayer.bounds = frame + maskLayer.anchorPoint = anchor + + gradientLayer.bounds = maskLayer.bounds + gradientLayer.anchorPoint = anchor + + // setup gradient properties + gradientLayer.start = start + gradientLayer.end = end + gradientLayer.numberOfColors = numberOfColors + gradientLayer.colors = colors + gradientLayer.opacity = Float(opacity) + gradientLayer.type = type + } + + // MARK: Private + + private let gradientLayer = GradientFillLayer() + private let maskLayer = LottieCAShapeLayer() + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/GradientStrokeRenderer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/GradientStrokeRenderer.swift new file mode 100644 index 0000000000..526b452a37 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/GradientStrokeRenderer.swift @@ -0,0 +1,66 @@ +// +// GradientStrokeRenderer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/30/19. +// + +import Foundation +import QuartzCore + +// MARK: - Renderer + +final class GradientStrokeRenderer: PassThroughOutputNode, Renderable { + + // MARK: Lifecycle + + override init(parent: NodeOutput?) { + strokeRender = StrokeRenderer(parent: nil) + gradientRender = GradientFillRenderer(parent: nil) + strokeRender.color = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1, 1, 1, 1]) + super.init(parent: parent) + } + + // MARK: Internal + + var shouldRenderInContext = true + + let strokeRender: StrokeRenderer + let gradientRender: GradientFillRenderer + + override func hasOutputUpdates(_ forFrame: CGFloat) -> Bool { + let updates = super.hasOutputUpdates(forFrame) + return updates || strokeRender.hasUpdate || gradientRender.hasUpdate + } + + func updateShapeLayer(layer _: CAShapeLayer) { + /// Not Applicable + } + + func setupSublayers(layer _: CAShapeLayer) { + /// Not Applicable + } + + func render(_ inContext: CGContext) { + guard inContext.path != nil && inContext.path!.isEmpty == false else { + return + } + + strokeRender.hasUpdate = false + hasUpdate = false + gradientRender.hasUpdate = false + + strokeRender.setupForStroke(inContext) + + inContext.replacePathWithStrokedPath() + + /// Now draw the gradient. + gradientRender.render(inContext) + + } + + func renderBoundsFor(_ boundingBox: CGRect) -> CGRect { + strokeRender.renderBoundsFor(boundingBox) + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/LegacyGradientFillRenderer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/LegacyGradientFillRenderer.swift new file mode 100644 index 0000000000..64783d86b3 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/LegacyGradientFillRenderer.swift @@ -0,0 +1,153 @@ +// +// LegacyGradientFillRenderer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/30/19. +// + +import Foundation +import QuartzCore + +/// A rendered for a Path Fill +final class LegacyGradientFillRenderer: PassThroughOutputNode, Renderable { + + var shouldRenderInContext = true + + var start: CGPoint = .zero { + didSet { + hasUpdate = true + } + } + + var numberOfColors = 0 { + didSet { + hasUpdate = true + } + } + + var colors: [CGFloat] = [] { + didSet { + hasUpdate = true + } + } + + var end: CGPoint = .zero { + didSet { + hasUpdate = true + } + } + + var opacity: CGFloat = 0 { + didSet { + hasUpdate = true + } + } + + var type: GradientType = .none { + didSet { + hasUpdate = true + } + } + + func updateShapeLayer(layer _: CAShapeLayer) { + // Not applicable + } + + func setupSublayers(layer _: CAShapeLayer) { + // Not applicable + } + + func render(_ inContext: CGContext) { + guard inContext.path != nil && inContext.path!.isEmpty == false else { + return + } + hasUpdate = false + var alphaColors = [CGColor]() + var alphaLocations = [CGFloat]() + + var gradientColors = [CGColor]() + var colorLocations = [CGFloat]() + let colorSpace = CGColorSpaceCreateDeviceRGB() + let maskColorSpace = CGColorSpaceCreateDeviceGray() + for i in 0.. ix, let color = CGColor( + colorSpace: colorSpace, + components: [colors[ix + 1], colors[ix + 2], colors[ix + 3], 1]) + { + gradientColors.append(color) + colorLocations.append(colors[ix]) + } + } + + var drawMask = false + for i in stride(from: numberOfColors * 4, to: colors.endIndex, by: 2) { + let alpha = colors[i + 1] + if alpha < 1 { + drawMask = true + } + if let color = CGColor(colorSpace: maskColorSpace, components: [alpha, 1]) { + alphaLocations.append(colors[i]) + alphaColors.append(color) + } + } + + inContext.setAlpha(opacity) + inContext.clip() + + /// First draw a mask is necessary. + if drawMask { + guard + let maskGradient = CGGradient( + colorsSpace: maskColorSpace, + colors: alphaColors as CFArray, + locations: alphaLocations), + let maskContext = CGContext( + data: nil, + width: inContext.width, + height: inContext.height, + bitsPerComponent: 8, + bytesPerRow: inContext.width, + space: maskColorSpace, + bitmapInfo: 0) else { return } + let flipVertical = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: CGFloat(maskContext.height)) + maskContext.concatenate(flipVertical) + maskContext.concatenate(inContext.ctm) + if type == .linear { + maskContext.drawLinearGradient( + maskGradient, + start: start, + end: end, + options: [.drawsAfterEndLocation, .drawsBeforeStartLocation]) + } else { + maskContext.drawRadialGradient( + maskGradient, + startCenter: start, + startRadius: 0, + endCenter: start, + endRadius: start.distanceTo(end), + options: [.drawsAfterEndLocation, .drawsBeforeStartLocation]) + } + /// Clips the gradient + if let alphaMask = maskContext.makeImage() { + inContext.clip(to: inContext.boundingBoxOfClipPath, mask: alphaMask) + } + } + + /// Now draw the gradient + guard let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors as CFArray, locations: colorLocations) + else { return } + if type == .linear { + inContext.drawLinearGradient(gradient, start: start, end: end, options: [.drawsAfterEndLocation, .drawsBeforeStartLocation]) + } else { + inContext.drawRadialGradient( + gradient, + startCenter: start, + startRadius: 0, + endCenter: start, + endRadius: start.distanceTo(end), + options: [.drawsAfterEndLocation, .drawsBeforeStartLocation]) + } + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/StrokeRenderer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/StrokeRenderer.swift new file mode 100644 index 0000000000..77f152b50c --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/StrokeRenderer.swift @@ -0,0 +1,166 @@ +// +// StrokeRenderer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/30/19. +// + +import Foundation +import QuartzCore + +extension LineJoin { + var cgLineJoin: CGLineJoin { + switch self { + case .bevel: + return .bevel + case .none: + return .miter + case .miter: + return .miter + case .round: + return .round + } + } + + var caLineJoin: CAShapeLayerLineJoin { + switch self { + case .none: + return CAShapeLayerLineJoin.miter + case .miter: + return CAShapeLayerLineJoin.miter + case .round: + return CAShapeLayerLineJoin.round + case .bevel: + return CAShapeLayerLineJoin.bevel + } + } +} + +extension LineCap { + var cgLineCap: CGLineCap { + switch self { + case .none: + return .butt + case .butt: + return .butt + case .round: + return .round + case .square: + return .square + } + } + + var caLineCap: CAShapeLayerLineCap { + switch self { + case .none: + return CAShapeLayerLineCap.butt + case .butt: + return CAShapeLayerLineCap.butt + case .round: + return CAShapeLayerLineCap.round + case .square: + return CAShapeLayerLineCap.square + } + } +} + +// MARK: - StrokeRenderer + +/// A rendered that renders a stroke on a path. +final class StrokeRenderer: PassThroughOutputNode, Renderable { + + var shouldRenderInContext = false + + var color: CGColor? { + didSet { + hasUpdate = true + } + } + + var opacity: CGFloat = 0 { + didSet { + hasUpdate = true + } + } + + var width: CGFloat = 0 { + didSet { + hasUpdate = true + } + } + + var miterLimit: CGFloat = 0 { + didSet { + hasUpdate = true + } + } + + var lineCap: LineCap = .none { + didSet { + hasUpdate = true + } + } + + var lineJoin: LineJoin = .none { + didSet { + hasUpdate = true + } + } + + var dashPhase: CGFloat? { + didSet { + hasUpdate = true + } + } + + var dashLengths: [CGFloat]? { + didSet { + hasUpdate = true + } + } + + func setupSublayers(layer _: CAShapeLayer) { + // empty + } + + func renderBoundsFor(_ boundingBox: CGRect) -> CGRect { + boundingBox.insetBy(dx: -width, dy: -width) + } + + func setupForStroke(_ inContext: CGContext) { + inContext.setLineWidth(width) + inContext.setMiterLimit(miterLimit) + inContext.setLineCap(lineCap.cgLineCap) + inContext.setLineJoin(lineJoin.cgLineJoin) + if let dashPhase = dashPhase, let lengths = dashLengths { + inContext.setLineDash(phase: dashPhase, lengths: lengths) + } else { + inContext.setLineDash(phase: 0, lengths: []) + } + } + + func render(_ inContext: CGContext) { + guard inContext.path != nil && inContext.path!.isEmpty == false else { + return + } + guard let color = color else { return } + hasUpdate = false + setupForStroke(inContext) + inContext.setAlpha(opacity) + inContext.setStrokeColor(color) + inContext.strokePath() + } + + func updateShapeLayer(layer: CAShapeLayer) { + layer.strokeColor = color + layer.opacity = Float(opacity) + layer.lineWidth = width + layer.lineJoin = lineJoin.caLineJoin + layer.lineCap = lineCap.caLineCap + layer.lineDashPhase = dashPhase ?? 0 + layer.fillColor = nil + if let dashPattern = dashLengths { + layer.lineDashPattern = dashPattern.map({ NSNumber(value: Double($0)) }) + } + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/EllipseNode.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/EllipseNode.swift new file mode 100644 index 0000000000..89ffffbbb2 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/EllipseNode.swift @@ -0,0 +1,139 @@ +// +// EllipseNode.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/17/19. +// + +import Foundation +import QuartzCore + +// MARK: - EllipseNodeProperties + +final class EllipseNodeProperties: NodePropertyMap, KeypathSearchable { + + // MARK: Lifecycle + + init(ellipse: Ellipse) { + keypathName = ellipse.name + direction = ellipse.direction + position = NodeProperty(provider: KeyframeInterpolator(keyframes: ellipse.position.keyframes)) + size = NodeProperty(provider: KeyframeInterpolator(keyframes: ellipse.size.keyframes)) + keypathProperties = [ + "Position" : position, + "Size" : size, + ] + properties = Array(keypathProperties.values) + } + + // MARK: Internal + + var keypathName: String + + let direction: PathDirection + let position: NodeProperty + let size: NodeProperty + + let keypathProperties: [String: AnyNodeProperty] + let properties: [AnyNodeProperty] +} + +// MARK: - EllipseNode + +final class EllipseNode: AnimatorNode, PathNode { + + // MARK: Lifecycle + + init(parentNode: AnimatorNode?, ellipse: Ellipse) { + pathOutput = PathOutputNode(parent: parentNode?.outputNode) + properties = EllipseNodeProperties(ellipse: ellipse) + self.parentNode = parentNode + } + + // MARK: Internal + + static let ControlPointConstant: CGFloat = 0.55228 + + let pathOutput: PathOutputNode + + let properties: EllipseNodeProperties + + let parentNode: AnimatorNode? + var hasLocalUpdates = false + var hasUpstreamUpdates = false + var lastUpdateFrame: CGFloat? = nil + + // MARK: Animator Node + + var propertyMap: NodePropertyMap & KeypathSearchable { + properties + } + + var isEnabled = true { + didSet { + pathOutput.isEnabled = isEnabled + } + } + + func rebuildOutputs(frame: CGFloat) { + pathOutput.setPath( + .ellipse( + size: properties.size.value.sizeValue, + center: properties.position.value.pointValue, + direction: properties.direction), + updateFrame: frame) + } + +} + +extension BezierPath { + /// Constructs a `BezierPath` in the shape of an ellipse + static func ellipse( + size: CGSize, + center: CGPoint, + direction: PathDirection) + -> BezierPath + { + // Unfortunately we HAVE to manually build out the ellipse. + // Every Apple method constructs an ellipse from the 3 o-clock position + // After effects constructs from the Noon position. + // After effects does clockwise, but also has a flag for reversed. + var half = size * 0.5 + if direction == .counterClockwise { + half.width = half.width * -1 + } + + let q1 = CGPoint(x: center.x, y: center.y - half.height) + let q2 = CGPoint(x: center.x + half.width, y: center.y) + let q3 = CGPoint(x: center.x, y: center.y + half.height) + let q4 = CGPoint(x: center.x - half.width, y: center.y) + + let cp = half * EllipseNode.ControlPointConstant + + var path = BezierPath(startPoint: CurveVertex( + point: q1, + inTangentRelative: CGPoint(x: -cp.width, y: 0), + outTangentRelative: CGPoint(x: cp.width, y: 0))) + path.addVertex(CurveVertex( + point: q2, + inTangentRelative: CGPoint(x: 0, y: -cp.height), + outTangentRelative: CGPoint(x: 0, y: cp.height))) + + path.addVertex(CurveVertex( + point: q3, + inTangentRelative: CGPoint(x: cp.width, y: 0), + outTangentRelative: CGPoint(x: -cp.width, y: 0))) + + path.addVertex(CurveVertex( + point: q4, + inTangentRelative: CGPoint(x: 0, y: cp.height), + outTangentRelative: CGPoint(x: 0, y: -cp.height))) + + path.addVertex(CurveVertex( + point: q1, + inTangentRelative: CGPoint(x: -cp.width, y: 0), + outTangentRelative: CGPoint(x: cp.width, y: 0))) + path.close() + return path + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/PolygonNode.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/PolygonNode.swift new file mode 100644 index 0000000000..57af7df6ab --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/PolygonNode.swift @@ -0,0 +1,170 @@ +// +// PolygonNode.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/21/19. +// + +import Foundation +import QuartzCore + +// MARK: - PolygonNodeProperties + +final class PolygonNodeProperties: NodePropertyMap, KeypathSearchable { + + // MARK: Lifecycle + + init(star: Star) { + keypathName = star.name + direction = star.direction + position = NodeProperty(provider: KeyframeInterpolator(keyframes: star.position.keyframes)) + outerRadius = NodeProperty(provider: KeyframeInterpolator(keyframes: star.outerRadius.keyframes)) + outerRoundedness = NodeProperty(provider: KeyframeInterpolator(keyframes: star.outerRoundness.keyframes)) + rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: star.rotation.keyframes)) + points = NodeProperty(provider: KeyframeInterpolator(keyframes: star.points.keyframes)) + keypathProperties = [ + "Position" : position, + "Outer Radius" : outerRadius, + "Outer Roundedness" : outerRoundedness, + "Rotation" : rotation, + "Points" : points, + ] + properties = Array(keypathProperties.values) + } + + // MARK: Internal + + var keypathName: String + + var childKeypaths: [KeypathSearchable] = [] + + let keypathProperties: [String: AnyNodeProperty] + let properties: [AnyNodeProperty] + + let direction: PathDirection + let position: NodeProperty + let outerRadius: NodeProperty + let outerRoundedness: NodeProperty + let rotation: NodeProperty + let points: NodeProperty +} + +// MARK: - PolygonNode + +final class PolygonNode: AnimatorNode, PathNode { + + // MARK: Lifecycle + + init(parentNode: AnimatorNode?, star: Star) { + pathOutput = PathOutputNode(parent: parentNode?.outputNode) + properties = PolygonNodeProperties(star: star) + self.parentNode = parentNode + } + + // MARK: Internal + + /// Magic number needed for constructing path. + static let PolygonConstant: CGFloat = 0.25 + + let properties: PolygonNodeProperties + + let pathOutput: PathOutputNode + + let parentNode: AnimatorNode? + var hasLocalUpdates = false + var hasUpstreamUpdates = false + var lastUpdateFrame: CGFloat? = nil + + // MARK: Animator Node + + var propertyMap: NodePropertyMap & KeypathSearchable { + properties + } + + var isEnabled = true { + didSet { + pathOutput.isEnabled = isEnabled + } + } + + func rebuildOutputs(frame: CGFloat) { + let path = BezierPath.polygon( + position: properties.position.value.pointValue, + numberOfPoints: properties.points.value.cgFloatValue, + outerRadius: properties.outerRadius.value.cgFloatValue, + outerRoundedness: properties.outerRoundedness.value.cgFloatValue, + rotation: properties.rotation.value.cgFloatValue, + direction: properties.direction) + + pathOutput.setPath(path, updateFrame: frame) + } + +} + +extension BezierPath { + /// Creates a `BezierPath` in the shape of a polygon + static func polygon( + position: CGPoint, + numberOfPoints: CGFloat, + outerRadius: CGFloat, + outerRoundedness inputOuterRoundedness: CGFloat, + rotation: CGFloat, + direction: PathDirection) + -> BezierPath + { + var currentAngle = (rotation - 90).toRadians() + let anglePerPoint = ((2 * CGFloat.pi) / numberOfPoints) + let outerRoundedness = inputOuterRoundedness * 0.01 + + var point = CGPoint( + x: outerRadius * cos(currentAngle), + y: outerRadius * sin(currentAngle)) + var vertices = [CurveVertex(point: point + position, inTangentRelative: .zero, outTangentRelative: .zero)] + + var previousPoint = point + currentAngle += anglePerPoint; + for _ in 0.. + let size: NodeProperty + let cornerRadius: NodeProperty + +} + +// MARK: - RectangleNode + +final class RectangleNode: AnimatorNode, PathNode { + + // MARK: Lifecycle + + init(parentNode: AnimatorNode?, rectangle: Rectangle) { + properties = RectNodeProperties(rectangle: rectangle) + pathOutput = PathOutputNode(parent: parentNode?.outputNode) + self.parentNode = parentNode + } + + // MARK: Internal + + let properties: RectNodeProperties + + let pathOutput: PathOutputNode + let parentNode: AnimatorNode? + var hasLocalUpdates = false + var hasUpstreamUpdates = false + var lastUpdateFrame: CGFloat? = nil + + // MARK: Animator Node + + var propertyMap: NodePropertyMap & KeypathSearchable { + properties + } + + var isEnabled = true { + didSet { + pathOutput.isEnabled = isEnabled + } + } + + func rebuildOutputs(frame: CGFloat) { + pathOutput.setPath( + .rectangle( + position: properties.position.value.pointValue, + size: properties.size.value.sizeValue, + cornerRadius: properties.cornerRadius.value.cgFloatValue, + direction: properties.direction), + updateFrame: frame) + } + +} + +// MARK: - BezierPath + rectangle + +extension BezierPath { + /// Constructs a `BezierPath` in the shape of a rectangle, optionally with rounded corners + static func rectangle( + position: CGPoint, + size inputSize: CGSize, + cornerRadius: CGFloat, + direction: PathDirection) + -> BezierPath + { + let size = inputSize * 0.5 + let radius = min(min(cornerRadius, size.width) , size.height) + + var bezierPath = BezierPath() + let points: [CurveVertex] + + if radius <= 0 { + /// No Corners + points = [ + /// Lead In + CurveVertex( + point: CGPoint(x: size.width, y: -size.height), + inTangentRelative: .zero, + outTangentRelative: .zero) + .translated(position), + /// Corner 1 + CurveVertex( + point: CGPoint(x: size.width, y: size.height), + inTangentRelative: .zero, + outTangentRelative: .zero) + .translated(position), + /// Corner 2 + CurveVertex( + point: CGPoint(x: -size.width, y: size.height), + inTangentRelative: .zero, + outTangentRelative: .zero) + .translated(position), + /// Corner 3 + CurveVertex( + point: CGPoint(x: -size.width, y: -size.height), + inTangentRelative: .zero, + outTangentRelative: .zero) + .translated(position), + /// Corner 4 + CurveVertex( + point: CGPoint(x: size.width, y: -size.height), + inTangentRelative: .zero, + outTangentRelative: .zero) + .translated(position), + ] + } else { + let controlPoint = radius * EllipseNode.ControlPointConstant + points = [ + /// Lead In + CurveVertex( + CGPoint(x: radius, y: 0), + CGPoint(x: radius, y: 0), + CGPoint(x: radius, y: 0)) + .translated(CGPoint(x: -radius, y: radius)) + .translated(CGPoint(x: size.width, y: -size.height)) + .translated(position), + /// Corner 1 + CurveVertex( + CGPoint(x: radius, y: 0), // In tangent + CGPoint(x: radius, y: 0), // Point + CGPoint(x: radius, y: controlPoint)) + .translated(CGPoint(x: -radius, y: -radius)) + .translated(CGPoint(x: size.width, y: size.height)) + .translated(position), + CurveVertex( + CGPoint(x: controlPoint, y: radius), // In tangent + CGPoint(x: 0, y: radius), // Point + CGPoint(x: 0, y: radius)) // Out Tangent + .translated(CGPoint(x: -radius, y: -radius)) + .translated(CGPoint(x: size.width, y: size.height)) + .translated(position), + /// Corner 2 + CurveVertex( + CGPoint(x: 0, y: radius), // In tangent + CGPoint(x: 0, y: radius), // Point + CGPoint(x: -controlPoint, y: radius))// Out tangent + .translated(CGPoint(x: radius, y: -radius)) + .translated(CGPoint(x: -size.width, y: size.height)) + .translated(position), + CurveVertex( + CGPoint(x: -radius, y: controlPoint), // In tangent + CGPoint(x: -radius, y: 0), // Point + CGPoint(x: -radius, y: 0)) // Out tangent + .translated(CGPoint(x: radius, y: -radius)) + .translated(CGPoint(x: -size.width, y: size.height)) + .translated(position), + /// Corner 3 + CurveVertex( + CGPoint(x: -radius, y: 0), // In tangent + CGPoint(x: -radius, y: 0), // Point + CGPoint(x: -radius, y: -controlPoint)) // Out tangent + .translated(CGPoint(x: radius, y: radius)) + .translated(CGPoint(x: -size.width, y: -size.height)) + .translated(position), + CurveVertex( + CGPoint(x: -controlPoint, y: -radius), // In tangent + CGPoint(x: 0, y: -radius), // Point + CGPoint(x: 0, y: -radius)) // Out tangent + .translated(CGPoint(x: radius, y: radius)) + .translated(CGPoint(x: -size.width, y: -size.height)) + .translated(position), + /// Corner 4 + CurveVertex( + CGPoint(x: 0, y: -radius), // In tangent + CGPoint(x: 0, y: -radius), // Point + CGPoint(x: controlPoint, y: -radius)) // Out tangent + .translated(CGPoint(x: -radius, y: radius)) + .translated(CGPoint(x: size.width, y: -size.height)) + .translated(position), + CurveVertex( + CGPoint(x: radius, y: -controlPoint), // In tangent + CGPoint(x: radius, y: 0), // Point + CGPoint(x: radius, y: 0)) // Out tangent + .translated(CGPoint(x: -radius, y: radius)) + .translated(CGPoint(x: size.width, y: -size.height)) + .translated(position), + ] + } + let reversed = direction == .counterClockwise + let pathPoints = reversed ? points.reversed() : points + for point in pathPoints { + bezierPath.addVertex(reversed ? point.reversed() : point) + } + bezierPath.close() + return bezierPath + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/ShapeNode.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/ShapeNode.swift new file mode 100644 index 0000000000..7bc7d9055d --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/ShapeNode.swift @@ -0,0 +1,74 @@ +// +// PathNode.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/16/19. +// + +import CoreGraphics +import Foundation + +// MARK: - ShapeNodeProperties + +final class ShapeNodeProperties: NodePropertyMap, KeypathSearchable { + + // MARK: Lifecycle + + init(shape: Shape) { + keypathName = shape.name + path = NodeProperty(provider: KeyframeInterpolator(keyframes: shape.path.keyframes)) + keypathProperties = [ + "Path" : path, + ] + properties = Array(keypathProperties.values) + } + + // MARK: Internal + + var keypathName: String + + let path: NodeProperty + let keypathProperties: [String: AnyNodeProperty] + let properties: [AnyNodeProperty] + +} + +// MARK: - ShapeNode + +final class ShapeNode: AnimatorNode, PathNode { + + // MARK: Lifecycle + + init(parentNode: AnimatorNode?, shape: Shape) { + pathOutput = PathOutputNode(parent: parentNode?.outputNode) + properties = ShapeNodeProperties(shape: shape) + self.parentNode = parentNode + } + + // MARK: Internal + + let properties: ShapeNodeProperties + + let pathOutput: PathOutputNode + + let parentNode: AnimatorNode? + var hasLocalUpdates = false + var hasUpstreamUpdates = false + var lastUpdateFrame: CGFloat? = nil + + // MARK: Animator Node + var propertyMap: NodePropertyMap & KeypathSearchable { + properties + } + + var isEnabled = true { + didSet { + pathOutput.isEnabled = isEnabled + } + } + + func rebuildOutputs(frame: CGFloat) { + pathOutput.setPath(properties.path.value, updateFrame: frame) + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/StarNode.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/StarNode.swift new file mode 100644 index 0000000000..11ef751d6d --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/StarNode.swift @@ -0,0 +1,222 @@ +// +// StarNode.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/21/19. +// + +import Foundation +import QuartzCore + +// MARK: - StarNodeProperties + +final class StarNodeProperties: NodePropertyMap, KeypathSearchable { + + // MARK: Lifecycle + + init(star: Star) { + keypathName = star.name + direction = star.direction + position = NodeProperty(provider: KeyframeInterpolator(keyframes: star.position.keyframes)) + outerRadius = NodeProperty(provider: KeyframeInterpolator(keyframes: star.outerRadius.keyframes)) + outerRoundedness = NodeProperty(provider: KeyframeInterpolator(keyframes: star.outerRoundness.keyframes)) + if let innerRadiusKeyframes = star.innerRadius?.keyframes { + innerRadius = NodeProperty(provider: KeyframeInterpolator(keyframes: innerRadiusKeyframes)) + } else { + innerRadius = NodeProperty(provider: SingleValueProvider(Vector1D(0))) + } + if let innderRoundedness = star.innerRoundness?.keyframes { + innerRoundedness = NodeProperty(provider: KeyframeInterpolator(keyframes: innderRoundedness)) + } else { + innerRoundedness = NodeProperty(provider: SingleValueProvider(Vector1D(0))) + } + rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: star.rotation.keyframes)) + points = NodeProperty(provider: KeyframeInterpolator(keyframes: star.points.keyframes)) + keypathProperties = [ + "Position" : position, + "Outer Radius" : outerRadius, + "Outer Roundedness" : outerRoundedness, + "Inner Radius" : innerRadius, + "Inner Roundedness" : innerRoundedness, + "Rotation" : rotation, + "Points" : points, + ] + properties = Array(keypathProperties.values) + } + + // MARK: Internal + + var keypathName: String + + let keypathProperties: [String: AnyNodeProperty] + let properties: [AnyNodeProperty] + + let direction: PathDirection + let position: NodeProperty + let outerRadius: NodeProperty + let outerRoundedness: NodeProperty + let innerRadius: NodeProperty + let innerRoundedness: NodeProperty + let rotation: NodeProperty + let points: NodeProperty +} + +// MARK: - StarNode + +final class StarNode: AnimatorNode, PathNode { + + // MARK: Lifecycle + + init(parentNode: AnimatorNode?, star: Star) { + pathOutput = PathOutputNode(parent: parentNode?.outputNode) + properties = StarNodeProperties(star: star) + self.parentNode = parentNode + } + + // MARK: Internal + + /// Magic number needed for building path data + static let PolystarConstant: CGFloat = 0.47829 + + let properties: StarNodeProperties + + let pathOutput: PathOutputNode + + let parentNode: AnimatorNode? + var hasLocalUpdates = false + var hasUpstreamUpdates = false + var lastUpdateFrame: CGFloat? = nil + + // MARK: Animator Node + var propertyMap: NodePropertyMap & KeypathSearchable { + properties + } + + var isEnabled = true { + didSet { + pathOutput.isEnabled = isEnabled + } + } + + func rebuildOutputs(frame: CGFloat) { + let path = BezierPath.star( + position: properties.position.value.pointValue, + outerRadius: properties.outerRadius.value.cgFloatValue, + innerRadius: properties.innerRadius.value.cgFloatValue, + outerRoundedness: properties.outerRoundedness.value.cgFloatValue, + innerRoundedness: properties.innerRoundedness.value.cgFloatValue, + numberOfPoints: properties.points.value.cgFloatValue, + rotation: properties.rotation.value.cgFloatValue, + direction: properties.direction) + + pathOutput.setPath(path, updateFrame: frame) + } + +} + +extension BezierPath { + /// Constructs a `BezierPath` in the shape of a star + static func star( + position: CGPoint, + outerRadius: CGFloat, + innerRadius: CGFloat, + outerRoundedness inoutOuterRoundedness: CGFloat, + innerRoundedness inputInnerRoundedness: CGFloat, + numberOfPoints: CGFloat, + rotation: CGFloat, + direction: PathDirection) + -> BezierPath + { + var currentAngle = (rotation - 90).toRadians() + let anglePerPoint = (2 * CGFloat.pi) / numberOfPoints + let halfAnglePerPoint = anglePerPoint / 2.0 + let partialPointAmount = numberOfPoints - floor(numberOfPoints) + let outerRoundedness = inoutOuterRoundedness * 0.01 + let innerRoundedness = inputInnerRoundedness * 0.01 + + var point: CGPoint = .zero + + var partialPointRadius: CGFloat = 0 + if partialPointAmount != 0 { + currentAngle += halfAnglePerPoint * (1 - partialPointAmount) + partialPointRadius = innerRadius + partialPointAmount * (outerRadius - innerRadius) + point.x = (partialPointRadius * cos(currentAngle)) + point.y = (partialPointRadius * sin(currentAngle)) + currentAngle += anglePerPoint * partialPointAmount / 2 + } else { + point.x = (outerRadius * cos(currentAngle)) + point.y = (outerRadius * sin(currentAngle)) + currentAngle += halfAnglePerPoint + } + + var vertices = [CurveVertex]() + vertices.append(CurveVertex(point: point + position, inTangentRelative: .zero, outTangentRelative: .zero)) + + var previousPoint = point + var longSegment = false + let numPoints = Int(ceil(numberOfPoints) * 2) + for i in 0.. + let position: NodeProperty + let scale: NodeProperty + let rotation: NodeProperty + let opacity: NodeProperty + let skew: NodeProperty + let skewAxis: NodeProperty + + var caTransform: CATransform3D { + let result = CATransform3D.makeTransform( + anchor: anchor.value.pointValue, + position: position.value.pointValue, + scale: scale.value.sizeValue, + rotation: rotation.value.cgFloatValue, + skew: skew.value.cgFloatValue, + skewAxis: skewAxis.value.cgFloatValue) + return result + } +} + +// MARK: - GroupNode + +final class GroupNode: AnimatorNode { + + // MARK: Lifecycle + + // MARK: Initializer + init(name: String, parentNode: AnimatorNode?, tree: NodeTree) { + self.parentNode = parentNode + keypathName = name + rootNode = tree.rootNode + properties = GroupNodeProperties(transform: tree.transform) + groupOutput = GroupOutputNode(parent: parentNode?.outputNode, rootNode: rootNode?.outputNode) + var childKeypaths: [KeypathSearchable] = tree.childrenNodes + childKeypaths.append(properties) + self.childKeypaths = childKeypaths + + for childContainer in tree.renderContainers { + container.insertRenderLayer(childContainer) + } + } + + // MARK: Internal + + // MARK: Properties + let groupOutput: GroupOutputNode + + let properties: GroupNodeProperties + + let rootNode: AnimatorNode? + + var container = ShapeContainerLayer() + + // MARK: Keypath Searchable + + let keypathName: String + + let childKeypaths: [KeypathSearchable] + + let parentNode: AnimatorNode? + var hasLocalUpdates = false + var hasUpstreamUpdates = false + var lastUpdateFrame: CGFloat? = nil + + var keypathLayer: CALayer? { + container + } + + // MARK: Animator Node Protocol + + var propertyMap: NodePropertyMap & KeypathSearchable { + properties + } + + var outputNode: NodeOutput { + groupOutput + } + + var isEnabled = true { + didSet { + container.isHidden = !isEnabled + } + } + + func performAdditionalLocalUpdates(frame: CGFloat, forceLocalUpdate: Bool) -> Bool { + rootNode?.updateContents(frame, forceLocalUpdate: forceLocalUpdate) ?? false + } + + func performAdditionalOutputUpdates(_ frame: CGFloat, forceOutputUpdate: Bool) { + rootNode?.updateOutputs(frame, forceOutputUpdate: forceOutputUpdate) + } + + func rebuildOutputs(frame: CGFloat) { + container.opacity = Float(properties.opacity.value.cgFloatValue) * 0.01 + container.transform = properties.caTransform + groupOutput.setTransform(container.transform, forFrame: frame) + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/FillNode.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/FillNode.swift new file mode 100644 index 0000000000..bb00759175 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/FillNode.swift @@ -0,0 +1,90 @@ +// +// FillNode.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/17/19. +// + +import CoreGraphics +import Foundation + +// MARK: - FillNodeProperties + +final class FillNodeProperties: NodePropertyMap, KeypathSearchable { + + // MARK: Lifecycle + + init(fill: Fill) { + keypathName = fill.name + color = NodeProperty(provider: KeyframeInterpolator(keyframes: fill.color.keyframes)) + opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: fill.opacity.keyframes)) + type = fill.fillRule + keypathProperties = [ + "Opacity" : opacity, + PropertyName.color.rawValue : color, + ] + properties = Array(keypathProperties.values) + } + + // MARK: Internal + + var keypathName: String + + let opacity: NodeProperty + let color: NodeProperty + let type: FillRule + + let keypathProperties: [String: AnyNodeProperty] + let properties: [AnyNodeProperty] + +} + +// MARK: - FillNode + +final class FillNode: AnimatorNode, RenderNode { + + // MARK: Lifecycle + + init(parentNode: AnimatorNode?, fill: Fill) { + fillRender = FillRenderer(parent: parentNode?.outputNode) + fillProperties = FillNodeProperties(fill: fill) + self.parentNode = parentNode + } + + // MARK: Internal + + let fillRender: FillRenderer + + let fillProperties: FillNodeProperties + + let parentNode: AnimatorNode? + var hasLocalUpdates = false + var hasUpstreamUpdates = false + var lastUpdateFrame: CGFloat? = nil + + var renderer: NodeOutput & Renderable { + fillRender + } + + // MARK: Animator Node Protocol + + var propertyMap: NodePropertyMap & KeypathSearchable { + fillProperties + } + + var isEnabled = true { + didSet { + fillRender.isEnabled = isEnabled + } + } + + func localUpdatesPermeateDownstream() -> Bool { + false + } + + func rebuildOutputs(frame _: CGFloat) { + fillRender.color = fillProperties.color.value.cgColorValue + fillRender.opacity = fillProperties.opacity.value.cgFloatValue * 0.01 + fillRender.fillRule = fillProperties.type + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientFillNode.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientFillNode.swift new file mode 100644 index 0000000000..be05c07408 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientFillNode.swift @@ -0,0 +1,102 @@ +// +// GradientFillNode.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/22/19. +// + +import Foundation +import QuartzCore + +// MARK: - GradientFillProperties + +final class GradientFillProperties: NodePropertyMap, KeypathSearchable { + + // MARK: Lifecycle + + init(gradientfill: GradientFill) { + keypathName = gradientfill.name + opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientfill.opacity.keyframes)) + startPoint = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientfill.startPoint.keyframes)) + endPoint = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientfill.endPoint.keyframes)) + colors = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientfill.colors.keyframes)) + gradientType = gradientfill.gradientType + numberOfColors = gradientfill.numberOfColors + keypathProperties = [ + "Opacity" : opacity, + "Start Point" : startPoint, + "End Point" : endPoint, + "Colors" : colors, + ] + properties = Array(keypathProperties.values) + } + + // MARK: Internal + + var keypathName: String + + let opacity: NodeProperty + let startPoint: NodeProperty + let endPoint: NodeProperty + let colors: NodeProperty<[Double]> + + let gradientType: GradientType + let numberOfColors: Int + + let keypathProperties: [String: AnyNodeProperty] + let properties: [AnyNodeProperty] + +} + +// MARK: - GradientFillNode + +final class GradientFillNode: AnimatorNode, RenderNode { + + // MARK: Lifecycle + + init(parentNode: AnimatorNode?, gradientFill: GradientFill) { + fillRender = GradientFillRenderer(parent: parentNode?.outputNode) + fillProperties = GradientFillProperties(gradientfill: gradientFill) + self.parentNode = parentNode + } + + // MARK: Internal + + let fillRender: GradientFillRenderer + + let fillProperties: GradientFillProperties + + let parentNode: AnimatorNode? + var hasLocalUpdates = false + var hasUpstreamUpdates = false + var lastUpdateFrame: CGFloat? = nil + + var renderer: NodeOutput & Renderable { + fillRender + } + + // MARK: Animator Node Protocol + + var propertyMap: NodePropertyMap & KeypathSearchable { + fillProperties + } + + var isEnabled = true { + didSet { + fillRender.isEnabled = isEnabled + } + } + + func localUpdatesPermeateDownstream() -> Bool { + false + } + + func rebuildOutputs(frame _: CGFloat) { + fillRender.start = fillProperties.startPoint.value.pointValue + fillRender.end = fillProperties.endPoint.value.pointValue + fillRender.opacity = fillProperties.opacity.value.cgFloatValue * 0.01 + fillRender.colors = fillProperties.colors.value.map { CGFloat($0) } + fillRender.type = fillProperties.gradientType + fillRender.numberOfColors = fillProperties.numberOfColors + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientStrokeNode.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientStrokeNode.swift new file mode 100644 index 0000000000..08ac147767 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientStrokeNode.swift @@ -0,0 +1,151 @@ +// +// GradientStrokeNode.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/23/19. +// + +import CoreGraphics +import Foundation + +// MARK: - GradientStrokeProperties + +final class GradientStrokeProperties: NodePropertyMap, KeypathSearchable { + + // MARK: Lifecycle + + init(gradientStroke: GradientStroke) { + keypathName = gradientStroke.name + opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientStroke.opacity.keyframes)) + startPoint = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientStroke.startPoint.keyframes)) + endPoint = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientStroke.endPoint.keyframes)) + colors = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientStroke.colors.keyframes)) + gradientType = gradientStroke.gradientType + numberOfColors = gradientStroke.numberOfColors + width = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientStroke.width.keyframes)) + miterLimit = CGFloat(gradientStroke.miterLimit) + lineCap = gradientStroke.lineCap + lineJoin = gradientStroke.lineJoin + + if let dashes = gradientStroke.dashPattern { + var dashPatterns = ContiguousArray>>() + var dashPhase = ContiguousArray>() + for dash in dashes { + if dash.type == .offset { + dashPhase = dash.value.keyframes + } else { + dashPatterns.append(dash.value.keyframes) + } + } + dashPattern = NodeProperty(provider: GroupInterpolator(keyframeGroups: dashPatterns)) + self.dashPhase = NodeProperty(provider: KeyframeInterpolator(keyframes: dashPhase)) + } else { + dashPattern = NodeProperty(provider: SingleValueProvider([Vector1D]())) + dashPhase = NodeProperty(provider: SingleValueProvider(Vector1D(0))) + } + keypathProperties = [ + "Opacity" : opacity, + "Start Point" : startPoint, + "End Point" : endPoint, + "Colors" : colors, + "Stroke Width" : width, + "Dashes" : dashPattern, + "Dash Phase" : dashPhase, + ] + properties = Array(keypathProperties.values) + } + + // MARK: Internal + + var keypathName: String + + let opacity: NodeProperty + let startPoint: NodeProperty + let endPoint: NodeProperty + let colors: NodeProperty<[Double]> + let width: NodeProperty + + let dashPattern: NodeProperty<[Vector1D]> + let dashPhase: NodeProperty + + let lineCap: LineCap + let lineJoin: LineJoin + let miterLimit: CGFloat + let gradientType: GradientType + let numberOfColors: Int + + let keypathProperties: [String: AnyNodeProperty] + let properties: [AnyNodeProperty] + +} + +// MARK: - GradientStrokeNode + +final class GradientStrokeNode: AnimatorNode, RenderNode { + + // MARK: Lifecycle + + init(parentNode: AnimatorNode?, gradientStroke: GradientStroke) { + strokeRender = GradientStrokeRenderer(parent: parentNode?.outputNode) + strokeProperties = GradientStrokeProperties(gradientStroke: gradientStroke) + self.parentNode = parentNode + } + + // MARK: Internal + + let strokeRender: GradientStrokeRenderer + + let strokeProperties: GradientStrokeProperties + + let parentNode: AnimatorNode? + var hasLocalUpdates = false + var hasUpstreamUpdates = false + var lastUpdateFrame: CGFloat? = nil + + var renderer: NodeOutput & Renderable { + strokeRender + } + + // MARK: Animator Node Protocol + + var propertyMap: NodePropertyMap & KeypathSearchable { + strokeProperties + } + + var isEnabled = true { + didSet { + strokeRender.isEnabled = isEnabled + } + } + + func localUpdatesPermeateDownstream() -> Bool { + false + } + + func rebuildOutputs(frame _: CGFloat) { + /// Update gradient properties + strokeRender.gradientRender.start = strokeProperties.startPoint.value.pointValue + strokeRender.gradientRender.end = strokeProperties.endPoint.value.pointValue + strokeRender.gradientRender.opacity = strokeProperties.opacity.value.cgFloatValue + strokeRender.gradientRender.colors = strokeProperties.colors.value.map { CGFloat($0) } + strokeRender.gradientRender.type = strokeProperties.gradientType + strokeRender.gradientRender.numberOfColors = strokeProperties.numberOfColors + + /// Now update stroke properties + strokeRender.strokeRender.opacity = strokeProperties.opacity.value.cgFloatValue + strokeRender.strokeRender.width = strokeProperties.width.value.cgFloatValue + strokeRender.strokeRender.miterLimit = strokeProperties.miterLimit + strokeRender.strokeRender.lineCap = strokeProperties.lineCap + strokeRender.strokeRender.lineJoin = strokeProperties.lineJoin + + /// Get dash lengths + let dashLengths = strokeProperties.dashPattern.value.map { $0.cgFloatValue } + if dashLengths.count > 0 { + strokeRender.strokeRender.dashPhase = strokeProperties.dashPhase.value.cgFloatValue + strokeRender.strokeRender.dashLengths = dashLengths + } else { + strokeRender.strokeRender.dashLengths = nil + strokeRender.strokeRender.dashPhase = nil + } + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.hpp new file mode 100644 index 0000000000..731a81149c --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.hpp @@ -0,0 +1,36 @@ +#ifndef StrokeNode_hpp +#define StrokeNode_hpp + +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.hpp" +#include "Lottie/Private/Model/ShapeItems/Stroke.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/SingleValueProvider.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/RenderNode.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/DashPatternInterpolator.hpp" + +namespace lottie { + +class StrokeShapeDashConfiguration { +public: + StrokeShapeDashConfiguration(std::vector const &elements) { + /// Converts the `[DashElement]` data model into `lineDashPattern` and `lineDashPhase` + /// representations usable in a `CAShapeLayer` + for (const auto &dash : elements) { + if (dash.type == DashElementType::Offset) { + dashPhase = dash.value.keyframes; + } else { + dashPatterns.push_back(dash.value.keyframes); + } + } + } + +public: + std::vector>> dashPatterns; + std::vector> dashPhase; +}; + +} + +#endif /* StrokeNode_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.swift new file mode 100644 index 0000000000..387dfafffb --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.swift @@ -0,0 +1,153 @@ +// +// StrokeNode.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/22/19. +// + +import Foundation +import QuartzCore + +// MARK: - StrokeNodeProperties + +final class StrokeNodeProperties: NodePropertyMap, KeypathSearchable { + + // MARK: Lifecycle + + init(stroke: Stroke) { + keypathName = stroke.name + color = NodeProperty(provider: KeyframeInterpolator(keyframes: stroke.color.keyframes)) + opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: stroke.opacity.keyframes)) + width = NodeProperty(provider: KeyframeInterpolator(keyframes: stroke.width.keyframes)) + miterLimit = CGFloat(stroke.miterLimit) + lineCap = stroke.lineCap + lineJoin = stroke.lineJoin + + if let dashes = stroke.dashPattern { + let (dashPatterns, dashPhase) = dashes.shapeLayerConfiguration + dashPattern = NodeProperty(provider: GroupInterpolator(keyframeGroups: dashPatterns)) + if dashPhase.count == 0 { + self.dashPhase = NodeProperty(provider: SingleValueProvider(Vector1D(0))) + } else { + self.dashPhase = NodeProperty(provider: KeyframeInterpolator(keyframes: dashPhase)) + } + } else { + dashPattern = NodeProperty(provider: SingleValueProvider([Vector1D]())) + dashPhase = NodeProperty(provider: SingleValueProvider(Vector1D(0))) + } + keypathProperties = [ + "Opacity" : opacity, + PropertyName.color.rawValue : color, + "Stroke Width" : width, + "Dashes" : dashPattern, + "Dash Phase" : dashPhase, + ] + properties = Array(keypathProperties.values) + } + + // MARK: Internal + + let keypathName: String + let keypathProperties: [String: AnyNodeProperty] + let properties: [AnyNodeProperty] + + let opacity: NodeProperty + let color: NodeProperty + let width: NodeProperty + + let dashPattern: NodeProperty<[Vector1D]> + let dashPhase: NodeProperty + + let lineCap: LineCap + let lineJoin: LineJoin + let miterLimit: CGFloat + +} + +// MARK: - StrokeNode + +/// Node that manages stroking a path +final class StrokeNode: AnimatorNode, RenderNode { + + // MARK: Lifecycle + + init(parentNode: AnimatorNode?, stroke: Stroke) { + strokeRender = StrokeRenderer(parent: parentNode?.outputNode) + strokeProperties = StrokeNodeProperties(stroke: stroke) + self.parentNode = parentNode + } + + // MARK: Internal + + let strokeRender: StrokeRenderer + + let strokeProperties: StrokeNodeProperties + + let parentNode: AnimatorNode? + var hasLocalUpdates = false + var hasUpstreamUpdates = false + var lastUpdateFrame: CGFloat? = nil + + var renderer: NodeOutput & Renderable { + strokeRender + } + + // MARK: Animator Node Protocol + + var propertyMap: NodePropertyMap & KeypathSearchable { + strokeProperties + } + + var isEnabled = true { + didSet { + strokeRender.isEnabled = isEnabled + } + } + + func localUpdatesPermeateDownstream() -> Bool { + false + } + + func rebuildOutputs(frame _: CGFloat) { + strokeRender.color = strokeProperties.color.value.cgColorValue + strokeRender.opacity = strokeProperties.opacity.value.cgFloatValue * 0.01 + strokeRender.width = strokeProperties.width.value.cgFloatValue + strokeRender.miterLimit = strokeProperties.miterLimit + strokeRender.lineCap = strokeProperties.lineCap + strokeRender.lineJoin = strokeProperties.lineJoin + + /// Get dash lengths + let dashLengths = strokeProperties.dashPattern.value.map { $0.cgFloatValue } + if dashLengths.count > 0 { + strokeRender.dashPhase = strokeProperties.dashPhase.value.cgFloatValue + strokeRender.dashLengths = dashLengths + } else { + strokeRender.dashLengths = nil + strokeRender.dashPhase = nil + } + } + +} + +// MARK: - [DashElement] + shapeLayerConfiguration + +extension Array where Element == DashElement { + typealias ShapeLayerConfiguration = ( + dashPatterns: ContiguousArray>>, + dashPhase: ContiguousArray>) + + /// Converts the `[DashElement]` data model into `lineDashPattern` and `lineDashPhase` + /// representations usable in a `CAShapeLayer` + var shapeLayerConfiguration: ShapeLayerConfiguration { + var dashPatterns = ContiguousArray>>() + var dashPhase = ContiguousArray>() + for dash in self { + if dash.type == .offset { + dashPhase = dash.value.keyframes + } else { + dashPatterns.append(dash.value.keyframes) + } + } + return (dashPatterns, dashPhase) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.hpp new file mode 100644 index 0000000000..3a3386e1da --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.hpp @@ -0,0 +1,361 @@ +#ifndef TextAnimatorNode_hpp +#define TextAnimatorNode_hpp + +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.hpp" +#include "Lottie/Private/Model/Text/TextAnimator.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.hpp" + +namespace lottie { + +class TextAnimatorNodeProperties: public KeypathSearchableNodePropertyMap { +public: + TextAnimatorNodeProperties(std::shared_ptr const &textAnimator) { + _keypathName = textAnimator->name.value_or(""); + + if (textAnimator->anchor) { + _anchor = std::make_shared>(std::make_shared>(textAnimator->anchor->keyframes)); + _keypathProperties.insert(std::make_pair("Anchor", _anchor)); + } + + if (textAnimator->position) { + _position = std::make_shared>(std::make_shared>(textAnimator->position->keyframes)); + _keypathProperties.insert(std::make_pair("Position", _position)); + } + + if (textAnimator->scale) { + _scale = std::make_shared>(std::make_shared>(textAnimator->scale->keyframes)); + _keypathProperties.insert(std::make_pair("Scale", _scale)); + } + + if (textAnimator->skew) { + _skew = std::make_shared>(std::make_shared>(textAnimator->skew->keyframes)); + _keypathProperties.insert(std::make_pair("Skew", _skew)); + } + + if (textAnimator->skewAxis) { + _skewAxis = std::make_shared>(std::make_shared>(textAnimator->skewAxis->keyframes)); + _keypathProperties.insert(std::make_pair("Skew Axis", _skewAxis)); + } + + if (textAnimator->rotation) { + _rotation = std::make_shared>(std::make_shared>(textAnimator->rotation->keyframes)); + _keypathProperties.insert(std::make_pair("Rotation", _rotation)); + } + + if (textAnimator->rotation) { + _opacity = std::make_shared>(std::make_shared>(textAnimator->opacity->keyframes)); + _keypathProperties.insert(std::make_pair("Opacity", _opacity)); + } + + if (textAnimator->strokeColor) { + _strokeColor = std::make_shared>(std::make_shared>(textAnimator->strokeColor->keyframes)); + _keypathProperties.insert(std::make_pair("Stroke Color", _strokeColor)); + } + + if (textAnimator->fillColor) { + _fillColor = std::make_shared>(std::make_shared>(textAnimator->fillColor->keyframes)); + _keypathProperties.insert(std::make_pair("Fill Color", _fillColor)); + } + + if (textAnimator->strokeWidth) { + _strokeWidth = std::make_shared>(std::make_shared>(textAnimator->strokeWidth->keyframes)); + _keypathProperties.insert(std::make_pair("Stroke Width", _strokeWidth)); + } + + if (textAnimator->tracking) { + _tracking = std::make_shared>(std::make_shared>(textAnimator->tracking->keyframes)); + _keypathProperties.insert(std::make_pair("Tracking", _tracking)); + } + + for (const auto &it : _keypathProperties) { + _properties.push_back(it.second); + } + } + + virtual std::string keypathName() const override { + return _keypathName; + } + + virtual std::map> keypathProperties() const override { + return _keypathProperties; + } + + virtual std::vector> &properties() override { + return _properties; + } + + virtual std::vector> const &childKeypaths() const override { + return _childKeypaths; + } + + CATransform3D caTransform() { + Vector2D anchor = Vector2D::Zero(); + if (_anchor) { + auto anchor3d = _anchor->value(); + anchor = Vector2D(anchor3d.x, anchor3d.y); + } + + Vector2D position = Vector2D::Zero(); + if (_position) { + auto position3d = _position->value(); + position = Vector2D(position3d.x, position3d.y); + } + + Vector2D scale = Vector2D(100.0, 100.0); + if (_scale) { + auto scale3d = _scale->value(); + scale = Vector2D(scale3d.x, scale3d.y); + } + + double rotation = 0.0; + if (_rotation) { + rotation = _rotation->value().value; + } + + std::optional skew; + if (_skew) { + skew = _skew->value().value; + } + std::optional skewAxis; + if (_skewAxis) { + skewAxis = _skewAxis->value().value; + } + + return CATransform3D::makeTransform( + anchor, + position, + scale, + rotation, + skew, + skewAxis + ); + } + + virtual std::shared_ptr keypathLayer() const override { + return nullptr; + } + + double opacity() { + if (_opacity) { + return _opacity->value().value; + } else { + return 100.0; + } + } + + std::optional strokeColor() { + if (_strokeColor) { + return _strokeColor->value(); + } else { + return std::nullopt; + } + } + + std::optional fillColor() { + if (_fillColor) { + return _fillColor->value(); + } else { + return std::nullopt; + } + } + + double tracking() { + if (_tracking) { + return _tracking->value().value; + } else { + return 1.0; + } + } + + double strokeWidth() { + if (_strokeWidth) { + return _strokeWidth->value().value; + } else { + return 0.0; + } + } + +private: + std::string _keypathName; + + std::shared_ptr> _anchor; + std::shared_ptr> _position; + std::shared_ptr> _scale; + std::shared_ptr> _skew; + std::shared_ptr> _skewAxis; + std::shared_ptr> _rotation; + std::shared_ptr> _opacity; + std::shared_ptr> _strokeColor; + std::shared_ptr> _fillColor; + std::shared_ptr> _strokeWidth; + std::shared_ptr> _tracking; + + std::map> _keypathProperties; + std::vector> _childKeypaths; + std::vector> _properties; +}; + +class TextOutputNode: virtual public NodeOutput { +public: + TextOutputNode(std::shared_ptr parent) : + _parentTextNode(parent) { + } + + virtual std::shared_ptr parent() override { + return _parentTextNode; + } + + CATransform3D xform() { + if (_xform.has_value()) { + return _xform.value(); + } else if (_parentTextNode) { + return _parentTextNode->xform(); + } else { + return CATransform3D::identity(); + } + } + void setXform(CATransform3D const &xform) { + _xform = xform; + } + + double opacity() { + if (_opacity.has_value()) { + return _opacity.value(); + } else if (_parentTextNode) { + return _parentTextNode->opacity(); + } else { + return 1.0; + } + } + void setOpacity(double opacity) { + _opacity = opacity; + } + + std::optional strokeColor() { + if (_strokeColor.has_value()) { + return _strokeColor.value(); + } else if (_parentTextNode) { + return _parentTextNode->strokeColor(); + } else { + return std::nullopt; + } + } + void setStrokeColor(std::optional strokeColor) { + _strokeColor = strokeColor; + } + + std::optional fillColor() { + if (_fillColor.has_value()) { + return _fillColor.value(); + } else if (_parentTextNode) { + return _parentTextNode->fillColor(); + } else { + return std::nullopt; + } + } + void setFillColor(std::optional fillColor) { + _fillColor = fillColor; + } + + double tracking() { + if (_tracking.has_value()) { + return _tracking.value(); + } else if (_parentTextNode) { + return _parentTextNode->tracking(); + } else { + return 0.0; + } + } + void setTracking(double tracking) { + _tracking = tracking; + } + + double strokeWidth() { + if (_strokeWidth.has_value()) { + return _strokeWidth.value(); + } else if (_parentTextNode) { + return _parentTextNode->strokeWidth(); + } else { + return 0.0; + } + } + void setStrokeWidth(double strokeWidth) { + _strokeWidth = strokeWidth; + } + + virtual bool hasOutputUpdates(double frame) override { + // TODO Fix This + return true; + } + + virtual std::shared_ptr outputPath() override { + return _outputPath; + } + + virtual bool isEnabled() const override { + return _isEnabled; + } + virtual void setIsEnabled(bool isEnabled) override { + _isEnabled = isEnabled; + } + +private: + std::shared_ptr _parentTextNode; + bool _isEnabled = true; + + std::shared_ptr _outputPath; + + std::optional _xform; + std::optional _opacity; + std::optional _strokeColor; + std::optional _fillColor; + std::optional _tracking; + std::optional _strokeWidth; +}; + +class TextAnimatorNode: public AnimatorNode { +public: + TextAnimatorNode(std::shared_ptr const &parentNode, std::shared_ptr const &textAnimator) : + AnimatorNode(parentNode) { + std::shared_ptr parentOutputNode; + if (parentNode) { + parentOutputNode = parentNode->_textOutputNode; + } + _textOutputNode = std::make_shared(parentOutputNode); + + _textAnimatorProperties = std::make_shared(textAnimator); + } + + virtual std::shared_ptr outputNode() override { + return _textOutputNode; + } + + virtual std::shared_ptr propertyMap() const override { + return _textAnimatorProperties; + } + + virtual bool localUpdatesPermeateDownstream() override { + return true; + } + + virtual void rebuildOutputs(double frame) override { + _textOutputNode->setXform(_textAnimatorProperties->caTransform()); + _textOutputNode->setOpacity(((float)_textAnimatorProperties->opacity()) * 0.01f); + _textOutputNode->setStrokeColor(_textAnimatorProperties->strokeColor()); + _textOutputNode->setFillColor(_textAnimatorProperties->fillColor()); + _textOutputNode->setTracking(_textAnimatorProperties->tracking()); + _textOutputNode->setStrokeWidth(_textAnimatorProperties->strokeWidth()); + } + +private: + std::shared_ptr _textOutputNode; + + std::shared_ptr _textAnimatorProperties; +}; + +} + +#endif /* TextAnimatorNode_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.swift new file mode 100644 index 0000000000..cb7d484126 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.swift @@ -0,0 +1,270 @@ +// +// TextAnimatorNode.swift +// lottie-ios-iOS +// +// Created by Brandon Withrow on 2/19/19. +// + +import CoreGraphics +import Foundation +import QuartzCore + +// MARK: - TextAnimatorNodeProperties + +final class TextAnimatorNodeProperties: NodePropertyMap, KeypathSearchable { + + // MARK: Lifecycle + + init(textAnimator: TextAnimator) { + keypathName = textAnimator.name + var properties = [String : AnyNodeProperty]() + + if let keyframeGroup = textAnimator.anchor { + anchor = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes)) + properties["Anchor"] = anchor + } else { + anchor = nil + } + + if let keyframeGroup = textAnimator.position { + position = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes)) + properties["Position"] = position + } else { + position = nil + } + + if let keyframeGroup = textAnimator.scale { + scale = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes)) + properties["Scale"] = scale + } else { + scale = nil + } + + if let keyframeGroup = textAnimator.skew { + skew = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes)) + properties["Skew"] = skew + } else { + skew = nil + } + + if let keyframeGroup = textAnimator.skewAxis { + skewAxis = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes)) + properties["Skew Axis"] = skewAxis + } else { + skewAxis = nil + } + + if let keyframeGroup = textAnimator.rotation { + rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes)) + properties["Rotation"] = rotation + } else { + rotation = nil + } + + if let keyframeGroup = textAnimator.opacity { + opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes)) + properties["Opacity"] = opacity + } else { + opacity = nil + } + + if let keyframeGroup = textAnimator.strokeColor { + strokeColor = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes)) + properties["Stroke Color"] = strokeColor + } else { + strokeColor = nil + } + + if let keyframeGroup = textAnimator.fillColor { + fillColor = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes)) + properties["Fill Color"] = fillColor + } else { + fillColor = nil + } + + if let keyframeGroup = textAnimator.strokeWidth { + strokeWidth = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes)) + properties["Stroke Width"] = strokeWidth + } else { + strokeWidth = nil + } + + if let keyframeGroup = textAnimator.tracking { + tracking = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes)) + properties["Tracking"] = tracking + } else { + tracking = nil + } + + keypathProperties = properties + + self.properties = Array(keypathProperties.values) + } + + // MARK: Internal + + let keypathName: String + + let anchor: NodeProperty? + let position: NodeProperty? + let scale: NodeProperty? + let skew: NodeProperty? + let skewAxis: NodeProperty? + let rotation: NodeProperty? + let opacity: NodeProperty? + let strokeColor: NodeProperty? + let fillColor: NodeProperty? + let strokeWidth: NodeProperty? + let tracking: NodeProperty? + + let keypathProperties: [String: AnyNodeProperty] + let properties: [AnyNodeProperty] + + var caTransform: CATransform3D { + CATransform3D.makeTransform( + anchor: anchor?.value.pointValue ?? .zero, + position: position?.value.pointValue ?? .zero, + scale: scale?.value.sizeValue ?? CGSize(width: 100, height: 100), + rotation: rotation?.value.cgFloatValue ?? 0, + skew: skew?.value.cgFloatValue, + skewAxis: skewAxis?.value.cgFloatValue) + } +} + +// MARK: - TextOutputNode + +final class TextOutputNode: NodeOutput { + + // MARK: Lifecycle + + init(parent: TextOutputNode?) { + parentTextNode = parent + } + + // MARK: Internal + + var parentTextNode: TextOutputNode? + var isEnabled = true + + var outputPath: CGPath? + + var parent: NodeOutput? { + parentTextNode + } + + var xform: CATransform3D { + get { + _xform ?? parentTextNode?.xform ?? CATransform3DIdentity + } + set { + _xform = newValue + } + } + + var opacity: CGFloat { + get { + _opacity ?? parentTextNode?.opacity ?? 1 + } + set { + _opacity = newValue + } + } + + var strokeColor: CGColor? { + get { + _strokeColor ?? parentTextNode?.strokeColor + } + set { + _strokeColor = newValue + } + } + + var fillColor: CGColor? { + get { + _fillColor ?? parentTextNode?.fillColor + } + set { + _fillColor = newValue + } + } + + var tracking: CGFloat { + get { + _tracking ?? parentTextNode?.tracking ?? 0 + } + set { + _tracking = newValue + } + } + + var strokeWidth: CGFloat { + get { + _strokeWidth ?? parentTextNode?.strokeWidth ?? 0 + } + set { + _strokeWidth = newValue + } + } + + func hasOutputUpdates(_: CGFloat) -> Bool { + // TODO Fix This + true + } + + // MARK: Fileprivate + + fileprivate var _xform: CATransform3D? + fileprivate var _opacity: CGFloat? + fileprivate var _strokeColor: CGColor? + fileprivate var _fillColor: CGColor? + fileprivate var _tracking: CGFloat? + fileprivate var _strokeWidth: CGFloat? +} + +// MARK: - TextAnimatorNode + +class TextAnimatorNode: AnimatorNode { + + // MARK: Lifecycle + + init(parentNode: TextAnimatorNode?, textAnimator: TextAnimator) { + textOutputNode = TextOutputNode(parent: parentNode?.textOutputNode) + textAnimatorProperties = TextAnimatorNodeProperties(textAnimator: textAnimator) + self.parentNode = parentNode + } + + // MARK: Internal + + let textOutputNode: TextOutputNode + + let textAnimatorProperties: TextAnimatorNodeProperties + + let parentNode: AnimatorNode? + var hasLocalUpdates = false + var hasUpstreamUpdates = false + var lastUpdateFrame: CGFloat? = nil + var isEnabled = true + + var outputNode: NodeOutput { + textOutputNode + } + + // MARK: Animator Node Protocol + + var propertyMap: NodePropertyMap & KeypathSearchable { + textAnimatorProperties + } + + func localUpdatesPermeateDownstream() -> Bool { + true + } + + func rebuildOutputs(frame _: CGFloat) { + textOutputNode.xform = textAnimatorProperties.caTransform + textOutputNode.opacity = (textAnimatorProperties.opacity?.value.cgFloatValue ?? 100) * 0.01 + textOutputNode.strokeColor = textAnimatorProperties.strokeColor?.value.cgColorValue + textOutputNode.fillColor = textAnimatorProperties.fillColor?.value.cgColorValue + textOutputNode.tracking = textAnimatorProperties.tracking?.value.cgFloatValue ?? 1 + textOutputNode.strokeWidth = textAnimatorProperties.strokeWidth?.value.cgFloatValue ?? 0 + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.hpp new file mode 100644 index 0000000000..1d62e6c8d2 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.hpp @@ -0,0 +1,235 @@ +#ifndef AnimatorNode_hpp +#define AnimatorNode_hpp + +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp" + +#include +#include + +namespace lottie { + +class LayerTransformNode; +class PathNode; +class RenderNode; + +/// The Animator Node is the base node in the render system tree. +/// +/// It defines a single node that has an output path and option input node. +/// At animation time the root animation node is asked to update its contents for +/// the current frame. +/// The node reaches up its chain of nodes until the first node that does not need +/// updating is found. Then each node updates its contents down the render pipeline. +/// Each node adds its local path to its input path and passes it forward. +/// +/// An animator node holds a group of interpolators. These interpolators determine +/// if the node needs an update for the current frame. +/// +class AnimatorNode: public KeypathSearchable { +public: + AnimatorNode(std::shared_ptr const &parentNode) : + _parentNode(parentNode) { + } + + AnimatorNode(const AnimatorNode&) = delete; + AnimatorNode& operator=(AnimatorNode&) = delete; + + /// The available properties of the Node. + /// + /// These properties are automatically updated each frame. + /// These properties are also settable and gettable through the dynamic + /// property system. + /// + virtual std::shared_ptr propertyMap() const = 0; + + /// The upstream input node + std::shared_ptr parentNode() { + return _parentNode; + } + void setParentNode(std::shared_ptr const &parentNode) { + _parentNode = parentNode; + } + + /// The output of the node. + virtual std::shared_ptr outputNode() = 0; + + /// Update the outputs of the node. Called if local contents were update or if outputsNeedUpdate returns true. + virtual void rebuildOutputs(double frame) = 0; + + /// Setters for marking current node state. + bool isEnabled() { + return _isEnabled; + } + virtual void setIsEnabled(bool isEnabled) { + _isEnabled = isEnabled; + } + + bool hasLocalUpdates() { + return _hasLocalUpdates; + } + virtual void setHasLocalUpdates(bool hasLocalUpdates) { + _hasLocalUpdates = hasLocalUpdates; + } + + bool hasUpstreamUpdates() { + return _hasUpstreamUpdates; + } + virtual void setHasUpstreamUpdates(bool hasUpstreamUpdates) { + _hasUpstreamUpdates = hasUpstreamUpdates; + } + + std::optional lastUpdateFrame() { + return _lastUpdateFrame; + } + virtual void setLastUpdateFrame(std::optional lastUpdateFrame) { + _lastUpdateFrame = lastUpdateFrame; + } + + /// Marks if updates to this node affect nodes downstream. + virtual bool localUpdatesPermeateDownstream() { + /// Optional override + return true; + } + virtual bool forceUpstreamOutputUpdates() { + /// Optional + return false; + } + + /// Called at the end of this nodes update cycle. Always called. Optional. + virtual bool performAdditionalLocalUpdates(double frame, bool forceLocalUpdate) { + /// Optional + return forceLocalUpdate; + } + virtual void performAdditionalOutputUpdates(double frame, bool forceOutputUpdate) { + /// Optional + } + + /// The default simply returns `hasLocalUpdates` + virtual bool shouldRebuildOutputs(double frame) { + return hasLocalUpdates(); + } + + virtual bool updateOutputs(double frame, bool forceOutputUpdate) { + if (!isEnabled()) { + setLastUpdateFrame(frame); + if (const auto parentNodeValue = parentNode()) { + return parentNodeValue->updateOutputs(frame, forceOutputUpdate); + } else { + return false; + } + } + + if (!forceOutputUpdate && lastUpdateFrame().has_value() && lastUpdateFrame().value() == frame) { + /// This node has already updated for this frame. Go ahead and return the results. + return hasUpstreamUpdates() || hasLocalUpdates(); + } + + /// Ask if this node should force output updates upstream. + bool forceUpstreamUpdates = forceOutputUpdate || forceUpstreamOutputUpdates(); + + /// Perform upstream output updates. Optionally mark upstream updates if any. + if (const auto parentNodeValue = parentNode()) { + setHasUpstreamUpdates(parentNodeValue->updateOutputs(frame, forceUpstreamUpdates) || hasUpstreamUpdates()); + } else { + setHasUpstreamUpdates(hasUpstreamUpdates()); + } + + /// Perform additional local output updates + performAdditionalOutputUpdates(frame, forceUpstreamUpdates); + + /// If there are local updates, or if updates have been force, rebuild outputs + if (forceUpstreamUpdates || shouldRebuildOutputs(frame)) { + setLastUpdateFrame(frame); + rebuildOutputs(frame); + } + return hasUpstreamUpdates() || hasLocalUpdates(); + } + + /// Rebuilds the content of this node, and upstream nodes if necessary. + virtual bool updateContents(double frame, bool forceLocalUpdate) { + if (!isEnabled()) { + // Disabled node, pass through. + if (const auto parentNodeValue = parentNode()) { + return parentNodeValue->updateContents(frame, forceLocalUpdate); + } else { + return false; + } + } + + if (forceLocalUpdate == false && lastUpdateFrame().has_value() && lastUpdateFrame().value() == frame) { + /// This node has already updated for this frame. Go ahead and return the results. + return localUpdatesPermeateDownstream() ? hasUpstreamUpdates() || hasLocalUpdates() : hasUpstreamUpdates(); + } + + /// Are there local updates? If so mark the node. + setHasLocalUpdates(forceLocalUpdate ? forceLocalUpdate : propertyMap()->needsLocalUpdate(frame)); + + /// Were there upstream updates? If so mark the node + if (const auto parentNodeValue = parentNode()) { + setHasUpstreamUpdates(parentNodeValue->updateContents(frame, forceLocalUpdate)); + } else { + setHasUpstreamUpdates(false); + } + + /// Perform property updates if necessary. + if (hasLocalUpdates()) { + /// Rebuild local properties + propertyMap()->updateNodeProperties(frame); + } + + /// Ask the node to perform any other updates it might have. + setHasUpstreamUpdates(performAdditionalLocalUpdates(frame, forceLocalUpdate) || hasUpstreamUpdates()); + + /// If the node can update nodes downstream, notify them, otherwise pass on any upstream updates downstream. + return localUpdatesPermeateDownstream() ? hasUpstreamUpdates() || hasLocalUpdates() : hasUpstreamUpdates(); + } + + virtual void updateTree(double frame, bool forceUpdates) { + updateContents(frame, forceUpdates); + updateOutputs(frame, forceUpdates); + } + + /// The name of the Keypath + virtual std::string keypathName() const override { + return propertyMap()->keypathName(); + } + + /// A list of properties belonging to the keypath. + virtual std::map> keypathProperties() const override { + return propertyMap()->keypathProperties(); + } + + /// Children Keypaths + virtual std::vector> const &childKeypaths() const override { + return propertyMap()->childKeypaths(); + } + + virtual std::shared_ptr keypathLayer() const override { + return nullptr; + } + +public: + virtual LayerTransformNode *asLayerTransformNode() { + return nullptr; + } + + virtual PathNode *asPathNode() { + return nullptr; + } + + virtual RenderNode *asRenderNode() { + return nullptr; + } + +private: + std::shared_ptr _parentNode; + bool _isEnabled = true; + bool _hasLocalUpdates = false; + bool _hasUpstreamUpdates = false; + std::optional _lastUpdateFrame; +}; + +} + +#endif /* AnimatorNode_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.swift new file mode 100644 index 0000000000..1327618f61 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.swift @@ -0,0 +1,198 @@ +// +// AnimatorNode.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/15/19. +// + +import Foundation +import QuartzCore + +// MARK: - NodeOutput + +/// Defines the basic outputs of an animator node. +/// +protocol NodeOutput { + + /// The parent node. + var parent: NodeOutput? { get } + + /// Returns true if there are any updates upstream. OutputPath must be built before returning. + func hasOutputUpdates(_ forFrame: CGFloat) -> Bool + + var outputPath: CGPath? { get } + + var isEnabled: Bool { get set } +} + +// MARK: - AnimatorNode + +/// The Animator Node is the base node in the render system tree. +/// +/// It defines a single node that has an output path and option input node. +/// At animation time the root animation node is asked to update its contents for +/// the current frame. +/// The node reaches up its chain of nodes until the first node that does not need +/// updating is found. Then each node updates its contents down the render pipeline. +/// Each node adds its local path to its input path and passes it forward. +/// +/// An animator node holds a group of interpolators. These interpolators determine +/// if the node needs an update for the current frame. +/// +protocol AnimatorNode: AnyObject, KeypathSearchable { + + /// The available properties of the Node. + /// + /// These properties are automatically updated each frame. + /// These properties are also settable and gettable through the dynamic + /// property system. + /// + var propertyMap: NodePropertyMap & KeypathSearchable { get } + + /// The upstream input node + var parentNode: AnimatorNode? { get } + + /// The output of the node. + var outputNode: NodeOutput { get } + + /// Update the outputs of the node. Called if local contents were update or if outputsNeedUpdate returns true. + func rebuildOutputs(frame: CGFloat) + + /// Setters for marking current node state. + var isEnabled: Bool { get set } + var hasLocalUpdates: Bool { get set } + var hasUpstreamUpdates: Bool { get set } + var lastUpdateFrame: CGFloat? { get set } + + // MARK: Optional + + /// Marks if updates to this node affect nodes downstream. + func localUpdatesPermeateDownstream() -> Bool + func forceUpstreamOutputUpdates() -> Bool + + /// Called at the end of this nodes update cycle. Always called. Optional. + func performAdditionalLocalUpdates(frame: CGFloat, forceLocalUpdate: Bool) -> Bool + func performAdditionalOutputUpdates(_ frame: CGFloat, forceOutputUpdate: Bool) + + /// The default simply returns `hasLocalUpdates` + func shouldRebuildOutputs(frame: CGFloat) -> Bool +} + +/// Basic Node Logic +extension AnimatorNode { + + func shouldRebuildOutputs(frame _: CGFloat) -> Bool { + hasLocalUpdates + } + + func localUpdatesPermeateDownstream() -> Bool { + /// Optional override + true + } + + func forceUpstreamOutputUpdates() -> Bool { + /// Optional + false + } + + func performAdditionalLocalUpdates(frame _: CGFloat, forceLocalUpdate: Bool) -> Bool { + /// Optional + forceLocalUpdate + } + + func performAdditionalOutputUpdates(_: CGFloat, forceOutputUpdate _: Bool) { + /// Optional + } + + @discardableResult + func updateOutputs(_ frame: CGFloat, forceOutputUpdate: Bool) -> Bool { + guard isEnabled else { + // Disabled node, pass through. + lastUpdateFrame = frame + return parentNode?.updateOutputs(frame, forceOutputUpdate: forceOutputUpdate) ?? false + } + + if forceOutputUpdate == false && lastUpdateFrame != nil && lastUpdateFrame! == frame { + /// This node has already updated for this frame. Go ahead and return the results. + return hasUpstreamUpdates || hasLocalUpdates + } + + /// Ask if this node should force output updates upstream. + let forceUpstreamUpdates = forceOutputUpdate || forceUpstreamOutputUpdates() + + /// Perform upstream output updates. Optionally mark upstream updates if any. + hasUpstreamUpdates = ( + parentNode? + .updateOutputs(frame, forceOutputUpdate: forceUpstreamUpdates) ?? false || hasUpstreamUpdates) + + /// Perform additional local output updates + performAdditionalOutputUpdates(frame, forceOutputUpdate: forceUpstreamUpdates) + + /// If there are local updates, or if updates have been force, rebuild outputs + if forceUpstreamUpdates || shouldRebuildOutputs(frame: frame) { + lastUpdateFrame = frame + rebuildOutputs(frame: frame) + } + return hasUpstreamUpdates || hasLocalUpdates + } + + /// Rebuilds the content of this node, and upstream nodes if necessary. + @discardableResult + func updateContents(_ frame: CGFloat, forceLocalUpdate: Bool) -> Bool { + guard isEnabled else { + // Disabled node, pass through. + return parentNode?.updateContents(frame, forceLocalUpdate: forceLocalUpdate) ?? false + } + + if forceLocalUpdate == false && lastUpdateFrame != nil && lastUpdateFrame! == frame { + /// This node has already updated for this frame. Go ahead and return the results. + return localUpdatesPermeateDownstream() ? hasUpstreamUpdates || hasLocalUpdates : hasUpstreamUpdates + } + + /// Are there local updates? If so mark the node. + hasLocalUpdates = forceLocalUpdate ? forceLocalUpdate : propertyMap.needsLocalUpdate(frame: frame) + + /// Were there upstream updates? If so mark the node + hasUpstreamUpdates = parentNode?.updateContents(frame, forceLocalUpdate: forceLocalUpdate) ?? false + + /// Perform property updates if necessary. + if hasLocalUpdates { + /// Rebuild local properties + propertyMap.updateNodeProperties(frame: frame) + } + + /// Ask the node to perform any other updates it might have. + hasUpstreamUpdates = performAdditionalLocalUpdates(frame: frame, forceLocalUpdate: forceLocalUpdate) || hasUpstreamUpdates + + /// If the node can update nodes downstream, notify them, otherwise pass on any upstream updates downstream. + return localUpdatesPermeateDownstream() ? hasUpstreamUpdates || hasLocalUpdates : hasUpstreamUpdates + } + + func updateTree(_ frame: CGFloat, forceUpdates: Bool = false) { + updateContents(frame, forceLocalUpdate: forceUpdates) + updateOutputs(frame, forceOutputUpdate: forceUpdates) + } + +} + +extension AnimatorNode { + /// Default implementation for Keypath searchable. + /// Forward all calls to the propertyMap. + + var keypathName: String { + propertyMap.keypathName + } + + var keypathProperties: [String: AnyNodeProperty] { + propertyMap.keypathProperties + } + + var childKeypaths: [KeypathSearchable] { + propertyMap.childKeypaths + } + + var keypathLayer: CALayer? { + nil + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp new file mode 100644 index 0000000000..970a51ae24 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp @@ -0,0 +1,28 @@ +#ifndef NodeOutput_hpp +#define NodeOutput_hpp + +#include "Lottie/Public/Primitives/CGPath.hpp" + +#include + +namespace lottie { + +/// Defines the basic outputs of an animator node. +/// +class NodeOutput { +public: + /// The parent node. + virtual std::shared_ptr parent() = 0; + + /// Returns true if there are any updates upstream. OutputPath must be built before returning. + virtual bool hasOutputUpdates(double forFrame) = 0; + + virtual std::shared_ptr outputPath() = 0; + + virtual bool isEnabled() const = 0; + virtual void setIsEnabled(bool isEnabled) = 0; +}; + +} + +#endif /* NodeOutput_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Protocols/PathNode.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Protocols/PathNode.swift new file mode 100644 index 0000000000..7eaa6cf45b --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Protocols/PathNode.swift @@ -0,0 +1,22 @@ +// +// PathNode.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/17/19. +// + +import Foundation + +// MARK: - PathNode + +protocol PathNode { + var pathOutput: PathOutputNode { get } +} + +extension PathNode where Self: AnimatorNode { + + var outputNode: NodeOutput { + pathOutput + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Protocols/RenderNode.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Protocols/RenderNode.hpp new file mode 100644 index 0000000000..5db444d408 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Protocols/RenderNode.hpp @@ -0,0 +1,73 @@ +#ifndef RenderNode_hpp +#define RenderNode_hpp + +#include "Lottie/Public/Primitives/CALayer.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasRenderUpdates.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasUpdate.hpp" + +namespace lottie { + +class StrokeRenderer; +class FillRenderer; +class GradientStrokeRenderer; +class GradientFillRenderer; + +/// A protocol that defines anything with render instructions +class Renderable: virtual public HasRenderUpdates, virtual public HasUpdate { +public: + enum RenderableType { + Fill, + Stroke, + GradientFill, + GradientStroke + }; + +public: + /// Determines if the renderer requires a custom context for drawing. + /// If yes the shape layer will perform a custom drawing pass. + /// If no the shape layer will be a standard CAShapeLayer + virtual bool shouldRenderInContext() = 0; + + /// Passes in the CAShapeLayer to update + virtual void updateShapeLayer(std::shared_ptr const &layer) = 0; + + /// Asks the renderer what the renderable bounds is for the given box. + virtual CGRect renderBoundsFor(CGRect const &boundingBox) { + /// Optional + return boundingBox; + } + + /// Opportunity for renderers to inject sublayers + virtual void setupSublayers(std::shared_ptr const &layer) = 0; + + virtual RenderableType renderableType() const = 0; + + virtual StrokeRenderer *asStrokeRenderer() { + return nullptr; + } + + virtual FillRenderer *asFillRenderer() { + return nullptr; + } + + virtual GradientStrokeRenderer *asGradientStrokeRenderer() { + return nullptr; + } + + virtual GradientFillRenderer *asGradientFillRenderer() { + return nullptr; + } +}; + +/// A protocol that defines a node that holds render instructions +class RenderNode { +public: + virtual std::shared_ptr renderer() = 0; + virtual std::shared_ptr nodeOutput() = 0; +}; + +} + +#endif /* RenderNode_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Protocols/RenderNode.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Protocols/RenderNode.swift new file mode 100644 index 0000000000..9b4cfbf225 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/Protocols/RenderNode.swift @@ -0,0 +1,62 @@ +// +// RenderNode.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/17/19. +// + +import CoreGraphics +import Foundation +import QuartzCore + +// MARK: - RenderNode + +/// A protocol that defines a node that holds render instructions +protocol RenderNode { + var renderer: Renderable & NodeOutput { get } +} + +// MARK: - Renderable + +/// A protocol that defines anything with render instructions +protocol Renderable { + + /// The last frame in which this node was updated. + var hasUpdate: Bool { get } + + func hasRenderUpdates(_ forFrame: CGFloat) -> Bool + + /// Determines if the renderer requires a custom context for drawing. + /// If yes the shape layer will perform a custom drawing pass. + /// If no the shape layer will be a standard CAShapeLayer + var shouldRenderInContext: Bool { get } + + /// Passes in the CAShapeLayer to update + func updateShapeLayer(layer: CAShapeLayer) + + /// Asks the renderer what the renderable bounds is for the given box. + func renderBoundsFor(_ boundingBox: CGRect) -> CGRect + + /// Opportunity for renderers to inject sublayers + func setupSublayers(layer: CAShapeLayer) + + /// Renders the shape in a custom context + func render(_ inContext: CGContext) +} + +extension RenderNode where Self: AnimatorNode { + + var outputNode: NodeOutput { + renderer + } + +} + +extension Renderable { + + func renderBoundsFor(_ boundingBox: CGRect) -> CGRect { + /// Optional + boundingBox + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/RenderLayers/GetGradientParameters.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/RenderLayers/GetGradientParameters.cpp new file mode 100644 index 0000000000..a1b830cf13 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/RenderLayers/GetGradientParameters.cpp @@ -0,0 +1,99 @@ +#include "GetGradientParameters.hpp" + +namespace lottie { + +void getGradientParameters(int numberOfColors, GradientColorSet const &colors, std::vector &outColors, std::vector &outLocations) { + std::vector alphaColors; + std::vector alphaValues; + std::vector alphaLocations; + + std::vector gradientColors; + std::vector colorLocations; + + for (int i = 0; i < numberOfColors; i++) { + int ix = i * 4; + if (colors.colors.size() > ix) { + Color color( + colors.colors[ix + 1], + colors.colors[ix + 2], + colors.colors[ix + 3], + 1 + ); + gradientColors.push_back(color); + colorLocations.push_back(colors.colors[ix]); + } + } + + bool drawMask = false; + for (int i = numberOfColors * 4; i < (int)colors.colors.size(); i += 2) { + double alpha = colors.colors[i + 1]; + if (alpha < 1.0) { + drawMask = true; + } + alphaLocations.push_back(colors.colors[i]); + alphaColors.push_back(Color(alpha, alpha, alpha, 1.0)); + alphaValues.push_back(alpha); + } + + if (drawMask) { + std::vector locations; + for (size_t i = 0; i < std::min(gradientColors.size(), colorLocations.size()); i++) { + if (std::find(locations.begin(), locations.end(), colorLocations[i]) == locations.end()) { + locations.push_back(colorLocations[i]); + } + } + for (size_t i = 0; i < std::min(alphaValues.size(), alphaLocations.size()); i++) { + if (std::find(locations.begin(), locations.end(), alphaLocations[i]) == locations.end()) { + locations.push_back(alphaLocations[i]); + } + } + + std::sort(locations.begin(), locations.end()); + if (locations[0] != 0.0) { + locations.insert(locations.begin(), 0.0); + } + if (locations[locations.size() - 1] != 1.0) { + locations.push_back(1.0); + } + + std::vector colors; + + for (const auto location : locations) { + Color color = gradientColors[0]; + for (size_t i = 0; i < std::min(gradientColors.size(), colorLocations.size()) - 1; i++) { + if (location >= colorLocations[i] && location <= colorLocations[i + 1]) { + double localLocation = 0.0; + if (colorLocations[i] != colorLocations[i + 1]) { + localLocation = remapDouble(location, colorLocations[i], colorLocations[i + 1], 0.0, 1.0); + } + color = ValueInterpolator::interpolate(gradientColors[i], gradientColors[i + 1], localLocation, std::nullopt, std::nullopt); + break; + } + } + + double alpha = 1.0; + for (size_t i = 0; i < std::min(alphaValues.size(), alphaLocations.size()) - 1; i++) { + if (location >= alphaLocations[i] && location <= alphaLocations[i + 1]) { + double localLocation = 0.0; + if (alphaLocations[i] != alphaLocations[i + 1]) { + localLocation = remapDouble(location, alphaLocations[i], alphaLocations[i + 1], 0.0, 1.0); + } + alpha = ValueInterpolator::interpolate(alphaValues[i], alphaValues[i + 1], localLocation, std::nullopt, std::nullopt); + break; + } + } + + color.a = alpha; + + colors.push_back(color); + } + + gradientColors = colors; + colorLocations = locations; + } + + outColors = gradientColors; + outLocations = colorLocations; +} + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/RenderLayers/GetGradientParameters.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/RenderLayers/GetGradientParameters.hpp new file mode 100644 index 0000000000..f14c578644 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/RenderLayers/GetGradientParameters.hpp @@ -0,0 +1,13 @@ +#ifndef ShapeRenderLayer_hpp +#define ShapeRenderLayer_hpp + +#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/RenderNode.hpp" +#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp" + +namespace lottie { + +void getGradientParameters(int numberOfColors, GradientColorSet const &colors, std::vector &outColors, std::vector &outLocations); + +} + +#endif /* ShapeRenderLayer_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/RenderLayers/ShapeContainerLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/RenderLayers/ShapeContainerLayer.swift new file mode 100644 index 0000000000..df1f07680c --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/RenderLayers/ShapeContainerLayer.swift @@ -0,0 +1,74 @@ +// +// ShapeContainerLayer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/30/19. +// + +import Foundation +import QuartzCore + +/// The base layer that holds Shapes and Shape Renderers +class ShapeContainerLayer: CALayer { + + // MARK: Lifecycle + + override init() { + super.init() + actions = [ + "position" : NSNull(), + "bounds" : NSNull(), + "anchorPoint" : NSNull(), + "transform" : NSNull(), + "opacity" : NSNull(), + "hidden" : NSNull(), + ] + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override init(layer: Any) { + guard let layer = layer as? ShapeContainerLayer else { + fatalError("init(layer:) wrong class.") + } + super.init(layer: layer) + } + + // MARK: Internal + + private(set) var renderLayers: [ShapeContainerLayer] = [] + + var renderScale: CGFloat = 1 { + didSet { + updateRenderScale() + } + } + + func insertRenderLayer(_ layer: ShapeContainerLayer) { + renderLayers.append(layer) + insertSublayer(layer, at: 0) + } + + func markRenderUpdates(forFrame: CGFloat) { + if hasRenderUpdate(forFrame: forFrame) { + rebuildContents(forFrame: forFrame) + } + guard isHidden == false else { return } + renderLayers.forEach { $0.markRenderUpdates(forFrame: forFrame) } + } + + func hasRenderUpdate(forFrame _: CGFloat) -> Bool { + false + } + + func rebuildContents(forFrame _: CGFloat) { + /// Override + } + + func updateRenderScale() { + contentsScale = renderScale + renderLayers.forEach( { $0.renderScale = renderScale } ) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/RenderLayers/ShapeRenderLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/RenderLayers/ShapeRenderLayer.swift new file mode 100644 index 0000000000..611603de8a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/MainThread/NodeRenderSystem/RenderLayers/ShapeRenderLayer.swift @@ -0,0 +1,100 @@ +// +// RenderLayer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/18/19. +// + +import Foundation +import QuartzCore + +/// The layer responsible for rendering shape objects +final class ShapeRenderLayer: ShapeContainerLayer, LottieDrawingLayer { + + // MARK: Lifecycle + + init(renderer: Renderable & NodeOutput) { + self.renderer = renderer + super.init() + anchorPoint = .zero + actions = [ + "position" : NSNull(), + "bounds" : NSNull(), + "anchorPoint" : NSNull(), + "path" : NSNull(), + "transform" : NSNull(), + "opacity" : NSNull(), + "hidden" : NSNull(), + ] + shapeLayer.actions = [ + "position" : NSNull(), + "bounds" : NSNull(), + "anchorPoint" : NSNull(), + "path" : NSNull(), + "fillColor" : NSNull(), + "strokeColor" : NSNull(), + "lineWidth" : NSNull(), + "miterLimit" : NSNull(), + "lineDashPhase" : NSNull(), + "opacity": NSNull(), + "hidden" : NSNull(), + ] + addSublayer(shapeLayer) + + renderer.setupSublayers(layer: shapeLayer) + } + + override init(layer: Any) { + guard let layer = layer as? ShapeRenderLayer else { + fatalError("init(layer:) wrong class.") + } + renderer = layer.renderer + super.init(layer: layer) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + + fileprivate(set) var renderer: Renderable & NodeOutput + + let shapeLayer = LottieCAShapeLayer() + + override func hasRenderUpdate(forFrame: CGFloat) -> Bool { + isHidden = !renderer.isEnabled + guard isHidden == false else { return false } + return renderer.hasRenderUpdates(forFrame) + } + + override func rebuildContents(forFrame _: CGFloat) { + + if renderer.shouldRenderInContext { + if let newPath = renderer.outputPath { + bounds = renderer.renderBoundsFor(lottieSwift_getPathNativeBoundingBox!(newPath)) + } else { + bounds = .zero + } + position = bounds.origin + setNeedsDisplay() + } else { + shapeLayer.path = renderer.outputPath + renderer.updateShapeLayer(layer: shapeLayer) + } + } + + override func draw(in ctx: CGContext) { + if let path = renderer.outputPath { + if !path.isEmpty { + ctx.addPath(path) + } + } + renderer.render(ctx) + } + + override func updateRenderScale() { + super.updateRenderScale() + shapeLayer.contentsScale = renderScale + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Animation.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Animation.cpp new file mode 100644 index 0000000000..577a56be35 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Animation.cpp @@ -0,0 +1,5 @@ +#include "Animation.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Animation.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Animation.hpp new file mode 100644 index 0000000000..f6ed1ab57e --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Animation.hpp @@ -0,0 +1,314 @@ +#ifndef Animation_hpp +#define Animation_hpp + +#include "Lottie/Public/Primitives/AnimationTime.hpp" +#include "Lottie/Private/Utility/Primitives/CoordinateSpace.hpp" +#include "Lottie/Private/Model/Layers/LayerModel.hpp" +#include "Lottie/Private/Model/Text/Glyph.hpp" +#include "Lottie/Private/Model/Text/Font.hpp" +#include "Lottie/Private/Model/Objects/Marker.hpp" +#include "Lottie/Private/Model/Assets/AssetLibrary.hpp" +#include "Lottie/Private/Model/Objects/FitzModifier.hpp" + +#include "json11/json11.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" +#include "Lottie/Private/Model/Layers/LayerModelSerialization.hpp" + +#include +#include +#include +#include + +namespace lottie { + +/// The `Animation` model is the top level model object in Lottie. +/// +/// An `Animation` holds all of the animation data backing a Lottie Animation. +/// Codable, see JSON schema [here](https://github.com/airbnb/lottie-web/tree/master/docs/json). +class Animation { +public: + Animation( + std::optional name_, + std::optional tgs_, + AnimationFrameTime startFrame_, + AnimationFrameTime endFrame_, + double framerate_, + std::string const &version_, + std::optional type_, + int width_, + int height_, + std::vector> const &layers_, + std::optional>> glyphs_, + std::optional> fonts_, + std::shared_ptr assetLibrary_, + std::optional> markers_, + std::optional> fitzModifiers_, + std::optional meta_, + std::optional comps_ + ) : + name(name_), + tgs(tgs_), + startFrame(startFrame_), + endFrame(endFrame_), + framerate(framerate_), + version(version_), + type(type_), + width(width_), + height(height_), + layers(layers_), + glyphs(glyphs_), + fonts(fonts_), + assetLibrary(assetLibrary_), + markers(markers_), + fitzModifiers(fitzModifiers_), + meta(meta_), + comps(comps_) { + if (markers) { + std::map parsedMarkerMap; + for (const auto &marker : markers.value()) { + parsedMarkerMap.insert(std::make_pair(marker.name, marker)); + } + markerMap = std::move(parsedMarkerMap); + } + } + + Animation(const Animation&) = delete; + Animation& operator=(Animation&) = delete; + + static std::shared_ptr fromJson(json11::Json::object const &json) noexcept(false) { + auto name = getOptionalString(json, "nm"); + auto version = getString(json, "v"); + + auto tgs = getOptionalInt(json, "tgs"); + + std::optional type; + if (const auto typeRawValue = getOptionalInt(json, "ddd")) { + if (typeRawValue.value() == 0) { + type = CoordinateSpace::Type2d; + } else { + type = CoordinateSpace::Type3d; + } + } + + AnimationFrameTime startFrame = getDouble(json, "ip"); + AnimationFrameTime endFrame = getDouble(json, "op"); + + double framerate = getDouble(json, "fr"); + + int width = getInt(json, "w"); + int height = getInt(json, "h"); + + auto layerDictionaries = getObjectArray(json, "layers"); + std::vector> layers; + for (size_t i = 0; i < layerDictionaries.size(); i++) { + try { + auto layer = parseLayerModel(layerDictionaries[i]); + layers.push_back(layer); + } catch(...) { + throw LottieParsingException(); + } + } + + std::optional>> glyphs; + if (const auto glyphDictionaries = getOptionalObjectArray(json, "chars")) { + glyphs = std::vector>(); + for (const auto &glyphDictionary : glyphDictionaries.value()) { + glyphs->push_back(std::make_shared(glyphDictionary)); + } + } else { + glyphs = std::nullopt; + } + + std::optional> fonts; + if (const auto fontsDictionary = getOptionalObject(json, "fonts")) { + fonts = std::make_shared(fontsDictionary.value()); + } + + std::shared_ptr assetLibrary; + if (const auto assetLibraryData = getOptionalAny(json, "assets")) { + assetLibrary = std::make_shared(assetLibraryData.value()); + } + + std::optional> markers; + if (const auto markerDictionaries = getOptionalObjectArray(json, "markers")) { + markers = std::vector(); + for (const auto &markerDictionary : markerDictionaries.value()) { + markers->push_back(Marker(markerDictionary)); + } + } + std::optional> fitzModifiers; + if (const auto fitzModifierDictionaries = getOptionalObjectArray(json, "fitz")) { + fitzModifiers = std::vector(); + for (const auto &fitzModifierDictionary : fitzModifierDictionaries.value()) { + fitzModifiers->push_back(FitzModifier(fitzModifierDictionary)); + } + } + + auto meta = getOptionalAny(json, "meta"); + auto comps = getOptionalAny(json, "comps"); + + return std::make_shared( + name, + tgs, + startFrame, + endFrame, + framerate, + version, + type, + width, + height, + std::move(layers), + std::move(glyphs), + std::move(fonts), + assetLibrary, + std::move(markers), + fitzModifiers, + meta, + comps + ); + } + + json11::Json::object toJson() const { + json11::Json::object result; + + if (name.has_value()) { + result.insert(std::make_pair("nm", name.value())); + } + + result.insert(std::make_pair("v", json11::Json(version))); + + if (tgs.has_value()) { + result.insert(std::make_pair("tgs", tgs.value())); + } + + if (type.has_value()) { + switch (type.value()) { + case CoordinateSpace::Type2d: + result.insert(std::make_pair("ddd", json11::Json(0))); + break; + case CoordinateSpace::Type3d: + result.insert(std::make_pair("ddd", json11::Json(1))); + break; + } + } + + result.insert(std::make_pair("ip", json11::Json(startFrame))); + result.insert(std::make_pair("op", json11::Json(endFrame))); + result.insert(std::make_pair("fr", json11::Json(framerate))); + result.insert(std::make_pair("w", json11::Json(width))); + result.insert(std::make_pair("h", json11::Json(height))); + + json11::Json::array layersArray; + for (const auto &layer : layers) { + json11::Json::object layerJson; + layer->toJson(layerJson); + layersArray.push_back(layerJson); + } + result.insert(std::make_pair("layers", json11::Json(layersArray))); + + if (glyphs.has_value()) { + json11::Json::array glyphArray; + for (const auto &glyph : glyphs.value()) { + glyphArray.push_back(glyph->toJson()); + } + result.insert(std::make_pair("chars", json11::Json(glyphArray))); + } + + if (fonts.has_value()) { + result.insert(std::make_pair("fonts", fonts.value()->toJson())); + } + + if (assetLibrary) { + result.insert(std::make_pair("assets", assetLibrary->toJson())); + } + + if (markers.has_value()) { + json11::Json::array markerArray; + for (const auto &marker : markers.value()) { + markerArray.push_back(marker.toJson()); + } + result.insert(std::make_pair("markers", json11::Json(markerArray))); + } + + if (fitzModifiers.has_value()) { + json11::Json::array fitzModifierArray; + for (const auto &fitzModifier : fitzModifiers.value()) { + fitzModifierArray.push_back(fitzModifier.toJson()); + } + result.insert(std::make_pair("fitz", json11::Json(fitzModifierArray))); + } + + if (meta.has_value()) { + result.insert(std::make_pair("meta", meta.value())); + } + if (comps.has_value()) { + result.insert(std::make_pair("comps", comps.value())); + } + + return result; + } + +public: + /// The start time of the composition in frameTime. + AnimationFrameTime startFrame; + + /// The end time of the composition in frameTime. + AnimationFrameTime endFrame; + + /// The frame rate of the composition. + double framerate; + + /// Return all marker names, in order, or an empty list if none are specified + std::vector markerNames() { + if (!markers.has_value()) { + return {}; + } + std::vector result; + for (const auto &marker : markers.value()) { + result.push_back(marker.name); + } + return result; + } + + /// Animation name + std::optional name; + + /// The version of the JSON Schema. + std::string version; + + std::optional tgs; + + /// The coordinate space of the composition. + std::optional type; + + /// The height of the composition in points. + int width; + + /// The width of the composition in points. + int height; + + /// The list of animation layers + std::vector> layers; + + /// The list of glyphs used for text rendering + std::optional>> glyphs; + + /// The list of fonts used for text rendering + std::optional> fonts; + + /// Asset Library + std::shared_ptr assetLibrary; + + /// Markers + std::optional> markers; + std::optional> markerMap; + + std::optional> fitzModifiers; + + std::optional meta; + std::optional comps; +}; + +} + +#endif /* Animation_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Animation.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Animation.swift new file mode 100644 index 0000000000..d580337a23 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Animation.swift @@ -0,0 +1,160 @@ +// +// Animation.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/7/19. +// + +import Foundation + +// MARK: - CoordinateSpace + +public enum CoordinateSpace: Int, Codable { + case type2d + case type3d +} + +// MARK: - Animation + +/// The `Animation` model is the top level model object in Lottie. +/// +/// An `Animation` holds all of the animation data backing a Lottie Animation. +/// Codable, see JSON schema [here](https://github.com/airbnb/lottie-web/tree/master/docs/json). +public final class Animation: Codable, DictionaryInitializable { + + // MARK: Lifecycle + + required public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: Animation.CodingKeys.self) + version = try container.decode(String.self, forKey: .version) + type = try container.decodeIfPresent(CoordinateSpace.self, forKey: .type) ?? .type2d + startFrame = try container.decode(AnimationFrameTime.self, forKey: .startFrame) + endFrame = try container.decode(AnimationFrameTime.self, forKey: .endFrame) + framerate = try container.decode(Double.self, forKey: .framerate) + width = try container.decode(Int.self, forKey: .width) + height = try container.decode(Int.self, forKey: .height) + layers = try container.decode([LayerModel].self, ofFamily: LayerType.self, forKey: .layers) + glyphs = try container.decodeIfPresent([Glyph].self, forKey: .glyphs) + fonts = try container.decodeIfPresent(FontList.self, forKey: .fonts) + assetLibrary = try container.decodeIfPresent(AssetLibrary.self, forKey: .assetLibrary) + markers = try container.decodeIfPresent([Marker].self, forKey: .markers) + + if let markers = markers { + var markerMap: [String: Marker] = [:] + for marker in markers { + markerMap[marker.name] = marker + } + self.markerMap = markerMap + } else { + markerMap = nil + } + } + + public init(dictionary: [String: Any]) throws { + version = try dictionary.value(for: CodingKeys.version) + if + let typeRawValue = dictionary[CodingKeys.type.rawValue] as? Int, + let type = CoordinateSpace(rawValue: typeRawValue) + { + self.type = type + } else { + type = .type2d + } + startFrame = try dictionary.value(for: CodingKeys.startFrame) + endFrame = try dictionary.value(for: CodingKeys.endFrame) + framerate = try dictionary.value(for: CodingKeys.framerate) + width = try dictionary.value(for: CodingKeys.width) + height = try dictionary.value(for: CodingKeys.height) + let layerDictionaries: [[String: Any]] = try dictionary.value(for: CodingKeys.layers) + layers = try [LayerModel].fromDictionaries(layerDictionaries) + if let glyphDictionaries = dictionary[CodingKeys.glyphs.rawValue] as? [[String: Any]] { + glyphs = try glyphDictionaries.map({ try Glyph(dictionary: $0) }) + } else { + glyphs = nil + } + if let fontsDictionary = dictionary[CodingKeys.fonts.rawValue] as? [String: Any] { + fonts = try FontList(dictionary: fontsDictionary) + } else { + fonts = nil + } + if let assetLibraryDictionaries = dictionary[CodingKeys.assetLibrary.rawValue] as? [[String: Any]] { + assetLibrary = try AssetLibrary(value: assetLibraryDictionaries) + } else { + assetLibrary = nil + } + if let markerDictionaries = dictionary[CodingKeys.markers.rawValue] as? [[String: Any]] { + let markers = try markerDictionaries.map({ try Marker(dictionary: $0) }) + var markerMap: [String: Marker] = [:] + for marker in markers { + markerMap[marker.name] = marker + } + self.markers = markers + self.markerMap = markerMap + } else { + markers = nil + markerMap = nil + } + } + + // MARK: Public + + /// The start time of the composition in frameTime. + public let startFrame: AnimationFrameTime + + /// The end time of the composition in frameTime. + public let endFrame: AnimationFrameTime + + /// The frame rate of the composition. + public let framerate: Double + + /// Return all marker names, in order, or an empty list if none are specified + public var markerNames: [String] { + guard let markers = markers else { return [] } + return markers.map { $0.name } + } + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case version = "v" + case type = "ddd" + case startFrame = "ip" + case endFrame = "op" + case framerate = "fr" + case width = "w" + case height = "h" + case layers + case glyphs = "chars" + case fonts + case assetLibrary = "assets" + case markers + } + + /// The version of the JSON Schema. + let version: String + + /// The coordinate space of the composition. + let type: CoordinateSpace + + /// The height of the composition in points. + public let width: Int + + /// The width of the composition in points. + public let height: Int + + /// The list of animation layers + let layers: [LayerModel] + + /// The list of glyphs used for text rendering + let glyphs: [Glyph]? + + /// The list of fonts used for text rendering + let fonts: FontList? + + /// Asset Library + let assetLibrary: AssetLibrary? + + /// Markers + let markers: [Marker]? + let markerMap: [String: Marker]? +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/Asset.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/Asset.cpp new file mode 100644 index 0000000000..12f67cdd8a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/Asset.cpp @@ -0,0 +1,5 @@ +#include "Asset.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/Asset.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/Asset.hpp new file mode 100644 index 0000000000..c071ab7854 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/Asset.hpp @@ -0,0 +1,50 @@ +#ifndef Asset_hpp +#define Asset_hpp + +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +#include +#include + +namespace lottie { + +class Asset { +public: + Asset(std::string id_) : + id(id_) { + } + + explicit Asset(json11::Json::object const &json) noexcept(false) { + auto idData = getAny(json, "id"); + if (idData.is_string()) { + id = idData.string_value(); + } else if (idData.is_number()) { + std::ostringstream idString; + idString << idData.int_value(); + id = idString.str(); + } + + objectName = getOptionalString(json, "nm"); + } + + Asset(const Asset&) = delete; + Asset& operator=(Asset&) = delete; + + virtual void toJson(json11::Json::object &json) const { + json.insert(std::make_pair("id", id)); + + if (objectName.has_value()) { + json.insert(std::make_pair("nm", objectName.value())); + } + } + +public: + /// The ID of the asset + std::string id; + + std::optional objectName; +}; + +} + +#endif /* Asset_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/Asset.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/Asset.swift new file mode 100644 index 0000000000..d610b1ce55 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/Asset.swift @@ -0,0 +1,43 @@ +// +// Asset.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/9/19. +// + +import Foundation + +public class Asset: Codable, DictionaryInitializable { + + // MARK: Lifecycle + + required public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: Asset.CodingKeys.self) + if let id = try? container.decode(String.self, forKey: .id) { + self.id = id + } else { + id = String(try container.decode(Int.self, forKey: .id)) + } + } + + required init(dictionary: [String: Any]) throws { + if let id = dictionary[CodingKeys.id.rawValue] as? String { + self.id = id + } else if let id = dictionary[CodingKeys.id.rawValue] as? Int { + self.id = String(id) + } else { + throw InitializableError.invalidInput + } + } + + // MARK: Public + + /// The ID of the asset + public let id: String + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case id + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/AssetLibrary.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/AssetLibrary.cpp new file mode 100644 index 0000000000..7feff3231e --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/AssetLibrary.cpp @@ -0,0 +1,5 @@ +#include "AssetLibrary.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/AssetLibrary.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/AssetLibrary.hpp new file mode 100644 index 0000000000..7ff3b07451 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/AssetLibrary.hpp @@ -0,0 +1,71 @@ +#ifndef AssetLibrary_hpp +#define AssetLibrary_hpp + +#include "Lottie/Private/Model/Assets/Asset.hpp" +#include "Lottie/Private/Model/Assets/ImageAsset.hpp" +#include "Lottie/Private/Model/Assets/PrecompAsset.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +#include + +namespace lottie { + +class AssetLibrary { +public: + AssetLibrary( + std::map> const &assets_, + std::map> const &imageAssets_, + std::map> const &precompAssets_ + ) : + assets(assets_), + imageAssets(imageAssets_), + precompAssets(precompAssets_) { + } + + explicit AssetLibrary(json11::Json const &json) noexcept(false) { + if (!json.is_array()) { + throw LottieParsingException(); + } + + for (const auto &item : json.array_items()) { + if (!item.is_object()) { + throw LottieParsingException(); + } + if (item.object_items().find("layers") != item.object_items().end()) { + auto asset = std::make_shared(item.object_items()); + assets.insert(std::make_pair(asset->id, asset)); + assetList.push_back(asset); + precompAssets.insert(std::make_pair(asset->id, asset)); + } else { + auto asset = std::make_shared(item.object_items()); + assets.insert(std::make_pair(asset->id, asset)); + assetList.push_back(asset); + imageAssets.insert(std::make_pair(asset->id, asset)); + } + } + } + + json11::Json::array toJson() const { + json11::Json::array result; + + for (const auto &asset : assetList) { + json11::Json::object assetJson; + asset->toJson(assetJson); + result.push_back(assetJson); + } + + return result; + } + +public: + /// The Assets + std::vector> assetList; + std::map> assets; + + std::map> imageAssets; + std::map> precompAssets; +}; + +} + +#endif /* AssetLibrary_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/AssetLibrary.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/AssetLibrary.swift new file mode 100644 index 0000000000..ba44c3bc44 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/AssetLibrary.swift @@ -0,0 +1,75 @@ +// +// AssetLibrary.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/9/19. +// + +import Foundation + +final class AssetLibrary: Codable, AnyInitializable { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + var containerForKeys = container + + var decodedAssets = [String : Asset]() + + var imageAssets = [String : ImageAsset]() + var precompAssets = [String : PrecompAsset]() + + while !container.isAtEnd { + let keyContainer = try containerForKeys.nestedContainer(keyedBy: PrecompAsset.CodingKeys.self) + if keyContainer.contains(.layers) { + let precompAsset = try container.decode(PrecompAsset.self) + decodedAssets[precompAsset.id] = precompAsset + precompAssets[precompAsset.id] = precompAsset + } else { + let imageAsset = try container.decode(ImageAsset.self) + decodedAssets[imageAsset.id] = imageAsset + imageAssets[imageAsset.id] = imageAsset + } + } + assets = decodedAssets + self.precompAssets = precompAssets + self.imageAssets = imageAssets + } + + init(value: Any) throws { + guard let dictionaries = value as? [[String: Any]] else { + throw InitializableError.invalidInput + } + var decodedAssets = [String : Asset]() + var imageAssets = [String : ImageAsset]() + var precompAssets = [String : PrecompAsset]() + try dictionaries.forEach { dictionary in + if dictionary[PrecompAsset.CodingKeys.layers.rawValue] != nil { + let asset = try PrecompAsset(dictionary: dictionary) + decodedAssets[asset.id] = asset + precompAssets[asset.id] = asset + } else { + let asset = try ImageAsset(dictionary: dictionary) + decodedAssets[asset.id] = asset + imageAssets[asset.id] = asset + } + } + assets = decodedAssets + self.precompAssets = precompAssets + self.imageAssets = imageAssets + } + + // MARK: Internal + + /// The Assets + let assets: [String: Asset] + + let imageAssets: [String: ImageAsset] + let precompAssets: [String: PrecompAsset] + + func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(contentsOf: Array(assets.values)) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/ImageAsset.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/ImageAsset.cpp new file mode 100644 index 0000000000..b5a2e57d52 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/ImageAsset.cpp @@ -0,0 +1,5 @@ +#include "ImageAsset.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/ImageAsset.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/ImageAsset.hpp new file mode 100644 index 0000000000..7fa78ee6e4 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/ImageAsset.hpp @@ -0,0 +1,117 @@ +#ifndef ImageAsset_hpp +#define ImageAsset_hpp + +#include "Lottie/Private/Model/Assets/Asset.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +namespace lottie { + +class ImageAsset: public Asset { +public: + ImageAsset( + std::string id_, + std::string name_, + std::string directory_, + double width_, + double height_ + ) : Asset(id_), + name(name_), + directory(directory_), + width(width_), + height(height_) { + } + + explicit ImageAsset(json11::Json::object const &json) noexcept(false) : + Asset(json) { + name = getString(json, "p"); + directory = getString(json, "u"); + width = getDouble(json, "w"); + height = getDouble(json, "h"); + + _e = getOptionalInt(json, "e"); + _t = getOptionalString(json, "t"); + } + + virtual void toJson(json11::Json::object &json) const override { + Asset::toJson(json); + + json.insert(std::make_pair("p", name)); + json.insert(std::make_pair("u", directory)); + json.insert(std::make_pair("w", width)); + json.insert(std::make_pair("h", height)); + + if (_e.has_value()) { + json.insert(std::make_pair("e", _e.value())); + } + if (_t.has_value()) { + json.insert(std::make_pair("t", _t.value())); + } + } + +public: + /// Image name + std::string name; + + /// Image Directory + std::string directory; + + /// Image Size + double width; + double height; + + std::optional _e; + std::optional _t; +}; + +/*extension Data { + + // MARK: Lifecycle + + /// Initializes `Data` from an `ImageAsset`. + /// + /// Returns nil when the input is not recognized as valid Data URL. + /// - parameter imageAsset: The image asset that contains Data URL. + internal init?(imageAsset: ImageAsset) { + self.init(dataString: imageAsset.name) + } + + /// Initializes `Data` from a [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) String. + /// + /// Returns nil when the input is not recognized as valid Data URL. + /// - parameter dataString: The data string to parse. + /// - parameter options: Options for the string parsing. Default value is `[]`. + internal init?(dataString: String, options: DataURLReadOptions = []) { + guard + dataString.hasPrefix("data:"), + let url = URL(string: dataString) + else { + return nil + } + // The code below is needed because Data(contentsOf:) floods logs + // with messages since url doesn't have a host. This only fixes flooding logs + // when data inside Data URL is base64 encoded. + if + let base64Range = dataString.range(of: ";base64,"), + !options.contains(DataURLReadOptions.legacy) + { + let encodedString = String(dataString[base64Range.upperBound...]) + self.init(base64Encoded: encodedString) + } else { + try? self.init(contentsOf: url) + } + } + + // MARK: Internal + + internal struct DataURLReadOptions: OptionSet { + let rawValue: Int + + /// Will read Data URL using Data(contentsOf:) + static let legacy = DataURLReadOptions(rawValue: 1 << 0) + } + +};*/ + +} + +#endif /* ImageAsset_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/ImageAsset.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/ImageAsset.swift new file mode 100644 index 0000000000..9b5df8f2aa --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/ImageAsset.swift @@ -0,0 +1,112 @@ +// +// ImageAsset.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/9/19. +// + +import Foundation + +// MARK: - ImageAsset + +public final class ImageAsset: Asset { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: ImageAsset.CodingKeys.self) + name = try container.decode(String.self, forKey: .name) + directory = try container.decode(String.self, forKey: .directory) + width = try container.decode(Double.self, forKey: .width) + height = try container.decode(Double.self, forKey: .height) + try super.init(from: decoder) + } + + required init(dictionary: [String: Any]) throws { + name = try dictionary.value(for: CodingKeys.name) + directory = try dictionary.value(for: CodingKeys.directory) + width = try dictionary.value(for: CodingKeys.width) + height = try dictionary.value(for: CodingKeys.height) + try super.init(dictionary: dictionary) + } + + // MARK: Public + + /// Image name + public let name: String + + /// Image Directory + public let directory: String + + /// Image Size + public let width: Double + + public let height: Double + + override public func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(name, forKey: .name) + try container.encode(directory, forKey: .directory) + try container.encode(width, forKey: .width) + try container.encode(height, forKey: .height) + } + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case name = "p" + case directory = "u" + case width = "w" + case height = "h" + } +} + +extension Data { + + // MARK: Lifecycle + + /// Initializes `Data` from an `ImageAsset`. + /// + /// Returns nil when the input is not recognized as valid Data URL. + /// - parameter imageAsset: The image asset that contains Data URL. + internal init?(imageAsset: ImageAsset) { + self.init(dataString: imageAsset.name) + } + + /// Initializes `Data` from a [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) String. + /// + /// Returns nil when the input is not recognized as valid Data URL. + /// - parameter dataString: The data string to parse. + /// - parameter options: Options for the string parsing. Default value is `[]`. + internal init?(dataString: String, options: DataURLReadOptions = []) { + guard + dataString.hasPrefix("data:"), + let url = URL(string: dataString) + else { + return nil + } + // The code below is needed because Data(contentsOf:) floods logs + // with messages since url doesn't have a host. This only fixes flooding logs + // when data inside Data URL is base64 encoded. + if + let base64Range = dataString.range(of: ";base64,"), + !options.contains(DataURLReadOptions.legacy) + { + let encodedString = String(dataString[base64Range.upperBound...]) + self.init(base64Encoded: encodedString) + } else { + try? self.init(contentsOf: url) + } + } + + // MARK: Internal + + internal struct DataURLReadOptions: OptionSet { + let rawValue: Int + + /// Will read Data URL using Data(contentsOf:) + static let legacy = DataURLReadOptions(rawValue: 1 << 0) + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/PrecompAsset.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/PrecompAsset.cpp new file mode 100644 index 0000000000..976044565a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/PrecompAsset.cpp @@ -0,0 +1,5 @@ +#include "PrecompAsset.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/PrecompAsset.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/PrecompAsset.hpp new file mode 100644 index 0000000000..36b5aa542f --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/PrecompAsset.hpp @@ -0,0 +1,62 @@ +#ifndef PrecompAsset_hpp +#define PrecompAsset_hpp + +#include "Lottie/Private/Model/Assets/Asset.hpp" +#include "Lottie/Private/Model/Layers/LayerModel.hpp" +#include "Lottie/Private/Model/Layers/LayerModelSerialization.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +#include + +namespace lottie { + +class PrecompAsset: public Asset { +public: + PrecompAsset( + std::string const &id_, + std::vector> const &layers_ + ) : Asset(id_), + layers(layers_) { + } + + explicit PrecompAsset(json11::Json::object const &json) noexcept(false) : + Asset(json) { + frameRate = getOptionalDouble(json, "fr"); + + auto layerDictionaries = getObjectArray(json, "layers"); + for (size_t i = 0; i < layerDictionaries.size(); i++) { + try { + auto layer = parseLayerModel(layerDictionaries[i]); + layers.push_back(layer); + } catch(...) { + throw LottieParsingException(); + } + } + } + + virtual void toJson(json11::Json::object &json) const override { + Asset::toJson(json); + + json11::Json::array layerArray; + for (const auto &layer : layers) { + json11::Json::object layerJson; + layer->toJson(layerJson); + layerArray.push_back(layerJson); + } + json.insert(std::make_pair("layers", layerArray)); + + if (frameRate.has_value()) { + json.insert(std::make_pair("fr", frameRate.value())); + } + } + +public: + /// Layers of the precomp + std::vector> layers; + + std::optional frameRate; +}; + +} + +#endif /* PrecompAsset_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/PrecompAsset.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/PrecompAsset.swift new file mode 100644 index 0000000000..e26ee7af54 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Assets/PrecompAsset.swift @@ -0,0 +1,40 @@ +// +// PrecompAsset.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/9/19. +// + +import Foundation + +final class PrecompAsset: Asset { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: PrecompAsset.CodingKeys.self) + layers = try container.decode([LayerModel].self, ofFamily: LayerType.self, forKey: .layers) + try super.init(from: decoder) + } + + required init(dictionary: [String: Any]) throws { + let layerDictionaries: [[String: Any]] = try dictionary.value(for: CodingKeys.layers) + layers = try [LayerModel].fromDictionaries(layerDictionaries) + try super.init(dictionary: dictionary) + } + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case layers + } + + /// Layers of the precomp + let layers: [LayerModel] + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(layers, forKey: .layers) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/DictionaryInitializable.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/DictionaryInitializable.swift new file mode 100644 index 0000000000..b53443a0d8 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/DictionaryInitializable.swift @@ -0,0 +1,67 @@ +// +// DictionaryInitializable.swift +// Lottie +// +// Created by Marcelo Fabri on 5/5/22. +// + +import Foundation + +// MARK: - InitializableError + +enum InitializableError: Error { + case invalidInput +} + +// MARK: - DictionaryInitializable + +protocol DictionaryInitializable { + + init(dictionary: [String: Any]) throws + +} + +// MARK: - AnyInitializable + +protocol AnyInitializable { + + init(value: Any) throws + +} + +extension Dictionary { + + @_disfavoredOverload + func value(for key: KeyType) throws -> T where KeyType.RawValue == Key { + guard let value = self[key.rawValue] as? T else { + throw InitializableError.invalidInput + } + return value + } + + func value(for key: KeyType) throws -> T where KeyType.RawValue == Key { + if let value = self[key.rawValue] as? T { + return value + } + + if let value = self[key.rawValue] { + return try T(value: value) + } + + throw InitializableError.invalidInput + } + +} + +// MARK: - Array + AnyInitializable + +extension Array: AnyInitializable where Element == Double { + + init(value: Any) throws { + guard let array = value as? [Double] else { + throw InitializableError.invalidInput + } + self = array + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Extensions/Bundle.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Extensions/Bundle.swift new file mode 100644 index 0000000000..0b57604805 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Extensions/Bundle.swift @@ -0,0 +1,34 @@ +import Foundation +#if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst) +import UIKit +#endif + +extension Bundle { + func getAnimationData(_ name: String, subdirectory: String? = nil) throws -> Data? { + // Check for files in the bundle at the given path + let name = name.removingJSONSuffix() + if let url = url(forResource: name, withExtension: "json", subdirectory: subdirectory) { + return try Data(contentsOf: url) + } + + // Check for data assets (not available on macOS) + #if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst) + let assetKey = subdirectory != nil ? "\(subdirectory ?? "")/\(name)" : name + return NSDataAsset(name: assetKey, bundle: self)?.data + #else + return nil + #endif + } +} + +extension String { + fileprivate func removingJSONSuffix() -> String { + // Allow filenames to be passed with a ".json" extension (but not other extensions) + // to keep the behavior from Lottie 2.x - instead of failing to load the animation + guard hasSuffix(".json") else { + return self + } + + return (self as NSString).deletingPathExtension + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Extensions/KeyedDecodingContainerExtensions.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Extensions/KeyedDecodingContainerExtensions.swift new file mode 100644 index 0000000000..c0d8d75508 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Extensions/KeyedDecodingContainerExtensions.swift @@ -0,0 +1,44 @@ +// From: https://medium.com/@kewindannerfjordremeczki/swift-4-0-decodable-heterogeneous-collections-ecc0e6b468cf + +import Foundation + +// MARK: - ClassFamily + +/// To support a new class family, create an enum that conforms to this protocol and contains the different types. +protocol ClassFamily: Decodable { + /// The discriminator key. + static var discriminator: Discriminator { get } + + /// Returns the class type of the object corresponding to the value. + func getType() -> AnyObject.Type +} + +// MARK: - Discriminator + +/// Discriminator key enum used to retrieve discriminator fields in JSON payloads. +enum Discriminator: String, CodingKey { + case type = "ty" +} + +extension KeyedDecodingContainer { + + /// Decode a heterogeneous list of objects for a given family. + /// - Parameters: + /// - heterogeneousType: The decodable type of the list. + /// - family: The ClassFamily enum for the type family. + /// - key: The CodingKey to look up the list in the current container. + /// - Returns: The resulting list of heterogeneousType elements. + func decode(_: [T].Type, ofFamily family: U.Type, forKey key: K) throws -> [T] { + var container = try nestedUnkeyedContainer(forKey: key) + var list = [T]() + var tmpContainer = container + while !container.isAtEnd { + let typeContainer = try container.nestedContainer(keyedBy: Discriminator.self) + let family: U = try typeContainer.decode(U.self, forKey: U.discriminator) + if let type = family.getType() as? T.Type { + list.append(try tmpContainer.decode(type)) + } + } + return list + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Keyframes/KeyframeData.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Keyframes/KeyframeData.swift new file mode 100644 index 0000000000..20f4ba8372 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Keyframes/KeyframeData.swift @@ -0,0 +1,113 @@ +// +// Keyframe.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/7/19. +// + +import CoreGraphics +import Foundation + +// MARK: - KeyframeData + +/// A generic class used to parse and remap keyframe json. +/// +/// Keyframe json has a couple of different variations and formats depending on the +/// type of keyframea and also the version of the JSON. By parsing the raw data +/// we can reconfigure it into a constant format. +final class KeyframeData { + + // MARK: Lifecycle + + init( + startValue: T?, + endValue: T?, + time: AnimationFrameTime?, + hold: Int?, + inTangent: Vector2D?, + outTangent: Vector2D?, + spatialInTangent: Vector3D?, + spatialOutTangent: Vector3D?) + { + self.startValue = startValue + self.endValue = endValue + self.time = time + self.hold = hold + self.inTangent = inTangent + self.outTangent = outTangent + self.spatialInTangent = spatialInTangent + self.spatialOutTangent = spatialOutTangent + } + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case startValue = "s" + case endValue = "e" + case time = "t" + case hold = "h" + case inTangent = "i" + case outTangent = "o" + case spatialInTangent = "ti" + case spatialOutTangent = "to" + } + + /// The start value of the keyframe + let startValue: T? + /// The End value of the keyframe. Note: Newer versions animation json do not have this field. + let endValue: T? + /// The time in frames of the keyframe. + let time: AnimationFrameTime? + /// A hold keyframe freezes interpolation until the next keyframe that is not a hold. + let hold: Int? + + /// The in tangent for the time interpolation curve. + let inTangent: Vector2D? + /// The out tangent for the time interpolation curve. + let outTangent: Vector2D? + + /// The spacial in tangent of the vector. + let spatialInTangent: Vector3D? + /// The spacial out tangent of the vector. + let spatialOutTangent: Vector3D? + + var isHold: Bool { + if let hold = hold { + return hold > 0 + } + return false + } +} + +// MARK: Encodable + +extension KeyframeData: Encodable where T: Encodable { } + +// MARK: Decodable + +extension KeyframeData: Decodable where T: Decodable { } + +// MARK: DictionaryInitializable + +extension KeyframeData: DictionaryInitializable where T: AnyInitializable { + convenience init(dictionary: [String: Any]) throws { + let startValue = try? dictionary[CodingKeys.startValue.rawValue].flatMap(T.init) + let endValue = try? dictionary[CodingKeys.endValue.rawValue].flatMap(T.init) + let time: AnimationFrameTime? = try? dictionary.value(for: CodingKeys.time) + let hold: Int? = try? dictionary.value(for: CodingKeys.hold) + let inTangent: Vector2D? = try? dictionary.value(for: CodingKeys.inTangent) + let outTangent: Vector2D? = try? dictionary.value(for: CodingKeys.outTangent) + let spatialInTangent: Vector3D? = try? dictionary.value(for: CodingKeys.spatialInTangent) + let spatialOutTangent: Vector3D? = try? dictionary.value(for: CodingKeys.spatialOutTangent) + + self.init( + startValue: startValue, + endValue: endValue, + time: time, + hold: hold, + inTangent: inTangent, + outTangent: outTangent, + spatialInTangent: spatialInTangent, + spatialOutTangent: spatialOutTangent) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Keyframes/KeyframeGroup.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Keyframes/KeyframeGroup.cpp new file mode 100644 index 0000000000..2f0c1a788a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Keyframes/KeyframeGroup.cpp @@ -0,0 +1,5 @@ +#include "KeyframeGroup.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Keyframes/KeyframeGroup.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Keyframes/KeyframeGroup.hpp new file mode 100644 index 0000000000..ce591c7655 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Keyframes/KeyframeGroup.hpp @@ -0,0 +1,150 @@ +#ifndef KeyframeGroup_hpp +#define KeyframeGroup_hpp + +#include "Lottie/Public/Keyframes/Keyframe.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +#include + +namespace lottie { + +/// Used for coding/decoding a group of Keyframes by type. +/// +/// Keyframe data is wrapped in a dictionary { "k" : KeyframeData }. +/// The keyframe data can either be an array of keyframes or, if no animation is present, the raw value. +/// This helper object is needed to properly decode the json. + +template +class KeyframeGroup { +public: + KeyframeGroup(std::vector> &&keyframes_) : + keyframes(std::move(keyframes_)), + isSingle(false) { + } + + KeyframeGroup(T const &value_) : + keyframes({ Keyframe(value_, std::nullopt, std::nullopt) }), + isSingle(false) { + } + + KeyframeGroup(json11::Json::object const &json) noexcept(false) { + isAnimated = getOptionalInt(json, "a"); + expression = getOptionalAny(json, "x"); + expressionIndex = getOptionalInt(json, "ix"); + _extraL = getOptionalInt(json, "l"); + + auto containerData = getAny(json, "k"); + + try { + LottieParsingException::Guard expectedException; + T keyframeData = T(containerData); + keyframes.push_back(Keyframe(keyframeData, std::nullopt, std::nullopt)); + isSingle = true; + } catch(...) { + // Decode and array of keyframes. + // + // Body Movin and Lottie deal with keyframes in different ways. + // + // A keyframe object in Body movin defines a span of time with a START + // and an END, from the current keyframe time to the next keyframe time. + // + // A keyframe object in Lottie defines a singular point in time/space. + // This point has an in-tangent and an out-tangent. + // + // To properly decode this we must iterate through keyframes while holding + // reference to the previous keyframe. + + if (!containerData.is_array()) { + throw LottieParsingException(); + } + + std::optional> previousKeyframeData; + for (const auto &containerItem : containerData.array_items()) { + // Ensure that Time and Value are present. + auto keyframeData = KeyframeData(containerItem); + rawKeyframeData.push_back(keyframeData); + + std::optional value; + if (keyframeData.startValue.has_value()) { + value = keyframeData.startValue; + } else if (previousKeyframeData.has_value()) { + value = previousKeyframeData->endValue; + } + if (!value.has_value()) { + throw LottieParsingException(); + } + if (!keyframeData.time.has_value()) { + throw LottieParsingException(); + } + + std::optional inTangent; + std::optional spatialInTangent; + if (previousKeyframeData.has_value()) { + inTangent = previousKeyframeData->inTangent; + spatialInTangent = previousKeyframeData->spatialInTangent; + } + + keyframes.emplace_back( + value.value(), + keyframeData.time.value(), + keyframeData.isHold(), + inTangent, + keyframeData.outTangent, + spatialInTangent, + keyframeData.spatialOutTangent + ); + + previousKeyframeData = keyframeData; + } + + isSingle = false; + } + } + + json11::Json::object toJson() const { + json11::Json::object result; + + assert(!keyframes.empty()); + + if (keyframes.size() == 1 && isSingle) { + result.insert(std::make_pair("k", keyframes[0].value.toJson())); + } else { + json11::Json::array containerData; + + for (const auto &keyframe : rawKeyframeData) { + containerData.push_back(keyframe.toJson()); + } + + result.insert(std::make_pair("k", containerData)); + } + + if (isAnimated.has_value()) { + result.insert(std::make_pair("a", isAnimated.value())); + } + if (expression.has_value()) { + result.insert(std::make_pair("x", expression.value())); + } + if (expressionIndex.has_value()) { + result.insert(std::make_pair("ix", expressionIndex.value())); + } + if (_extraL.has_value()) { + result.insert(std::make_pair("l", _extraL.value())); + } + + return result; + } + +public: + std::vector> keyframes; + std::optional isAnimated; + + std::optional expression; + std::optional expressionIndex; + std::vector> rawKeyframeData; + bool isSingle = false; + std::optional _extraL; +}; + +} + +#endif /* KeyframeGroup_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Keyframes/KeyframeGroup.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Keyframes/KeyframeGroup.swift new file mode 100644 index 0000000000..74c09cca7a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Keyframes/KeyframeGroup.swift @@ -0,0 +1,197 @@ +// +// KeyframeGroup.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/14/19. +// + +import Foundation + +// MARK: - KeyframeGroup + +/// Used for coding/decoding a group of Keyframes by type. +/// +/// Keyframe data is wrapped in a dictionary { "k" : KeyframeData }. +/// The keyframe data can either be an array of keyframes or, if no animation is present, the raw value. +/// This helper object is needed to properly decode the json. + +final class KeyframeGroup { + + // MARK: Lifecycle + + init(keyframes: ContiguousArray>) { + self.keyframes = keyframes + } + + init(_ value: T) { + keyframes = [Keyframe(value)] + } + + // MARK: Internal + + enum KeyframeWrapperKey: String, CodingKey { + case keyframeData = "k" + } + + let keyframes: ContiguousArray> + +} + +// MARK: Decodable + +extension KeyframeGroup: Decodable where T: Decodable { + convenience init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: KeyframeWrapperKey.self) + + if let keyframeData: T = try? container.decode(T.self, forKey: .keyframeData) { + /// Try to decode raw value; No keyframe data. + self.init(keyframes: [Keyframe(keyframeData)]) + } else { + // Decode and array of keyframes. + // + // Body Movin and Lottie deal with keyframes in different ways. + // + // A keyframe object in Body movin defines a span of time with a START + // and an END, from the current keyframe time to the next keyframe time. + // + // A keyframe object in Lottie defines a singular point in time/space. + // This point has an in-tangent and an out-tangent. + // + // To properly decode this we must iterate through keyframes while holding + // reference to the previous keyframe. + + var keyframesContainer = try container.nestedUnkeyedContainer(forKey: .keyframeData) + var keyframes = ContiguousArray>() + var previousKeyframeData: KeyframeData? + while !keyframesContainer.isAtEnd { + // Ensure that Time and Value are present. + + let keyframeData = try keyframesContainer.decode(KeyframeData.self) + + guard + let value: T = keyframeData.startValue ?? previousKeyframeData?.endValue, + let time = keyframeData.time else + { + /// Missing keyframe data. JSON must be corrupt. + throw DecodingError.dataCorruptedError( + forKey: KeyframeWrapperKey.keyframeData, + in: container, + debugDescription: "Missing keyframe data.") + } + + keyframes.append(Keyframe( + value: value, + time: AnimationFrameTime(time), + isHold: keyframeData.isHold, + inTangent: previousKeyframeData?.inTangent, + outTangent: keyframeData.outTangent, + spatialInTangent: previousKeyframeData?.spatialInTangent, + spatialOutTangent: keyframeData.spatialOutTangent)) + previousKeyframeData = keyframeData + } + self.init(keyframes: keyframes) + } + } +} + +// MARK: Encodable + +extension KeyframeGroup: Encodable where T: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: KeyframeWrapperKey.self) + + if keyframes.count == 1 { + let keyframe = keyframes[0] + try container.encode(keyframe.value, forKey: .keyframeData) + } else { + var keyframeContainer = container.nestedUnkeyedContainer(forKey: .keyframeData) + + for i in 1..( + startValue: keyframe.value, + endValue: nextKeyframe.value, + time: keyframe.time, + hold: keyframe.isHold ? 1 : nil, + inTangent: nextKeyframe.inTangent, + outTangent: keyframe.outTangent, + spatialInTangent: nil, + spatialOutTangent: nil) + try keyframeContainer.encode(keyframeData) + } + } + } +} + +// MARK: DictionaryInitializable + +extension KeyframeGroup: DictionaryInitializable where T: AnyInitializable { + convenience init(dictionary: [String: Any]) throws { + var keyframes = ContiguousArray>() + if + let rawValue = dictionary[KeyframeWrapperKey.keyframeData.rawValue], + let value = try? T(value: rawValue) + { + keyframes = [Keyframe(value)] + } else { + var frameDictionaries: [[String: Any]] + if let singleFrameDictionary = dictionary[KeyframeWrapperKey.keyframeData.rawValue] as? [String: Any] { + frameDictionaries = [singleFrameDictionary] + } else { + frameDictionaries = try dictionary.value(for: KeyframeWrapperKey.keyframeData) + } + var previousKeyframeData: KeyframeData? + for frameDictionary in frameDictionaries { + let data = try KeyframeData(dictionary: frameDictionary) + guard + let value: T = data.startValue ?? previousKeyframeData?.endValue, + let time = data.time else + { + throw InitializableError.invalidInput + } + keyframes.append(Keyframe( + value: value, + time: time, + isHold: data.isHold, + inTangent: previousKeyframeData?.inTangent, + outTangent: data.outTangent, + spatialInTangent: previousKeyframeData?.spatialInTangent, + spatialOutTangent: data.spatialOutTangent)) + previousKeyframeData = data + } + } + + self.init(keyframes: keyframes) + } +} + +// MARK: Equatable + +extension KeyframeGroup: Equatable where T: Equatable { + static func == (_ lhs: KeyframeGroup, _ rhs: KeyframeGroup) -> Bool { + lhs.keyframes == rhs.keyframes + } +} + +// MARK: Hashable + +extension KeyframeGroup: Hashable where T: Hashable { + func hash(into hasher: inout Hasher) { + hasher.combine(keyframes) + } +} + +extension Keyframe { + /// Creates a copy of this `Keyframe` with the same timing data, but a different value + func withValue(_ newValue: Value) -> Keyframe { + Keyframe( + value: newValue, + time: time, + isHold: isHold, + inTangent: inTangent, + outTangent: outTangent, + spatialInTangent: spatialInTangent, + spatialOutTangent: spatialOutTangent) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/ImageLayerModel.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/ImageLayerModel.cpp new file mode 100644 index 0000000000..4251973760 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/ImageLayerModel.cpp @@ -0,0 +1,5 @@ +#include "ImageLayerModel.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/ImageLayerModel.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/ImageLayerModel.hpp new file mode 100644 index 0000000000..b929fcc64a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/ImageLayerModel.hpp @@ -0,0 +1,38 @@ +#ifndef ImageLayerModel_hpp +#define ImageLayerModel_hpp + +#include "Lottie/Private/Model/Layers/LayerModel.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +namespace lottie { + +/// A layer that holds an image. +class ImageLayerModel: public LayerModel { +public: + explicit ImageLayerModel(json11::Json::object const &json) noexcept(false) : + LayerModel(json) { + referenceID = getString(json, "refId"); + + _sc = getOptionalString(json, "sc"); + } + + virtual void toJson(json11::Json::object &json) const override { + LayerModel::toJson(json); + + json.insert(std::make_pair("refId", referenceID)); + + if (_sc.has_value()) { + json.insert(std::make_pair("sc", _sc.value())); + } + } + +public: + /// The reference ID of the image. + std::string referenceID; + + std::optional _sc; +}; + +} + +#endif /* ImageLayerModel_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/ImageLayerModel.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/ImageLayerModel.swift new file mode 100644 index 0000000000..91ea324799 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/ImageLayerModel.swift @@ -0,0 +1,42 @@ +// +// ImageLayer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/8/19. +// + +import Foundation + +/// A layer that holds an image. +final class ImageLayerModel: LayerModel { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: ImageLayerModel.CodingKeys.self) + referenceID = try container.decode(String.self, forKey: .referenceID) + try super.init(from: decoder) + } + + required init(dictionary: [String: Any]) throws { + referenceID = try dictionary.value(for: CodingKeys.referenceID) + try super.init(dictionary: dictionary) + } + + // MARK: Internal + + /// The reference ID of the image. + let referenceID: String + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(referenceID, forKey: .referenceID) + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case referenceID = "refId" + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/LayerModel.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/LayerModel.cpp new file mode 100644 index 0000000000..6ceabadcf5 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/LayerModel.cpp @@ -0,0 +1,45 @@ +#include "LayerModel.hpp" + +namespace lottie { + +LayerType parseLayerType(json11::Json::object const &json, std::string const &key) { + if (const auto layerTypeValue = getOptionalInt(json, "ty")) { + switch (layerTypeValue.value()) { + case 0: + return LayerType::Precomp; + case 1: + return LayerType::Solid; + case 2: + return LayerType::Image; + case 3: + return LayerType::Null; + case 4: + return LayerType::Shape; + case 5: + return LayerType::Text; + default: + return LayerType::Null; + } + } else { + return LayerType::Null; + } +} + +int serializeLayerType(LayerType value) { + switch (value) { + case LayerType::Precomp: + return 0; + case LayerType::Solid: + return 1; + case LayerType::Image: + return 2; + case LayerType::Null: + return 3; + case LayerType::Shape: + return 4; + case LayerType::Text: + return 5; + } +} + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/LayerModel.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/LayerModel.hpp new file mode 100644 index 0000000000..90e2278c12 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/LayerModel.hpp @@ -0,0 +1,316 @@ +#ifndef LayerModel_hpp +#define LayerModel_hpp + +#include "Lottie/Private/Model/Objects/Transform.hpp" +#include "Lottie/Private/Model/Objects/Mask.hpp" +#include "Lottie/Private/Utility/Primitives/CoordinateSpace.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +#include +#include +#include + +namespace lottie { + +enum class LayerType { + Precomp, + Solid, + Image, + Null, + Shape, + Text +}; + +LayerType parseLayerType(json11::Json::object const &json, std::string const &key); +int serializeLayerType(LayerType value); + +enum class MatteType: int { + None = 0, + Add = 1, + Invert = 2, + Unknown = 3 +}; + +enum class BlendMode: int { + Normal = 0, + Multiply = 1, + Screen = 2, + Overlay = 3, + Darken = 4, + Lighten = 5, + ColorDodge = 6, + ColorBurn = 7, + HardLight = 8, + SoftLight = 9, + Difference = 10, + Exclusion = 11, + Hue = 12, + Saturation = 13, + Color = 14, + Luminosity = 15 +}; + +/// A base top container for shapes, images, and other view objects. +class LayerModel { +public: + explicit LayerModel(json11::Json::object const &json) noexcept(false) { + name = getOptionalString(json, "nm"); + index = getOptionalInt(json, "ind"); + + type = parseLayerType(json, "ty"); + + autoOrient = getOptionalInt(json, "ao"); + + if (const auto typeRawValue = getOptionalInt(json, "ddd")) { + if (typeRawValue.value() == 0) { + coordinateSpace = CoordinateSpace::Type2d; + } else { + coordinateSpace = CoordinateSpace::Type3d; + } + } else { + coordinateSpace = std::nullopt; + } + + inFrame = getDouble(json, "ip"); + outFrame = getDouble(json, "op"); + startTime = getDouble(json, "st"); + + transform = std::make_shared(getObject(json, "ks")); + parent = getOptionalInt(json, "parent"); + + if (const auto blendModeRawValue = getOptionalInt(json, "bm")) { + switch (blendModeRawValue.value()) { + case 0: + blendMode = BlendMode::Normal; + break; + case 1: + blendMode = BlendMode::Multiply; + break; + case 2: + blendMode = BlendMode::Screen; + break; + case 3: + blendMode = BlendMode::Overlay; + break; + case 4: + blendMode = BlendMode::Darken; + break; + case 5: + blendMode = BlendMode::Lighten; + break; + case 6: + blendMode = BlendMode::ColorDodge; + break; + case 7: + blendMode = BlendMode::ColorBurn; + break; + case 8: + blendMode = BlendMode::HardLight; + break; + case 9: + blendMode = BlendMode::SoftLight; + break; + case 10: + blendMode = BlendMode::Difference; + break; + case 11: + blendMode = BlendMode::Exclusion; + break; + case 12: + blendMode = BlendMode::Hue; + break; + case 13: + blendMode = BlendMode::Saturation; + break; + case 14: + blendMode = BlendMode::Color; + break; + case 15: + blendMode = BlendMode::Luminosity; + break; + default: + throw LottieParsingException(); + } + } + + if (const auto maskDictionaries = getOptionalObjectArray(json, "masksProperties")) { + masks = std::vector>(); + for (const auto &maskDictionary : maskDictionaries.value()) { + masks->push_back(std::make_shared(maskDictionary)); + } + } + + if (const auto timeStretchData = getOptionalDouble(json, "sr")) { + _timeStretch = timeStretchData.value(); + } + + if (const auto matteRawValue = getOptionalInt(json, "tt")) { + switch (matteRawValue.value()) { + case 0: + matte = MatteType::None; + break; + case 1: + matte = MatteType::Add; + break; + case 2: + matte = MatteType::Invert; + break; + case 3: + matte = MatteType::Unknown; + break; + default: + throw LottieParsingException(); + } + } + + if (const auto hiddenData = getOptionalBool(json, "hd")) { + hidden = hiddenData.value(); + } + + hasMask = getOptionalBool(json, "hasMask"); + td = getOptionalInt(json, "td"); + effectsData = getOptionalAny(json, "ef"); + layerClass = getOptionalString(json, "cl"); + _extraHidden = getOptionalAny(json, "hidden"); + } + + LayerModel(const LayerModel&) = delete; + LayerModel& operator=(LayerModel&) = delete; + + virtual void toJson(json11::Json::object &json) const { + if (name.has_value()) { + json.insert(std::make_pair("nm", name.value())); + } + if (index.has_value()) { + json.insert(std::make_pair("ind", index.value())); + } + + if (autoOrient.has_value()) { + json.insert(std::make_pair("ao", autoOrient.value())); + } + + json.insert(std::make_pair("ty", serializeLayerType(type))); + + if (coordinateSpace.has_value()) { + switch (coordinateSpace.value()) { + case CoordinateSpace::Type2d: + json.insert(std::make_pair("ddd", 0)); + break; + case CoordinateSpace::Type3d: + json.insert(std::make_pair("ddd", 1)); + break; + } + } + + json.insert(std::make_pair("ip", inFrame)); + json.insert(std::make_pair("op", outFrame)); + json.insert(std::make_pair("st", startTime)); + + json.insert(std::make_pair("ks", transform->toJson())); + + if (parent.has_value()) { + json.insert(std::make_pair("parent", parent.value())); + } + + if (blendMode.has_value()) { + json.insert(std::make_pair("bm", (int)blendMode.value())); + } + + if (masks.has_value()) { + json11::Json::array maskArray; + for (const auto &mask : masks.value()) { + maskArray.push_back(mask->toJson()); + } + json.insert(std::make_pair("masksProperties", maskArray)); + } + + if (_timeStretch.has_value()) { + json.insert(std::make_pair("sr", _timeStretch.value())); + } + + if (matte.has_value()) { + json.insert(std::make_pair("tt", (int)matte.value())); + } + + if (hidden.has_value()) { + json.insert(std::make_pair("hd", hidden.value())); + } + + if (hasMask.has_value()) { + json.insert(std::make_pair("hasMask", hasMask.value())); + } + if (td.has_value()) { + json.insert(std::make_pair("td", td.value())); + } + if (effectsData.has_value()) { + json.insert(std::make_pair("ef", effectsData.value())); + } + if (layerClass.has_value()) { + json.insert(std::make_pair("cl", layerClass.value())); + } + if (_extraHidden.has_value()) { + json.insert(std::make_pair("hidden", _extraHidden.value())); + } + } + + double timeStretch() { + if (_timeStretch.has_value()) { + return _timeStretch.value(); + } else { + return 1.0; + } + } + +public: + /// The readable name of the layer + std::optional name; + + /// The index of the layer + std::optional index; + + /// The type of the layer. + LayerType type; + + std::optional autoOrient; + + /// The coordinate space + std::optional coordinateSpace; + + /// The in time of the layer in frames. + double inFrame; + /// The out time of the layer in frames. + double outFrame; + + /// The start time of the layer in frames. + double startTime; + + /// The transform of the layer + std::shared_ptr transform; + + /// The index of the parent layer, if applicable. + std::optional parent; + + /// The blending mode for the layer + std::optional blendMode; + + /// An array of masks for the layer. + std::optional>> masks; + + /// A number that stretches time by a multiplier + std::optional _timeStretch; + + /// The type of matte if any. + std::optional matte; + + std::optional hidden; + + std::optional hasMask; + std::optional td; + std::optional effectsData; + std::optional layerClass; + std::optional _extraHidden; +}; + +} + +#endif /* LayerModel_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/LayerModel.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/LayerModel.swift new file mode 100644 index 0000000000..e4604205ce --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/LayerModel.swift @@ -0,0 +1,228 @@ +// +// Layer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/7/19. +// + +import Foundation + +// MARK: - LayerType + ClassFamily + +/// Used for mapping a heterogeneous list to classes for parsing. +extension LayerType: ClassFamily { + static var discriminator: Discriminator = .type + + func getType() -> AnyObject.Type { + switch self { + case .precomp: + return PreCompLayerModel.self + case .solid: + return SolidLayerModel.self + case .image: + return ImageLayerModel.self + case .null: + return LayerModel.self + case .shape: + return ShapeLayerModel.self + case .text: + return TextLayerModel.self + } + } +} + +// MARK: - LayerType + +public enum LayerType: Int, Codable { + case precomp + case solid + case image + case null + case shape + case text + + public init(from decoder: Decoder) throws { + self = try LayerType(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .null + } +} + +// MARK: - MatteType + +public enum MatteType: Int, Codable { + case none + case add + case invert + case unknown +} + +// MARK: - BlendMode + +public enum BlendMode: Int, Codable { + case normal + case multiply + case screen + case overlay + case darken + case lighten + case colorDodge + case colorBurn + case hardLight + case softLight + case difference + case exclusion + case hue + case saturation + case color + case luminosity +} + +// MARK: - LayerModel + +/// A base top container for shapes, images, and other view objects. +class LayerModel: Codable, DictionaryInitializable { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: LayerModel.CodingKeys.self) + name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Layer" + index = try container.decodeIfPresent(Int.self, forKey: .index) ?? .random(in: Int.min...Int.max) + type = try container.decode(LayerType.self, forKey: .type) + coordinateSpace = try container.decodeIfPresent(CoordinateSpace.self, forKey: .coordinateSpace) ?? .type2d + inFrame = try container.decode(Double.self, forKey: .inFrame) + outFrame = try container.decode(Double.self, forKey: .outFrame) + startTime = try container.decode(Double.self, forKey: .startTime) + transform = try container.decode(Transform.self, forKey: .transform) + parent = try container.decodeIfPresent(Int.self, forKey: .parent) + blendMode = try container.decodeIfPresent(BlendMode.self, forKey: .blendMode) ?? .normal + masks = try container.decodeIfPresent([Mask].self, forKey: .masks) + timeStretch = try container.decodeIfPresent(Double.self, forKey: .timeStretch) ?? 1 + matte = try container.decodeIfPresent(MatteType.self, forKey: .matte) + hidden = try container.decodeIfPresent(Bool.self, forKey: .hidden) ?? false + } + + required init(dictionary: [String: Any]) throws { + name = (try? dictionary.value(for: CodingKeys.name)) ?? "Layer" + index = try dictionary.value(for: CodingKeys.index) ?? .random(in: Int.min...Int.max) + type = LayerType(rawValue: try dictionary.value(for: CodingKeys.type)) ?? .null + if + let coordinateSpaceRawValue = dictionary[CodingKeys.coordinateSpace.rawValue] as? Int, + let coordinateSpace = CoordinateSpace(rawValue: coordinateSpaceRawValue) + { + self.coordinateSpace = coordinateSpace + } else { + coordinateSpace = .type2d + } + inFrame = try dictionary.value(for: CodingKeys.inFrame) + outFrame = try dictionary.value(for: CodingKeys.outFrame) + startTime = try dictionary.value(for: CodingKeys.startTime) + transform = try Transform(dictionary: try dictionary.value(for: CodingKeys.transform)) + parent = try? dictionary.value(for: CodingKeys.parent) + if + let blendModeRawValue = dictionary[CodingKeys.blendMode.rawValue] as? Int, + let blendMode = BlendMode(rawValue: blendModeRawValue) + { + self.blendMode = blendMode + } else { + blendMode = .normal + } + if let maskDictionaries = dictionary[CodingKeys.masks.rawValue] as? [[String: Any]] { + masks = try maskDictionaries.map({ try Mask(dictionary: $0) }) + } else { + masks = nil + } + timeStretch = (try? dictionary.value(for: CodingKeys.timeStretch)) ?? 1 + if let matteRawValue = dictionary[CodingKeys.matte.rawValue] as? Int { + matte = MatteType(rawValue: matteRawValue) + } else { + matte = nil + } + hidden = (try? dictionary.value(for: CodingKeys.hidden)) ?? false + } + + // MARK: Internal + + /// The readable name of the layer + let name: String + + /// The index of the layer + let index: Int + + /// The type of the layer. + let type: LayerType + + /// The coordinate space + let coordinateSpace: CoordinateSpace + + /// The in time of the layer in frames. + let inFrame: Double + /// The out time of the layer in frames. + let outFrame: Double + + /// The start time of the layer in frames. + let startTime: Double + + /// The transform of the layer + let transform: Transform + + /// The index of the parent layer, if applicable. + let parent: Int? + + /// The blending mode for the layer + let blendMode: BlendMode + + /// An array of masks for the layer. + let masks: [Mask]? + + /// A number that stretches time by a multiplier + let timeStretch: Double + + /// The type of matte if any. + let matte: MatteType? + + let hidden: Bool + + // MARK: Fileprivate + + fileprivate enum CodingKeys: String, CodingKey { + case name = "nm" + case index = "ind" + case type = "ty" + case coordinateSpace = "ddd" + case inFrame = "ip" + case outFrame = "op" + case startTime = "st" + case transform = "ks" + case parent + case blendMode = "bm" + case masks = "masksProperties" + case timeStretch = "sr" + case matte = "tt" + case hidden = "hd" + } +} + +extension Array where Element == LayerModel { + + static func fromDictionaries(_ dictionaries: [[String: Any]]) throws -> [LayerModel] { + try dictionaries.compactMap { dictionary in + let layerType = dictionary[LayerModel.CodingKeys.type.rawValue] as? Int + switch LayerType(rawValue: layerType ?? LayerType.null.rawValue) { + case .precomp: + return try PreCompLayerModel(dictionary: dictionary) + case .solid: + return try SolidLayerModel(dictionary: dictionary) + case .image: + return try ImageLayerModel(dictionary: dictionary) + case .null: + return try LayerModel(dictionary: dictionary) + case .shape: + return try ShapeLayerModel(dictionary: dictionary) + case .text: + return try TextLayerModel(dictionary: dictionary) + case .none: + return nil + } + } + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/LayerModelSerialization.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/LayerModelSerialization.cpp new file mode 100644 index 0000000000..6183d51be3 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/LayerModelSerialization.cpp @@ -0,0 +1,34 @@ +#include "LayerModelSerialization.hpp" + +#include "Lottie/Private/Model/Layers/PreCompLayerModel.hpp" +#include "Lottie/Private/Model/Layers/SolidLayerModel.hpp" +#include "Lottie/Private/Model/Layers/ImageLayerModel.hpp" +#include "Lottie/Private/Model/Layers/ShapeLayerModel.hpp" +#include "Lottie/Private/Model/Layers/TextLayerModel.hpp" + +namespace lottie { + +std::shared_ptr parseLayerModel(json11::Json::object const &json) noexcept(false) { + LayerType layerType = parseLayerType(json, "ty"); + + switch (layerType) { + case LayerType::Precomp: + return std::make_shared(json); + case LayerType::Solid: + return std::make_shared(json); + case LayerType::Image: + return std::make_shared(json); + case LayerType::Null: + return std::make_shared(json); + case LayerType::Shape: + try { + return std::make_shared(json); + } catch(...) { + throw LottieParsingException(); + } + case LayerType::Text: + return std::make_shared(json); + } +} + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/LayerModelSerialization.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/LayerModelSerialization.hpp new file mode 100644 index 0000000000..9619f45f89 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/LayerModelSerialization.hpp @@ -0,0 +1,14 @@ +#ifndef LayerModelSerialization_hpp +#define LayerModelSerialization_hpp + +#include "json11/json11.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" +#include "Lottie/Private/Model/Layers/LayerModel.hpp" + +namespace lottie { + +std::shared_ptr parseLayerModel(json11::Json::object const &json) noexcept(false); + +} + +#endif /* LayerModelSerialization_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/PreCompLayerModel.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/PreCompLayerModel.cpp new file mode 100644 index 0000000000..2a4c7b1363 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/PreCompLayerModel.cpp @@ -0,0 +1,5 @@ +#include "PreCompLayerModel.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/PreCompLayerModel.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/PreCompLayerModel.hpp new file mode 100644 index 0000000000..f2f4d6bc83 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/PreCompLayerModel.hpp @@ -0,0 +1,55 @@ +#ifndef PreCompLayerModel_hpp +#define PreCompLayerModel_hpp + +#include "Lottie/Private/Model/Layers/LayerModel.hpp" +#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" +#include "Lottie/Public/Primitives/Vectors.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +#include + +namespace lottie { + +/// A layer that holds another animation composition. +class PreCompLayerModel: public LayerModel { +public: + PreCompLayerModel(json11::Json::object const &json) : + LayerModel(json) { + referenceID = getString(json, "refId"); + if (const auto timeRemappingData = getOptionalObject(json, "tm")) { + timeRemapping = KeyframeGroup(timeRemappingData.value()); + } + width = getDouble(json, "w"); + height = getDouble(json, "h"); + } + + virtual void toJson(json11::Json::object &json) const override { + LayerModel::toJson(json); + + json.insert(std::make_pair("refId", referenceID)); + + if (timeRemapping.has_value()) { + json.insert(std::make_pair("tm", timeRemapping->toJson())); + } + + json.insert(std::make_pair("w", width)); + json.insert(std::make_pair("h", height)); + } + +public: + /// The reference ID of the precomp. + std::string referenceID; + + /// A value that remaps time over time. + std::optional> timeRemapping; + + /// Precomp Width + double width; + + /// Precomp Height + double height; +}; + +} + +#endif /* PreCompLayerModel_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/PreCompLayerModel.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/PreCompLayerModel.swift new file mode 100644 index 0000000000..d9e426a2a5 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/PreCompLayerModel.swift @@ -0,0 +1,67 @@ +// +// PreCompLayer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/8/19. +// + +import Foundation + +/// A layer that holds another animation composition. +final class PreCompLayerModel: LayerModel { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: PreCompLayerModel.CodingKeys.self) + referenceID = try container.decode(String.self, forKey: .referenceID) + timeRemapping = try container.decodeIfPresent(KeyframeGroup.self, forKey: .timeRemapping) + width = try container.decode(Double.self, forKey: .width) + height = try container.decode(Double.self, forKey: .height) + try super.init(from: decoder) + } + + required init(dictionary: [String: Any]) throws { + referenceID = try dictionary.value(for: CodingKeys.referenceID) + if let timeRemappingDictionary = dictionary[CodingKeys.timeRemapping.rawValue] as? [String: Any] { + timeRemapping = try KeyframeGroup(dictionary: timeRemappingDictionary) + } else { + timeRemapping = nil + } + width = try dictionary.value(for: CodingKeys.width) + height = try dictionary.value(for: CodingKeys.height) + try super.init(dictionary: dictionary) + } + + // MARK: Internal + + /// The reference ID of the precomp. + let referenceID: String + + /// A value that remaps time over time. + let timeRemapping: KeyframeGroup? + + /// Precomp Width + let width: Double + + /// Precomp Height + let height: Double + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(referenceID, forKey: .referenceID) + try container.encode(timeRemapping, forKey: .timeRemapping) + try container.encode(width, forKey: .width) + try container.encode(height, forKey: .height) + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case referenceID = "refId" + case timeRemapping = "tm" + case width = "w" + case height = "h" + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/ShapeLayerModel.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/ShapeLayerModel.cpp new file mode 100644 index 0000000000..608dca567b --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/ShapeLayerModel.cpp @@ -0,0 +1,5 @@ +#include "ShapeLayerModel.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/ShapeLayerModel.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/ShapeLayerModel.hpp new file mode 100644 index 0000000000..25111fd674 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/ShapeLayerModel.hpp @@ -0,0 +1,43 @@ +#ifndef ShapeLayerModel_hpp +#define ShapeLayerModel_hpp + +#include "Lottie/Private/Model/Layers/LayerModel.hpp" +#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +#include + +namespace lottie { + +/// A layer that holds vector shape objects. +class ShapeLayerModel: public LayerModel { +public: + ShapeLayerModel(json11::Json::object const &json) noexcept(false) : + LayerModel(json) { + auto shapeItemsData = getObjectArray(json, "shapes"); + for (const auto &shapeItemData : shapeItemsData) { + items.push_back(parseShapeItem(shapeItemData)); + } + } + + virtual void toJson(json11::Json::object &json) const override { + LayerModel::toJson(json); + + json11::Json::array shapeItemArray; + for (const auto &item : items) { + json11::Json::object itemJson; + item->toJson(itemJson); + shapeItemArray.push_back(itemJson); + } + + json.insert(std::make_pair("shapes", shapeItemArray)); + } + +public: + /// A list of shape items. + std::vector> items; +}; + +} + +#endif /* ShapeLayerModel_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/ShapeLayerModel.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/ShapeLayerModel.swift new file mode 100644 index 0000000000..b9562ad420 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/ShapeLayerModel.swift @@ -0,0 +1,43 @@ +// +// ShapeLayer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/8/19. +// + +import Foundation + +/// A layer that holds vector shape objects. +final class ShapeLayerModel: LayerModel { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: ShapeLayerModel.CodingKeys.self) + items = try container.decode([ShapeItem].self, ofFamily: ShapeType.self, forKey: .items) + try super.init(from: decoder) + } + + required init(dictionary: [String: Any]) throws { + let itemDictionaries: [[String: Any]] = try dictionary.value(for: CodingKeys.items) + items = try [ShapeItem].fromDictionaries(itemDictionaries) + try super.init(dictionary: dictionary) + } + + // MARK: Internal + + /// A list of shape items. + let items: [ShapeItem] + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(items, forKey: .items) + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case items = "shapes" + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/SolidLayerModel.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/SolidLayerModel.cpp new file mode 100644 index 0000000000..153c283953 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/SolidLayerModel.cpp @@ -0,0 +1,5 @@ +#include "SolidLayerModel.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/SolidLayerModel.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/SolidLayerModel.hpp new file mode 100644 index 0000000000..0ba379c7a8 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/SolidLayerModel.hpp @@ -0,0 +1,40 @@ +#ifndef SolidLayerModel_hpp +#define SolidLayerModel_hpp + +#include "Lottie/Private/Model/Layers/LayerModel.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +namespace lottie { + +/// A layer that holds a solid color. +class SolidLayerModel: public LayerModel { +public: + explicit SolidLayerModel(json11::Json::object const &json) noexcept(false) : + LayerModel(json) { + colorHex = getString(json, "sc"); + width = getDouble(json, "sw"); + height = getDouble(json, "sh"); + } + + virtual void toJson(json11::Json::object &json) const override { + LayerModel::toJson(json); + + json.insert(std::make_pair("sc", colorHex)); + json.insert(std::make_pair("sw", width)); + json.insert(std::make_pair("sh", height)); + } + +public: + /// The color of the solid in Hex // Change to value provider. + std::string colorHex; + + /// The Width of the color layer + double width; + + /// The height of the color layer + double height; +}; + +} + +#endif /* SolidLayerModel_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/SolidLayerModel.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/SolidLayerModel.swift new file mode 100644 index 0000000000..9ad232a238 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/SolidLayerModel.swift @@ -0,0 +1,56 @@ +// +// SolidLayer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/8/19. +// + +import Foundation + +/// A layer that holds a solid color. +final class SolidLayerModel: LayerModel { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: SolidLayerModel.CodingKeys.self) + colorHex = try container.decode(String.self, forKey: .colorHex) + width = try container.decode(Double.self, forKey: .width) + height = try container.decode(Double.self, forKey: .height) + try super.init(from: decoder) + } + + required init(dictionary: [String: Any]) throws { + colorHex = try dictionary.value(for: CodingKeys.colorHex) + width = try dictionary.value(for: CodingKeys.width) + height = try dictionary.value(for: CodingKeys.height) + try super.init(dictionary: dictionary) + } + + // MARK: Internal + + /// The color of the solid in Hex // Change to value provider. + let colorHex: String + + /// The Width of the color layer + let width: Double + + /// The height of the color layer + let height: Double + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(colorHex, forKey: .colorHex) + try container.encode(width, forKey: .width) + try container.encode(height, forKey: .height) + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case colorHex = "sc" + case width = "sw" + case height = "sh" + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/TextLayerModel.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/TextLayerModel.cpp new file mode 100644 index 0000000000..9e2ad034b1 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/TextLayerModel.cpp @@ -0,0 +1,5 @@ +#include "TextLayerModel.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/TextLayerModel.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/TextLayerModel.hpp new file mode 100644 index 0000000000..5b6b7ae77e --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/TextLayerModel.hpp @@ -0,0 +1,81 @@ +#ifndef TextLayerModel_hpp +#define TextLayerModel_hpp + +#include "Lottie/Private/Model/Layers/LayerModel.hpp" +#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" +#include "Lottie/Private/Model/Text/TextDocument.hpp" +#include "Lottie/Private/Model/Text/TextAnimator.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +namespace lottie { + +/// A layer that holds text. +class TextLayerModel: public LayerModel { +public: + TextLayerModel(json11::Json::object const &json) : + LayerModel(json), + text(KeyframeGroup(TextDocument( + "", + 0.0, + "", + TextJustification::Left, + 0, + 0.0, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt + ))) { + auto textContainer = getObject(json, "t"); + + auto textData = getObject(textContainer, "d"); + text = KeyframeGroup(textData); + + if (auto animatorsData = getOptionalObjectArray(textContainer, "a")) { + for (const auto &animatorData : animatorsData.value()) { + animators.push_back(std::make_shared(animatorData)); + } + } + + _extraM = getOptionalAny(textContainer, "m"); + _extraP = getOptionalAny(textContainer, "p"); + } + + virtual void toJson(json11::Json::object &json) const override { + LayerModel::toJson(json); + + json11::Json::object textContainer; + textContainer.insert(std::make_pair("d", text.toJson())); + if (_extraM.has_value()) { + textContainer.insert(std::make_pair("m", _extraM.value())); + } + if (_extraP.has_value()) { + textContainer.insert(std::make_pair("p", _extraP.value())); + } + json11::Json::array animatorArray; + for (const auto &animator : animators) { + animatorArray.push_back(animator->toJson()); + } + textContainer.insert(std::make_pair("a", animatorArray)); + + json.insert(std::make_pair("t", textContainer)); + } + +public: + /// The text for the layer + KeyframeGroup text; + + /// Text animators + std::vector> animators; + + std::optional _extraM; + std::optional _extraP; + std::optional _extraA; +}; + +} + +#endif /* TextLayerModel_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/TextLayerModel.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/TextLayerModel.swift new file mode 100644 index 0000000000..f4db922a5b --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Layers/TextLayerModel.swift @@ -0,0 +1,58 @@ +// +// TextLayer.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/8/19. +// + +import Foundation + +/// A layer that holds text. +final class TextLayerModel: LayerModel { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: TextLayerModel.CodingKeys.self) + let textContainer = try container.nestedContainer(keyedBy: TextCodingKeys.self, forKey: .textGroup) + text = try textContainer.decode(KeyframeGroup.self, forKey: .text) + animators = try textContainer.decode([TextAnimator].self, forKey: .animators) + try super.init(from: decoder) + } + + required init(dictionary: [String: Any]) throws { + let containerDictionary: [String: Any] = try dictionary.value(for: CodingKeys.textGroup) + let textDictionary: [String: Any] = try containerDictionary.value(for: TextCodingKeys.text) + text = try KeyframeGroup(dictionary: textDictionary) + let animatorDictionaries: [[String: Any]] = try containerDictionary.value(for: TextCodingKeys.animators) + animators = try animatorDictionaries.map({ try TextAnimator(dictionary: $0) }) + try super.init(dictionary: dictionary) + } + + // MARK: Internal + + /// The text for the layer + let text: KeyframeGroup + + /// Text animators + let animators: [TextAnimator] + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + var textContainer = container.nestedContainer(keyedBy: TextCodingKeys.self, forKey: .textGroup) + try textContainer.encode(text, forKey: .text) + try textContainer.encode(animators, forKey: .animators) + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case textGroup = "t" + } + + private enum TextCodingKeys: String, CodingKey { + case text = "d" + case animators = "a" + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/DashElement.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/DashElement.cpp new file mode 100644 index 0000000000..33426de338 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/DashElement.cpp @@ -0,0 +1,5 @@ +#include "DashPattern.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/DashElement.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/DashElement.hpp new file mode 100644 index 0000000000..6cd73101ff --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/DashElement.hpp @@ -0,0 +1,78 @@ +#ifndef DashElement_hpp +#define DashElement_hpp + +#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" +#include "Lottie/Public/Primitives/DashPattern.hpp" + +namespace lottie { + +enum class DashElementType { + Offset, + Dash, + Gap +}; + +class DashElement { +public: + DashElement( + DashElementType type_, + KeyframeGroup const &value_ + ) : + type(type_), + value(value_) { + } + + explicit DashElement(json11::Json::object const &json) noexcept(false) : + type(DashElementType::Offset), + value(KeyframeGroup(Vector1D(0.0))) { + auto typeRawValue = getString(json, "n"); + if (typeRawValue == "o") { + type = DashElementType::Offset; + } else if (typeRawValue == "d") { + type = DashElementType::Dash; + } else if (typeRawValue == "g") { + type = DashElementType::Gap; + } else { + throw LottieParsingException(); + } + + value = KeyframeGroup(getObject(json, "v")); + + name = getOptionalString(json, "nm"); + } + + json11::Json::object toJson() const { + json11::Json::object result; + + switch (type) { + case DashElementType::Offset: + result.insert(std::make_pair("n", "o")); + break; + case DashElementType::Dash: + result.insert(std::make_pair("n", "d")); + break; + case DashElementType::Gap: + result.insert(std::make_pair("n", "g")); + break; + } + + result.insert(std::make_pair("v", value.toJson())); + + if (name.has_value()) { + result.insert(std::make_pair("nm", name.value())); + } + + return result; + } + +public: + DashElementType type; + KeyframeGroup value; + + std::optional name; +}; + +} + +#endif /* DashElement_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/DashPattern.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/DashPattern.swift new file mode 100644 index 0000000000..5fc74d93e9 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/DashPattern.swift @@ -0,0 +1,44 @@ +// +// DashPattern.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/22/19. +// + +import Foundation + +// MARK: - DashElementType + +enum DashElementType: String, Codable { + case offset = "o" + case dash = "d" + case gap = "g" +} + +// MARK: - DashElement + +final class DashElement: Codable, DictionaryInitializable { + + // MARK: Lifecycle + + init(dictionary: [String: Any]) throws { + let typeRawValue: String = try dictionary.value(for: CodingKeys.type) + guard let type = DashElementType(rawValue: typeRawValue) else { + throw InitializableError.invalidInput + } + self.type = type + let valueDictionary: [String: Any] = try dictionary.value(for: CodingKeys.value) + value = try KeyframeGroup(dictionary: valueDictionary) + } + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case type = "n" + case value = "v" + } + + let type: DashElementType + let value: KeyframeGroup + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/FitzModifier.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/FitzModifier.cpp new file mode 100644 index 0000000000..7b18e01e4a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/FitzModifier.cpp @@ -0,0 +1,5 @@ +#include "FitzModifier.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/FitzModifier.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/FitzModifier.hpp new file mode 100644 index 0000000000..163cf09c21 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/FitzModifier.hpp @@ -0,0 +1,53 @@ +#ifndef FitzModifier_hpp +#define FitzModifier_hpp + +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +namespace lottie { + +class FitzModifier { +public: + explicit FitzModifier(json11::Json::object const &json) noexcept(false) { + original = getInt(json, "o"); + type12 = getOptionalInt(json, "f12"); + type3 = getOptionalInt(json, "f3"); + type4 = getOptionalInt(json, "f4"); + type5 = getOptionalInt(json, "f5"); + type6 = getOptionalInt(json, "f6"); + } + + json11::Json::object toJson() const { + json11::Json::object result; + + result.insert(std::make_pair("o", (double)original)); + if (type12.has_value()) { + result.insert(std::make_pair("f12", (double)type12.value())); + } + if (type3.has_value()) { + result.insert(std::make_pair("f3", (double)type3.value())); + } + if (type4.has_value()) { + result.insert(std::make_pair("f4", (double)type4.value())); + } + if (type5.has_value()) { + result.insert(std::make_pair("f5", (double)type5.value())); + } + if (type6.has_value()) { + result.insert(std::make_pair("f6", (double)type6.value())); + } + + return result; + } + +public: + double original; + std::optional type12; + std::optional type3; + std::optional type4; + std::optional type5; + std::optional type6; +}; + +} + +#endif /* FitzModifier_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Marker.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Marker.cpp new file mode 100644 index 0000000000..ea2664d416 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Marker.cpp @@ -0,0 +1,5 @@ +#include "Marker.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Marker.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Marker.hpp new file mode 100644 index 0000000000..4219996a4b --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Marker.hpp @@ -0,0 +1,52 @@ +#ifndef Marker_hpp +#define Marker_hpp + +#include "Lottie/Public/Primitives/AnimationTime.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +#include + +namespace lottie { + +class Marker { +public: + Marker( + std::string const &name_, + AnimationFrameTime frameTime_ + ) : + name(name_), + frameTime(frameTime_) { + } + + explicit Marker(json11::Json::object const &json) noexcept(false) { + name = getString(json, "cm"); + frameTime = getDouble(json, "tm"); + dr = getOptionalInt(json, "dr"); + } + + json11::Json::object toJson() const { + json11::Json::object result; + + result.insert(std::make_pair("cm", name)); + result.insert(std::make_pair("tm", frameTime)); + + if (dr.has_value()) { + result.insert(std::make_pair("dr", dr.value())); + } + + return result; + } + +public: + /// The Marker Name + std::string name; + + /// The Frame time of the marker + AnimationFrameTime frameTime; + + std::optional dr; +}; + +} + +#endif /* Marker_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Marker.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Marker.swift new file mode 100644 index 0000000000..830977efc9 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Marker.swift @@ -0,0 +1,33 @@ +// +// Marker.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/9/19. +// + +import Foundation + +/// A time marker +final class Marker: Codable, DictionaryInitializable { + + // MARK: Lifecycle + + init(dictionary: [String: Any]) throws { + name = try dictionary.value(for: CodingKeys.name) + frameTime = try dictionary.value(for: CodingKeys.frameTime) + } + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case name = "cm" + case frameTime = "tm" + } + + /// The Marker Name + let name: String + + /// The Frame time of the marker + let frameTime: AnimationFrameTime + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Mask.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Mask.cpp new file mode 100644 index 0000000000..8cb64d6709 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Mask.cpp @@ -0,0 +1,5 @@ +#include "Mask.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Mask.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Mask.hpp new file mode 100644 index 0000000000..d453a1c374 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Mask.hpp @@ -0,0 +1,139 @@ +#ifndef Mask_hpp +#define Mask_hpp + +#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" +#include "Lottie/Private/Utility/Primitives/BezierPath.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +namespace lottie { + +enum class MaskMode { + Add, + Subtract, + Intersect, + Lighten, + Darken, + Difference, + None +}; + +class Mask { +public: + explicit Mask(json11::Json::object const &json) noexcept(false) : + opacity(KeyframeGroup(Vector1D(100.0))), + shape(KeyframeGroup(BezierPath())), + inverted(false), + expansion(KeyframeGroup(Vector1D(0.0))) { + if (const auto modeRawValue = getOptionalString(json, "mode")) { + if (modeRawValue.value() == "a") { + _mode = MaskMode::Add; + } else if (modeRawValue.value() == "s") { + _mode = MaskMode::Subtract; + } else if (modeRawValue.value() == "i") { + _mode = MaskMode::Intersect; + } else if (modeRawValue.value() == "l") { + _mode = MaskMode::Lighten; + } else if (modeRawValue.value() == "d") { + _mode = MaskMode::Darken; + } else if (modeRawValue.value() == "f") { + _mode = MaskMode::Difference; + } else if (modeRawValue.value() == "n") { + _mode = MaskMode::None; + } else { + throw LottieParsingException(); + } + } + + if (const auto opacityData = getOptionalObject(json, "o")) { + opacity = KeyframeGroup(opacityData.value()); + } + + shape = KeyframeGroup(getObject(json, "pt")); + + if (const auto invertedData = getOptionalBool(json, "inv")) { + inverted = invertedData.value(); + } + + if (const auto expansionData = getOptionalObject(json, "x")) { + expansion = KeyframeGroup(expansionData.value()); + } + + name = getOptionalString(json, "nm"); + } + + json11::Json::object toJson() const { + json11::Json::object result; + + if (_mode.has_value()) { + switch (_mode.value()) { + case MaskMode::Add: + result.insert(std::make_pair("mode", "a")); + break; + case MaskMode::Subtract: + result.insert(std::make_pair("mode", "s")); + break; + case MaskMode::Intersect: + result.insert(std::make_pair("mode", "i")); + break; + case MaskMode::Lighten: + result.insert(std::make_pair("mode", "l")); + break; + case MaskMode::Darken: + result.insert(std::make_pair("mode", "d")); + break; + case MaskMode::Difference: + result.insert(std::make_pair("mode", "f")); + break; + case MaskMode::None: + result.insert(std::make_pair("mode", "n")); + break; + } + } + + if (opacity.has_value()) { + result.insert(std::make_pair("o", opacity->toJson())); + } + + result.insert(std::make_pair("pt", shape.toJson())); + + if (inverted.has_value()) { + result.insert(std::make_pair("inv", inverted.value())); + } + + if (expansion.has_value()) { + result.insert(std::make_pair("x", expansion->toJson())); + } + + if (name.has_value()) { + result.insert(std::make_pair("nm", name.value())); + } + + return result; + } + +public: + MaskMode mode() const { + if (_mode.has_value()) { + return _mode.value(); + } else { + return MaskMode::Add; + } + } + +public: + std::optional _mode; + + std::optional> opacity; + + KeyframeGroup shape; + + std::optional inverted; + + std::optional> expansion; + + std::optional name; +}; + +} + +#endif /* Mask_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Mask.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Mask.swift new file mode 100644 index 0000000000..a3c6ce4a62 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Mask.swift @@ -0,0 +1,80 @@ +// +// Mask.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/8/19. +// + +import Foundation + +// MARK: - MaskMode + +enum MaskMode: String, Codable { + case add = "a" + case subtract = "s" + case intersect = "i" + case lighten = "l" + case darken = "d" + case difference = "f" + case none = "n" +} + +// MARK: - Mask + +final class Mask: Codable, DictionaryInitializable { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: Mask.CodingKeys.self) + mode = try container.decodeIfPresent(MaskMode.self, forKey: .mode) ?? .add + opacity = try container.decodeIfPresent(KeyframeGroup.self, forKey: .opacity) ?? KeyframeGroup(Vector1D(100)) + shape = try container.decode(KeyframeGroup.self, forKey: .shape) + inverted = try container.decodeIfPresent(Bool.self, forKey: .inverted) ?? false + expansion = try container.decodeIfPresent(KeyframeGroup.self, forKey: .expansion) ?? KeyframeGroup(Vector1D(0)) + } + + init(dictionary: [String: Any]) throws { + if + let modeRawType = dictionary[CodingKeys.mode.rawValue] as? String, + let mode = MaskMode(rawValue: modeRawType) + { + self.mode = mode + } else { + mode = .add + } + if let opacityDictionary = dictionary[CodingKeys.opacity.rawValue] as? [String: Any] { + opacity = try KeyframeGroup(dictionary: opacityDictionary) + } else { + opacity = KeyframeGroup(Vector1D(100)) + } + let shapeDictionary: [String: Any] = try dictionary.value(for: CodingKeys.shape) + shape = try KeyframeGroup(dictionary: shapeDictionary) + inverted = (try? dictionary.value(for: CodingKeys.inverted)) ?? false + if let expansionDictionary = dictionary[CodingKeys.expansion.rawValue] as? [String: Any] { + expansion = try KeyframeGroup(dictionary: expansionDictionary) + } else { + expansion = KeyframeGroup(Vector1D(0)) + } + } + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case mode + case opacity = "o" + case inverted = "inv" + case shape = "pt" + case expansion = "x" + } + + let mode: MaskMode + + let opacity: KeyframeGroup + + let shape: KeyframeGroup + + let inverted: Bool + + let expansion: KeyframeGroup +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Transform.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Transform.cpp new file mode 100644 index 0000000000..e55154304f --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Transform.cpp @@ -0,0 +1,5 @@ +#include "Transform.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Transform.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Transform.hpp new file mode 100644 index 0000000000..d3bfa4471e --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Transform.hpp @@ -0,0 +1,267 @@ +#ifndef Transform_hpp +#define Transform_hpp + +#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +#include + +namespace lottie { + +class Transform { +public: + enum class PositionInternalRepresentation { + None, + TopLevelXY, + TopLevelCombined, + NestedXY + }; + + enum class RotationZInternalRepresentation { + RZ, + R + }; + +public: + Transform( + std::optional> anchorPoint_, + std::optional> position_, + std::optional> positionX_, + std::optional> positionY_, + std::optional> scale_, + std::optional> rotation_, + std::optional> &opacity_, + std::optional> rotationZ_ + ) : + _anchorPoint(anchorPoint_), + _position(position_), + _positionX(positionX_), + _positionY(positionY_), + _scale(scale_), + _rotation(rotation_), + _opacity(opacity_), + _rotationZ(rotationZ_) { + } + + explicit Transform(json11::Json::object const &json) noexcept(false) { + // AnchorPoint + if (const auto anchorPointDictionary = getOptionalObject(json, "a")) { + _anchorPoint = KeyframeGroup(anchorPointDictionary.value()); + } + + try { + auto xDictionary = getOptionalObject(json, "px"); + auto yDictionary = getOptionalObject(json, "py"); + if (xDictionary.has_value() && yDictionary.has_value()) { + _positionX = KeyframeGroup(xDictionary.value()); + _positionY = KeyframeGroup(yDictionary.value()); + _position = std::nullopt; + _positionInternalRepresentation = PositionInternalRepresentation::TopLevelXY; + } else if (const auto positionData = getOptionalObject(json, "p")) { + try { + LottieParsingException::Guard expectedGuard; + + _position = KeyframeGroup(positionData.value()); + _positionX = std::nullopt; + _positionX = std::nullopt; + _positionInternalRepresentation = PositionInternalRepresentation::TopLevelCombined; + } catch(...) { + auto xData = getObject(positionData.value(), "x"); + auto yData = getObject(positionData.value(), "y"); + _positionX = KeyframeGroup(xData); + _positionY = KeyframeGroup(yData); + _position = std::nullopt; + _positionInternalRepresentation = PositionInternalRepresentation::NestedXY; + _extra_positionS = getOptionalBool(positionData.value(), "s"); + } + } else { + _position = std::nullopt; + _positionX = std::nullopt; + _positionY = std::nullopt; + _positionInternalRepresentation = PositionInternalRepresentation::None; + } + } catch(...) { + throw LottieParsingException(); + } + + try { + // Scale + if (const auto scaleData = getOptionalObject(json, "s")) { + _scale = KeyframeGroup(scaleData.value()); + } + + // Rotation + if (const auto rotationZData = getOptionalObject(json, "rz")) { + _rotationZ = KeyframeGroup(rotationZData.value()); + _rotationZInternalRepresentation = RotationZInternalRepresentation::RZ; + } else if (const auto rotationData = getOptionalObject(json, "r")) { + _rotation = KeyframeGroup(rotationData.value()); + _rotationZInternalRepresentation = RotationZInternalRepresentation::R; + } + + // Opacity + if (const auto opacityData = getOptionalObject(json, "o")) { + _opacity = KeyframeGroup(opacityData.value()); + } + } catch(...) { + throw LottieParsingException(); + } + + _extraTy = getOptionalString(json, "ty"); + _extraSa = getOptionalAny(json, "sa"); + _extraSk = getOptionalAny(json, "sk"); + } + + json11::Json::object toJson() const { + json11::Json::object result; + + if (_anchorPoint.has_value()) { + result.insert(std::make_pair("a", _anchorPoint->toJson())); + } + + switch (_positionInternalRepresentation) { + case PositionInternalRepresentation::None: + assert(!_positionX.has_value()); + assert(!_positionY.has_value()); + assert(!_position.has_value()); + break; + case PositionInternalRepresentation::TopLevelXY: + assert(_positionX.has_value()); + assert(_positionY.has_value()); + assert(!_position.has_value()); + result.insert(std::make_pair("x", _positionX->toJson())); + result.insert(std::make_pair("y", _positionY->toJson())); + break; + case PositionInternalRepresentation::TopLevelCombined: + assert(_position.has_value()); + assert(!_positionX.has_value()); + assert(!_positionY.has_value()); + result.insert(std::make_pair("p", _position->toJson())); + break; + case PositionInternalRepresentation::NestedXY: + json11::Json::object nestedPosition; + assert(_positionX.has_value()); + assert(_positionY.has_value()); + assert(!_position.has_value()); + nestedPosition.insert(std::make_pair("x", _positionX->toJson())); + nestedPosition.insert(std::make_pair("y", _positionY->toJson())); + if (_extra_positionS.has_value()) { + nestedPosition.insert(std::make_pair("s", _extra_positionS.value())); + } + result.insert(std::make_pair("p", nestedPosition)); + break; + } + + if (_scale.has_value()) { + result.insert(std::make_pair("s", _scale->toJson())); + } + + if (_rotation.has_value()) { + switch (_rotationZInternalRepresentation) { + case RotationZInternalRepresentation::RZ: + result.insert(std::make_pair("rz", _rotation->toJson())); + break; + case RotationZInternalRepresentation::R: + result.insert(std::make_pair("r", _rotation->toJson())); + break; + } + } + + if (_opacity.has_value()) { + result.insert(std::make_pair("o", _opacity->toJson())); + } + + if (_extraTy.has_value()) { + result.insert(std::make_pair("ty", _extraTy.value())); + } + if (_extraSa.has_value()) { + result.insert(std::make_pair("sa", _extraSa.value())); + } + if (_extraSk.has_value()) { + result.insert(std::make_pair("sk", _extraSk.value())); + } + + return result; + } + + KeyframeGroup anchorPoint() { + if (_anchorPoint.has_value()) { + return _anchorPoint.value(); + } else { + return KeyframeGroup(Vector3D(0.0, 0.0, 0.0)); + } + } + + KeyframeGroup scale() const { + if (_scale) { + return _scale.value(); + } else { + return KeyframeGroup(Vector3D(100.0, 100.0, 100.0)); + } + } + + KeyframeGroup rotation() const { + if (_rotation) { + return _rotation.value(); + } else { + return KeyframeGroup(Vector1D(0.0)); + } + } + + KeyframeGroup opacity() const { + if (_opacity) { + return _opacity.value(); + } else { + return KeyframeGroup(Vector1D(100.0)); + } + } + + std::optional> const &position() const { + return _position; + } + + std::optional> const &positionX() const { + return _positionX; + } + + std::optional> const &positionY() const { + return _positionY; + } + +private: + /// The anchor point of the transform. + std::optional> _anchorPoint; + + /// The position of the transform. This is nil if the position data was split. + std::optional> _position; + + /// The positionX of the transform. This is nil if the position property is set. + std::optional> _positionX; + + /// The positionY of the transform. This is nil if the position property is set. + std::optional> _positionY; + + PositionInternalRepresentation _positionInternalRepresentation = PositionInternalRepresentation::None; + + /// The scale of the transform + std::optional> _scale; + + /// The rotation of the transform. Note: This is single dimensional rotation. + std::optional> _rotation; + + /// The opacity of the transform. + std::optional> _opacity; + + /// Should always be nil. + std::optional> _rotationZ; + RotationZInternalRepresentation _rotationZInternalRepresentation; + + std::optional _extra_positionS; + std::optional _extraTy; + std::optional _extraSa; + std::optional _extraSk; +}; + +} + +#endif /* Transform_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Transform.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Transform.swift new file mode 100644 index 0000000000..c67bc2caae --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Objects/Transform.swift @@ -0,0 +1,179 @@ +// +// Transform.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/7/19. +// + +import Foundation + +/// The animatable transform for a layer. Controls position, rotation, scale, and opacity. +final class Transform: Codable, DictionaryInitializable { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + /// This manual override of decode is required because we want to throw an error + /// in the case that there is not position data. + let container = try decoder.container(keyedBy: Transform.CodingKeys.self) + + // AnchorPoint + anchorPoint = try container + .decodeIfPresent(KeyframeGroup.self, forKey: .anchorPoint) ?? KeyframeGroup(Vector3D(x: Double(0), y: 0, z: 0)) + + // Position + if container.contains(.positionX), container.contains(.positionY) { + // Position dimensions are split into two keyframe groups + positionX = try container.decode(KeyframeGroup.self, forKey: .positionX) + positionY = try container.decode(KeyframeGroup.self, forKey: .positionY) + position = nil + } else if let positionKeyframes = try? container.decode(KeyframeGroup.self, forKey: .position) { + // Position dimensions are a single keyframe group. + position = positionKeyframes + positionX = nil + positionY = nil + } else if + let positionContainer = try? container.nestedContainer(keyedBy: PositionCodingKeys.self, forKey: .position), + let positionX = try? positionContainer.decode(KeyframeGroup.self, forKey: .positionX), + let positionY = try? positionContainer.decode(KeyframeGroup.self, forKey: .positionY) + { + /// Position keyframes are split and nested. + self.positionX = positionX + self.positionY = positionY + position = nil + } else { + /// Default value. + position = KeyframeGroup(Vector3D(x: Double(0), y: 0, z: 0)) + positionX = nil + positionY = nil + } + + // Scale + scale = try container + .decodeIfPresent(KeyframeGroup.self, forKey: .scale) ?? KeyframeGroup(Vector3D(x: Double(100), y: 100, z: 100)) + + // Rotation + if let rotationZ = try container.decodeIfPresent(KeyframeGroup.self, forKey: .rotationZ) { + rotation = rotationZ + } else { + rotation = try container.decodeIfPresent(KeyframeGroup.self, forKey: .rotation) ?? KeyframeGroup(Vector1D(0)) + } + rotationZ = nil + + // Opacity + opacity = try container.decodeIfPresent(KeyframeGroup.self, forKey: .opacity) ?? KeyframeGroup(Vector1D(100)) + } + + init(dictionary: [String: Any]) throws { + if + let anchorPointDictionary = dictionary[CodingKeys.anchorPoint.rawValue] as? [String: Any], + let anchorPoint = try? KeyframeGroup(dictionary: anchorPointDictionary) + { + self.anchorPoint = anchorPoint + } else { + anchorPoint = KeyframeGroup(Vector3D(x: Double(0), y: 0, z: 0)) + } + + if + let xDictionary = dictionary[CodingKeys.positionX.rawValue] as? [String: Any], + let yDictionary = dictionary[CodingKeys.positionY.rawValue] as? [String: Any] + { + positionX = try KeyframeGroup(dictionary: xDictionary) + positionY = try KeyframeGroup(dictionary: yDictionary) + position = nil + } else if + let positionDictionary = dictionary[CodingKeys.position.rawValue] as? [String: Any], + positionDictionary[KeyframeGroup.KeyframeWrapperKey.keyframeData.rawValue] != nil + { + position = try KeyframeGroup(dictionary: positionDictionary) + positionX = nil + positionY = nil + } else if + let positionDictionary = dictionary[CodingKeys.position.rawValue] as? [String: Any], + let xDictionary = positionDictionary[PositionCodingKeys.positionX.rawValue] as? [String: Any], + let yDictionary = positionDictionary[PositionCodingKeys.positionY.rawValue] as? [String: Any] + { + positionX = try KeyframeGroup(dictionary: xDictionary) + positionY = try KeyframeGroup(dictionary: yDictionary) + position = nil + } else { + position = KeyframeGroup(Vector3D(x: Double(0), y: 0, z: 0)) + positionX = nil + positionY = nil + } + + if + let scaleDictionary = dictionary[CodingKeys.scale.rawValue] as? [String: Any], + let scale = try? KeyframeGroup(dictionary: scaleDictionary) + { + self.scale = scale + } else { + scale = KeyframeGroup(Vector3D(x: Double(100), y: 100, z: 100)) + } + if + let rotationDictionary = dictionary[CodingKeys.rotationZ.rawValue] as? [String: Any], + let rotation = try? KeyframeGroup(dictionary: rotationDictionary) + { + self.rotation = rotation + } else if + let rotationDictionary = dictionary[CodingKeys.rotation.rawValue] as? [String: Any], + let rotation = try? KeyframeGroup(dictionary: rotationDictionary) + { + self.rotation = rotation + } else { + rotation = KeyframeGroup(Vector1D(0)) + } + rotationZ = nil + if + let opacityDictionary = dictionary[CodingKeys.opacity.rawValue] as? [String: Any], + let opacity = try? KeyframeGroup(dictionary: opacityDictionary) + { + self.opacity = opacity + } else { + opacity = KeyframeGroup(Vector1D(100)) + } + } + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case anchorPoint = "a" + case position = "p" + case positionX = "px" + case positionY = "py" + case scale = "s" + case rotation = "r" + case rotationZ = "rz" + case opacity = "o" + } + + enum PositionCodingKeys: String, CodingKey { + case split = "s" + case positionX = "x" + case positionY = "y" + } + + /// The anchor point of the transform. + let anchorPoint: KeyframeGroup + + /// The position of the transform. This is nil if the position data was split. + let position: KeyframeGroup? + + /// The positionX of the transform. This is nil if the position property is set. + let positionX: KeyframeGroup? + + /// The positionY of the transform. This is nil if the position property is set. + let positionY: KeyframeGroup? + + /// The scale of the transform + let scale: KeyframeGroup + + /// The rotation of the transform. Note: This is single dimensional rotation. + let rotation: KeyframeGroup + + /// The opacity of the transform. + let opacity: KeyframeGroup + + /// Should always be nil. + let rotationZ: KeyframeGroup? +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Ellipse.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Ellipse.cpp new file mode 100644 index 0000000000..9378276f24 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Ellipse.cpp @@ -0,0 +1,5 @@ +#include "Ellipse.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Ellipse.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Ellipse.hpp new file mode 100644 index 0000000000..51f1c1879d --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Ellipse.hpp @@ -0,0 +1,62 @@ +#ifndef Ellipse_hpp +#define Ellipse_hpp + +#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" +#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" +#include "Lottie/Public/Primitives/Vectors.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +namespace lottie { + +enum class PathDirection: int { + Clockwise = 1, + UserSetClockwise = 2, + CounterClockwise = 3 +}; + +/// An item that define an ellipse shape +class Ellipse: public ShapeItem { +public: + explicit Ellipse(json11::Json::object const &json) noexcept(false) : + ShapeItem(json), + position(KeyframeGroup(Vector3D(0.0, 0.0, 0.0))), + size(KeyframeGroup(Vector3D(0.0, 0.0, 0.0))) { + if (const auto directionRawValue = getOptionalInt(json, "d")) { + switch (directionRawValue.value()) { + case 1: + direction = PathDirection::Clockwise; + break; + case 2: + direction = PathDirection::UserSetClockwise; + break; + case 3: + direction = PathDirection::CounterClockwise; + break; + default: + throw LottieParsingException(); + } + } + + position = KeyframeGroup(getObject(json, "p")); + size = KeyframeGroup(getObject(json, "s")); + } + + virtual void toJson(json11::Json::object &json) const override { + ShapeItem::toJson(json); + + if (direction.has_value()) { + json.insert(std::make_pair("d", (int)direction.value())); + } + json.insert(std::make_pair("p", position.toJson())); + json.insert(std::make_pair("s", size.toJson())); + } + +public: + std::optional direction; + KeyframeGroup position; + KeyframeGroup size; +}; + +} + +#endif /* Ellipse_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Ellipse.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Ellipse.swift new file mode 100644 index 0000000000..0ce70da75f --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Ellipse.swift @@ -0,0 +1,75 @@ +// +// EllipseItem.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/8/19. +// + +import Foundation + +// MARK: - PathDirection + +enum PathDirection: Int, Codable { + case clockwise = 1 + case userSetClockwise = 2 + case counterClockwise = 3 +} + +// MARK: - Ellipse + +/// An item that define an ellipse shape +final class Ellipse: ShapeItem { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: Ellipse.CodingKeys.self) + direction = try container.decodeIfPresent(PathDirection.self, forKey: .direction) ?? .clockwise + position = try container.decode(KeyframeGroup.self, forKey: .position) + size = try container.decode(KeyframeGroup.self, forKey: .size) + try super.init(from: decoder) + } + + required init(dictionary: [String: Any]) throws { + if + let directionRawType = dictionary[CodingKeys.direction.rawValue] as? Int, + let direction = PathDirection(rawValue: directionRawType) + { + self.direction = direction + } else { + direction = .clockwise + } + let positionDictionary: [String: Any] = try dictionary.value(for: CodingKeys.position) + position = try KeyframeGroup(dictionary: positionDictionary) + let sizeDictionary: [String: Any] = try dictionary.value(for: CodingKeys.size) + size = try KeyframeGroup(dictionary: sizeDictionary) + try super.init(dictionary: dictionary) + } + + // MARK: Internal + + /// The direction of the ellipse. + let direction: PathDirection + + /// The position of the ellipse + let position: KeyframeGroup + + /// The size of the ellipse + let size: KeyframeGroup + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(direction, forKey: .direction) + try container.encode(position, forKey: .position) + try container.encode(size, forKey: .size) + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case direction = "d" + case position = "p" + case size = "s" + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Fill.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Fill.cpp new file mode 100644 index 0000000000..992a25a5be --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Fill.cpp @@ -0,0 +1,6 @@ +#include "Fill.hpp" + +namespace lottie { + +} + diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Fill.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Fill.hpp new file mode 100644 index 0000000000..3e89d2a8c8 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Fill.hpp @@ -0,0 +1,74 @@ +#ifndef Fill_hpp +#define Fill_hpp + +#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" +#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" +#include "Lottie/Public/Primitives/Color.hpp" +#include "Lottie/Public/Primitives/Vectors.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +namespace lottie { + +enum class FillRule: int { + None = 0, + NonZeroWinding = 1, + EvenOdd = 2 +}; + +class Fill: public ShapeItem { +public: + explicit Fill(json11::Json::object const &json) noexcept(false) : + ShapeItem(json), + opacity(KeyframeGroup(Vector1D(0.0))), + color(KeyframeGroup(Color(0.0, 0.0, 0.0, 0.0))) { + opacity = KeyframeGroup(getObject(json, "o")); + color = KeyframeGroup(getObject(json, "c")); + + if (const auto fillRuleRawValue = getOptionalInt(json, "r")) { + switch (fillRuleRawValue.value()) { + case 0: + fillRule = FillRule::None; + break; + case 1: + fillRule = FillRule::NonZeroWinding; + break; + case 2: + fillRule = FillRule::EvenOdd; + break; + default: + throw LottieParsingException(); + } + } + + fillEnabled = getOptionalBool(json, "fillEnabled"); + } + + virtual void toJson(json11::Json::object &json) const override { + ShapeItem::toJson(json); + + json.insert(std::make_pair("o", opacity.toJson())); + json.insert(std::make_pair("c", color.toJson())); + + if (fillRule.has_value()) { + json.insert(std::make_pair("r", (int)fillRule.value())); + } + + if (fillEnabled.has_value()) { + json.insert(std::make_pair("fillEnabled", fillEnabled.value())); + } + } + +public: + KeyframeGroup opacity; + + /// The color keyframes for the fill + KeyframeGroup color; + + std::optional fillRule; + + std::optional fillEnabled; +}; + +} + +#endif /* Fill_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Fill.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Fill.swift new file mode 100644 index 0000000000..c158ae45ae --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Fill.swift @@ -0,0 +1,74 @@ +// +// FillShape.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/8/19. +// + +import Foundation + +// MARK: - FillRule + +enum FillRule: Int, Codable { + case none + case nonZeroWinding + case evenOdd +} + +// MARK: - Fill + +/// An item that defines a fill render +final class Fill: ShapeItem { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: Fill.CodingKeys.self) + opacity = try container.decode(KeyframeGroup.self, forKey: .opacity) + color = try container.decode(KeyframeGroup.self, forKey: .color) + fillRule = try container.decodeIfPresent(FillRule.self, forKey: .fillRule) ?? .nonZeroWinding + try super.init(from: decoder) + } + + required init(dictionary: [String: Any]) throws { + let opacityDictionary: [String: Any] = try dictionary.value(for: CodingKeys.opacity) + opacity = try KeyframeGroup(dictionary: opacityDictionary) + let colorDictionary: [String: Any] = try dictionary.value(for: CodingKeys.color) + color = try KeyframeGroup(dictionary: colorDictionary) + if + let fillRuleRawValue = dictionary[CodingKeys.fillRule.rawValue] as? Int, + let fillRule = FillRule(rawValue: fillRuleRawValue) + { + self.fillRule = fillRule + } else { + fillRule = .nonZeroWinding + } + try super.init(dictionary: dictionary) + } + + // MARK: Internal + + /// The opacity of the fill + let opacity: KeyframeGroup + + /// The color keyframes for the fill + let color: KeyframeGroup + + let fillRule: FillRule + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(opacity, forKey: .opacity) + try container.encode(color, forKey: .color) + try container.encode(fillRule, forKey: .fillRule) + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case opacity = "o" + case color = "c" + case fillRule = "r" + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/GradientFill.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/GradientFill.cpp new file mode 100644 index 0000000000..f2ed96d1b9 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/GradientFill.cpp @@ -0,0 +1,5 @@ +#include "GradientFill.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/GradientFill.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/GradientFill.hpp new file mode 100644 index 0000000000..58711c2cbe --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/GradientFill.hpp @@ -0,0 +1,116 @@ +#ifndef GradientFill_hpp +#define GradientFill_hpp + +#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" +#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" +#include "Lottie/Public/Primitives/GradientColorSet.hpp" + +namespace lottie { + +enum class GradientType: int { + None = 0, + Linear = 1, + Radial = 2 +}; + +/// An item that define a gradient fill +class GradientFill: public ShapeItem { +public: + explicit GradientFill(json11::Json::object const &json) noexcept(false) : + ShapeItem(json), + opacity(KeyframeGroup(Vector1D(100.0))), + startPoint(KeyframeGroup(Vector3D(0.0, 0.0, 0.0))), + endPoint(KeyframeGroup(Vector3D(0.0, 0.0, 0.0))), + gradientType(GradientType::None), + numberOfColors(0), + colors(KeyframeGroup(GradientColorSet())) { + opacity = KeyframeGroup(getObject(json, "o")); + startPoint = KeyframeGroup(getObject(json, "s")); + endPoint = KeyframeGroup(getObject(json, "e")); + + auto gradientTypeRawValue = getInt(json, "t"); + switch (gradientTypeRawValue) { + case 0: + gradientType = GradientType::None; + break; + case 1: + gradientType = GradientType::Linear; + break; + case 2: + gradientType = GradientType::Radial; + break; + default: + throw LottieParsingException(); + } + + if (const auto highlightLengthData = getOptionalObject(json, "h")) { + highlightLength = KeyframeGroup(highlightLengthData.value()); + } + if (const auto highlightAngleData = getOptionalObject(json, "a")) { + highlightAngle = KeyframeGroup(highlightAngleData.value()); + } + + auto colorsContainer = getObject(json, "g"); + numberOfColors = getInt(colorsContainer, "p"); + colors = KeyframeGroup(getObject(colorsContainer, "k")); + + rValue = getOptionalInt(json, "r"); + } + + virtual void toJson(json11::Json::object &json) const override { + ShapeItem::toJson(json); + + json.insert(std::make_pair("o", opacity.toJson())); + json.insert(std::make_pair("s", startPoint.toJson())); + json.insert(std::make_pair("e", endPoint.toJson())); + json.insert(std::make_pair("t", (int)gradientType)); + + if (highlightLength.has_value()) { + json.insert(std::make_pair("h", highlightLength->toJson())); + } + if (highlightAngle.has_value()) { + json.insert(std::make_pair("a", highlightAngle->toJson())); + } + + json11::Json::object colorsContainer; + colorsContainer.insert(std::make_pair("p", numberOfColors)); + colorsContainer.insert(std::make_pair("k", colors.toJson())); + json.insert(std::make_pair("g", colorsContainer)); + + if (rValue.has_value()) { + json.insert(std::make_pair("r", rValue.value())); + } + } + +public: + /// The opacity of the fill + KeyframeGroup opacity; + + /// The start of the gradient + KeyframeGroup startPoint; + + /// The end of the gradient + KeyframeGroup endPoint; + + /// The type of gradient + GradientType gradientType; + + /// Gradient Highlight Length. Only if type is Radial + std::optional> highlightLength; + + /// Highlight Angle. Only if type is Radial + std::optional> highlightAngle; + + /// The number of color points in the gradient + int numberOfColors; + + /// The Colors of the gradient. + KeyframeGroup colors; + + std::optional rValue; +}; + +} + +#endif /* GradientFill_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/GradientFill.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/GradientFill.swift new file mode 100644 index 0000000000..426d101a89 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/GradientFill.swift @@ -0,0 +1,124 @@ +// +// GradientFill.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/8/19. +// + +import Foundation + +// MARK: - GradientType + +enum GradientType: Int, Codable { + case none + case linear + case radial +} + +// MARK: - GradientFill + +/// An item that define a gradient fill +final class GradientFill: ShapeItem { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: GradientFill.CodingKeys.self) + opacity = try container.decode(KeyframeGroup.self, forKey: .opacity) + startPoint = try container.decode(KeyframeGroup.self, forKey: .startPoint) + endPoint = try container.decode(KeyframeGroup.self, forKey: .endPoint) + gradientType = try container.decode(GradientType.self, forKey: .gradientType) + highlightLength = try container.decodeIfPresent(KeyframeGroup.self, forKey: .highlightLength) + highlightAngle = try container.decodeIfPresent(KeyframeGroup.self, forKey: .highlightAngle) + let colorsContainer = try container.nestedContainer(keyedBy: GradientDataKeys.self, forKey: .colors) + colors = try colorsContainer.decode(KeyframeGroup<[Double]>.self, forKey: .colors) + numberOfColors = try colorsContainer.decode(Int.self, forKey: .numberOfColors) + try super.init(from: decoder) + } + + required init(dictionary: [String: Any]) throws { + let opacityDictionary: [String: Any] = try dictionary.value(for: CodingKeys.opacity) + opacity = try KeyframeGroup(dictionary: opacityDictionary) + let startPointDictionary: [String: Any] = try dictionary.value(for: CodingKeys.startPoint) + startPoint = try KeyframeGroup(dictionary: startPointDictionary) + let endPointDictionary: [String: Any] = try dictionary.value(for: CodingKeys.endPoint) + endPoint = try KeyframeGroup(dictionary: endPointDictionary) + let gradientRawType: Int = try dictionary.value(for: CodingKeys.gradientType) + guard let gradient = GradientType(rawValue: gradientRawType) else { + throw InitializableError.invalidInput + } + gradientType = gradient + if let highlightLengthDictionary = dictionary[CodingKeys.highlightLength.rawValue] as? [String: Any] { + highlightLength = try? KeyframeGroup(dictionary: highlightLengthDictionary) + } else { + highlightLength = nil + } + if let highlightAngleDictionary = dictionary[CodingKeys.highlightAngle.rawValue] as? [String: Any] { + highlightAngle = try? KeyframeGroup(dictionary: highlightAngleDictionary) + } else { + highlightAngle = nil + } + let colorsDictionary: [String: Any] = try dictionary.value(for: CodingKeys.colors) + let nestedColorsDictionary: [String: Any] = try colorsDictionary.value(for: GradientDataKeys.colors) + colors = try KeyframeGroup<[Double]>(dictionary: nestedColorsDictionary) + numberOfColors = try colorsDictionary.value(for: GradientDataKeys.numberOfColors) + try super.init(dictionary: dictionary) + } + + // MARK: Internal + + /// The opacity of the fill + let opacity: KeyframeGroup + + /// The start of the gradient + let startPoint: KeyframeGroup + + /// The end of the gradient + let endPoint: KeyframeGroup + + /// The type of gradient + let gradientType: GradientType + + /// Gradient Highlight Length. Only if type is Radial + let highlightLength: KeyframeGroup? + + /// Highlight Angle. Only if type is Radial + let highlightAngle: KeyframeGroup? + + /// The number of color points in the gradient + let numberOfColors: Int + + /// The Colors of the gradient. + let colors: KeyframeGroup<[Double]> + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(opacity, forKey: .opacity) + try container.encode(startPoint, forKey: .startPoint) + try container.encode(endPoint, forKey: .endPoint) + try container.encode(gradientType, forKey: .gradientType) + try container.encodeIfPresent(highlightLength, forKey: .highlightLength) + try container.encodeIfPresent(highlightAngle, forKey: .highlightAngle) + var colorsContainer = container.nestedContainer(keyedBy: GradientDataKeys.self, forKey: .colors) + try colorsContainer.encode(numberOfColors, forKey: .numberOfColors) + try colorsContainer.encode(colors, forKey: .colors) + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case opacity = "o" + case startPoint = "s" + case endPoint = "e" + case gradientType = "t" + case highlightLength = "h" + case highlightAngle = "a" + case colors = "g" + } + + private enum GradientDataKeys: String, CodingKey { + case numberOfColors = "p" + case colors = "k" + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/GradientStroke.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/GradientStroke.cpp new file mode 100644 index 0000000000..465b20fde8 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/GradientStroke.cpp @@ -0,0 +1,5 @@ +#include "GradientStroke.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/GradientStroke.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/GradientStroke.hpp new file mode 100644 index 0000000000..ba5fc18f44 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/GradientStroke.hpp @@ -0,0 +1,191 @@ +#ifndef GradientStroke_hpp +#define GradientStroke_hpp + +#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" +#include "Lottie/Private/Model/ShapeItems/GradientFill.hpp" +#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" +#include "Lottie/Private/Model/Objects/DashElement.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" +#include "Lottie/Public/Primitives/DrawingAttributes.hpp" + +namespace lottie { + +/// An item that define a gradient stroke +class GradientStroke: public ShapeItem { +public: + explicit GradientStroke(json11::Json::object const &json) noexcept(false) : + ShapeItem(json), + opacity(KeyframeGroup(Vector1D(100.0))), + startPoint(KeyframeGroup(Vector3D(0.0, 0.0, 0.0))), + endPoint(KeyframeGroup(Vector3D(0.0, 0.0, 0.0))), + gradientType(GradientType::None), + numberOfColors(0), + colors(KeyframeGroup(GradientColorSet())), + width(KeyframeGroup(Vector1D(0.0))), + lineCap(LineCap::Round), + lineJoin(LineJoin::Round) { + opacity = KeyframeGroup(getObject(json, "o")); + startPoint = KeyframeGroup(getObject(json, "s")); + endPoint = KeyframeGroup(getObject(json, "e")); + + auto gradientTypeRawValue = getInt(json, "t"); + switch (gradientTypeRawValue) { + case 0: + gradientType = GradientType::None; + break; + case 1: + gradientType = GradientType::Linear; + break; + case 2: + gradientType = GradientType::Radial; + break; + default: + throw LottieParsingException(); + } + + if (const auto highlightLengthData = getOptionalObject(json, "h")) { + highlightLength = KeyframeGroup(highlightLengthData.value()); + } + if (const auto highlightAngleData = getOptionalObject(json, "a")) { + highlightAngle = KeyframeGroup(highlightAngleData.value()); + } + + width = KeyframeGroup(getObject(json, "w")); + + if (const auto lineCapRawValue = getOptionalInt(json, "lc")) { + switch (lineCapRawValue.value()) { + case 0: + lineCap = LineCap::None; + break; + case 1: + lineCap = LineCap::Butt; + break; + case 2: + lineCap = LineCap::Round; + break; + case 3: + lineCap = LineCap::Square; + break; + default: + throw LottieParsingException(); + } + } + + if (const auto lineJoinRawValue = getOptionalInt(json, "lj")) { + switch (lineJoinRawValue.value()) { + case 0: + lineJoin = LineJoin::None; + break; + case 1: + lineJoin = LineJoin::Miter; + break; + case 2: + lineJoin = LineJoin::Round; + break; + case 3: + lineJoin = LineJoin::Bevel; + break; + default: + throw LottieParsingException(); + } + } + + if (const auto miterLimitData = getOptionalDouble(json, "ml")) { + miterLimit = miterLimitData.value(); + } + + auto colorsContainer = getObject(json, "g"); + numberOfColors = getInt(colorsContainer, "p"); + auto colorsData = getObject(colorsContainer, "k"); + colors = KeyframeGroup(colorsData); + + if (const auto dashElementsData = getOptionalObjectArray(json, "d")) { + dashPattern = std::vector(); + for (const auto &dashElementData : dashElementsData.value()) { + dashPattern->push_back(DashElement(dashElementData)); + } + } + } + + virtual void toJson(json11::Json::object &json) const override { + ShapeItem::toJson(json); + + json.insert(std::make_pair("o", opacity.toJson())); + json.insert(std::make_pair("s", startPoint.toJson())); + json.insert(std::make_pair("e", endPoint.toJson())); + json.insert(std::make_pair("t", (int)gradientType)); + + if (highlightLength.has_value()) { + json.insert(std::make_pair("h", highlightLength->toJson())); + } + if (highlightAngle.has_value()) { + json.insert(std::make_pair("a", highlightAngle->toJson())); + } + + json.insert(std::make_pair("w", width.toJson())); + + json.insert(std::make_pair("lc", (int)lineCap)); + json.insert(std::make_pair("lj", (int)lineJoin)); + + if (miterLimit.has_value()) { + json.insert(std::make_pair("ml", miterLimit.value())); + } + + json11::Json::object colorsContainer; + colorsContainer.insert(std::make_pair("p", numberOfColors)); + colorsContainer.insert(std::make_pair("k", colors.toJson())); + json.insert(std::make_pair("g", colorsContainer)); + + if (dashPattern.has_value()) { + json11::Json::array dashElements; + for (const auto &dashElement : dashPattern.value()) { + dashElements.push_back(dashElement.toJson()); + } + json.insert(std::make_pair("d", dashElements)); + } + } + +public: + /// The opacity of the fill + KeyframeGroup opacity; + + /// The start of the gradient + KeyframeGroup startPoint; + + /// The end of the gradient + KeyframeGroup endPoint; + + /// The type of gradient + GradientType gradientType; + + /// Gradient Highlight Length. Only if type is Radial + std::optional> highlightLength; + + /// Highlight Angle. Only if type is Radial + std::optional> highlightAngle; + + /// The number of color points in the gradient + int numberOfColors; + + /// The Colors of the gradient. + KeyframeGroup colors; + + /// The width of the stroke + KeyframeGroup width; + + /// Line Cap + LineCap lineCap; + + /// Line Join + LineJoin lineJoin; + + /// Miter Limit + std::optional miterLimit; + + /// The dash pattern of the stroke + std::optional> dashPattern; +}; + +} + +#endif /* GradientStroke_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/GradientStroke.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/GradientStroke.swift new file mode 100644 index 0000000000..90d60eef87 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/GradientStroke.swift @@ -0,0 +1,186 @@ +// +// GradientStroke.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/8/19. +// + +import Foundation + +// MARK: - LineCap + +enum LineCap: Int, Codable { + case none + case butt + case round + case square +} + +// MARK: - LineJoin + +enum LineJoin: Int, Codable { + case none + case miter + case round + case bevel +} + +// MARK: - GradientStroke + +/// An item that define an ellipse shape +final class GradientStroke: ShapeItem { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: GradientStroke.CodingKeys.self) + opacity = try container.decode(KeyframeGroup.self, forKey: .opacity) + startPoint = try container.decode(KeyframeGroup.self, forKey: .startPoint) + endPoint = try container.decode(KeyframeGroup.self, forKey: .endPoint) + gradientType = try container.decode(GradientType.self, forKey: .gradientType) + highlightLength = try container.decodeIfPresent(KeyframeGroup.self, forKey: .highlightLength) + highlightAngle = try container.decodeIfPresent(KeyframeGroup.self, forKey: .highlightAngle) + width = try container.decode(KeyframeGroup.self, forKey: .width) + lineCap = try container.decodeIfPresent(LineCap.self, forKey: .lineCap) ?? .round + lineJoin = try container.decodeIfPresent(LineJoin.self, forKey: .lineJoin) ?? .round + miterLimit = try container.decodeIfPresent(Double.self, forKey: .miterLimit) ?? 4 + // TODO Decode Color Objects instead of array. + let colorsContainer = try container.nestedContainer(keyedBy: GradientDataKeys.self, forKey: .colors) + colors = try colorsContainer.decode(KeyframeGroup<[Double]>.self, forKey: .colors) + numberOfColors = try colorsContainer.decode(Int.self, forKey: .numberOfColors) + dashPattern = try container.decodeIfPresent([DashElement].self, forKey: .dashPattern) + try super.init(from: decoder) + } + + required init(dictionary: [String: Any]) throws { + let opacityDictionary: [String: Any] = try dictionary.value(for: CodingKeys.opacity) + opacity = try KeyframeGroup(dictionary: opacityDictionary) + let startPointDictionary: [String: Any] = try dictionary.value(for: CodingKeys.startPoint) + startPoint = try KeyframeGroup(dictionary: startPointDictionary) + let endPointDictionary: [String: Any] = try dictionary.value(for: CodingKeys.endPoint) + endPoint = try KeyframeGroup(dictionary: endPointDictionary) + let gradientRawType: Int = try dictionary.value(for: CodingKeys.gradientType) + guard let gradient = GradientType(rawValue: gradientRawType) else { + throw InitializableError.invalidInput + } + gradientType = gradient + if let highlightLengthDictionary = dictionary[CodingKeys.highlightLength.rawValue] as? [String: Any] { + highlightLength = try? KeyframeGroup(dictionary: highlightLengthDictionary) + } else { + highlightLength = nil + } + if let highlightAngleDictionary = dictionary[CodingKeys.highlightAngle.rawValue] as? [String: Any] { + highlightAngle = try? KeyframeGroup(dictionary: highlightAngleDictionary) + } else { + highlightAngle = nil + } + let widthDictionary: [String: Any] = try dictionary.value(for: CodingKeys.width) + width = try KeyframeGroup(dictionary: widthDictionary) + if + let lineCapRawValue = dictionary[CodingKeys.lineCap.rawValue] as? Int, + let lineCap = LineCap(rawValue: lineCapRawValue) + { + self.lineCap = lineCap + } else { + lineCap = .round + } + if + let lineJoinRawValue = dictionary[CodingKeys.lineJoin.rawValue] as? Int, + let lineJoin = LineJoin(rawValue: lineJoinRawValue) + { + self.lineJoin = lineJoin + } else { + lineJoin = .round + } + miterLimit = (try? dictionary.value(for: CodingKeys.miterLimit)) ?? 4 + let colorsDictionary: [String: Any] = try dictionary.value(for: CodingKeys.colors) + let nestedColorsDictionary: [String: Any] = try colorsDictionary.value(for: GradientDataKeys.colors) + colors = try KeyframeGroup<[Double]>(dictionary: nestedColorsDictionary) + numberOfColors = try colorsDictionary.value(for: GradientDataKeys.numberOfColors) + let dashPatternDictionaries = dictionary[CodingKeys.dashPattern.rawValue] as? [[String: Any]] + dashPattern = try? dashPatternDictionaries?.map({ try DashElement(dictionary: $0) }) + try super.init(dictionary: dictionary) + } + + // MARK: Internal + + /// The opacity of the fill + let opacity: KeyframeGroup + + /// The start of the gradient + let startPoint: KeyframeGroup + + /// The end of the gradient + let endPoint: KeyframeGroup + + /// The type of gradient + let gradientType: GradientType + + /// Gradient Highlight Length. Only if type is Radial + let highlightLength: KeyframeGroup? + + /// Highlight Angle. Only if type is Radial + let highlightAngle: KeyframeGroup? + + /// The number of color points in the gradient + let numberOfColors: Int + + /// The Colors of the gradient. + let colors: KeyframeGroup<[Double]> + + /// The width of the stroke + let width: KeyframeGroup + + /// Line Cap + let lineCap: LineCap + + /// Line Join + let lineJoin: LineJoin + + /// Miter Limit + let miterLimit: Double + + /// The dash pattern of the stroke + let dashPattern: [DashElement]? + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(opacity, forKey: .opacity) + try container.encode(startPoint, forKey: .startPoint) + try container.encode(endPoint, forKey: .endPoint) + try container.encode(gradientType, forKey: .gradientType) + try container.encodeIfPresent(highlightLength, forKey: .highlightLength) + try container.encodeIfPresent(highlightAngle, forKey: .highlightAngle) + try container.encode(width, forKey: .width) + try container.encode(lineCap, forKey: .lineCap) + try container.encode(lineJoin, forKey: .lineJoin) + try container.encode(miterLimit, forKey: .miterLimit) + var colorsContainer = container.nestedContainer(keyedBy: GradientDataKeys.self, forKey: .colors) + try colorsContainer.encode(numberOfColors, forKey: .numberOfColors) + try colorsContainer.encode(colors, forKey: .colors) + try container.encodeIfPresent(dashPattern, forKey: .dashPattern) + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case opacity = "o" + case startPoint = "s" + case endPoint = "e" + case gradientType = "t" + case highlightLength = "h" + case highlightAngle = "a" + case colors = "g" + case width = "w" + case lineCap = "lc" + case lineJoin = "lj" + case miterLimit = "ml" + case dashPattern = "d" + } + + private enum GradientDataKeys: String, CodingKey { + case numberOfColors = "p" + case colors = "k" + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Group.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Group.cpp new file mode 100644 index 0000000000..89ea7daea7 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Group.cpp @@ -0,0 +1,5 @@ +#include "Group.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Group.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Group.hpp new file mode 100644 index 0000000000..cde67f30d8 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Group.hpp @@ -0,0 +1,51 @@ +#ifndef Group_hpp +#define Group_hpp + +#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +#include +#include + +namespace lottie { + +/// An item that define an ellipse shape +class Group: public ShapeItem { +public: + explicit Group(json11::Json::object const &json) noexcept(false) : + ShapeItem(json) { + auto itemsData = getObjectArray(json, "it"); + for (const auto &itemData : itemsData) { + items.push_back(parseShapeItem(itemData)); + } + + numberOfProperties = getOptionalInt(json, "np"); + } + + virtual void toJson(json11::Json::object &json) const override { + ShapeItem::toJson(json); + + json11::Json::array itemArray; + for (const auto &item : items) { + json11::Json::object itemJson; + item->toJson(itemJson); + itemArray.push_back(itemJson); + } + + json.insert(std::make_pair("it", itemArray)); + + if (numberOfProperties.has_value()) { + json.insert(std::make_pair("np", numberOfProperties.value())); + } + } + +public: + /// A list of shape items. + std::vector> items; + + std::optional numberOfProperties; +}; + +} + +#endif /* Group_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Group.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Group.swift new file mode 100644 index 0000000000..97436e3d4a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Group.swift @@ -0,0 +1,43 @@ +// +// GroupItem.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/8/19. +// + +import Foundation + +/// An item that define an ellipse shape +final class Group: ShapeItem { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: Group.CodingKeys.self) + items = try container.decode([ShapeItem].self, ofFamily: ShapeType.self, forKey: .items) + try super.init(from: decoder) + } + + required init(dictionary: [String: Any]) throws { + let itemDictionaries: [[String: Any]] = try dictionary.value(for: CodingKeys.items) + items = try [ShapeItem].fromDictionaries(itemDictionaries) + try super.init(dictionary: dictionary) + } + + // MARK: Internal + + /// A list of shape items. + let items: [ShapeItem] + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(items, forKey: .items) + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case items = "it" + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Merge.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Merge.cpp new file mode 100644 index 0000000000..b22b05d1f6 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Merge.cpp @@ -0,0 +1,5 @@ +#include "Merge.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Merge.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Merge.hpp new file mode 100644 index 0000000000..e0f70a8265 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Merge.hpp @@ -0,0 +1,62 @@ +#ifndef Merge_hpp +#define Merge_hpp + +#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +namespace lottie { + +enum class MergeMode: int { + None = 0, + Merge = 1, + Add = 2, + Subtract = 3, + Intersect = 4, + Exclude = 5 +}; + +/// An item that define an ellipse shape +class Merge: public ShapeItem { +public: + explicit Merge(json11::Json::object const &json) noexcept(false) : + ShapeItem(json), + mode(MergeMode::None) { + auto modeRawValue = getInt(json, "mm"); + switch (modeRawValue) { + case 0: + mode = MergeMode::None; + break; + case 1: + mode = MergeMode::Merge; + break; + case 2: + mode = MergeMode::Add; + break; + case 3: + mode = MergeMode::Subtract; + break; + case 4: + mode = MergeMode::Intersect; + break; + case 5: + mode = MergeMode::Exclude; + break; + default: + throw LottieParsingException(); + } + } + + virtual void toJson(json11::Json::object &json) const override { + ShapeItem::toJson(json); + + json.insert(std::make_pair("mm", (int)mode)); + } + +public: + /// The mode of the merge path + MergeMode mode; +}; + +} + +#endif /* Merge_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Merge.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Merge.swift new file mode 100644 index 0000000000..a7b0c95181 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Merge.swift @@ -0,0 +1,59 @@ +// +// Merge.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/8/19. +// + +import Foundation + +// MARK: - MergeMode + +enum MergeMode: Int, Codable { + case none + case merge + case add + case subtract + case intersect + case exclude +} + +// MARK: - Merge + +/// An item that define an ellipse shape +final class Merge: ShapeItem { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: Merge.CodingKeys.self) + mode = try container.decode(MergeMode.self, forKey: .mode) + try super.init(from: decoder) + } + + required init(dictionary: [String: Any]) throws { + let modeRawType: Int = try dictionary.value(for: CodingKeys.mode) + guard let mode = MergeMode(rawValue: modeRawType) else { + throw InitializableError.invalidInput + } + self.mode = mode + try super.init(dictionary: dictionary) + } + + // MARK: Internal + + /// The mode of the merge path + let mode: MergeMode + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(mode, forKey: .mode) + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case mode = "mm" + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Rectangle.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Rectangle.cpp new file mode 100644 index 0000000000..c686130aa9 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Rectangle.cpp @@ -0,0 +1,5 @@ +#include "Rectangle.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Rectangle.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Rectangle.hpp new file mode 100644 index 0000000000..634ae44155 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Rectangle.hpp @@ -0,0 +1,99 @@ +#ifndef Rectangle_hpp +#define Rectangle_hpp + +#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" +#include "Lottie/Private/Model/ShapeItems/Ellipse.hpp" +#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +namespace lottie { + +/// An item that define an ellipse shape +class Rectangle: public ShapeItem { +public: + explicit Rectangle(json11::Json::object const &json) noexcept(false) : + ShapeItem(json), + position(KeyframeGroup(Vector3D(0.0, 0.0, 0.0))), + size(KeyframeGroup(Vector3D(0.0, 0.0, 0.0))), + cornerRadius(KeyframeGroup(Vector1D(0.0))) { + if (const auto directionRawValue = getOptionalInt(json, "d")) { + switch (directionRawValue.value()) { + case 1: + direction = PathDirection::Clockwise; + break; + case 2: + direction = PathDirection::UserSetClockwise; + break; + case 3: + direction = PathDirection::CounterClockwise; + break; + default: + throw LottieParsingException(); + } + } + + position = KeyframeGroup(getObject(json, "p")); + size = KeyframeGroup(getObject(json, "s")); + cornerRadius = KeyframeGroup(getObject(json, "r")); + } + + explicit Rectangle( + std::optional name_, + std::optional matchName_, + std::optional expressionIndex_, + std::optional cix_, + std::optional _hidden_, + std::optional index_, + std::optional blendMode_, + std::optional layerClass_, + std::optional direction_, + KeyframeGroup position_, + KeyframeGroup size_, + KeyframeGroup cornerRadius_ + ) : + ShapeItem( + name_, + matchName_, + expressionIndex_, + cix_, + ShapeType::Rectangle, + _hidden_, + index_, + blendMode_, + layerClass_ + ), + direction(direction_), + position(position_), + size(size_), + cornerRadius(cornerRadius_) { + } + + virtual void toJson(json11::Json::object &json) const override { + ShapeItem::toJson(json); + + if (direction.has_value()) { + json.insert(std::make_pair("d", (int)direction.value())); + } + + json.insert(std::make_pair("p", position.toJson())); + json.insert(std::make_pair("s", size.toJson())); + json.insert(std::make_pair("r", cornerRadius.toJson())); + } + +public: + /// The direction of the rect. + std::optional direction; + + /// The position + KeyframeGroup position; + + /// The size + KeyframeGroup size; + + /// The Corner radius of the rectangle + KeyframeGroup cornerRadius; +}; + +} + +#endif /* Rectangle_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Rectangle.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Rectangle.swift new file mode 100644 index 0000000000..03d221172a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Rectangle.swift @@ -0,0 +1,73 @@ +// +// Rectangle.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/8/19. +// + +import Foundation + +/// An item that define an ellipse shape +final class Rectangle: ShapeItem { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: Rectangle.CodingKeys.self) + direction = try container.decodeIfPresent(PathDirection.self, forKey: .direction) ?? .clockwise + position = try container.decode(KeyframeGroup.self, forKey: .position) + size = try container.decode(KeyframeGroup.self, forKey: .size) + cornerRadius = try container.decode(KeyframeGroup.self, forKey: .cornerRadius) + try super.init(from: decoder) + } + + required init(dictionary: [String: Any]) throws { + if + let directionRawType = dictionary[CodingKeys.direction.rawValue] as? Int, + let direction = PathDirection(rawValue: directionRawType) + { + self.direction = direction + } else { + direction = .clockwise + } + let positionDictionary: [String: Any] = try dictionary.value(for: CodingKeys.position) + position = try KeyframeGroup(dictionary: positionDictionary) + let sizeDictionary: [String: Any] = try dictionary.value(for: CodingKeys.size) + size = try KeyframeGroup(dictionary: sizeDictionary) + let cornerRadiusDictionary: [String: Any] = try dictionary.value(for: CodingKeys.cornerRadius) + cornerRadius = try KeyframeGroup(dictionary: cornerRadiusDictionary) + try super.init(dictionary: dictionary) + } + + // MARK: Internal + + /// The direction of the rect. + let direction: PathDirection + + /// The position + let position: KeyframeGroup + + /// The size + let size: KeyframeGroup + + /// The Corner radius of the rectangle + let cornerRadius: KeyframeGroup + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(direction, forKey: .direction) + try container.encode(position, forKey: .position) + try container.encode(size, forKey: .size) + try container.encode(cornerRadius, forKey: .cornerRadius) + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case direction = "d" + case position = "p" + case size = "s" + case cornerRadius = "r" + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Repeater.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Repeater.cpp new file mode 100644 index 0000000000..70477e31af --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Repeater.cpp @@ -0,0 +1,5 @@ +#include "Repeater.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Repeater.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Repeater.hpp new file mode 100644 index 0000000000..9f283d56c9 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Repeater.hpp @@ -0,0 +1,98 @@ +#ifndef Repeater_hpp +#define Repeater_hpp + +#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" +#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +namespace lottie { + +/// An item that define a repeater +class Repeater: public ShapeItem { +public: + explicit Repeater(json11::Json::object const &json) noexcept(false) : + ShapeItem(json) { + if (const auto copiesData = getOptionalObject(json, "c")) { + copies = KeyframeGroup(copiesData.value()); + } + if (const auto offsetData = getOptionalObject(json, "o")) { + offset = KeyframeGroup(offsetData.value()); + } + + auto transformContainer = getObject(json, "tr"); + if (const auto startOpacityData = getOptionalObject(transformContainer, "so")) { + startOpacity = KeyframeGroup(startOpacityData.value()); + } + if (const auto endOpacityData = getOptionalObject(transformContainer, "eo")) { + endOpacity = KeyframeGroup(endOpacityData.value()); + } + if (const auto rotationData = getOptionalObject(transformContainer, "r")) { + rotation = KeyframeGroup(rotationData.value()); + } + if (const auto positionData = getOptionalObject(transformContainer, "p")) { + position = KeyframeGroup(positionData.value()); + } + if (const auto scaleData = getOptionalObject(transformContainer, "s")) { + scale = KeyframeGroup(scaleData.value()); + } + } + + virtual void toJson(json11::Json::object &json) const override { + ShapeItem::toJson(json); + + if (copies.has_value()) { + json.insert(std::make_pair("c", copies->toJson())); + } + if (offset.has_value()) { + json.insert(std::make_pair("o", offset->toJson())); + } + + json11::Json::object transformContainer; + if (startOpacity.has_value()) { + json.insert(std::make_pair("so", startOpacity->toJson())); + } + if (endOpacity.has_value()) { + json.insert(std::make_pair("eo", endOpacity->toJson())); + } + if (rotation.has_value()) { + json.insert(std::make_pair("r", rotation->toJson())); + } + if (position.has_value()) { + json.insert(std::make_pair("p", position->toJson())); + } + if (scale.has_value()) { + json.insert(std::make_pair("s", scale->toJson())); + } + + json.insert(std::make_pair("tr", transformContainer)); + } + +public: + /// The number of copies to repeat + std::optional> copies; + + /// The offset of each copy + std::optional> offset; + + /// Start Opacity + std::optional> startOpacity; + + /// End opacity + std::optional> endOpacity; + + /// The rotation + std::optional> rotation; + + /// Anchor Point + std::optional> anchorPoint; + + /// Position + std::optional> position; + + /// Scale + std::optional> scale; +}; + +} + +#endif /* Repeater_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Repeater.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Repeater.swift new file mode 100644 index 0000000000..ced87e5d26 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Repeater.swift @@ -0,0 +1,136 @@ +// +// Repeater.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/8/19. +// + +import Foundation + +/// An item that define an ellipse shape +final class Repeater: ShapeItem { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: Repeater.CodingKeys.self) + copies = try container.decodeIfPresent(KeyframeGroup.self, forKey: .copies) ?? KeyframeGroup(Vector1D(0)) + offset = try container.decodeIfPresent(KeyframeGroup.self, forKey: .offset) ?? KeyframeGroup(Vector1D(0)) + let transformContainer = try container.nestedContainer(keyedBy: TransformKeys.self, forKey: .transform) + startOpacity = try transformContainer + .decodeIfPresent(KeyframeGroup.self, forKey: .startOpacity) ?? KeyframeGroup(Vector1D(100)) + endOpacity = try transformContainer + .decodeIfPresent(KeyframeGroup.self, forKey: .endOpacity) ?? KeyframeGroup(Vector1D(100)) + rotation = try transformContainer + .decodeIfPresent(KeyframeGroup.self, forKey: .rotation) ?? KeyframeGroup(Vector1D(0)) + position = try transformContainer + .decodeIfPresent(KeyframeGroup.self, forKey: .position) ?? KeyframeGroup(Vector3D(x: Double(0), y: 0, z: 0)) + anchorPoint = try transformContainer + .decodeIfPresent(KeyframeGroup.self, forKey: .anchorPoint) ?? KeyframeGroup(Vector3D(x: Double(0), y: 0, z: 0)) + scale = try transformContainer + .decodeIfPresent(KeyframeGroup.self, forKey: .scale) ?? KeyframeGroup(Vector3D(x: Double(100), y: 100, z: 100)) + try super.init(from: decoder) + } + + required init(dictionary: [String: Any]) throws { + if let copiesDictionary = dictionary[CodingKeys.copies.rawValue] as? [String: Any] { + copies = try KeyframeGroup(dictionary: copiesDictionary) + } else { + copies = KeyframeGroup(Vector1D(0)) + } + if let offsetDictionary = dictionary[CodingKeys.offset.rawValue] as? [String: Any] { + offset = try KeyframeGroup(dictionary: offsetDictionary) + } else { + offset = KeyframeGroup(Vector1D(0)) + } + let transformDictionary: [String: Any] = try dictionary.value(for: CodingKeys.transform) + if let startOpacityDictionary = transformDictionary[TransformKeys.startOpacity.rawValue] as? [String: Any] { + startOpacity = try KeyframeGroup(dictionary: startOpacityDictionary) + } else { + startOpacity = KeyframeGroup(Vector1D(100)) + } + if let endOpacityDictionary = transformDictionary[TransformKeys.endOpacity.rawValue] as? [String: Any] { + endOpacity = try KeyframeGroup(dictionary: endOpacityDictionary) + } else { + endOpacity = KeyframeGroup(Vector1D(100)) + } + if let rotationDictionary = transformDictionary[TransformKeys.rotation.rawValue] as? [String: Any] { + rotation = try KeyframeGroup(dictionary: rotationDictionary) + } else { + rotation = KeyframeGroup(Vector1D(0)) + } + if let positionDictionary = transformDictionary[TransformKeys.position.rawValue] as? [String: Any] { + position = try KeyframeGroup(dictionary: positionDictionary) + } else { + position = KeyframeGroup(Vector3D(x: Double(0), y: 0, z: 0)) + } + if let anchorPointDictionary = transformDictionary[TransformKeys.anchorPoint.rawValue] as? [String: Any] { + anchorPoint = try KeyframeGroup(dictionary: anchorPointDictionary) + } else { + anchorPoint = KeyframeGroup(Vector3D(x: Double(0), y: 0, z: 0)) + } + if let scaleDictionary = transformDictionary[TransformKeys.scale.rawValue] as? [String: Any] { + scale = try KeyframeGroup(dictionary: scaleDictionary) + } else { + scale = KeyframeGroup(Vector3D(x: Double(100), y: 100, z: 100)) + } + try super.init(dictionary: dictionary) + } + + // MARK: Internal + + /// The number of copies to repeat + let copies: KeyframeGroup + + /// The offset of each copy + let offset: KeyframeGroup + + /// Start Opacity + let startOpacity: KeyframeGroup + + /// End opacity + let endOpacity: KeyframeGroup + + /// The rotation + let rotation: KeyframeGroup + + /// Anchor Point + let anchorPoint: KeyframeGroup + + /// Position + let position: KeyframeGroup + + /// Scale + let scale: KeyframeGroup + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(copies, forKey: .copies) + try container.encode(offset, forKey: .offset) + var transformContainer = container.nestedContainer(keyedBy: TransformKeys.self, forKey: .transform) + try transformContainer.encode(startOpacity, forKey: .startOpacity) + try transformContainer.encode(endOpacity, forKey: .endOpacity) + try transformContainer.encode(rotation, forKey: .rotation) + try transformContainer.encode(position, forKey: .position) + try transformContainer.encode(anchorPoint, forKey: .anchorPoint) + try transformContainer.encode(scale, forKey: .scale) + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case copies = "c" + case offset = "o" + case transform = "tr" + } + + private enum TransformKeys: String, CodingKey { + case rotation = "r" + case startOpacity = "so" + case endOpacity = "eo" + case anchorPoint = "a" + case position = "p" + case scale = "s" + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/RoundedRectangle.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/RoundedRectangle.cpp new file mode 100644 index 0000000000..64cba09363 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/RoundedRectangle.cpp @@ -0,0 +1,5 @@ +#include "RoundedRectangle.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/RoundedRectangle.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/RoundedRectangle.hpp new file mode 100644 index 0000000000..c380dc22d4 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/RoundedRectangle.hpp @@ -0,0 +1,75 @@ +#ifndef RoundedRectangle_hpp +#define RoundedRectangle_hpp + +#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" +#include "Lottie/Private/Model/ShapeItems/Ellipse.hpp" +#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +namespace lottie { + +/// An item that define an ellipse shape +class RoundedRectangle: public ShapeItem { +public: + explicit RoundedRectangle(json11::Json::object const &json) noexcept(false) : + ShapeItem(json), + cornerRadius(KeyframeGroup(Vector1D(0.0))) { + if (const auto directionRawValue = getOptionalInt(json, "d")) { + switch (directionRawValue.value()) { + case 1: + direction = PathDirection::Clockwise; + break; + case 2: + direction = PathDirection::UserSetClockwise; + break; + case 3: + direction = PathDirection::CounterClockwise; + break; + default: + throw LottieParsingException(); + } + } + + if (const auto positionData = getOptionalObject(json, "p")) { + position = KeyframeGroup(positionData.value()); + } + if (const auto sizeData = getOptionalObject(json, "s")) { + size = KeyframeGroup(sizeData.value()); + } + + cornerRadius = KeyframeGroup(getObject(json, "r")); + } + + virtual void toJson(json11::Json::object &json) const override { + ShapeItem::toJson(json); + + if (direction.has_value()) { + json.insert(std::make_pair("d", (int)direction.value())); + } + if (position.has_value()) { + json.insert(std::make_pair("p", position->toJson())); + } + if (size.has_value()) { + json.insert(std::make_pair("s", size->toJson())); + } + + json.insert(std::make_pair("r", cornerRadius.toJson())); + } + +public: + /// The direction of the rect. + std::optional direction; + + /// The position + std::optional> position; + + /// The size + std::optional> size; + + /// The Corner radius of the rectangle + KeyframeGroup cornerRadius; +}; + +} + +#endif /* RoundedRectangle_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Shape.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Shape.cpp new file mode 100644 index 0000000000..bf5a96876c --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Shape.cpp @@ -0,0 +1,5 @@ +#include "Shape.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Shape.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Shape.hpp new file mode 100644 index 0000000000..5ebaa9881c --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Shape.hpp @@ -0,0 +1,53 @@ +#ifndef Shape_hpp +#define Shape_hpp + +#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" +#include "Lottie/Private/Model/ShapeItems/Ellipse.hpp" +#include "Lottie/Private/Utility/Primitives/BezierPath.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +#include + +namespace lottie { + +/// An item that defines an custom shape +class Shape: public ShapeItem { +public: + explicit Shape(json11::Json::object const &json) noexcept(false) : + ShapeItem(json), + path(KeyframeGroup(getObject(json, "ks"))) { + if (const auto directionRawValue = getOptionalInt(json, "d")) { + switch (directionRawValue.value()) { + case 1: + direction = PathDirection::Clockwise; + break; + case 2: + direction = PathDirection::UserSetClockwise; + break; + case 3: + direction = PathDirection::CounterClockwise; + break; + default: + throw LottieParsingException(); + } + } + } + + virtual void toJson(json11::Json::object &json) const override { + ShapeItem::toJson(json); + + json.insert(std::make_pair("ks", path.toJson())); + + if (direction.has_value()) { + json.insert(std::make_pair("d", (int)direction.value())); + } + } + +public: + KeyframeGroup path; + std::optional direction; +}; + +} + +#endif /* Shape_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Shape.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Shape.swift new file mode 100644 index 0000000000..c1eeb66e68 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Shape.swift @@ -0,0 +1,56 @@ +// +// VectorShape.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/8/19. +// + +import Foundation + +/// An item that defines an custom shape +final class Shape: ShapeItem { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: Shape.CodingKeys.self) + path = try container.decode(KeyframeGroup.self, forKey: .path) + direction = try container.decodeIfPresent(PathDirection.self, forKey: .direction) + try super.init(from: decoder) + } + + required init(dictionary: [String: Any]) throws { + let pathDictionary: [String: Any] = try dictionary.value(for: CodingKeys.path) + path = try KeyframeGroup(dictionary: pathDictionary) + if + let directionRawValue = dictionary[CodingKeys.direction.rawValue] as? Int, + let direction = PathDirection(rawValue: directionRawValue) + { + self.direction = direction + } else { + direction = nil + } + try super.init(dictionary: dictionary) + } + + // MARK: Internal + + /// The Path + let path: KeyframeGroup + + let direction: PathDirection? + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(path, forKey: .path) + try container.encodeIfPresent(direction, forKey: .direction) + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case path = "ks" + case direction = "d" + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/ShapeItem.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/ShapeItem.cpp new file mode 100644 index 0000000000..ca859c56dd --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/ShapeItem.cpp @@ -0,0 +1,55 @@ +#include "ShapeItem.hpp" + +#include "Ellipse.hpp" +#include "Fill.hpp" +#include "GradientFill.hpp" +#include "Group.hpp" +#include "GradientStroke.hpp" +#include "Merge.hpp" +#include "Rectangle.hpp" +#include "RoundedRectangle.hpp" +#include "Repeater.hpp" +#include "Shape.hpp" +#include "Star.hpp" +#include "Stroke.hpp" +#include "Trim.hpp" +#include "ShapeTransform.hpp" + +namespace lottie { + +std::shared_ptr parseShapeItem(json11::Json::object const &json) noexcept(false) { + auto typeRawValue = getString(json, "ty"); + if (typeRawValue == "el") { + return std::make_shared(json); + } else if (typeRawValue == "fl") { + return std::make_shared(json); + } else if (typeRawValue == "gf") { + return std::make_shared(json); + } else if (typeRawValue == "gr") { + return std::make_shared(json); + } else if (typeRawValue == "gs") { + return std::make_shared(json); + } else if (typeRawValue == "mm") { + return std::make_shared(json); + } else if (typeRawValue == "rc") { + return std::make_shared(json); + } else if (typeRawValue == "rp") { + return std::make_shared(json); + } else if (typeRawValue == "sh") { + return std::make_shared(json); + } else if (typeRawValue == "sr") { + return std::make_shared(json); + } else if (typeRawValue == "st") { + return std::make_shared(json); + } else if (typeRawValue == "tm") { + return std::make_shared(json); + } else if (typeRawValue == "tr") { + return std::make_shared(json); + } else if (typeRawValue == "rd") { + return std::make_shared(json); + } else { + throw LottieParsingException(); + } +} + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/ShapeItem.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/ShapeItem.hpp new file mode 100644 index 0000000000..2b05dba93a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/ShapeItem.hpp @@ -0,0 +1,209 @@ +#ifndef ShapeItem_hpp +#define ShapeItem_hpp + +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +#include + +namespace lottie { + +enum class ShapeType { + Ellipse, + Fill, + GradientFill, + Group, + GradientStroke, + Merge, + Rectangle, + Repeater, + Shape, + Star, + Stroke, + Trim, + Transform, + RoundedRectangle +}; + +/// An item belonging to a Shape Layer +class ShapeItem { +public: + ShapeItem(json11::Json const &jsonAny) noexcept(false) : + type(ShapeType::Ellipse) { + if (!jsonAny.is_object()) { + throw LottieParsingException(); + } + + json11::Json::object const &json = jsonAny.object_items(); + + name = getOptionalString(json, "nm"); + matchName = getOptionalString(json, "mn"); + expressionIndex = getOptionalInt(json, "ix"); + cix = getOptionalInt(json, "cix"); + + index = getOptionalInt(json, "ind"); + blendMode = getOptionalInt(json, "bm"); + + auto typeRawValue = getString(json, "ty"); + if (typeRawValue == "el") { + type = ShapeType::Ellipse; + } else if (typeRawValue == "fl") { + type = ShapeType::Fill; + } else if (typeRawValue == "gf") { + type = ShapeType::GradientFill; + } else if (typeRawValue == "gr") { + type = ShapeType::Group; + } else if (typeRawValue == "gs") { + type = ShapeType::GradientStroke; + } else if (typeRawValue == "mm") { + type = ShapeType::Merge; + } else if (typeRawValue == "rc") { + type = ShapeType::Rectangle; + } else if (typeRawValue == "rp") { + type = ShapeType::Repeater; + } else if (typeRawValue == "sh") { + type = ShapeType::Shape; + } else if (typeRawValue == "sr") { + type = ShapeType::Star; + } else if (typeRawValue == "st") { + type = ShapeType::Stroke; + } else if (typeRawValue == "tm") { + type = ShapeType::Trim; + } else if (typeRawValue == "tr") { + type = ShapeType::Transform; + } else if (typeRawValue == "rd") { + type = ShapeType::RoundedRectangle; + } else { + throw LottieParsingException(); + } + + _hidden = getOptionalBool(json, "hd"); + + layerClass = getOptionalString(json, "cl"); + } + + ShapeItem( + std::optional name_, + std::optional matchName_, + std::optional expressionIndex_, + std::optional cix_, + ShapeType type_, + std::optional _hidden_, + std::optional index_, + std::optional blendMode_, + std::optional layerClass_ + ) : + name(name_), + matchName(matchName_), + expressionIndex(expressionIndex_), + cix(cix_), + type(type_), + _hidden(_hidden_), + index(index_), + blendMode(blendMode_), + layerClass(layerClass_) { + } + + ShapeItem(const ShapeItem&) = delete; + ShapeItem& operator=(ShapeItem&) = delete; + + virtual void toJson(json11::Json::object &json) const { + if (name.has_value()) { + json.insert(std::make_pair("nm", name.value())); + } + if (matchName.has_value()) { + json.insert(std::make_pair("mn", matchName.value())); + } + if (expressionIndex.has_value()) { + json.insert(std::make_pair("ix", expressionIndex.value())); + } + if (cix.has_value()) { + json.insert(std::make_pair("cix", cix.value())); + } + + if (index.has_value()) { + json.insert(std::make_pair("ind", index.value())); + } + if (blendMode.has_value()) { + json.insert(std::make_pair("bm", blendMode.value())); + } + + switch (type) { + case ShapeType::Ellipse: + json.insert(std::make_pair("ty", "el")); + break; + case ShapeType::Fill: + json.insert(std::make_pair("ty", "fl")); + break; + case ShapeType::GradientFill: + json.insert(std::make_pair("ty", "gf")); + break; + case ShapeType::Group: + json.insert(std::make_pair("ty", "gr")); + break; + case ShapeType::GradientStroke: + json.insert(std::make_pair("ty", "gs")); + break; + case ShapeType::Merge: + json.insert(std::make_pair("ty", "mm")); + break; + case ShapeType::Rectangle: + json.insert(std::make_pair("ty", "rc")); + break; + case ShapeType::RoundedRectangle: + json.insert(std::make_pair("ty", "rd")); + break; + case ShapeType::Repeater: + json.insert(std::make_pair("ty", "rp")); + break; + case ShapeType::Shape: + json.insert(std::make_pair("ty", "sh")); + break; + case ShapeType::Star: + json.insert(std::make_pair("ty", "sr")); + break; + case ShapeType::Stroke: + json.insert(std::make_pair("ty", "st")); + break; + case ShapeType::Trim: + json.insert(std::make_pair("ty", "tm")); + break; + case ShapeType::Transform: + json.insert(std::make_pair("ty", "tr")); + break; + } + + if (_hidden.has_value()) { + json.insert(std::make_pair("hd", _hidden.value())); + } + + if (layerClass.has_value()) { + json.insert(std::make_pair("cl", layerClass.value())); + } + } + + bool hidden() const { + if (_hidden.has_value()) { + return _hidden.value(); + } else { + return false; + } + } + +public: + std::optional name; + std::optional matchName; + std::optional expressionIndex; + std::optional cix; + ShapeType type; + std::optional _hidden; + std::optional index; + std::optional blendMode; + + std::optional layerClass; +}; + +std::shared_ptr parseShapeItem(json11::Json::object const &json) noexcept(false); + +} + +#endif /* ShapeItem_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/ShapeItem.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/ShapeItem.swift new file mode 100644 index 0000000000..2e28498a91 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/ShapeItem.swift @@ -0,0 +1,163 @@ +// +// ShapeItem.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/8/19. +// + +import Foundation + +// MARK: - ShapeType + ClassFamily + +/// Used for mapping a heterogeneous list to classes for parsing. +extension ShapeType: ClassFamily { + + static var discriminator: Discriminator = .type + + func getType() -> AnyObject.Type { + switch self { + case .ellipse: + return Ellipse.self + case .fill: + return Fill.self + case .gradientFill: + return GradientFill.self + case .group: + return Group.self + case .gradientStroke: + return GradientStroke.self + case .merge: + return Merge.self + case .rectangle: + return Rectangle.self + case .repeater: + return Repeater.self + case .shape: + return Shape.self + case .star: + return Star.self + case .stroke: + return Stroke.self + case .trim: + return Trim.self + case .transform: + return ShapeTransform.self + default: + return ShapeItem.self + } + } +} + +// MARK: - ShapeType + +enum ShapeType: String, Codable { + case ellipse = "el" + case fill = "fl" + case gradientFill = "gf" + case group = "gr" + case gradientStroke = "gs" + case merge = "mm" + case rectangle = "rc" + case repeater = "rp" + case round = "rd" + case shape = "sh" + case star = "sr" + case stroke = "st" + case trim = "tm" + case transform = "tr" + case unknown + + public init(from decoder: Decoder) throws { + self = try ShapeType(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown + } +} + +// MARK: - ShapeItem + +/// An item belonging to a Shape Layer +class ShapeItem: Codable, DictionaryInitializable { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: ShapeItem.CodingKeys.self) + name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Layer" + type = try container.decode(ShapeType.self, forKey: .type) + hidden = try container.decodeIfPresent(Bool.self, forKey: .hidden) ?? false + } + + required init(dictionary: [String: Any]) throws { + name = (try? dictionary.value(for: CodingKeys.name)) ?? "Layer" + type = ShapeType(rawValue: try dictionary.value(for: CodingKeys.type)) ?? .unknown + hidden = (try? dictionary.value(for: CodingKeys.hidden)) ?? false + } + + init( + name: String, + type: ShapeType, + hidden: Bool) + { + self.name = name + self.type = type + self.hidden = hidden + } + + // MARK: Internal + + /// The name of the shape + let name: String + + /// The type of shape + let type: ShapeType + + let hidden: Bool + + // MARK: Fileprivate + + fileprivate enum CodingKeys: String, CodingKey { + case name = "nm" + case type = "ty" + case hidden = "hd" + } +} + +extension Array where Element == ShapeItem { + + static func fromDictionaries(_ dictionaries: [[String: Any]]) throws -> [ShapeItem] { + try dictionaries.compactMap { dictionary in + let shapeType = dictionary[ShapeItem.CodingKeys.type.rawValue] as? String + switch ShapeType(rawValue: shapeType ?? ShapeType.unknown.rawValue) { + case .ellipse: + return try Ellipse(dictionary: dictionary) + case .fill: + return try Fill(dictionary: dictionary) + case .gradientFill: + return try GradientFill(dictionary: dictionary) + case .group: + return try Group(dictionary: dictionary) + case .gradientStroke: + return try GradientStroke(dictionary: dictionary) + case .merge: + return try Merge(dictionary: dictionary) + case .rectangle: + return try Rectangle(dictionary: dictionary) + case .repeater: + return try Repeater(dictionary: dictionary) + case .shape: + return try Shape(dictionary: dictionary) + case .star: + return try Star(dictionary: dictionary) + case .stroke: + return try Stroke(dictionary: dictionary) + case .trim: + return try Trim(dictionary: dictionary) + case .transform: + return try ShapeTransform(dictionary: dictionary) + case .none: + return nil + default: + return try ShapeItem(dictionary: dictionary) + } + } + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/ShapeTransform.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/ShapeTransform.cpp new file mode 100644 index 0000000000..1df07218bc --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/ShapeTransform.cpp @@ -0,0 +1,5 @@ +#include "ShapeTransform.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/ShapeTransform.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/ShapeTransform.hpp new file mode 100644 index 0000000000..584e37e10e --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/ShapeTransform.hpp @@ -0,0 +1,89 @@ +#ifndef ShapeTransform_hpp +#define ShapeTransform_hpp + +#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" +#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +namespace lottie { + +/// An item that define a shape transform +class ShapeTransform: public ShapeItem { +public: + explicit ShapeTransform(json11::Json::object const &json) noexcept(false) : + ShapeItem(json) { + if (const auto anchorData = getOptionalObject(json, "a")) { + anchor = KeyframeGroup(anchorData.value()); + } + if (const auto positionData = getOptionalObject(json, "p")) { + position = KeyframeGroup(positionData.value()); + } + if (const auto scaleData = getOptionalObject(json, "s")) { + scale = KeyframeGroup(scaleData.value()); + } + if (const auto rotationData = getOptionalObject(json, "r")) { + rotation = KeyframeGroup(rotationData.value()); + } + if (const auto opacityData = getOptionalObject(json, "o")) { + opacity = KeyframeGroup(opacityData.value()); + } + if (const auto skewData = getOptionalObject(json, "sk")) { + skew = KeyframeGroup(skewData.value()); + } + if (const auto skewAxisData = getOptionalObject(json, "sa")) { + skewAxis = KeyframeGroup(skewAxisData.value()); + } + } + + virtual void toJson(json11::Json::object &json) const override { + ShapeItem::toJson(json); + + if (anchor.has_value()) { + json.insert(std::make_pair("a", anchor->toJson())); + } + if (position.has_value()) { + json.insert(std::make_pair("p", position->toJson())); + } + if (scale.has_value()) { + json.insert(std::make_pair("s", scale->toJson())); + } + if (rotation.has_value()) { + json.insert(std::make_pair("r", rotation->toJson())); + } + if (opacity.has_value()) { + json.insert(std::make_pair("o", opacity->toJson())); + } + if (skew.has_value()) { + json.insert(std::make_pair("sk", skew->toJson())); + } + if (skewAxis.has_value()) { + json.insert(std::make_pair("sa", skewAxis->toJson())); + } + } + +public: + /// Anchor Point + std::optional> anchor; + + /// Position + std::optional> position; + + /// Scale + std::optional> scale; + + /// Rotation + std::optional> rotation; + + /// opacity + std::optional> opacity; + + /// Skew + std::optional> skew; + + /// Skew Axis + std::optional> skewAxis; +}; + +} + +#endif /* ShapeTransform_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/ShapeTransform.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/ShapeTransform.swift new file mode 100644 index 0000000000..734f01e114 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/ShapeTransform.swift @@ -0,0 +1,136 @@ +// +// TransformItem.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/8/19. +// + +import Foundation + +/// An item that define an ellipse shape +final class ShapeTransform: ShapeItem { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: ShapeTransform.CodingKeys.self) + anchor = try container + .decodeIfPresent(KeyframeGroup.self, forKey: .anchor) ?? KeyframeGroup(Vector3D(x: Double(0), y: 0, z: 0)) + position = try container + .decodeIfPresent(KeyframeGroup.self, forKey: .position) ?? KeyframeGroup(Vector3D(x: Double(0), y: 0, z: 0)) + scale = try container + .decodeIfPresent(KeyframeGroup.self, forKey: .scale) ?? KeyframeGroup(Vector3D(x: Double(100), y: 100, z: 100)) + rotation = try container.decodeIfPresent(KeyframeGroup.self, forKey: .rotation) ?? KeyframeGroup(Vector1D(0)) + opacity = try container.decodeIfPresent(KeyframeGroup.self, forKey: .opacity) ?? KeyframeGroup(Vector1D(100)) + skew = try container.decodeIfPresent(KeyframeGroup.self, forKey: .skew) ?? KeyframeGroup(Vector1D(0)) + skewAxis = try container.decodeIfPresent(KeyframeGroup.self, forKey: .skewAxis) ?? KeyframeGroup(Vector1D(0)) + try super.init(from: decoder) + } + + required init(dictionary: [String: Any]) throws { + if + let anchorDictionary = dictionary[CodingKeys.anchor.rawValue] as? [String: Any], + let anchor = try? KeyframeGroup(dictionary: anchorDictionary) + { + self.anchor = anchor + } else { + anchor = KeyframeGroup(Vector3D(x: Double(0), y: 0, z: 0)) + } + if + let positionDictionary = dictionary[CodingKeys.position.rawValue] as? [String: Any], + let position = try? KeyframeGroup(dictionary: positionDictionary) + { + self.position = position + } else { + position = KeyframeGroup(Vector3D(x: Double(0), y: 0, z: 0)) + } + if + let scaleDictionary = dictionary[CodingKeys.scale.rawValue] as? [String: Any], + let scale = try? KeyframeGroup(dictionary: scaleDictionary) + { + self.scale = scale + } else { + scale = KeyframeGroup(Vector3D(x: Double(100), y: 100, z: 100)) + } + if + let rotationDictionary = dictionary[CodingKeys.rotation.rawValue] as? [String: Any], + let rotation = try? KeyframeGroup(dictionary: rotationDictionary) + { + self.rotation = rotation + } else { + rotation = KeyframeGroup(Vector1D(0)) + } + if + let opacityDictionary = dictionary[CodingKeys.opacity.rawValue] as? [String: Any], + let opacity = try? KeyframeGroup(dictionary: opacityDictionary) + { + self.opacity = opacity + } else { + opacity = KeyframeGroup(Vector1D(100)) + } + if + let skewDictionary = dictionary[CodingKeys.skew.rawValue] as? [String: Any], + let skew = try? KeyframeGroup(dictionary: skewDictionary) + { + self.skew = skew + } else { + skew = KeyframeGroup(Vector1D(0)) + } + if + let skewAxisDictionary = dictionary[CodingKeys.skewAxis.rawValue] as? [String: Any], + let skewAxis = try? KeyframeGroup(dictionary: skewAxisDictionary) + { + self.skewAxis = skewAxis + } else { + skewAxis = KeyframeGroup(Vector1D(0)) + } + try super.init(dictionary: dictionary) + } + + // MARK: Internal + + /// Anchor Point + let anchor: KeyframeGroup + + /// Position + let position: KeyframeGroup + + /// Scale + let scale: KeyframeGroup + + /// Rotation + let rotation: KeyframeGroup + + /// opacity + let opacity: KeyframeGroup + + /// Skew + let skew: KeyframeGroup + + /// Skew Axis + let skewAxis: KeyframeGroup + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(anchor, forKey: .anchor) + try container.encode(position, forKey: .position) + try container.encode(scale, forKey: .scale) + try container.encode(rotation, forKey: .rotation) + try container.encode(opacity, forKey: .opacity) + try container.encode(skew, forKey: .skew) + try container.encode(skewAxis, forKey: .skewAxis) + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case anchor = "a" + case position = "p" + case scale = "s" + case rotation = "r" + case opacity = "o" + case skew = "sk" + case skewAxis = "sa" + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Star.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Star.cpp new file mode 100644 index 0000000000..c5f38f5946 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Star.cpp @@ -0,0 +1,5 @@ +#include "Star.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Star.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Star.hpp new file mode 100644 index 0000000000..5f5a58dad0 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Star.hpp @@ -0,0 +1,131 @@ +#ifndef Star_hpp +#define Star_hpp + +#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" +#include "Lottie/Private/Model/ShapeItems/Ellipse.hpp" +#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +#include + +namespace lottie { + +enum class StarType: int { + None = 0, + Star = 1, + Polygon = 2 +}; + +/// An item that define a star shape +class Star: public ShapeItem { +public: + explicit Star(json11::Json::object const &json) noexcept(false) : + ShapeItem(json), + position(KeyframeGroup(Vector3D(0.0, 0.0, 0.0))), + outerRadius(KeyframeGroup(Vector1D(0.0))), + outerRoundness(KeyframeGroup(Vector1D(0.0))), + rotation(KeyframeGroup(Vector1D(0.0))), + points(KeyframeGroup(Vector1D(0.0))), + starType(StarType::None) { + if (const auto directionRawValue = getOptionalInt(json, "d")) { + switch (directionRawValue.value()) { + case 1: + direction = PathDirection::Clockwise; + break; + case 2: + direction = PathDirection::UserSetClockwise; + break; + case 3: + direction = PathDirection::CounterClockwise; + break; + default: + throw LottieParsingException(); + } + } + + position = KeyframeGroup(getObject(json, "p")); + outerRadius = KeyframeGroup(getObject(json, "or")); + outerRoundness = KeyframeGroup(getObject(json, "os")); + + if (const auto innerRadiusData = getOptionalObject(json, "ir")) { + innerRadius = KeyframeGroup(innerRadiusData.value()); + } + if (const auto innerRoundnessData = getOptionalObject(json, "is")) { + innerRoundness = KeyframeGroup(innerRoundnessData.value()); + } + + rotation = KeyframeGroup(getObject(json, "r")); + points = KeyframeGroup(getObject(json, "pt")); + + auto starTypeRawValue = getInt(json, "sy"); + switch (starTypeRawValue) { + case 0: + starType = StarType::None; + break; + case 1: + starType = StarType::Star; + break; + case 2: + starType = StarType::Polygon; + break; + default: + throw LottieParsingException(); + } + } + + virtual void toJson(json11::Json::object &json) const override { + ShapeItem::toJson(json); + + if (direction.has_value()) { + json.insert(std::make_pair("d", (int)direction.value())); + } + + json.insert(std::make_pair("p", position.toJson())); + json.insert(std::make_pair("or", outerRadius.toJson())); + json.insert(std::make_pair("os", outerRoundness.toJson())); + + if (innerRadius.has_value()) { + json.insert(std::make_pair("ir", innerRadius->toJson())); + } + if (innerRoundness.has_value()) { + json.insert(std::make_pair("is", innerRoundness->toJson())); + } + + json.insert(std::make_pair("r", rotation.toJson())); + json.insert(std::make_pair("pt", points.toJson())); + + json.insert(std::make_pair("sy", (int)starType)); + } + +public: + /// The direction of the star. + std::optional direction; + + /// The position of the star + KeyframeGroup position; + + /// The outer radius of the star + KeyframeGroup outerRadius; + + /// The outer roundness of the star + KeyframeGroup outerRoundness; + + /// The outer radius of the star + std::optional> innerRadius; + + /// The outer roundness of the star + std::optional> innerRoundness; + + /// The rotation of the star + KeyframeGroup rotation; + + /// The number of points on the star + KeyframeGroup points; + + /// The type of star + StarType starType; +}; + +} + +#endif /* Star_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Star.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Star.swift new file mode 100644 index 0000000000..74c4e3d907 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Star.swift @@ -0,0 +1,132 @@ +// +// Star.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/8/19. +// + +import Foundation + +// MARK: - StarType + +enum StarType: Int, Codable { + case none + case star + case polygon +} + +// MARK: - Star + +/// An item that define an ellipse shape +final class Star: ShapeItem { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: Star.CodingKeys.self) + direction = try container.decodeIfPresent(PathDirection.self, forKey: .direction) ?? .clockwise + position = try container.decode(KeyframeGroup.self, forKey: .position) + outerRadius = try container.decode(KeyframeGroup.self, forKey: .outerRadius) + outerRoundness = try container.decode(KeyframeGroup.self, forKey: .outerRoundness) + innerRadius = try container.decodeIfPresent(KeyframeGroup.self, forKey: .innerRadius) + innerRoundness = try container.decodeIfPresent(KeyframeGroup.self, forKey: .innerRoundness) + rotation = try container.decode(KeyframeGroup.self, forKey: .rotation) + points = try container.decode(KeyframeGroup.self, forKey: .points) + starType = try container.decode(StarType.self, forKey: .starType) + try super.init(from: decoder) + } + + required init(dictionary: [String: Any]) throws { + if + let directionRawValue = dictionary[CodingKeys.direction.rawValue] as? Int, + let direction = PathDirection(rawValue: directionRawValue) + { + self.direction = direction + } else { + direction = .clockwise + } + let positionDictionary: [String: Any] = try dictionary.value(for: CodingKeys.position) + position = try KeyframeGroup(dictionary: positionDictionary) + let outerRadiusDictionary: [String: Any] = try dictionary.value(for: CodingKeys.outerRadius) + outerRadius = try KeyframeGroup(dictionary: outerRadiusDictionary) + let outerRoundnessDictionary: [String: Any] = try dictionary.value(for: CodingKeys.outerRoundness) + outerRoundness = try KeyframeGroup(dictionary: outerRoundnessDictionary) + if let innerRadiusDictionary = dictionary[CodingKeys.innerRadius.rawValue] as? [String: Any] { + innerRadius = try KeyframeGroup(dictionary: innerRadiusDictionary) + } else { + innerRadius = nil + } + if let innerRoundnessDictionary = dictionary[CodingKeys.innerRoundness.rawValue] as? [String: Any] { + innerRoundness = try KeyframeGroup(dictionary: innerRoundnessDictionary) + } else { + innerRoundness = nil + } + let rotationDictionary: [String: Any] = try dictionary.value(for: CodingKeys.rotation) + rotation = try KeyframeGroup(dictionary: rotationDictionary) + let pointsDictionary: [String: Any] = try dictionary.value(for: CodingKeys.points) + points = try KeyframeGroup(dictionary: pointsDictionary) + let starTypeRawValue: Int = try dictionary.value(for: CodingKeys.starType) + guard let starType = StarType(rawValue: starTypeRawValue) else { + throw InitializableError.invalidInput + } + self.starType = starType + try super.init(dictionary: dictionary) + } + + // MARK: Internal + + /// The direction of the star. + let direction: PathDirection + + /// The position of the star + let position: KeyframeGroup + + /// The outer radius of the star + let outerRadius: KeyframeGroup + + /// The outer roundness of the star + let outerRoundness: KeyframeGroup + + /// The outer radius of the star + let innerRadius: KeyframeGroup? + + /// The outer roundness of the star + let innerRoundness: KeyframeGroup? + + /// The rotation of the star + let rotation: KeyframeGroup + + /// The number of points on the star + let points: KeyframeGroup + + /// The type of star + let starType: StarType + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(direction, forKey: .direction) + try container.encode(position, forKey: .position) + try container.encode(outerRadius, forKey: .outerRadius) + try container.encode(outerRoundness, forKey: .outerRoundness) + try container.encode(innerRadius, forKey: .innerRadius) + try container.encode(innerRoundness, forKey: .innerRoundness) + try container.encode(rotation, forKey: .rotation) + try container.encode(points, forKey: .points) + try container.encode(starType, forKey: .starType) + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case direction = "d" + case position = "p" + case outerRadius = "or" + case outerRoundness = "os" + case innerRadius = "ir" + case innerRoundness = "is" + case rotation = "r" + case points = "pt" + case starType = "sy" + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Stroke.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Stroke.cpp new file mode 100644 index 0000000000..0c858f2442 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Stroke.cpp @@ -0,0 +1,5 @@ +#include "Stroke.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Stroke.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Stroke.hpp new file mode 100644 index 0000000000..2fe24d3050 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Stroke.hpp @@ -0,0 +1,140 @@ +#ifndef Stroke_hpp +#define Stroke_hpp + +#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" +#include "Lottie/Private/Model/ShapeItems/GradientStroke.hpp" +#include "Lottie/Public/Primitives/Color.hpp" +#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" +#include "Lottie/Private/Model/Objects/DashElement.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +#include + +namespace lottie { + +/// An item that define an ellipse shape +class Stroke: public ShapeItem { +public: + explicit Stroke(json11::Json::object const &json) noexcept(false) : + ShapeItem(json), + opacity(KeyframeGroup(Vector1D(100.0))), + color(KeyframeGroup(Color(0.0, 0.0, 0.0, 0.0))), + width(KeyframeGroup(Vector1D(0.0))), + lineCap(LineCap::Round), + lineJoin(LineJoin::Round) { + opacity = KeyframeGroup(getObject(json, "o")); + color = KeyframeGroup(getObject(json, "c")); + width = KeyframeGroup(getObject(json, "w")); + + if (const auto lineCapRawValue = getOptionalInt(json, "lc")) { + switch (lineCapRawValue.value()) { + case 0: + lineCap = LineCap::None; + break; + case 1: + lineCap = LineCap::Butt; + break; + case 2: + lineCap = LineCap::Round; + break; + case 3: + lineCap = LineCap::Square; + break; + default: + throw LottieParsingException(); + } + } + + if (const auto lineJoinRawValue = getOptionalInt(json, "lj")) { + switch (lineJoinRawValue.value()) { + case 0: + lineJoin = LineJoin::None; + break; + case 1: + lineJoin = LineJoin::Miter; + break; + case 2: + lineJoin = LineJoin::Round; + break; + case 3: + lineJoin = LineJoin::Bevel; + break; + default: + throw LottieParsingException(); + } + } + + if (const auto miterLimitData = getOptionalDouble(json, "ml")) { + miterLimit = miterLimitData.value(); + } + + if (const auto dashElementsData = getOptionalObjectArray(json, "d")) { + dashPattern = std::vector(); + for (const auto &dashElementData : dashElementsData.value()) { + dashPattern->push_back(DashElement(dashElementData)); + } + } + + fillEnabled = getOptionalBool(json, "fillEnabled"); + ml2 = getOptionalAny(json, "ml2"); + } + + virtual void toJson(json11::Json::object &json) const override { + ShapeItem::toJson(json); + + json.insert(std::make_pair("o", opacity.toJson())); + json.insert(std::make_pair("c", color.toJson())); + json.insert(std::make_pair("w", width.toJson())); + + json.insert(std::make_pair("lc", (int)lineCap)); + json.insert(std::make_pair("lj", (int)lineJoin)); + + if (miterLimit.has_value()) { + json.insert(std::make_pair("ml", miterLimit.value())); + } + + if (dashPattern.has_value()) { + json11::Json::array dashElements; + for (const auto &dashElement : dashPattern.value()) { + dashElements.push_back(dashElement.toJson()); + } + json.insert(std::make_pair("d", dashElements)); + } + + if (fillEnabled.has_value()) { + json.insert(std::make_pair("fillEnabled", fillEnabled.value())); + } + if (ml2.has_value()) { + json.insert(std::make_pair("ml2", ml2.value())); + } + } + +public: + /// The opacity of the stroke + KeyframeGroup opacity; + + /// The Color of the stroke + KeyframeGroup color; + + /// The width of the stroke + KeyframeGroup width; + + /// Line Cap + LineCap lineCap; + + /// Line Join + LineJoin lineJoin; + + /// Miter Limit + std::optional miterLimit; + + /// The dash pattern of the stroke + std::optional> dashPattern; + + std::optional fillEnabled; + std::optional ml2; +}; + +} + +#endif /* Stroke_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Stroke.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Stroke.swift new file mode 100644 index 0000000000..6caebb61be --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Stroke.swift @@ -0,0 +1,102 @@ +// +// Stroke.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/8/19. +// + +import Foundation + +/// An item that define an ellipse shape +final class Stroke: ShapeItem { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: Stroke.CodingKeys.self) + opacity = try container.decode(KeyframeGroup.self, forKey: .opacity) + color = try container.decode(KeyframeGroup.self, forKey: .color) + width = try container.decode(KeyframeGroup.self, forKey: .width) + lineCap = try container.decodeIfPresent(LineCap.self, forKey: .lineCap) ?? .round + lineJoin = try container.decodeIfPresent(LineJoin.self, forKey: .lineJoin) ?? .round + miterLimit = try container.decodeIfPresent(Double.self, forKey: .miterLimit) ?? 4 + dashPattern = try container.decodeIfPresent([DashElement].self, forKey: .dashPattern) + try super.init(from: decoder) + } + + required init(dictionary: [String: Any]) throws { + let opacityDictionary: [String: Any] = try dictionary.value(for: CodingKeys.opacity) + opacity = try KeyframeGroup(dictionary: opacityDictionary) + let colorDictionary: [String: Any] = try dictionary.value(for: CodingKeys.color) + color = try KeyframeGroup(dictionary: colorDictionary) + let widthDictionary: [String: Any] = try dictionary.value(for: CodingKeys.width) + width = try KeyframeGroup(dictionary: widthDictionary) + if + let lineCapRawValue = dictionary[CodingKeys.lineCap.rawValue] as? Int, + let lineCap = LineCap(rawValue: lineCapRawValue) + { + self.lineCap = lineCap + } else { + lineCap = .round + } + if + let lineJoinRawValue = dictionary[CodingKeys.lineJoin.rawValue] as? Int, + let lineJoin = LineJoin(rawValue: lineJoinRawValue) + { + self.lineJoin = lineJoin + } else { + lineJoin = .round + } + miterLimit = (try? dictionary.value(for: CodingKeys.miterLimit)) ?? 4 + let dashPatternDictionaries = dictionary[CodingKeys.dashPattern.rawValue] as? [[String: Any]] + dashPattern = try? dashPatternDictionaries?.map({ try DashElement(dictionary: $0) }) + try super.init(dictionary: dictionary) + } + + // MARK: Internal + + /// The opacity of the stroke + let opacity: KeyframeGroup + + /// The Color of the stroke + let color: KeyframeGroup + + /// The width of the stroke + let width: KeyframeGroup + + /// Line Cap + let lineCap: LineCap + + /// Line Join + let lineJoin: LineJoin + + /// Miter Limit + let miterLimit: Double + + /// The dash pattern of the stroke + let dashPattern: [DashElement]? + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(opacity, forKey: .opacity) + try container.encode(color, forKey: .color) + try container.encode(width, forKey: .width) + try container.encode(lineCap, forKey: .lineCap) + try container.encode(lineJoin, forKey: .lineJoin) + try container.encode(miterLimit, forKey: .miterLimit) + try container.encodeIfPresent(dashPattern, forKey: .dashPattern) + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case opacity = "o" + case color = "c" + case width = "w" + case lineCap = "lc" + case lineJoin = "lj" + case miterLimit = "ml" + case dashPattern = "d" + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Trim.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Trim.cpp new file mode 100644 index 0000000000..85f95d8b0c --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Trim.cpp @@ -0,0 +1,5 @@ +#include "Trim.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Trim.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Trim.hpp new file mode 100644 index 0000000000..6bc05b1f04 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Trim.hpp @@ -0,0 +1,60 @@ +#ifndef Trim_hpp +#define Trim_hpp + +#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" +#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" +#include "Lottie/Public/Primitives/Vectors.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +namespace lottie { + +enum class TrimType: int { + Simultaneously = 1, + Individually = 2 +}; + +/// An item that defines trim +class Trim: public ShapeItem { +public: + explicit Trim(json11::Json::object const &json) noexcept(false) : + ShapeItem(json), + start(KeyframeGroup(Vector1D(0.0))), + end(KeyframeGroup(Vector1D(0.0))), + offset(KeyframeGroup(Vector1D(0.0))), + trimType(TrimType::Simultaneously) { + start = KeyframeGroup(getObject(json, "s")); + end = KeyframeGroup(getObject(json, "e")); + offset = KeyframeGroup(getObject(json, "o")); + + auto trimTypeRawValue = getInt(json, "m"); + switch (trimTypeRawValue) { + case 1: + trimType = TrimType::Simultaneously; + break; + case 2: + trimType = TrimType::Individually; + break; + default: + throw LottieParsingException(); + } + } + + virtual void toJson(json11::Json::object &json) const override { + ShapeItem::toJson(json); + + json.insert(std::make_pair("s", start.toJson())); + json.insert(std::make_pair("e", end.toJson())); + json.insert(std::make_pair("o", offset.toJson())); + json.insert(std::make_pair("m", (int)trimType)); + } + +public: + KeyframeGroup start; + KeyframeGroup end; + KeyframeGroup offset; + TrimType trimType; +}; + +} + +#endif /* Trim_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Trim.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Trim.swift new file mode 100644 index 0000000000..7dc0814432 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/ShapeItems/Trim.swift @@ -0,0 +1,78 @@ +// +// Trim.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/8/19. +// + +import Foundation + +// MARK: - TrimType + +enum TrimType: Int, Codable { + case simultaneously = 1 + case individually = 2 +} + +// MARK: - Trim + +/// An item that define an ellipse shape +final class Trim: ShapeItem { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: Trim.CodingKeys.self) + start = try container.decode(KeyframeGroup.self, forKey: .start) + end = try container.decode(KeyframeGroup.self, forKey: .end) + offset = try container.decode(KeyframeGroup.self, forKey: .offset) + trimType = try container.decode(TrimType.self, forKey: .trimType) + try super.init(from: decoder) + } + + required init(dictionary: [String: Any]) throws { + let startDictionary: [String: Any] = try dictionary.value(for: CodingKeys.start) + start = try KeyframeGroup(dictionary: startDictionary) + let endDictionary: [String: Any] = try dictionary.value(for: CodingKeys.end) + end = try KeyframeGroup(dictionary: endDictionary) + let offsetDictionary: [String: Any] = try dictionary.value(for: CodingKeys.offset) + offset = try KeyframeGroup(dictionary: offsetDictionary) + let trimTypeRawValue: Int = try dictionary.value(for: CodingKeys.trimType) + guard let trimType = TrimType(rawValue: trimTypeRawValue) else { + throw InitializableError.invalidInput + } + self.trimType = trimType + try super.init(dictionary: dictionary) + } + + // MARK: Internal + + /// The start of the trim + let start: KeyframeGroup + + /// The end of the trim + let end: KeyframeGroup + + /// The offset of the trim + let offset: KeyframeGroup + + let trimType: TrimType + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(start, forKey: .start) + try container.encode(end, forKey: .end) + try container.encode(offset, forKey: .offset) + try container.encode(trimType, forKey: .trimType) + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case start = "s" + case end = "e" + case offset = "o" + case trimType = "m" + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/Font.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/Font.cpp new file mode 100644 index 0000000000..7dfd91b3cc --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/Font.cpp @@ -0,0 +1,5 @@ +#include "Font.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/Font.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/Font.hpp new file mode 100644 index 0000000000..8bde83023e --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/Font.hpp @@ -0,0 +1,103 @@ +#ifndef Font_hpp +#define Font_hpp + +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +#include +#include + +namespace lottie { + +class Font { +public: + Font( + std::string const &name_, + std::string const &familyName_, + std::string const &style_, + double ascent_ + ) : + name(name_), + familyName(familyName_), + style(style_), + ascent(ascent_) { + } + + explicit Font(json11::Json::object const &json) noexcept(false) { + name = getString(json, "fName"); + familyName = getString(json, "fFamily"); + path = getOptionalString(json, "fPath"); + weight = getOptionalString(json, "fWeight"); + fontClass = getOptionalString(json, "fClass"); + style = getString(json, "fStyle"); + ascent = getDouble(json, "ascent"); + origin = getOptionalInt(json, "origin"); + } + + json11::Json::object toJson() const { + json11::Json::object result; + + result.insert(std::make_pair("fName", name)); + result.insert(std::make_pair("fFamily", familyName)); + if (path.has_value()) { + result.insert(std::make_pair("fPath", path.value())); + } + if (weight.has_value()) { + result.insert(std::make_pair("fWeight", weight.value())); + } + if (fontClass.has_value()) { + result.insert(std::make_pair("fClass", fontClass.value())); + } + result.insert(std::make_pair("fStyle", style)); + result.insert(std::make_pair("ascent", ascent)); + if (origin.has_value()) { + result.insert(std::make_pair("origin", origin.value())); + } + + return result; + } + +public: + std::string name; + std::string familyName; + std::optional path; + std::optional weight; + std::optional fontClass; + std::string style; + double ascent; + std::optional origin; +}; + +/// A list of fonts +class FontList { +public: + FontList(std::vector const &fonts_) : + fonts(fonts_) { + } + + explicit FontList(json11::Json::object const &json) noexcept(false) { + if (const auto fontsData = getOptionalObjectArray(json, "list")) { + for (const auto &fontData : fontsData.value()) { + fonts.emplace_back(fontData); + } + } + } + + json11::Json::object toJson() const { + json11::Json::array fontArray; + + for (const auto &font : fonts) { + fontArray.push_back(font.toJson()); + } + + json11::Json::object result; + result.insert(std::make_pair("list", fontArray)); + return result; + } + +public: + std::vector fonts; +}; + +} + +#endif /* Font_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/Font.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/Font.swift new file mode 100644 index 0000000000..d78ee0dcdd --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/Font.swift @@ -0,0 +1,61 @@ +// +// Font.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/9/19. +// + +import Foundation + +// MARK: - Font + +final class Font: Codable, DictionaryInitializable { + + // MARK: Lifecycle + + init(dictionary: [String: Any]) throws { + name = try dictionary.value(for: CodingKeys.name) + familyName = try dictionary.value(for: CodingKeys.familyName) + style = try dictionary.value(for: CodingKeys.style) + ascent = try dictionary.value(for: CodingKeys.ascent) + } + + // MARK: Internal + + let name: String + let familyName: String + let style: String + let ascent: Double + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case name = "fName" + case familyName = "fFamily" + case style = "fStyle" + case ascent + } + +} + +// MARK: - FontList + +/// A list of fonts +final class FontList: Codable, DictionaryInitializable { + + // MARK: Lifecycle + + init(dictionary: [String: Any]) throws { + let fontDictionaries: [[String: Any]] = try dictionary.value(for: CodingKeys.fonts) + fonts = try fontDictionaries.map({ try Font(dictionary: $0) }) + } + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case fonts = "list" + } + + let fonts: [Font] + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/Glyph.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/Glyph.cpp new file mode 100644 index 0000000000..071050db7a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/Glyph.cpp @@ -0,0 +1,5 @@ +#include "Glyph.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/Glyph.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/Glyph.hpp new file mode 100644 index 0000000000..25d0af5fa9 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/Glyph.hpp @@ -0,0 +1,108 @@ +#ifndef Glyph_hpp +#define Glyph_hpp + +#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +#include +#include + +namespace lottie { + +class Glyph { +public: + Glyph( + std::string const &character_, + double fontSize_, + std::string const &fontFamily_, + std::string const &fontStyle_, + double width_, + std::optional>> shapes_ + ) : + character(character_), + fontSize(fontSize_), + fontFamily(fontFamily_), + fontStyle(fontStyle_), + width(width_), + shapes(shapes_) { + } + + explicit Glyph(json11::Json::object const &json) noexcept(false) : + character(""), + fontSize(0.0), + fontFamily(""), + fontStyle(""), + width(0.0) { + character = getString(json, "ch"); + fontSize = getDouble(json, "size"); + fontFamily = getString(json, "fFamily"); + fontStyle = getString(json, "style"); + width = getDouble(json, "w"); + + if (const auto shapeContainer = getOptionalObject(json, "data")) { + internalHasData = true; + + if (const auto shapesData = getOptionalObjectArray(shapeContainer.value(), "shapes")) { + shapes = std::vector>(); + + for (const auto &shapeData : shapesData.value()) { + shapes->push_back(parseShapeItem(shapeData)); + } + } + } + } + + json11::Json::object toJson() const { + json11::Json::object result; + + result.insert(std::make_pair("ch", character)); + result.insert(std::make_pair("size", fontSize)); + result.insert(std::make_pair("fFamily", fontFamily)); + result.insert(std::make_pair("style", fontStyle)); + result.insert(std::make_pair("w", width)); + + if (internalHasData || shapes.has_value()) { + json11::Json::object shapeContainer; + + if (shapes.has_value()) { + json11::Json::array shapeArray; + + for (const auto &shape : shapes.value()) { + json11::Json::object shapeJson; + shape->toJson(shapeJson); + shapeArray.push_back(shapeJson); + } + + shapeContainer.insert(std::make_pair("shapes", shapeArray)); + } + result.insert(std::make_pair("data", shapeContainer)); + } + + return result; + } + +public: + /// The character + std::string character; + + /// The font size of the character + double fontSize; + + /// The font family of the character + std::string fontFamily; + + /// The Style of the character + std::string fontStyle; + + /// The Width of the character + double width; + + /// The Shape Data of the Character + std::optional>> shapes; + + bool internalHasData = false; +}; + +} + +#endif /* Glyph_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/Glyph.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/Glyph.swift new file mode 100644 index 0000000000..03a7c58e55 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/Glyph.swift @@ -0,0 +1,96 @@ +// +// Glyph.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/9/19. +// + +import Foundation + +/// A model that holds a vector character +final class Glyph: Codable, DictionaryInitializable { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: Glyph.CodingKeys.self) + character = try container.decode(String.self, forKey: .character) + fontSize = try container.decode(Double.self, forKey: .fontSize) + fontFamily = try container.decode(String.self, forKey: .fontFamily) + fontStyle = try container.decode(String.self, forKey: .fontStyle) + width = try container.decode(Double.self, forKey: .width) + if + container.contains(.shapeWrapper), + let shapeContainer = try? container.nestedContainer(keyedBy: ShapeKey.self, forKey: .shapeWrapper), + shapeContainer.contains(.shapes) + { + shapes = try shapeContainer.decode([ShapeItem].self, ofFamily: ShapeType.self, forKey: .shapes) + } else { + shapes = [] + } + } + + init(dictionary: [String: Any]) throws { + character = try dictionary.value(for: CodingKeys.character) + fontSize = try dictionary.value(for: CodingKeys.fontSize) + fontFamily = try dictionary.value(for: CodingKeys.fontFamily) + fontStyle = try dictionary.value(for: CodingKeys.fontStyle) + width = try dictionary.value(for: CodingKeys.width) + if + let shapes = dictionary[CodingKeys.shapeWrapper.rawValue] as? [String: Any], + let shapeDictionaries = shapes[ShapeKey.shapes.rawValue] as? [[String: Any]] + { + self.shapes = try [ShapeItem].fromDictionaries(shapeDictionaries) + } else { + shapes = [ShapeItem]() + } + } + + // MARK: Internal + + /// The character + let character: String + + /// The font size of the character + let fontSize: Double + + /// The font family of the character + let fontFamily: String + + /// The Style of the character + let fontStyle: String + + /// The Width of the character + let width: Double + + /// The Shape Data of the Character + let shapes: [ShapeItem] + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(character, forKey: .character) + try container.encode(fontSize, forKey: .fontSize) + try container.encode(fontFamily, forKey: .fontFamily) + try container.encode(fontStyle, forKey: .fontStyle) + try container.encode(width, forKey: .width) + + var shapeContainer = container.nestedContainer(keyedBy: ShapeKey.self, forKey: .shapeWrapper) + try shapeContainer.encode(shapes, forKey: .shapes) + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case character = "ch" + case fontSize = "size" + case fontFamily = "fFamily" + case fontStyle = "style" + case width = "w" + case shapeWrapper = "data" + } + + private enum ShapeKey: String, CodingKey { + case shapes + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/TextAnimator.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/TextAnimator.cpp new file mode 100644 index 0000000000..7cd9b25715 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/TextAnimator.cpp @@ -0,0 +1,5 @@ +#include "TextAnimator.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/TextAnimator.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/TextAnimator.hpp new file mode 100644 index 0000000000..e302a9051f --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/TextAnimator.hpp @@ -0,0 +1,183 @@ +#ifndef TextAnimator_hpp +#define TextAnimator_hpp + +#include "Lottie/Public/Primitives/Vectors.hpp" +#include "Lottie/Public/Primitives/Color.hpp" +#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +#include +#include + +namespace lottie { + +class TextAnimator { +public: + TextAnimator( + std::optional &name_, + std::optional> anchor_, + std::optional> position_, + std::optional> scale_, + std::optional> skew_, + std::optional> skewAxis_, + std::optional> rotation_, + std::optional> opacity_, + std::optional> strokeColor_, + std::optional> fillColor_, + std::optional> strokeWidth_, + std::optional> tracking_ + ) : + name(name_), + anchor(anchor_), + position(position_), + scale(scale_), + skew(skew_), + skewAxis(skewAxis_), + rotation(rotation_), + opacity(opacity_), + strokeColor(strokeColor_), + fillColor(fillColor_), + strokeWidth(strokeWidth_), + tracking(tracking_) { + } + + explicit TextAnimator(json11::Json const &jsonAny) { + if (!jsonAny.is_object()) { + throw LottieParsingException(); + } + json11::Json::object const &json = jsonAny.object_items(); + + if (const auto nameData = getOptionalString(json, "nm")) { + name = nameData.value(); + } + _extraS = getOptionalAny(json, "s"); + + json11::Json::object const &animatorContainer = getObject(json, "a"); + + if (const auto fillColorData = getOptionalObject(animatorContainer, "fc")) { + fillColor = KeyframeGroup(fillColorData.value()); + } + if (const auto strokeColorData = getOptionalObject(animatorContainer, "sc")) { + strokeColor = KeyframeGroup(strokeColorData.value()); + } + if (const auto strokeWidthData = getOptionalObject(animatorContainer, "sw")) { + strokeWidth = KeyframeGroup(strokeWidthData.value()); + } + if (const auto trackingData = getOptionalObject(animatorContainer, "t")) { + tracking = KeyframeGroup(trackingData.value()); + } + if (const auto anchorData = getOptionalObject(animatorContainer, "a")) { + anchor = KeyframeGroup(anchorData.value()); + } + if (const auto positionData = getOptionalObject(animatorContainer, "p")) { + position = KeyframeGroup(positionData.value()); + } + if (const auto scaleData = getOptionalObject(animatorContainer, "s")) { + scale = KeyframeGroup(scaleData.value()); + } + if (const auto skewData = getOptionalObject(animatorContainer, "sk")) { + skew = KeyframeGroup(skewData.value()); + } + if (const auto skewAxisData = getOptionalObject(animatorContainer, "sa")) { + skewAxis = KeyframeGroup(skewAxisData.value()); + } + if (const auto rotationData = getOptionalObject(animatorContainer, "r")) { + rotation = KeyframeGroup(rotationData.value()); + } + if (const auto opacityData = getOptionalObject(animatorContainer, "o")) { + opacity = KeyframeGroup(opacityData.value()); + } + } + + json11::Json::object toJson() const { + json11::Json::object animatorContainer; + + if (fillColor.has_value()) { + animatorContainer.insert(std::make_pair("fc", fillColor->toJson())); + } + if (strokeColor.has_value()) { + animatorContainer.insert(std::make_pair("sc", strokeColor->toJson())); + } + if (strokeWidth.has_value()) { + animatorContainer.insert(std::make_pair("sw", strokeWidth->toJson())); + } + if (tracking.has_value()) { + animatorContainer.insert(std::make_pair("t", tracking->toJson())); + } + if (anchor.has_value()) { + animatorContainer.insert(std::make_pair("a", anchor->toJson())); + } + if (position.has_value()) { + animatorContainer.insert(std::make_pair("p", position->toJson())); + } + if (scale.has_value()) { + animatorContainer.insert(std::make_pair("s", scale->toJson())); + } + if (skew.has_value()) { + animatorContainer.insert(std::make_pair("sk", skew->toJson())); + } + if (skewAxis.has_value()) { + animatorContainer.insert(std::make_pair("sa", skewAxis->toJson())); + } + if (rotation.has_value()) { + animatorContainer.insert(std::make_pair("r", rotation->toJson())); + } + if (opacity.has_value()) { + animatorContainer.insert(std::make_pair("o", opacity->toJson())); + } + + json11::Json::object result; + result.insert(std::make_pair("a", animatorContainer)); + + if (name.has_value()) { + result.insert(std::make_pair("nm", name.value())); + } + if (_extraS.has_value()) { + result.insert(std::make_pair("s", _extraS.value())); + } + + return result; + } + +public: + std::optional name; + + /// Anchor + std::optional> anchor; + + /// Position + std::optional> position; + + /// Scale + std::optional> scale; + + /// Skew + std::optional> skew; + + /// Skew Axis + std::optional> skewAxis; + + /// Rotation + std::optional> rotation; + + /// Opacity + std::optional> opacity; + + /// Stroke Color + std::optional> strokeColor; + + /// Fill Color + std::optional> fillColor; + + /// Stroke Width + std::optional> strokeWidth; + + /// Tracking + std::optional> tracking; + + std::optional _extraS; +}; + +} + +#endif /* TextAnimator_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/TextAnimator.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/TextAnimator.swift new file mode 100644 index 0000000000..f8307267c1 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/TextAnimator.swift @@ -0,0 +1,165 @@ +// +// TextAnimator.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/9/19. +// + +import Foundation + +final class TextAnimator: Codable, DictionaryInitializable { + + // MARK: Lifecycle + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: TextAnimator.CodingKeys.self) + name = try container.decodeIfPresent(String.self, forKey: .name) ?? "" + let animatorContainer = try container.nestedContainer(keyedBy: TextAnimatorKeys.self, forKey: .textAnimator) + fillColor = try animatorContainer.decodeIfPresent(KeyframeGroup.self, forKey: .fillColor) + strokeColor = try animatorContainer.decodeIfPresent(KeyframeGroup.self, forKey: .strokeColor) + strokeWidth = try animatorContainer.decodeIfPresent(KeyframeGroup.self, forKey: .strokeWidth) + tracking = try animatorContainer.decodeIfPresent(KeyframeGroup.self, forKey: .tracking) + anchor = try animatorContainer.decodeIfPresent(KeyframeGroup.self, forKey: .anchor) + position = try animatorContainer.decodeIfPresent(KeyframeGroup.self, forKey: .position) + scale = try animatorContainer.decodeIfPresent(KeyframeGroup.self, forKey: .scale) + skew = try animatorContainer.decodeIfPresent(KeyframeGroup.self, forKey: .skew) + skewAxis = try animatorContainer.decodeIfPresent(KeyframeGroup.self, forKey: .skewAxis) + rotation = try animatorContainer.decodeIfPresent(KeyframeGroup.self, forKey: .rotation) + opacity = try animatorContainer.decodeIfPresent(KeyframeGroup.self, forKey: .opacity) + + } + + init(dictionary: [String: Any]) throws { + name = (try? dictionary.value(for: CodingKeys.name)) ?? "" + let animatorDictionary: [String: Any] = try dictionary.value(for: CodingKeys.textAnimator) + if let fillColorDictionary = animatorDictionary[TextAnimatorKeys.fillColor.rawValue] as? [String: Any] { + fillColor = try? KeyframeGroup(dictionary: fillColorDictionary) + } else { + fillColor = nil + } + if let strokeColorDictionary = animatorDictionary[TextAnimatorKeys.strokeColor.rawValue] as? [String: Any] { + strokeColor = try? KeyframeGroup(dictionary: strokeColorDictionary) + } else { + strokeColor = nil + } + if let strokeWidthDictionary = animatorDictionary[TextAnimatorKeys.strokeWidth.rawValue] as? [String: Any] { + strokeWidth = try? KeyframeGroup(dictionary: strokeWidthDictionary) + } else { + strokeWidth = nil + } + if let trackingDictionary = animatorDictionary[TextAnimatorKeys.tracking.rawValue] as? [String: Any] { + tracking = try? KeyframeGroup(dictionary: trackingDictionary) + } else { + tracking = nil + } + if let anchorDictionary = animatorDictionary[TextAnimatorKeys.anchor.rawValue] as? [String: Any] { + anchor = try? KeyframeGroup(dictionary: anchorDictionary) + } else { + anchor = nil + } + if let positionDictionary = animatorDictionary[TextAnimatorKeys.position.rawValue] as? [String: Any] { + position = try? KeyframeGroup(dictionary: positionDictionary) + } else { + position = nil + } + if let scaleDictionary = animatorDictionary[TextAnimatorKeys.scale.rawValue] as? [String: Any] { + scale = try? KeyframeGroup(dictionary: scaleDictionary) + } else { + scale = nil + } + if let skewDictionary = animatorDictionary[TextAnimatorKeys.skew.rawValue] as? [String: Any] { + skew = try? KeyframeGroup(dictionary: skewDictionary) + } else { + skew = nil + } + if let skewAxisDictionary = animatorDictionary[TextAnimatorKeys.skewAxis.rawValue] as? [String: Any] { + skewAxis = try? KeyframeGroup(dictionary: skewAxisDictionary) + } else { + skewAxis = nil + } + if let rotationDictionary = animatorDictionary[TextAnimatorKeys.rotation.rawValue] as? [String: Any] { + rotation = try? KeyframeGroup(dictionary: rotationDictionary) + } else { + rotation = nil + } + if let opacityDictionary = animatorDictionary[TextAnimatorKeys.opacity.rawValue] as? [String: Any] { + opacity = try KeyframeGroup(dictionary: opacityDictionary) + } else { + opacity = nil + } + } + + // MARK: Internal + + let name: String + + /// Anchor + let anchor: KeyframeGroup? + + /// Position + let position: KeyframeGroup? + + /// Scale + let scale: KeyframeGroup? + + /// Skew + let skew: KeyframeGroup? + + /// Skew Axis + let skewAxis: KeyframeGroup? + + /// Rotation + let rotation: KeyframeGroup? + + /// Opacity + let opacity: KeyframeGroup? + + /// Stroke Color + let strokeColor: KeyframeGroup? + + /// Fill Color + let fillColor: KeyframeGroup? + + /// Stroke Width + let strokeWidth: KeyframeGroup? + + /// Tracking + let tracking: KeyframeGroup? + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + var animatorContainer = container.nestedContainer(keyedBy: TextAnimatorKeys.self, forKey: .textAnimator) + try animatorContainer.encodeIfPresent(fillColor, forKey: .fillColor) + try animatorContainer.encodeIfPresent(strokeColor, forKey: .strokeColor) + try animatorContainer.encodeIfPresent(strokeWidth, forKey: .strokeWidth) + try animatorContainer.encodeIfPresent(tracking, forKey: .tracking) + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { +// case textSelector = "s" TODO + case textAnimator = "a" + case name = "nm" + } + + private enum TextSelectorKeys: String, CodingKey { + case start = "s" + case end = "e" + case offset = "o" + } + + private enum TextAnimatorKeys: String, CodingKey { + case fillColor = "fc" + case strokeColor = "sc" + case strokeWidth = "sw" + case tracking = "t" + case anchor = "a" + case position = "p" + case scale = "s" + case skew = "sk" + case skewAxis = "sa" + case rotation = "r" + case opacity = "o" + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/TextDocument.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/TextDocument.cpp new file mode 100644 index 0000000000..95c3be2b67 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/TextDocument.cpp @@ -0,0 +1,5 @@ +#include "TextDocument.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/TextDocument.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/TextDocument.hpp new file mode 100644 index 0000000000..9eb3b1f154 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/TextDocument.hpp @@ -0,0 +1,184 @@ +#ifndef TextDocument_hpp +#define TextDocument_hpp + +#include "Lottie/Public/Primitives/Vectors.hpp" +#include "Lottie/Public/Primitives/Color.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +#include +#include + +namespace lottie { + +enum class TextJustification: int { + Left = 0, + Right = 1, + Center = 2 +}; + +class TextDocument { +public: + TextDocument( + std::string const &text_, + double fontSize_, + std::string const &fontFamily_, + TextJustification justification_, + int tracking_, + double lineHeight_, + std::optional baseline_, + std::optional fillColorData_, + std::optional strokeColorData_, + std::optional strokeWidth_, + std::optional strokeOverFill_, + std::optional textFramePosition_, + std::optional textFrameSize_ + ) : + text(text_), + fontSize(fontSize_), + fontFamily(fontFamily_), + justification(justification_), + tracking(tracking_), + lineHeight(lineHeight_), + baseline(baseline_), + fillColorData(fillColorData_), + strokeColorData(strokeColorData_), + strokeWidth(strokeWidth_), + strokeOverFill(strokeOverFill_), + textFramePosition(textFramePosition_), + textFrameSize(textFrameSize_) { + } + + explicit TextDocument(json11::Json const &jsonAny) noexcept(false) : + text(""), + fontSize(0.0), + fontFamily(""), + justification(TextJustification::Left), + tracking(0), + lineHeight(0.0) { + if (!jsonAny.is_object()) { + throw LottieParsingException(); + } + + json11::Json::object const &json = jsonAny.object_items(); + + text = getString(json, "t"); + fontSize = getDouble(json, "s"); + fontFamily = getString(json, "f"); + + auto justificationRawValue = getInt(json, "j"); + switch (justificationRawValue) { + case 0: + justification = TextJustification::Left; + break; + case 1: + justification = TextJustification::Right; + break; + case 2: + justification = TextJustification::Center; + break; + default: + throw LottieParsingException(); + } + + tracking = getInt(json, "tr"); + lineHeight = getDouble(json, "lh"); + baseline = getOptionalDouble(json, "ls"); + + if (const auto fillColorDataValue = getOptionalAny(json, "fc")) { + fillColorData = Color(fillColorDataValue.value()); + } + + if (const auto strokeColorDataValue = getOptionalAny(json, "sc")) { + strokeColorData = Color(strokeColorDataValue.value()); + } + + strokeWidth = getOptionalDouble(json, "sw"); + strokeOverFill = getOptionalBool(json, "of"); + + if (const auto textFramePositionData = getOptionalAny(json, "ps")) { + textFramePosition = Vector3D(textFramePositionData.value()); + } + if (const auto textFrameSizeData = getOptionalAny(json, "sz")) { + textFrameSize = Vector3D(textFrameSizeData.value()); + } + } + + json11::Json::object toJson() const { + json11::Json::object result; + + result.insert(std::make_pair("t", text)); + result.insert(std::make_pair("s", fontSize)); + result.insert(std::make_pair("f", fontFamily)); + result.insert(std::make_pair("j", (int)justification)); + result.insert(std::make_pair("tr", tracking)); + result.insert(std::make_pair("lh", lineHeight)); + + if (baseline.has_value()) { + result.insert(std::make_pair("ls", baseline.value())); + } + + if (fillColorData.has_value()) { + result.insert(std::make_pair("fc", fillColorData->toJson())); + } + if (strokeColorData.has_value()) { + result.insert(std::make_pair("sc", strokeColorData->toJson())); + } + + if (strokeWidth.has_value()) { + result.insert(std::make_pair("sw", strokeWidth.value())); + } + if (strokeOverFill.has_value()) { + result.insert(std::make_pair("of", strokeOverFill.value())); + } + if (textFramePosition.has_value()) { + result.insert(std::make_pair("ps", textFramePosition->toJson())); + } + if (textFrameSize.has_value()) { + result.insert(std::make_pair("sz", textFrameSize->toJson())); + } + + return result; + } + +public: + /// The Text + std::string text; + + /// The Font size + double fontSize; + + /// The Font Family + std::string fontFamily; + + /// Justification + TextJustification justification; + + /// Tracking + int tracking; + + /// Line Height + double lineHeight; + + /// Baseline + std::optional baseline; + + /// Fill Color data + std::optional fillColorData; + + /// Scroke Color data + std::optional strokeColorData; + + /// Stroke Width + std::optional strokeWidth; + + /// Stroke Over Fill + std::optional strokeOverFill; + + std::optional textFramePosition; + + std::optional textFrameSize; +}; + +} + +#endif /* TextDocument_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/TextDocument.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/TextDocument.swift new file mode 100644 index 0000000000..c32c042863 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Model/Text/TextDocument.swift @@ -0,0 +1,123 @@ +// +// TextDocument.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/9/19. +// + +import Foundation + +// MARK: - TextJustification + +enum TextJustification: Int, Codable { + case left + case right + case center +} + +// MARK: - TextDocument + +final class TextDocument: Codable, DictionaryInitializable, AnyInitializable { + + // MARK: Lifecycle + + init(dictionary: [String: Any]) throws { + text = try dictionary.value(for: CodingKeys.text) + fontSize = try dictionary.value(for: CodingKeys.fontSize) + fontFamily = try dictionary.value(for: CodingKeys.fontFamily) + let justificationValue: Int = try dictionary.value(for: CodingKeys.justification) + guard let justification = TextJustification(rawValue: justificationValue) else { + throw InitializableError.invalidInput + } + self.justification = justification + tracking = try dictionary.value(for: CodingKeys.tracking) + lineHeight = try dictionary.value(for: CodingKeys.lineHeight) + baseline = try dictionary.value(for: CodingKeys.baseline) + if let fillColorRawValue = dictionary[CodingKeys.fillColorData.rawValue] { + fillColorData = try? Color(value: fillColorRawValue) + } else { + fillColorData = nil + } + if let strokeColorRawValue = dictionary[CodingKeys.strokeColorData.rawValue] { + strokeColorData = try? Color(value: strokeColorRawValue) + } else { + strokeColorData = nil + } + strokeWidth = try? dictionary.value(for: CodingKeys.strokeWidth) + strokeOverFill = try? dictionary.value(for: CodingKeys.strokeOverFill) + if let textFramePositionRawValue = dictionary[CodingKeys.textFramePosition.rawValue] { + textFramePosition = try? Vector3D(value: textFramePositionRawValue) + } else { + textFramePosition = nil + } + if let textFrameSizeRawValue = dictionary[CodingKeys.textFrameSize.rawValue] { + textFrameSize = try? Vector3D(value: textFrameSizeRawValue) + } else { + textFrameSize = nil + } + } + + convenience init(value: Any) throws { + guard let dictionary = value as? [String: Any] else { + throw InitializableError.invalidInput + } + try self.init(dictionary: dictionary) + } + + // MARK: Internal + + /// The Text + let text: String + + /// The Font size + let fontSize: Double + + /// The Font Family + let fontFamily: String + + /// Justification + let justification: TextJustification + + /// Tracking + let tracking: Int + + /// Line Height + let lineHeight: Double + + /// Baseline + let baseline: Double? + + /// Fill Color data + let fillColorData: Color? + + /// Scroke Color data + let strokeColorData: Color? + + /// Stroke Width + let strokeWidth: Double? + + /// Stroke Over Fill + let strokeOverFill: Bool? + + let textFramePosition: Vector3D? + + let textFrameSize: Vector3D? + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case text = "t" + case fontSize = "s" + case fontFamily = "f" + case justification = "j" + case tracking = "tr" + case lineHeight = "lh" + case baseline = "ls" + case fillColorData = "fc" + case strokeColorData = "sc" + case strokeWidth = "sw" + case strokeOverFill = "of" + case textFramePosition = "ps" + case textFrameSize = "sz" + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Parsing/JsonParsing.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Parsing/JsonParsing.cpp new file mode 100644 index 0000000000..da1dcb51e6 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Parsing/JsonParsing.cpp @@ -0,0 +1,219 @@ +#include "JsonParsing.hpp" + +#include + +namespace lottie { + +thread_local int isExceptionExpectedLevel = 0; + +LottieParsingException::Guard::Guard() { + assert(isExceptionExpectedLevel >= 0); + isExceptionExpectedLevel++; +} + +LottieParsingException::Guard::~Guard() { + assert(isExceptionExpectedLevel - 1 >= 0); + isExceptionExpectedLevel--; +} + +LottieParsingException::LottieParsingException() { + if (isExceptionExpectedLevel == 0) { + assert(true); + } +} + +const char* LottieParsingException::what() const throw() { + return "Lottie parsing exception"; +} + +json11::Json getAny(json11::Json::object const &object, std::string const &key) noexcept(false) { + auto value = object.find(key); + if (value == object.end()) { + throw LottieParsingException(); + } + return value->second; +} + +std::optional getOptionalAny(json11::Json::object const &object, std::string const &key) noexcept(false) { + auto value = object.find(key); + if (value == object.end()) { + return std::nullopt; + } + return value->second; +} + +json11::Json::object getObject(json11::Json::object const &object, std::string const &key) noexcept(false) { + auto value = object.find(key); + if (value == object.end()) { + throw LottieParsingException(); + } + if (!value->second.is_object()) { + throw LottieParsingException(); + } + return value->second.object_items(); +} + +std::optional getOptionalObject(json11::Json::object const &object, std::string const &key) noexcept(false) { + auto value = object.find(key); + if (value == object.end()) { + return std::nullopt; + } + if (!value->second.is_object()) { + throw LottieParsingException(); + } + return value->second.object_items(); +} + +std::vector getObjectArray(json11::Json::object const &object, std::string const &key) noexcept(false) { + auto value = object.find(key); + if (value == object.end()) { + throw LottieParsingException(); + } + if (!value->second.is_array()) { + throw LottieParsingException(); + } + + std::vector result; + for (const auto &item : value->second.array_items()) { + if (!item.is_object()) { + throw LottieParsingException(); + } + result.push_back(item.object_items()); + } + + return result; +} + +std::optional> getOptionalObjectArray(json11::Json::object const &object, std::string const &key) noexcept(false) { + auto value = object.find(key); + if (value == object.end()) { + return std::nullopt; + } + if (!value->second.is_array()) { + throw LottieParsingException(); + } + + std::vector result; + for (const auto &item : value->second.array_items()) { + if (!item.is_object()) { + throw LottieParsingException(); + } + result.push_back(item.object_items()); + } + + return result; +} + +std::vector getAnyArray(json11::Json::object const &object, std::string const &key) noexcept(false) { + auto value = object.find(key); + if (value == object.end()) { + throw LottieParsingException(); + } + if (!value->second.is_array()) { + throw LottieParsingException(); + } + + return value->second.array_items(); +} + +std::optional> getOptionalAnyArray(json11::Json::object const &object, std::string const &key) noexcept(false) { + auto value = object.find(key); + if (value == object.end()) { + throw std::nullopt; + } + if (!value->second.is_array()) { + throw LottieParsingException(); + } + + return value->second.array_items(); +} + +std::string getString(json11::Json::object const &object, std::string const &key) noexcept(false) { + auto value = object.find(key); + if (value == object.end()) { + throw LottieParsingException(); + } + if (!value->second.is_string()) { + throw LottieParsingException(); + } + return value->second.string_value(); +} + +std::optional getOptionalString(json11::Json::object const &object, std::string const &key) noexcept(false) { + auto value = object.find(key); + if (value == object.end()) { + return std::nullopt; + } + if (!value->second.is_string()) { + throw LottieParsingException(); + } + return value->second.string_value(); +} + +int32_t getInt(json11::Json::object const &object, std::string const &key) noexcept(false) { + auto value = object.find(key); + if (value == object.end()) { + throw LottieParsingException(); + } + if (!value->second.is_number()) { + throw LottieParsingException(); + } + return value->second.int_value(); +} + +std::optional getOptionalInt(json11::Json::object const &object, std::string const &key) noexcept(false) { + auto value = object.find(key); + if (value == object.end()) { + return std::nullopt; + } + if (!value->second.is_number()) { + throw LottieParsingException(); + } + return value->second.int_value(); +} + +double getDouble(json11::Json::object const &object, std::string const &key) noexcept(false) { + auto value = object.find(key); + if (value == object.end()) { + throw LottieParsingException(); + } + if (!value->second.is_number()) { + throw LottieParsingException(); + } + return value->second.number_value(); +} + +std::optional getOptionalDouble(json11::Json::object const &object, std::string const &key) noexcept(false) { + auto value = object.find(key); + if (value == object.end()) { + return std::nullopt; + } + if (!value->second.is_number()) { + throw LottieParsingException(); + } + return value->second.number_value(); +} + +bool getBool(json11::Json::object const &object, std::string const &key) noexcept(false) { + auto value = object.find(key); + if (value == object.end()) { + throw LottieParsingException(); + } + if (!value->second.is_bool()) { + throw LottieParsingException(); + } + return value->second.bool_value(); +} + +std::optional getOptionalBool(json11::Json::object const &object, std::string const &key) noexcept(false) { + auto value = object.find(key); + if (value == object.end()) { + return std::nullopt; + } + if (!value->second.is_bool()) { + throw LottieParsingException(); + } + return value->second.bool_value(); +} + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Parsing/JsonParsing.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Parsing/JsonParsing.hpp new file mode 100644 index 0000000000..9d4c760d47 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Parsing/JsonParsing.hpp @@ -0,0 +1,52 @@ +#ifndef JsonParsing_hpp +#define JsonParsing_hpp + +#include "json11/json11.hpp" + +#include +#include +#include + +namespace lottie { + +class LottieParsingException: public std::exception { +public: + class Guard { + public: + Guard(); + ~Guard(); + }; + +public: + LottieParsingException(); + + virtual const char* what() const throw(); +}; + +json11::Json getAny(json11::Json::object const &object, std::string const &key) noexcept(false); +std::optional getOptionalAny(json11::Json::object const &object, std::string const &key) noexcept(false); + +json11::Json::object getObject(json11::Json::object const &object, std::string const &key) noexcept(false); +std::optional getOptionalObject(json11::Json::object const &object, std::string const &key) noexcept(false); + +std::vector getObjectArray(json11::Json::object const &object, std::string const &key) noexcept(false); +std::optional> getOptionalObjectArray(json11::Json::object const &object, std::string const &key) noexcept(false); + +std::vector getAnyArray(json11::Json::object const &object, std::string const &key) noexcept(false); +std::optional> getOptionalAnyArray(json11::Json::object const &object, std::string const &key) noexcept(false); + +std::string getString(json11::Json::object const &object, std::string const &key) noexcept(false); +std::optional getOptionalString(json11::Json::object const &object, std::string const &key) noexcept(false); + +int32_t getInt(json11::Json::object const &object, std::string const &key) noexcept(false); +std::optional getOptionalInt(json11::Json::object const &object, std::string const &key) noexcept(false); + +double getDouble(json11::Json::object const &object, std::string const &key) noexcept(false); +std::optional getOptionalDouble(json11::Json::object const &object, std::string const &key) noexcept(false); + +bool getBool(json11::Json::object const &object, std::string const &key) noexcept(false); +std::optional getOptionalBool(json11::Json::object const &object, std::string const &key) noexcept(false); + +} + +#endif /* JsonParsing_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/RootAnimationLayer.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/RootAnimationLayer.swift new file mode 100644 index 0000000000..d3ab01085e --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/RootAnimationLayer.swift @@ -0,0 +1,51 @@ +// Created by Cal Stephens on 12/13/21. +// Copyright © 2021 Airbnb Inc. All rights reserved. + +import QuartzCore + +// MARK: - RootAnimationLayer + +/// A root `CALayer` responsible for playing a Lottie animation +protocol RootAnimationLayer: CALayer { + var currentFrame: AnimationFrameTime { get set } + var renderScale: CGFloat { get set } + var respectAnimationFrameRate: Bool { get set } + + var _animationLayers: [CALayer] { get } + var imageProvider: AnimationImageProvider { get set } + var textProvider: AnimationTextProvider { get set } + var fontProvider: AnimationFontProvider { get set } + + /// The `CAAnimation` key corresponding to the primary animation. + /// - `AnimationView` uses this key to check if the animation is still active + var primaryAnimationKey: AnimationKey { get } + + /// Whether or not this layer is currently playing an animation + /// - If the layer returns `nil`, `AnimationView` determines if an animation + /// is playing by checking if there is an active animation for `primaryAnimationKey` + var isAnimationPlaying: Bool? { get } + + /// Instructs this layer to remove all `CAAnimation`s, + /// other than the `CAAnimation` managed by `AnimationView` (if applicable) + func removeAnimations() + + func reloadImages() + func forceDisplayUpdate() + func logHierarchyKeypaths() + + func setValueProvider(_ valueProvider: AnyValueProvider, keypath: AnimationKeypath) + func getValue(for keypath: AnimationKeypath, atFrame: AnimationFrameTime?) -> Any? + func getOriginalValue(for keypath: AnimationKeypath, atFrame: AnimationFrameTime?) -> Any? + + func layer(for keypath: AnimationKeypath) -> CALayer? + func animatorNodes(for keypath: AnimationKeypath) -> [AnimatorNode]? +} + +// MARK: - AnimationKey + +enum AnimationKey { + /// The primary animation and its key should be managed by `AnimationView` + case managed + /// The primary animation always uses the given key + case specific(String) +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Debugging/AnimatorNodeDebugging.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Debugging/AnimatorNodeDebugging.swift new file mode 100644 index 0000000000..3e8a511481 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Debugging/AnimatorNodeDebugging.swift @@ -0,0 +1,25 @@ +// +// AnimatorNodeDebugging.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/18/19. +// + +import Foundation + +extension AnimatorNode { + + func printNodeTree() { + parentNode?.printNodeTree() + print(String(describing: type(of: self))) + + if let group = self as? GroupNode { + print("* |Children") + group.rootNode?.printNodeTree() + print("*") + } else { + print("|") + } + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Debugging/LayerDebugging.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Debugging/LayerDebugging.swift new file mode 100644 index 0000000000..c8298ddff9 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Debugging/LayerDebugging.swift @@ -0,0 +1,228 @@ +// +// LayerDebugging.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/24/19. +// + +import Foundation +import QuartzCore + +// MARK: - LayerDebugStyle + +struct LayerDebugStyle { + let anchorColor: CGColor + let boundsColor: CGColor + let anchorWidth: CGFloat + let boundsWidth: CGFloat +} + +// MARK: - LayerDebugging + +protocol LayerDebugging { + var debugStyle: LayerDebugStyle { get } +} + +// MARK: - CustomLayerDebugging + +protocol CustomLayerDebugging { + func layerForDebugging() -> CALayer +} + +// MARK: - DebugLayer + +class DebugLayer: CALayer { + init(style: LayerDebugStyle) { + super.init() + zPosition = 1000 + bounds = CGRect(x: 0, y: 0, width: style.anchorWidth, height: style.anchorWidth) + backgroundColor = style.anchorColor + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension CALayer { + + @nonobjc + public func logLayerTree(withIndent: Int = 0) { + var string = "" + for _ in 0...withIndent { + string = string + " " + } + string = string + "|_" + String(describing: self) + print(string) + if let sublayers = sublayers { + for sublayer in sublayers { + sublayer.logLayerTree(withIndent: withIndent + 1) + } + } + } + +} + +// MARK: - CompositionLayer + CustomLayerDebugging + +extension CompositionLayer: CustomLayerDebugging { + func layerForDebugging() -> CALayer { + contentsLayer + } +} + +extension CALayer { + + @nonobjc + func setDebuggingState(visible: Bool) { + + var sublayers = self.sublayers + if let cust = self as? CustomLayerDebugging { + sublayers = cust.layerForDebugging().sublayers + } + + if let sublayers = sublayers { + for i in 0.. LayerDebugStyle { + let colorSpace = CGColorSpaceCreateDeviceRGB() + + let anchorColor = CGColor(colorSpace: colorSpace, components: [1, 0, 0, 1])! + let boundsColor = CGColor(colorSpace: colorSpace, components: [1, 1, 0, 1])! + return LayerDebugStyle( + anchorColor: anchorColor, + boundsColor: boundsColor, + anchorWidth: 10, + boundsWidth: 2) + } + + static func topLayerStyle() -> LayerDebugStyle { + let colorSpace = CGColorSpaceCreateDeviceRGB() + let anchorColor = CGColor(colorSpace: colorSpace, components: [1, 0.5, 0, 0])! + let boundsColor = CGColor(colorSpace: colorSpace, components: [0, 1, 0, 1])! + + return LayerDebugStyle( + anchorColor: anchorColor, + boundsColor: boundsColor, + anchorWidth: 10, + boundsWidth: 2) + } + + static func nullLayerStyle() -> LayerDebugStyle { + let colorSpace = CGColorSpaceCreateDeviceRGB() + let anchorColor = CGColor(colorSpace: colorSpace, components: [0, 0, 1, 0])! + let boundsColor = CGColor(colorSpace: colorSpace, components: [0, 1, 0, 1])! + + return LayerDebugStyle( + anchorColor: anchorColor, + boundsColor: boundsColor, + anchorWidth: 10, + boundsWidth: 2) + } + + static func shapeLayerStyle() -> LayerDebugStyle { + let colorSpace = CGColorSpaceCreateDeviceRGB() + let anchorColor = CGColor(colorSpace: colorSpace, components: [0, 1, 0, 0])! + let boundsColor = CGColor(colorSpace: colorSpace, components: [0, 1, 0, 1])! + + return LayerDebugStyle( + anchorColor: anchorColor, + boundsColor: boundsColor, + anchorWidth: 10, + boundsWidth: 2) + } + + static func shapeRenderLayerStyle() -> LayerDebugStyle { + let colorSpace = CGColorSpaceCreateDeviceRGB() + let anchorColor = CGColor(colorSpace: colorSpace, components: [0, 1, 1, 0])! + let boundsColor = CGColor(colorSpace: colorSpace, components: [0, 1, 0, 1])! + + return LayerDebugStyle( + anchorColor: anchorColor, + boundsColor: boundsColor, + anchorWidth: 10, + boundsWidth: 2) + } +} + +extension Array where Element == LayerModel { + + var parents: [Int] { + var array = [Int]() + for layer in self { + if let parent = layer.parent { + array.append(parent) + } else { + array.append(-1) + } + } + return array + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Debugging/TestHelpers.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Debugging/TestHelpers.swift new file mode 100644 index 0000000000..cfe5d4813e --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Debugging/TestHelpers.swift @@ -0,0 +1,10 @@ +// Created by Cal Stephens on 1/28/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +enum TestHelpers { + /// Whether or not snapshot tests are currently running in a test target + static var snapshotTestsAreRunning = false + + /// Whether or not performance tests are currently running in a test target + static var performanceTestsAreRunning = false +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift new file mode 100644 index 0000000000..08b6e3aa6d --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift @@ -0,0 +1,266 @@ +// +// KeypathSearchableExtension.swift +// lottie-swift +// +// Created by Brandon Withrow on 2/4/19. +// + +import Foundation +import QuartzCore + +extension KeypathSearchable { + + func animatorNodes(for keyPath: AnimationKeypath) -> [AnimatorNode]? { + // Make sure there is a current key path. + guard let currentKey = keyPath.currentKey else { return nil } + + // Now try popping the keypath for wildcard / child search + guard let nextKeypath = keyPath.popKey(keypathName) else { + // We may be on the final keypath. Check for match. + if + let node = self as? AnimatorNode, + currentKey.equalsKeypath(keypathName) + { + // This is the final keypath and matches self. Return.s + return [node] + } + /// Nope. Stop Search + return nil + } + + var results: [AnimatorNode] = [] + + if + let node = self as? AnimatorNode, + nextKeypath.currentKey == nil + { + // Keypath matched self and was the final keypath. + results.append(node) + } + + for childNode in childKeypaths { + // Check if the child has any nodes matching the next keypath. + if let foundNodes = childNode.animatorNodes(for: nextKeypath) { + results.append(contentsOf: foundNodes) + } + + // In this case the current key is fuzzy, and both child and self match the next keyname. Keep digging! + if + currentKey.keyPathType == .fuzzyWildcard, + let nextKeypath = keyPath.nextKeypath, + nextKeypath.equalsKeypath(childNode.keypathName), + let foundNodes = childNode.animatorNodes(for: keyPath) + { + results.append(contentsOf: foundNodes) + } + } + + guard results.count > 0 else { + return nil + } + + return results + } + + func nodeProperties(for keyPath: AnimationKeypath) -> [AnyNodeProperty]? { + guard let nextKeypath = keyPath.popKey(keypathName) else { + /// Nope. Stop Search + return nil + } + + /// Keypath matches in some way. Continue the search. + var results: [AnyNodeProperty] = [] + + /// Check if we have a property keypath yet + if + let propertyKey = nextKeypath.propertyKey, + let property = keypathProperties[propertyKey] + { + /// We found a property! + results.append(property) + } + + if nextKeypath.nextKeypath != nil { + /// Now check child keypaths. + for child in childKeypaths { + if let childProperties = child.nodeProperties(for: nextKeypath) { + results.append(contentsOf: childProperties) + } + } + } + + guard results.count > 0 else { + return nil + } + + return results + } + + func layer(for keyPath: AnimationKeypath) -> CALayer? { + if keyPath.nextKeypath == nil, let layerKey = keyPath.currentKey, layerKey.equalsKeypath(keypathName) { + /// We found our layer! + return keypathLayer + } + guard let nextKeypath = keyPath.popKey(keypathName) else { + /// Nope. Stop Search + return nil + } + + /// Now check child keypaths. + for child in childKeypaths { + if let layer = child.layer(for: nextKeypath) { + return layer + } + } + return nil + } + + func logKeypaths(for keyPath: AnimationKeypath?) { + let newKeypath: AnimationKeypath + if let previousKeypath = keyPath { + newKeypath = previousKeypath.appendingKey(keypathName) + } else { + newKeypath = AnimationKeypath(keys: [keypathName]) + } + print(newKeypath.fullPath) + for key in keypathProperties.keys { + print(newKeypath.appendingKey(key).fullPath) + } + for child in childKeypaths { + child.logKeypaths(for: newKeypath) + } + } +} + +extension AnimationKeypath { + var currentKey: String? { + keys.first + } + + var nextKeypath: String? { + guard keys.count > 1 else { + return nil + } + return keys[1] + } + + var propertyKey: String? { + if nextKeypath == nil { + /// There are no more keypaths. This is a property key. + return currentKey + } + if keys.count == 2, currentKey?.keyPathType == .fuzzyWildcard { + /// The next keypath is the last and the current is a fuzzy key. + return nextKeypath + } + return nil + } + + var fullPath: String { + keys.joined(separator: ".") + } + + // Pops the top keypath from the stack if the keyname matches. + func popKey(_ keyname: String) -> AnimationKeypath? { + guard + let currentKey = currentKey, + currentKey.equalsKeypath(keyname), + keys.count > 1 else + { + // Current key either doesnt match or we are on the last key. + return nil + } + + // Pop the keypath from the stack and return the new stack. + let newKeys: [String] + + if currentKey.keyPathType == .fuzzyWildcard { + /// Dont remove if current key is a fuzzy wildcard, and if the next keypath doesnt equal keypathname + if + let nextKeypath = nextKeypath, + nextKeypath.equalsKeypath(keyname) + { + /// Remove next two keypaths. This keypath breaks the wildcard. + var oldKeys = keys + oldKeys.remove(at: 0) + oldKeys.remove(at: 0) + newKeys = oldKeys + } else { + newKeys = keys + } + } else { + var oldKeys = keys + oldKeys.remove(at: 0) + newKeys = oldKeys + } + + return AnimationKeypath(keys: newKeys) + } + + func appendingKey(_ key: String) -> AnimationKeypath { + var newKeys = keys + newKeys.append(key) + return AnimationKeypath(keys: newKeys) + } +} + +extension String { + var keyPathType: KeyType { + switch self { + case "*": + return .wildcard + case "**": + return .fuzzyWildcard + default: + return .specific + } + } + + func equalsKeypath(_ keyname: String) -> Bool { + if keyPathType == .wildcard || keyPathType == .fuzzyWildcard { + return true + } + if self == keyname { + return true + } + if let index = firstIndex(of: "*") { + // Wildcard search. + let prefix = String(self.prefix(upTo: index)) + let suffix = String(self.suffix(from: self.index(after: index))) + + if prefix.count > 0 { + // Match prefix. + if keyname.count < prefix.count { + return false + } + let testPrefix = String(keyname.prefix(upTo: keyname.index(keyname.startIndex, offsetBy: prefix.count))) + if testPrefix != prefix { + // Prefix doesnt match + return false + } + } + if suffix.count > 0 { + // Match suffix. + if keyname.count < suffix.count { + // Suffix doesnt match + return false + } + let index = keyname.index(keyname.endIndex, offsetBy: -suffix.count) + let testSuffix = String(keyname.suffix(from: index)) + if testSuffix != suffix { + return false + } + } + return true + } + return false + } +} + +// MARK: - KeyType + +enum KeyType { + case specific + case wildcard + case fuzzyWildcard +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/BlendMode+Filter.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/BlendMode+Filter.swift new file mode 100644 index 0000000000..ef93a39c25 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/BlendMode+Filter.swift @@ -0,0 +1,31 @@ +// +// File.swift +// +// +// Created by Denis Koryttsev on 10.05.2022. +// + +extension BlendMode { + /// The Core Image filter name for this `BlendMode`, that can be applied to a `CALayer`'s `compositingFilter`. + /// Supported compositing filters are defined here: https://developer.apple.com/library/archive/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html#//apple_ref/doc/uid/TP30000136-SW71 + var filterName: String? { + switch self { + case .normal: return nil + case .multiply: return "multiplyBlendMode" + case .screen: return "screenBlendMode" + case .overlay: return "overlayBlendMode" + case .darken: return "darkenBlendMode" + case .lighten: return "lightenBlendMode" + case .colorDodge: return "colorDodgeBlendMode" + case .colorBurn: return "colorBurnBlendMode" + case .hardLight: return "hardLightBlendMode" + case .softLight: return "softLightBlendMode" + case .difference: return "differenceBlendMode" + case .exclusion: return "exclusionBlendMode" + case .hue: return "hueBlendMode" + case .saturation: return "saturationBlendMode" + case .color: return "colorBlendMode" + case .luminosity: return "luminosityBlendMode" + } + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/CGColor+RGB.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/CGColor+RGB.swift new file mode 100644 index 0000000000..c1e2a5c47d --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/CGColor+RGB.swift @@ -0,0 +1,22 @@ +// Created by Cal Stephens on 1/7/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +import QuartzCore + +extension CGColor { + /// Initializes a `CGColor` using the given `RGB` values + static func rgb(_ red: CGFloat, _ green: CGFloat, _ blue: CGFloat) -> CGColor { + if #available(iOS 13.0, tvOS 13.0, macOS 10.5, *) { + return CGColor(red: red, green: green, blue: blue, alpha: 1) + } else { + return CGColor( + colorSpace: CGColorSpaceCreateDeviceRGB(), + components: [red, green, blue])! + } + } + + /// Initializes a `CGColor` using the given `RGBA` values + static func rgba(_ red: CGFloat, _ green: CGFloat, _ blue: CGFloat, _ alpha: CGFloat) -> CGColor { + CGColor.rgb(red, green, blue).copy(alpha: alpha)! + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/CGFloatExtensions.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/CGFloatExtensions.swift new file mode 100644 index 0000000000..015a10ce45 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/CGFloatExtensions.swift @@ -0,0 +1,152 @@ +// +// CGFloatExtensions.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/14/19. +// + +import Foundation +import QuartzCore + +extension CGFloat { + + // MARK: Internal + + var squared: CGFloat { + self * self + } + + var cubed: CGFloat { + self * self * self + } + + var cubicRoot: CGFloat { + CGFloat(pow(Double(self), 1.0 / 3.0)) + } + + func isInRangeOrEqual(_ from: CGFloat, _ to: CGFloat) -> Bool { + from <= self && self <= to + } + + func isInRange(_ from: CGFloat, _ to: CGFloat) -> Bool { + from < self && self < to + } + + func cubicBezierInterpolate(_ P0: CGPoint, _ P1: CGPoint, _ P2: CGPoint, _ P3: CGPoint) -> CGFloat { + var t: CGFloat + if self == P0.x { + // Handle corner cases explicitly to prevent rounding errors + t = 0 + } else if self == P3.x { + t = 1 + } else { + // Calculate t + let a = -P0.x + 3 * P1.x - 3 * P2.x + P3.x; + let b = 3 * P0.x - 6 * P1.x + 3 * P2.x; + let c = -3 * P0.x + 3 * P1.x; + let d = P0.x - self; + let tTemp = CGFloat.SolveCubic(a, b, c, d); + if tTemp == -1 { + return -1; + } + t = tTemp + } + + // Calculate y from t + return (1 - t).cubed * P0.y + 3 * t * (1 - t).squared * P1.y + 3 * t.squared * (1 - t) * P2.y + t.cubed * P3.y; + } + + func cubicBezier(_ t: CGFloat, _ c1: CGFloat, _ c2: CGFloat, _ end: CGFloat) -> CGFloat { + let t_ = (1.0 - t) + let tt_ = t_ * t_ + let ttt_ = t_ * t_ * t_ + let tt = t * t + let ttt = t * t * t + + return self * ttt_ + + 3.0 * c1 * tt_ * t + + 3.0 * c2 * t_ * tt + + end * ttt; + } + + // MARK: Fileprivate + + fileprivate static func SolveQuadratic(_ a: CGFloat, _ b: CGFloat, _ c: CGFloat) -> CGFloat { + var result = (-b + sqrt(b.squared - 4 * a * c)) / (2 * a); + guard !result.isInRangeOrEqual(0, 1) else { + return result + } + + result = (-b - sqrt(b.squared - 4 * a * c)) / (2 * a); + guard !result.isInRangeOrEqual(0, 1) else { + return result + } + + return -1; + } + + fileprivate static func SolveCubic(_ a: CGFloat, _ b: CGFloat, _ c: CGFloat, _ d: CGFloat) -> CGFloat { + if a == 0 { + return SolveQuadratic(b, c, d) + } + if d == 0 { + return 0 + } + let a = a + var b = b + var c = c + var d = d + b /= a + c /= a + d /= a + var q = (3.0 * c - b.squared) / 9.0 + let r = (-27.0 * d + b * (9.0 * c - 2.0 * b.squared)) / 54.0 + let disc = q.cubed + r.squared + let term1 = b / 3.0 + + if disc > 0 { + var s = r + sqrt(disc) + s = (s < 0) ? -((-s).cubicRoot) : s.cubicRoot + var t = r - sqrt(disc) + t = (t < 0) ? -((-t).cubicRoot) : t.cubicRoot + + let result = -term1 + s + t; + if result.isInRangeOrEqual(0, 1) { + return result + } + } else if disc == 0 { + let r13 = (r < 0) ? -((-r).cubicRoot) : r.cubicRoot; + + var result = -term1 + 2.0 * r13; + if result.isInRangeOrEqual(0, 1) { + return result + } + + result = -(r13 + term1); + if result.isInRangeOrEqual(0, 1) { + return result + } + + } else { + q = -q; + var dum1 = q * q * q; + dum1 = acos(r / sqrt(dum1)); + let r13 = 2.0 * sqrt(q); + + var result = -term1 + r13 * cos(dum1 / 3.0); + if result.isInRangeOrEqual(0, 1) { + return result + } + result = -term1 + r13 * cos((dum1 + 2.0 * .pi) / 3.0); + if result.isInRangeOrEqual(0, 1) { + return result + } + result = -term1 + r13 * cos((dum1 + 4.0 * .pi) / 3.0); + if result.isInRangeOrEqual(0, 1) { + return result + } + } + + return -1; + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/DataExtension.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/DataExtension.swift new file mode 100644 index 0000000000..e029be93dd --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/DataExtension.swift @@ -0,0 +1,27 @@ +// +// DataExtension.swift +// Lottie +// +// Created by René Fouquet on 03.05.21. +// + +import Foundation +#if canImport(UIKit) +import UIKit +#elseif canImport(AppKit) +import AppKit +#endif + +extension Data { + + static func jsonData(from assetName: String, in bundle: Bundle) -> Data? { + #if canImport(UIKit) + return NSDataAsset(name: assetName, bundle: bundle)?.data + #else + if #available(macOS 10.11, *) { + return NSDataAsset(name: assetName, bundle: bundle)?.data + } + return nil + #endif + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/MathKit.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/MathKit.swift new file mode 100644 index 0000000000..63d5465a0b --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/MathKit.swift @@ -0,0 +1,451 @@ +// +// MathKit.swift +// UIToolBox +// +// Created by Brandon Withrow on 10/10/18. +// +// From https://github.com/buba447/UIToolBox + +import CoreGraphics +import Foundation + +extension Int { + var cgFloat: CGFloat { + CGFloat(self) + } +} + +extension Double { + var cgFloat: CGFloat { + CGFloat(self) + } +} + +// MARK: - CGFloat + Interpolatable + +extension CGFloat { + + func remap(fromLow: CGFloat, fromHigh: CGFloat, toLow: CGFloat, toHigh: CGFloat) -> CGFloat { + guard (fromHigh - fromLow) != 0 else { + // Would produce NAN + return 0 + } + return toLow + (self - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) + } + + /// Returns a value that is clamped between the two numbers + /// + /// 1. The order of arguments does not matter. + func clamp(_ a: CGFloat, _ b: CGFloat) -> CGFloat { + CGFloat(Double(self).clamp(Double(a), Double(b))) + } + + /// Returns the difference between the receiver and the given number. + /// - Parameter absolute: If *true* (Default) the returned value will always be positive. + func diff(_ a: CGFloat, absolute: Bool = true) -> CGFloat { + absolute ? abs(a - self) : a - self + } + + func toRadians() -> CGFloat { self * .pi / 180 } + func toDegrees() -> CGFloat { self * 180 / .pi } + +} + +// MARK: - Double + +extension Double { + + func remap(fromLow: Double, fromHigh: Double, toLow: Double, toHigh: Double) -> Double { + toLow + (self - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) + } + + /// Returns a value that is clamped between the two numbers + /// + /// 1. The order of arguments does not matter. + func clamp(_ a: Double, _ b: Double) -> Double { + let minValue = a <= b ? a : b + let maxValue = a <= b ? b : a + return max(min(self, maxValue), minValue) + } + +} + +public extension CGRect { + + // MARK: Lifecycle + + /// Initializes a new CGRect with a center point and size. + init(center: CGPoint, size: CGSize) { + self.init( + x: center.x - (size.width * 0.5), + y: center.y - (size.height * 0.5), + width: size.width, + height: size.height) + } + + // MARK: Internal + + /// Returns the total area of the rect. + var area: CGFloat { + width * height + } + + /// The center point of the rect. Settable. + var center: CGPoint { + get { + CGPoint(x: midX, y: midY) + } + set { + origin = CGPoint( + x: newValue.x - (size.width * 0.5), + y: newValue.y - (size.height * 0.5)) + } + } + + /// The top left point of the rect. Settable. + var topLeft: CGPoint { + get { + CGPoint(x: minX, y: minY) + } + set { + origin = CGPoint( + x: newValue.x, + y: newValue.y) + } + } + + /// The bottom left point of the rect. Settable. + var bottomLeft: CGPoint { + get { + CGPoint(x: minX, y: maxY) + } + set { + origin = CGPoint( + x: newValue.x, + y: newValue.y - size.height) + } + } + + /// The top right point of the rect. Settable. + var topRight: CGPoint { + get { + CGPoint(x: maxX, y: minY) + } + set { + origin = CGPoint( + x: newValue.x - size.width, + y: newValue.y) + } + } + + /// The bottom right point of the rect. Settable. + var bottomRight: CGPoint { + get { + CGPoint(x: maxX, y: maxY) + } + set { + origin = CGPoint( + x: newValue.x - size.width, + y: newValue.y - size.height) + } + } + +} + +public extension CGSize { + + /// Operator convenience to add sizes with + + static func +(left: CGSize, right: CGSize) -> CGSize { + left.add(right) + } + + /// Operator convenience to subtract sizes with - + static func -(left: CGSize, right: CGSize) -> CGSize { + left.subtract(right) + } + + /// Operator convenience to multiply sizes with * + static func *(left: CGSize, right: CGFloat) -> CGSize { + CGSize(width: left.width * right, height: left.height * right) + } + + /// Returns the scale float that will fit the receive inside of the given size. + func scaleThatFits(_ size: CGSize) -> CGFloat { + CGFloat.minimum(width / size.width, height / size.height) + } + + /// Adds receiver size to give size. + func add(_ size: CGSize) -> CGSize { + CGSize(width: width + size.width, height: height + size.height) + } + + /// Subtracts given size from receiver size. + func subtract(_ size: CGSize) -> CGSize { + CGSize(width: width - size.width, height: height - size.height) + } + + /// Multiplies receiver size by the given size. + func multiply(_ size: CGSize) -> CGSize { + CGSize(width: width * size.width, height: height * size.height) + } +} + +// MARK: - CGLine + +/// A struct that defines a line segment with two CGPoints +struct CGLine { + + // MARK: Lifecycle + + /// Initializes a line segment with start and end points + init(start: CGPoint, end: CGPoint) { + self.start = start + self.end = end + } + + // MARK: Internal + + /// The Start of the line segment. + var start: CGPoint + /// The End of the line segment. + var end: CGPoint + + /// The length of the line segment. + var length: CGFloat { + end.distanceTo(start) + } + + /// Returns a line segment that is normalized to a length of 1 + func normalize() -> CGLine { + let len = length + guard len > 0 else { + return self + } + let relativeEnd = end - start + let relativeVector = CGPoint(x: relativeEnd.x / len, y: relativeEnd.y / len) + let absoluteVector = relativeVector + start + return CGLine(start: start, end: absoluteVector) + } + + /// Trims a line segment to the given length + func trimmedToLength(_ toLength: CGFloat) -> CGLine { + let len = length + guard len > 0 else { + return self + } + let relativeEnd = end - start + let relativeVector = CGPoint(x: relativeEnd.x / len, y: relativeEnd.y / len) + let sizedVector = CGPoint(x: relativeVector.x * toLength, y: relativeVector.y * toLength) + let absoluteVector = sizedVector + start + return CGLine(start: start, end: absoluteVector) + } + + /// Flips a line vertically and horizontally from the start point. + func flipped() -> CGLine { + let relativeEnd = end - start + let flippedEnd = CGPoint(x: relativeEnd.x * -1, y: relativeEnd.y * -1) + return CGLine(start: start, end: flippedEnd + start) + } + + /// Move the line to the new start point. + func transpose(_ toPoint: CGPoint) -> CGLine { + let diff = toPoint - start + let newEnd = end + diff + return CGLine(start: toPoint, end: newEnd) + } + +} + +infix operator +| +infix operator +- + +extension CGPoint { + + /// Returns the length between the receiver and *CGPoint.zero* + var vectorLength: CGFloat { + distanceTo(.zero) + } + + var isZero: Bool { + x == 0 && y == 0 + } + + /// Operator convenience to divide points with / + static func / (lhs: CGPoint, rhs: CGFloat) -> CGPoint { + CGPoint(x: lhs.x / CGFloat(rhs), y: lhs.y / CGFloat(rhs)) + } + + /// Operator convenience to multiply points with * + static func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint { + CGPoint(x: lhs.x * CGFloat(rhs), y: lhs.y * CGFloat(rhs)) + } + + /// Operator convenience to add points with + + static func +(left: CGPoint, right: CGPoint) -> CGPoint { + left.add(right) + } + + /// Operator convenience to subtract points with - + static func -(left: CGPoint, right: CGPoint) -> CGPoint { + left.subtract(right) + } + + static func +|(left: CGPoint, right: CGFloat) -> CGPoint { + CGPoint(x: left.x, y: left.y + right) + } + + static func +-(left: CGPoint, right: CGFloat) -> CGPoint { + CGPoint(x: left.x + right, y: left.y) + } + + /// Returns the distance between the receiver and the given point. + func distanceTo(_ a: CGPoint) -> CGFloat { + let xDist = a.x - x + let yDist = a.y - y + return CGFloat(sqrt((xDist * xDist) + (yDist * yDist))) + } + + func rounded(decimal: CGFloat) -> CGPoint { + CGPoint(x: round(decimal * x) / decimal, y: round(decimal * y) / decimal) + } + + func interpolate( + _ to: CGPoint, + outTangent: CGPoint, + inTangent: CGPoint, + amount: CGFloat, + maxIterations: Int = 3, + samples: Int = 20, + accuracy: CGFloat = 1) + -> CGPoint + { + if amount == 0 { + return self + } + if amount == 1 { + return to + } + + if + colinear(outTangent, inTangent) == true, + outTangent.colinear(inTangent, to) == true + { + return interpolate(to: to, amount: amount) + } + + let step = 1 / CGFloat(samples) + + var points: [(point: CGPoint, distance: CGFloat)] = [(point: self, distance: 0)] + var totalLength: CGFloat = 0 + + var previousPoint = self + var previousAmount = CGFloat(0) + + var closestPoint = 0 + + while previousAmount < 1 { + + previousAmount = previousAmount + step + + if previousAmount < amount { + closestPoint = closestPoint + 1 + } + + let newPoint = pointOnPath(to, outTangent: outTangent, inTangent: inTangent, amount: previousAmount) + let distance = previousPoint.distanceTo(newPoint) + totalLength = totalLength + distance + points.append((point: newPoint, distance: totalLength)) + previousPoint = newPoint + } + + let accurateDistance = amount * totalLength + var point = points[closestPoint] + + var foundPoint = false + + var pointAmount = CGFloat(closestPoint) * step + var nextPointAmount: CGFloat = pointAmount + step + + var refineIterations = 0 + while foundPoint == false { + refineIterations = refineIterations + 1 + /// First see if the next point is still less than the projected length. + let nextPoint = points[closestPoint + 1] + if nextPoint.distance < accurateDistance { + point = nextPoint + closestPoint = closestPoint + 1 + pointAmount = CGFloat(closestPoint) * step + nextPointAmount = pointAmount + step + if closestPoint == points.count { + foundPoint = true + } + continue + } + if accurateDistance < point.distance { + closestPoint = closestPoint - 1 + if closestPoint < 0 { + foundPoint = true + continue + } + point = points[closestPoint] + pointAmount = CGFloat(closestPoint) * step + nextPointAmount = pointAmount + step + continue + } + + /// Now we are certain the point is the closest point under the distance + let pointDiff = nextPoint.distance - point.distance + let proposedPointAmount = ((accurateDistance - point.distance) / pointDiff) + .remap(fromLow: 0, fromHigh: 1, toLow: pointAmount, toHigh: nextPointAmount) + + let newPoint = pointOnPath(to, outTangent: outTangent, inTangent: inTangent, amount: proposedPointAmount) + let newDistance = point.distance + point.point.distanceTo(newPoint) + pointAmount = proposedPointAmount + point = (point: newPoint, distance: newDistance) + if + accurateDistance - newDistance <= accuracy || + newDistance - accurateDistance <= accuracy + { + foundPoint = true + } + + if refineIterations == maxIterations { + foundPoint = true + } + } + return point.point + } + + func pointOnPath(_ to: CGPoint, outTangent: CGPoint, inTangent: CGPoint, amount: CGFloat) -> CGPoint { + let a = interpolate(to: outTangent, amount: amount) + let b = outTangent.interpolate(to: inTangent, amount: amount) + let c = inTangent.interpolate(to: to, amount: amount) + let d = a.interpolate(to: b, amount: amount) + let e = b.interpolate(to: c, amount: amount) + let f = d.interpolate(to: e, amount: amount) + return f + } + + func colinear(_ a: CGPoint, _ b: CGPoint) -> Bool { + let area = x * (a.y - b.y) + a.x * (b.y - y) + b.x * (y - a.y); + let accuracy: CGFloat = 0.05 + if area < accuracy && area > -accuracy { + return true + } + return false + } + + /// Subtracts the given point from the receiving point. + func subtract(_ point: CGPoint) -> CGPoint { + CGPoint( + x: x - point.x, + y: y - point.y) + } + + /// Adds the given point from the receiving point. + func add(_ point: CGPoint) -> CGPoint { + CGPoint( + x: x + point.x, + y: y + point.y) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/StringExtensions.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/StringExtensions.swift new file mode 100644 index 0000000000..2a536fd23e --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/StringExtensions.swift @@ -0,0 +1,39 @@ +// +// StringExtensions.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/25/19. +// + +import CoreGraphics +import Foundation + +extension String { + + var cgColor: CGColor { + let (red, green, blue) = hexColorComponents() + return .rgb(red, green, blue) + } + + func hexColorComponents() -> (red: CGFloat, green: CGFloat, blue: CGFloat) { + + var cString: String = trimmingCharacters(in: .whitespacesAndNewlines).uppercased() + + if cString.hasPrefix("#") { + cString.remove(at: cString.startIndex) + } + + if (cString.count) != 6 { + return (red: 0, green: 0, blue: 0) + } + + var rgbValue: UInt64 = 0 + Scanner(string: cString).scanHexInt64(&rgbValue) + + return ( + red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0, + green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0, + blue: CGFloat(rgbValue & 0x0000FF) / 255.0) + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Helpers/AnimationContext.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Helpers/AnimationContext.swift new file mode 100644 index 0000000000..b8c0ef433a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Helpers/AnimationContext.swift @@ -0,0 +1,91 @@ +// +// AnimationContext.swift +// lottie-swift +// +// Created by Brandon Withrow on 2/1/19. +// + +import CoreGraphics +import Foundation +import QuartzCore + +/// A completion block for animations. `true` is passed in if the animation completed playing. +public typealias LottieCompletionBlock = (Bool) -> Void + +// MARK: - AnimationContext + +struct AnimationContext { + + init( + playFrom: AnimationFrameTime, + playTo: AnimationFrameTime, + closure: LottieCompletionBlock?) + { + self.playTo = playTo + self.playFrom = playFrom + self.closure = AnimationCompletionDelegate(completionBlock: closure) + } + + var playFrom: AnimationFrameTime + var playTo: AnimationFrameTime + var closure: AnimationCompletionDelegate + +} + +// MARK: Equatable + +extension AnimationContext: Equatable { + /// Whether or not the two given `AnimationContext`s are functionally equivalent + /// - This checks whether or not a completion handler was provided, + /// but does not check whether or not the two completion handlers are equivalent. + static func == (_ lhs: AnimationContext, _ rhs: AnimationContext) -> Bool { + lhs.playTo == rhs.playTo + && lhs.playFrom == rhs.playFrom + && (lhs.closure.completionBlock == nil) == (rhs.closure.completionBlock == nil) + } +} + +// MARK: - AnimationContextState + +enum AnimationContextState { + case playing + case cancelled + case complete +} + +// MARK: - AnimationCompletionDelegate + +class AnimationCompletionDelegate: NSObject, CAAnimationDelegate { + + // MARK: Lifecycle + + init(completionBlock: LottieCompletionBlock?) { + self.completionBlock = completionBlock + super.init() + } + + // MARK: Public + + public func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { + guard ignoreDelegate == false else { return } + animationState = flag ? .complete : .cancelled + if let animationLayer = animationLayer, let key = animationKey { + animationLayer.removeAnimation(forKey: key) + if flag { + animationLayer.currentFrame = (anim as! CABasicAnimation).toValue as! CGFloat + } + } + if let completionBlock = completionBlock { + completionBlock(flag) + } + } + + // MARK: Internal + + var animationLayer: RootAnimationLayer? + var animationKey: String? + var ignoreDelegate = false + var animationState: AnimationContextState = .playing + + let completionBlock: LottieCompletionBlock? +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Interpolatable/InterpolatableExtensions.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Interpolatable/InterpolatableExtensions.swift new file mode 100644 index 0000000000..9a28f61017 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Interpolatable/InterpolatableExtensions.swift @@ -0,0 +1,135 @@ +// +// InterpolatableExtensions.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/14/19. +// + +import CoreGraphics +import Foundation + +extension Color { + + // MARK: Lifecycle + + /// Initialize a new color with Hue Saturation and Value + init(h: Double, s: Double, v: Double, a: Double) { + + let i = floor(h * 6) + let f = h * 6 - i + let p = v * (1 - s); + let q = v * (1 - f * s) + let t = v * (1 - (1 - f) * s) + + switch i.truncatingRemainder(dividingBy: 6) { + case 0: + r = v + g = t + b = p + case 1: + r = q + g = v + b = p + case 2: + r = p + g = v + b = t + case 3: + r = p + g = q + b = v + case 4: + r = t + g = p + b = v + case 5: + r = v + g = p + b = q + default: + r = 0 + g = 0 + b = 0 + } + self.a = a + } + + init(y: Double, u: Double, v: Double, a: Double) { + // From https://www.fourcc.org/fccyvrgb.php + r = y + 1.403 * v + g = y - 0.344 * u + b = y + 1.770 * u + self.a = a + } + + // MARK: Internal + + /// Hue Saturation Value of the color. + var hsva: (h: Double, s: Double, v: Double, a: Double) { + let maxValue = max(r, g, b) + let minValue = min(r, g, b) + + var h: Double, s: Double, v: Double = maxValue + + let d = maxValue - minValue + s = maxValue == 0 ? 0 : d / maxValue; + + if maxValue == minValue { + h = 0; // achromatic + } else { + switch maxValue { + case r: h = (g - b) / d + (g < b ? 6 : 0) + case g: h = (b - r) / d + 2 + case b: h = (r - g) / d + 4 + default: h = maxValue + } + h = h / 6 + } + return (h: h, s: s, v: v, a: a) + } + + var yuv: (y: Double, u: Double, v: Double, a: Double) { + /// From https://www.fourcc.org/fccyvrgb.php + let y = 0.299 * r + 0.587 * g + 0.114 * b + let u = -0.14713 * r - 0.28886 * g + 0.436 * b + let v = 0.615 * r - 0.51499 * g - 0.10001 * b + return (y: y, u: u, v: v, a: a) + } + +} + +// MARK: - CurveVertex + Interpolatable + +extension CurveVertex: Interpolatable { + func interpolate(to: CurveVertex, amount: CGFloat) -> CurveVertex { + CurveVertex( + point: point.interpolate(to: to.point, amount: amount), + inTangent: inTangent.interpolate(to: to.inTangent, amount: amount), + outTangent: outTangent.interpolate(to: to.outTangent, amount: amount)) + } +} + +// MARK: - BezierPath + Interpolatable + +extension BezierPath: Interpolatable { + func interpolate(to: BezierPath, amount: CGFloat) -> BezierPath { + var newPath = BezierPath() + for i in 0.. TextDocument { + if amount == 1 { + return to + } + return self + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Interpolatable/KeyframeExtensions.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Interpolatable/KeyframeExtensions.swift new file mode 100644 index 0000000000..e6e0c1811f --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Interpolatable/KeyframeExtensions.swift @@ -0,0 +1,46 @@ +// +// KeyframeExtensions.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/14/19. +// + +import CoreGraphics +import Foundation + +extension Keyframe where T: AnyInterpolatable { + func interpolate(to: Keyframe, progress: CGFloat) -> T { + value._interpolate( + to: to.value, + amount: progress, + spatialOutTangent: spatialOutTangent?.pointValue, + spatialInTangent: to.spatialInTangent?.pointValue) + } +} + +extension Keyframe { + /// Interpolates the keyTime into a value from 0-1 + func interpolatedProgress(_ to: Keyframe, keyTime: CGFloat) -> CGFloat { + let startTime = time + let endTime = to.time + if keyTime <= startTime { + return 0 + } + if endTime <= keyTime { + return 1 + } + + if isHold { + return 0 + } + + let outTanPoint = outTangent?.pointValue ?? .zero + let inTanPoint = to.inTangent?.pointValue ?? CGPoint(x: 1, y: 1) + var progress: CGFloat = keyTime.remap(fromLow: startTime, fromHigh: endTime, toLow: 0, toHigh: 1) + if !outTanPoint.isZero || !inTanPoint.equalTo(CGPoint(x: 1, y: 1)) { + /// Cubic interpolation + progress = progress.cubicBezierInterpolate(.zero, outTanPoint, inTanPoint, CGPoint(x: 1, y: 1)) + } + return progress + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/BezierPath.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/BezierPath.cpp new file mode 100644 index 0000000000..571bd152d8 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/BezierPath.cpp @@ -0,0 +1,134 @@ +#include "BezierPath.hpp" + +#include +#include + +namespace lottie { + +CGRect calculateBoundingRectOpt(float const *pointsX, float const *pointsY, int count) { + float minX = 0.0; + float maxX = 0.0; + vDSP_minv(pointsX, 1, &minX, count); + vDSP_maxv(pointsX, 1, &maxX, count); + + float minY = 0.0; + float maxY = 0.0; + vDSP_minv(pointsY, 1, &minY, count); + vDSP_maxv(pointsY, 1, &maxY, count); + + return CGRect(minX, minY, maxX - minX, maxY - minY); +} + +CGRect bezierPathsBoundingBoxParallel(BezierPathsBoundingBoxContext &context, std::vector const &paths) { + int pointCount = 0; + + float *pointsX = context.pointsX; + float *pointsY = context.pointsY; + int pointsSize = context.pointsSize; + + for (const auto &path : paths) { + PathElement const *pathElements = path.elements().data(); + int pathElementCount = (int)path.elements().size(); + + for (int i = 0; i < pathElementCount; i++) { + const auto &element = pathElements[i]; + + if (pointsSize < pointCount + 1) { + pointsSize = (pointCount + 1) * 2; + pointsX = (float *)realloc(pointsX, pointsSize * 4); + pointsY = (float *)realloc(pointsY, pointsSize * 4); + } + pointsX[pointCount] = (float)element.vertex.point.x; + pointsY[pointCount] = (float)element.vertex.point.y; + pointCount++; + + if (i != 0) { + const auto &previousElement = pathElements[i - 1]; + if (previousElement.vertex.outTangentRelative().isZero() && element.vertex.inTangentRelative().isZero()) { + } else { + if (pointsSize < pointCount + 1) { + pointsSize = (pointCount + 2) * 2; + pointsX = (float *)realloc(pointsX, pointsSize * 4); + pointsY = (float *)realloc(pointsY, pointsSize * 4); + } + pointsX[pointCount] = (float)previousElement.vertex.outTangent.x; + pointsY[pointCount] = (float)previousElement.vertex.outTangent.y; + pointCount++; + pointsX[pointCount] = (float)element.vertex.inTangent.x; + pointsY[pointCount] = (float)element.vertex.inTangent.y; + pointCount++; + } + } + } + } + + context.pointsX = pointsX; + context.pointsY = pointsY; + context.pointsSize = pointsSize; + + if (pointCount == 0) { + return CGRect(0.0, 0.0, 0.0, 0.0); + } + + return calculateBoundingRectOpt(pointsX, pointsY, pointCount); +} + +CGRect bezierPathsBoundingBox(std::vector const &paths) { + int pointCount = 0; + + float *pointsX = (float *)malloc(128 * 4); + float *pointsY = (float *)malloc(128 * 4); + int pointsSize = 128; + + for (const auto &path : paths) { + PathElement const *pathElements = path.elements().data(); + int pathElementCount = (int)path.elements().size(); + + for (int i = 0; i < pathElementCount; i++) { + const auto &element = pathElements[i]; + + if (pointsSize < pointCount + 1) { + pointsSize = (pointCount + 1) * 2; + pointsX = (float *)realloc(pointsX, pointsSize * 4); + pointsY = (float *)realloc(pointsY, pointsSize * 4); + } + pointsX[pointCount] = (float)element.vertex.point.x; + pointsY[pointCount] = (float)element.vertex.point.y; + pointCount++; + + if (i != 0) { + const auto &previousElement = pathElements[i - 1]; + if (previousElement.vertex.outTangentRelative().isZero() && element.vertex.inTangentRelative().isZero()) { + } else { + if (pointsSize < pointCount + 1) { + pointsSize = (pointCount + 2) * 2; + pointsX = (float *)realloc(pointsX, pointsSize * 4); + pointsY = (float *)realloc(pointsY, pointsSize * 4); + } + pointsX[pointCount] = (float)previousElement.vertex.outTangent.x; + pointsY[pointCount] = (float)previousElement.vertex.outTangent.y; + pointCount++; + pointsX[pointCount] = (float)element.vertex.inTangent.x; + pointsY[pointCount] = (float)element.vertex.inTangent.y; + pointCount++; + } + } + } + } + + if (pointCount == 0) { + free(pointsX); + free(pointsY); + + return CGRect(0.0, 0.0, 0.0, 0.0); + } + + auto result = calculateBoundingRectOpt(pointsX, pointsY, pointCount); + + free(pointsX); + free(pointsY); + + return result; +} + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/BezierPath.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/BezierPath.hpp new file mode 100644 index 0000000000..c7717cbd2d --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/BezierPath.hpp @@ -0,0 +1,594 @@ +#ifndef BezierPath_hpp +#define BezierPath_hpp + +#include "Lottie/Private/Utility/Primitives/CurveVertex.hpp" +#include "Lottie/Private/Utility/Primitives/PathElement.hpp" +#include "Lottie/Private/Parsing/JsonParsing.hpp" +#include "Lottie/Public/Primitives/CGPath.hpp" + +#include + +namespace lottie { + +struct BezierTrimPathPosition { + double start; + double end; + + explicit BezierTrimPathPosition(double start_, double end_) : + start(start_), + end(end_) { + } +}; + +class BezierPathContents: public std::enable_shared_from_this { +public: + /// Initializes a new Bezier Path. + explicit BezierPathContents(CurveVertex const &startPoint) : + elements({ PathElement(startPoint) }) { + } + + BezierPathContents() : + elements({}), + closed(false) { + } + + explicit BezierPathContents(json11::Json const &jsonAny) noexcept(false) : + elements({}) { + json11::Json::object const *json = nullptr; + if (jsonAny.is_object()) { + json = &jsonAny.object_items(); + } else if (jsonAny.is_array()) { + if (jsonAny.array_items().empty()) { + throw LottieParsingException(); + } + if (!jsonAny.array_items()[0].is_object()) { + throw LottieParsingException(); + } + json = &jsonAny.array_items()[0].object_items(); + } + + if (const auto closedData = getOptionalBool(*json, "c")) { + closed = closedData.value(); + } + + auto vertexContainer = getAnyArray(*json, "v"); + auto inPointsContainer = getAnyArray(*json, "i"); + auto outPointsContainer = getAnyArray(*json, "o"); + + if (vertexContainer.size() != inPointsContainer.size() || inPointsContainer.size() != outPointsContainer.size()) { + throw LottieParsingException(); + } + if (vertexContainer.empty()) { + return; + } + + /// Create first point + Vector2D firstPoint(vertexContainer[0]); + Vector2D firstInPoint(inPointsContainer[0]); + Vector2D firstOutPoint(outPointsContainer[0]); + CurveVertex firstVertex = CurveVertex::relative( + firstPoint, + firstInPoint, + firstOutPoint + ); + PathElement previousElement(firstVertex); + elements.push_back(previousElement); + + for (size_t i = 1; i < vertexContainer.size(); i++) { + Vector2D point(vertexContainer[i]); + Vector2D inPoint(inPointsContainer[i]); + Vector2D outPoint(outPointsContainer[i]); + CurveVertex vertex = CurveVertex::relative( + point, + inPoint, + outPoint + ); + auto pathElement = previousElement.pathElementTo(vertex); + elements.push_back(pathElement); + previousElement = pathElement; + } + + if (closed.value_or(false)) { + auto closeElement = previousElement.pathElementTo(firstVertex); + elements.push_back(closeElement); + } + } + + BezierPathContents(const BezierPathContents&) = delete; + BezierPathContents& operator=(BezierPathContents&) = delete; + + json11::Json toJson() const { + json11::Json::object result; + + json11::Json::array vertices; + json11::Json::array inPoints; + json11::Json::array outPoints; + + for (const auto &element : elements) { + vertices.push_back(element.vertex.point.toJson()); + inPoints.push_back(element.vertex.inTangentRelative().toJson()); + outPoints.push_back(element.vertex.outTangentRelative().toJson()); + } + + result.insert(std::make_pair("v", vertices)); + result.insert(std::make_pair("i", inPoints)); + result.insert(std::make_pair("o", outPoints)); + + if (closed.has_value()) { + result.insert(std::make_pair("c", closed.value())); + } + + return json11::Json(result); + } + + std::shared_ptr cgPath() const { + auto cgPath = CGPath::makePath(); + + std::optional previousElement; + for (const auto &element : elements) { + if (previousElement.has_value()) { + if (previousElement->vertex.outTangentRelative().isZero() && element.vertex.inTangentRelative().isZero()) { + cgPath->addLineTo(element.vertex.point); + } else { + cgPath->addCurveTo(element.vertex.point, previousElement->vertex.outTangent, element.vertex.inTangent); + } + } else { + cgPath->moveTo(element.vertex.point); + } + previousElement = element; + } + if (closed.value_or(true)) { + cgPath->closeSubpath(); + } + return cgPath; + } + +public: + std::vector elements; + std::optional closed; + + double length() { + if (_length.has_value()) { + return _length.value(); + } else { + double result = 0.0; + for (size_t i = 1; i < elements.size(); i++) { + result += elements[i].length(elements[i - 1]); + } + _length = result; + return result; + } + } + +private: + std::optional _length; + +public: + void moveToStartPoint(CurveVertex const &vertex) { + elements = { PathElement(vertex) }; + _length = std::nullopt; + } + + void addVertex(CurveVertex const &vertex) { + addElement(PathElement(vertex)); + } + + void reserveCapacity(size_t capacity) { + elements.reserve(capacity); + } + + void setElementCount(size_t count) { + elements.resize(count, PathElement(CurveVertex::absolute(Vector2D(0.0, 0.0), Vector2D(0.0, 0.0), Vector2D(0.0, 0.0)))); + } + + void invalidateLength() { + _length.reset(); + } + + void addCurve(Vector2D const &toPoint, Vector2D const &outTangent, Vector2D const &inTangent) { + if (elements.empty()) { + return; + } + auto previous = elements[elements.size() - 1]; + auto newVertex = CurveVertex::absolute(toPoint, inTangent, toPoint); + updateVertex( + CurveVertex::absolute(previous.vertex.point, previous.vertex.inTangent, outTangent), + (int)elements.size() - 1, + false + ); + addVertex(newVertex); + } + + void addLine(Vector2D const &toPoint) { + if (elements.empty()) { + return; + } + auto previous = elements[elements.size() - 1]; + auto newVertex = CurveVertex::relative(toPoint, Vector2D::Zero(), Vector2D::Zero()); + updateVertex( + CurveVertex::absolute(previous.vertex.point, previous.vertex.inTangent, previous.vertex.point), + (int)elements.size() - 1, + false + ); + addVertex(newVertex); + } + + void close() { + closed = true; + } + + void addElement(PathElement const &pathElement) { + elements.push_back(pathElement); + } + + void updateVertex(CurveVertex const &vertex, int atIndex, bool remeasure) { + if (remeasure) { + PathElement newElement(CurveVertex::absolute(Vector2D::Zero(), Vector2D::Zero(), Vector2D::Zero())); + if (atIndex > 0) { + auto previousElement = elements[atIndex - 1]; + newElement = previousElement.pathElementTo(vertex); + } else { + newElement = PathElement(vertex); + } + elements[atIndex] = newElement; + + if (atIndex + 1 < elements.size()) { + auto nextElement = elements[atIndex + 1]; + elements[atIndex + 1] = newElement.pathElementTo(nextElement.vertex); + } + + } else { + auto oldElement = elements[atIndex]; + elements[atIndex] = oldElement.updateVertex(vertex); + } + } + + /// Trims a path fromLength toLength with an offset. + /// + /// Length and offset are defined in the length coordinate space. + /// If any argument is outside the range of this path, then it will be looped over the path from finish to start. + /// + /// Cutting the curve when fromLength is less than toLength + /// x x x x + /// ~~~~~~~~~~~~~~~ooooooooooooooooooooooooooooooooooooooooooooooooo------------------- + /// |Offset |fromLength toLength| | + /// + /// Cutting the curve when from Length is greater than toLength + /// x x x x x + /// oooooooooooooooooo--------------------~~~~~~~~~~~~~~~~ooooooooooooooooooooooooooooo + /// | toLength| |Offset |fromLength | + /// + std::vector> trim(double fromLength, double toLength, double offsetLength) { + if (elements.size() <= 1) { + return {}; + } + + if (fromLength == toLength) { + return {}; + } + + double lengthValue = length(); + + /// Normalize lengths to the curve length. + auto start = fmod(fromLength + offsetLength, lengthValue); + auto end = fmod(toLength + offsetLength, lengthValue); + + if (start < 0.0) { + start = lengthValue + start; + } + + if (end < 0.0) { + end = lengthValue + end; + } + + if (start == lengthValue) { + start = 0.0; + } + if (end == 0.0) { + end = lengthValue; + } + + if ( + (start == 0.0 && end == lengthValue) || + start == end || + (start == lengthValue && end == 0.0) + ) { + /// The trim encompasses the entire path. Return. + return { shared_from_this() }; + } + + if (start > end) { + // Start is greater than end. Two paths are returned. + return trimPathAtLengths({ + BezierTrimPathPosition(0.0, end), + BezierTrimPathPosition(start, lengthValue) + }); + } + + return trimPathAtLengths({ BezierTrimPathPosition(start, end) }); + } + + // MARK: Private + + std::vector> trimPathAtLengths(std::vector const &positions) { + if (positions.empty()) { + return {}; + } + auto remainingPositions = positions; + + auto trim = remainingPositions[0]; + remainingPositions.erase(remainingPositions.begin()); + + std::vector> paths; + + double runningLength = 0.0; + bool finishedTrimming = false; + auto pathElements = elements; + + auto currentPath = std::make_shared(); + int i = 0; + + while (!finishedTrimming) { + if (pathElements.size() <= i) { + /// Do this for rounding errors + paths.push_back(currentPath); + finishedTrimming = true; + continue; + } + /// Loop through and add elements within start->end range. + /// Get current element + auto element = pathElements[i]; + double elementLength = 0.0; + if (i != 0) { + elementLength = element.length(pathElements[i - 1]); + } + + /// Calculate new running length. + auto newLength = runningLength + elementLength; + + if (newLength < trim.start) { + /// Element is not included in the trim, continue. + runningLength = newLength; + i = i + 1; + /// Increment index, we are done with this element. + continue; + } + + if (newLength == trim.start) { + /// Current element IS the start element. + /// For start we want to add a zero length element. + currentPath->moveToStartPoint(element.vertex); + runningLength = newLength; + i = i + 1; + /// Increment index, we are done with this element. + continue; + } + + if (runningLength < trim.start && trim.start < newLength && currentPath->elements.size() == 0) { + /// The start of the trim is between this element and the previous, trim. + /// Get previous element. + auto previousElement = pathElements[i - 1]; + /// Trim it + auto trimLength = trim.start - runningLength; + auto trimResults = element.splitElementAtPosition(previousElement, trimLength); + /// Add the right span start. + currentPath->moveToStartPoint(trimResults.rightSpan.start.vertex); + + pathElements[i] = trimResults.rightSpan.end; + pathElements[i - 1] = trimResults.rightSpan.start; + runningLength = runningLength + trimResults.leftSpan.end.length(trimResults.leftSpan.start); + /// Dont increment index or the current length, the end of this path can be within this span. + continue; + } + + if (trim.start < newLength && newLength < trim.end) { + /// Element lies within the trim span. + currentPath->addElement(element); + runningLength = newLength; + i = i + 1; + continue; + } + + if (newLength == trim.end) { + /// Element is the end element. + /// The element could have a new length if it's added right after the start node. + currentPath->addElement(element); + /// We are done with this span. + runningLength = newLength; + i = i + 1; + /// Allow the path to be finalized. + /// Fall through to finalize path and move to next position + } + + if (runningLength < trim.end && trim.end < newLength) { + /// New element must be cut for end. + /// Get previous element. + auto previousElement = pathElements[i - 1]; + /// Trim it + auto trimLength = trim.end - runningLength; + auto trimResults = element.splitElementAtPosition(previousElement, trimLength); + /// Add the left span end. + + currentPath->updateVertex(trimResults.leftSpan.start.vertex, (int)currentPath->elements.size() - 1, false); + currentPath->addElement(trimResults.leftSpan.end); + + pathElements[i] = trimResults.rightSpan.end; + pathElements[i - 1] = trimResults.rightSpan.start; + runningLength = runningLength + trimResults.leftSpan.end.length(trimResults.leftSpan.start); + /// Dont increment index or the current length, the start of the next path can be within this span. + /// We are done with this span. + /// Allow the path to be finalized. + /// Fall through to finalize path and move to next position + } + + paths.push_back(currentPath); + currentPath = std::make_shared(); + if (remainingPositions.size() > 0) { + trim = remainingPositions[0]; + remainingPositions.erase(remainingPositions.begin()); + } else { + finishedTrimming = true; + } + } + return paths; + } +}; + +class BezierPath { +public: + /// Initializes a new Bezier Path. + explicit BezierPath(CurveVertex const &startPoint) : + _contents(std::make_shared(startPoint)) { + } + + BezierPath() : + _contents(std::make_shared()) { + } + + explicit BezierPath(json11::Json const &jsonAny) noexcept(false) : + _contents(std::make_shared(jsonAny)) { + } + + json11::Json toJson() const { + return _contents->toJson(); + } + + double length() { + return _contents->length(); + } + + void moveToStartPoint(CurveVertex const &vertex) { + _contents->moveToStartPoint(vertex); + } + + void addVertex(CurveVertex const &vertex) { + _contents->addVertex(vertex); + } + + void reserveCapacity(size_t capacity) { + _contents->reserveCapacity(capacity); + } + + void setElementCount(size_t count) { + _contents->setElementCount(count); + } + + void invalidateLength() { + _contents->invalidateLength(); + } + + void addCurve(Vector2D const &toPoint, Vector2D const &outTangent, Vector2D const &inTangent) { + _contents->addCurve(toPoint, outTangent, inTangent); + } + + void addLine(Vector2D const &toPoint) { + _contents->addLine(toPoint); + } + + void close() { + _contents->close(); + } + + void addElement(PathElement const &pathElement) { + _contents->addElement(pathElement); + } + + void updateVertex(CurveVertex const &vertex, int atIndex, bool remeasure) { + _contents->updateVertex(vertex, atIndex, remeasure); + } + + /// Trims a path fromLength toLength with an offset. + /// + /// Length and offset are defined in the length coordinate space. + /// If any argument is outside the range of this path, then it will be looped over the path from finish to start. + /// + /// Cutting the curve when fromLength is less than toLength + /// x x x x + /// ~~~~~~~~~~~~~~~ooooooooooooooooooooooooooooooooooooooooooooooooo------------------- + /// |Offset |fromLength toLength| | + /// + /// Cutting the curve when from Length is greater than toLength + /// x x x x x + /// oooooooooooooooooo--------------------~~~~~~~~~~~~~~~~ooooooooooooooooooooooooooooo + /// | toLength| |Offset |fromLength | + /// + std::vector trim(double fromLength, double toLength, double offsetLength) { + std::vector result; + + auto resultContents = _contents->trim(fromLength, toLength, offsetLength); + for (const auto &resultContent : resultContents) { + result.emplace_back(resultContent); + } + + return result; + } + + // MARK: Private + + std::vector const &elements() const { + return _contents->elements; + } + + std::vector &mutableElements() { + return _contents->elements; + } + + std::optional const &closed() const { + return _contents->closed; + } + void setClosed(std::optional const &closed) { + _contents->closed = closed; + } + + std::shared_ptr cgPath() const { + return _contents->cgPath(); + } + + BezierPath copyUsingTransform(CATransform3D const &transform) const { + if (transform == CATransform3D::identity()) { + return (*this); + } + BezierPath result; + result._contents->closed = _contents->closed; + result.reserveCapacity(_contents->elements.size()); + for (const auto &element : _contents->elements) { + result._contents->elements.emplace_back(element.vertex.transformed(transform)); + } + return result; + } + +public: + BezierPath(std::shared_ptr contents) : + _contents(contents) { + } + +private: + std::shared_ptr _contents; +}; + +class BezierPathsBoundingBoxContext { +public: + BezierPathsBoundingBoxContext() : + pointsX((float *)malloc(1024 * 4)), + pointsY((float *)malloc(1024 * 4)), + pointsSize(1024) { + } + + ~BezierPathsBoundingBoxContext() { + free(pointsX); + free(pointsY); + } + +public: + float *pointsX = nullptr; + float *pointsY = nullptr; + int pointsSize = 0; +}; + +CGRect bezierPathsBoundingBox(std::vector const &paths); +CGRect bezierPathsBoundingBoxParallel(BezierPathsBoundingBoxContext &context, std::vector const &paths); +CGRect calculateBoundingRectOpt(float const *pointsX, float const *pointsY, int count); + +} + +#endif /* BezierPath_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/BezierPath.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/BezierPath.swift new file mode 100644 index 0000000000..39efa0ab9d --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/BezierPath.swift @@ -0,0 +1,488 @@ +// +// Shape.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/8/19. +// + +import CoreGraphics +import Foundation + +// MARK: - BezierPath + +/// A container that holds instructions for creating a single, unbroken Bezier Path. +struct BezierPath { + + // MARK: Lifecycle + + /// Initializes a new Bezier Path. + init(startPoint: CurveVertex) { + elements = [PathElement(vertex: startPoint)] + length = 0 + closed = false + } + + init() { + elements = [] + length = 0 + closed = false + } + + // MARK: Internal + + /// The elements of the path + private(set) var elements: [PathElement] + + /// If the path is closed or not. + private(set) var closed: Bool + + /// The total length of the path. + private(set) var length: CGFloat + + mutating func moveToStartPoint(_ vertex: CurveVertex) { + elements = [PathElement(vertex: vertex)] + length = 0 + } + + mutating func addVertex(_ vertex: CurveVertex) { + guard let previous = elements.last else { + addElement(PathElement(vertex: vertex)) + return + } + addElement(previous.pathElementTo(vertex)) + } + + mutating func addCurve(toPoint: CGPoint, outTangent: CGPoint, inTangent: CGPoint) { + guard let previous = elements.last else { return } + let newVertex = CurveVertex(inTangent, toPoint, toPoint) + updateVertex( + CurveVertex(previous.vertex.inTangent, previous.vertex.point, outTangent), + atIndex: elements.endIndex - 1, + remeasure: false) + addVertex(newVertex) + } + + mutating func addLine(toPoint: CGPoint) { + guard let previous = elements.last else { return } + let newVertex = CurveVertex(point: toPoint, inTangentRelative: .zero, outTangentRelative: .zero) + updateVertex( + CurveVertex(previous.vertex.inTangent, previous.vertex.point, previous.vertex.point), + atIndex: elements.endIndex - 1, + remeasure: false) + addVertex(newVertex) + } + + mutating func close() { + closed = true + } + + mutating func addElement(_ pathElement: PathElement) { + elements.append(pathElement) + length = length + pathElement.length + } + + mutating func updateVertex(_ vertex: CurveVertex, atIndex: Int, remeasure: Bool) { + if remeasure { + var newElement: PathElement + if atIndex > 0 { + let previousElement = elements[atIndex - 1] + newElement = previousElement.pathElementTo(vertex) + } else { + newElement = PathElement(vertex: vertex) + } + elements[atIndex] = newElement + + if atIndex + 1 < elements.count { + let nextElement = elements[atIndex + 1] + elements[atIndex + 1] = newElement.pathElementTo(nextElement.vertex) + } + + } else { + let oldElement = elements[atIndex] + elements[atIndex] = oldElement.updateVertex(newVertex: vertex) + } + } + + /// Trims a path fromLength toLength with an offset. + /// + /// Length and offset are defined in the length coordinate space. + /// If any argument is outside the range of this path, then it will be looped over the path from finish to start. + /// + /// Cutting the curve when fromLength is less than toLength + /// x x x x + /// ~~~~~~~~~~~~~~~ooooooooooooooooooooooooooooooooooooooooooooooooo------------------- + /// |Offset |fromLength toLength| | + /// + /// Cutting the curve when from Length is greater than toLength + /// x x x x x + /// oooooooooooooooooo--------------------~~~~~~~~~~~~~~~~ooooooooooooooooooooooooooooo + /// | toLength| |Offset |fromLength | + /// + func trim(fromLength: CGFloat, toLength: CGFloat, offsetLength: CGFloat) -> [BezierPath] { + guard elements.count > 1 else { + return [] + } + + if fromLength == toLength { + return [] + } + + /// Normalize lengths to the curve length. + var start = (fromLength + offsetLength).truncatingRemainder(dividingBy: length) + var end = (toLength + offsetLength).truncatingRemainder(dividingBy: length) + + if start < 0 { + start = length + start + } + + if end < 0 { + end = length + end + } + + if start == length { + start = 0 + } + if end == 0 { + end = length + } + + if + start == 0 && end == length || + start == end || + start == length && end == 0 + { + /// The trim encompasses the entire path. Return. + return [self] + } + + if start > end { + // Start is greater than end. Two paths are returned. + return trimPathAtLengths(positions: [(start: 0, end: end), (start: start, end: length)]) + } + + return trimPathAtLengths(positions: [(start: start, end: end)]) + } + + // MARK: Private + + private func trimPathAtLengths(positions: [(start: CGFloat, end: CGFloat)]) -> [BezierPath] { + guard positions.count > 0 else { + return [] + } + var remainingPositions = positions + + var trim = remainingPositions.remove(at: 0) + + var paths = [BezierPath]() + + var runningLength: CGFloat = 0 + var finishedTrimming = false + var pathElements = elements + + var currentPath = BezierPath() + var i = 0 + + while !finishedTrimming { + if pathElements.count <= i { + /// Do this for rounding errors + paths.append(currentPath) + finishedTrimming = true + continue + } + /// Loop through and add elements within start->end range. + /// Get current element + let element = pathElements[i] + + /// Calculate new running length. + let newLength = runningLength + element.length + + if newLength < trim.start { + /// Element is not included in the trim, continue. + runningLength = newLength + i = i + 1 + /// Increment index, we are done with this element. + continue + } + + if newLength == trim.start { + /// Current element IS the start element. + /// For start we want to add a zero length element. + currentPath.moveToStartPoint(element.vertex) + runningLength = newLength + i = i + 1 + /// Increment index, we are done with this element. + continue + } + + if runningLength < trim.start, trim.start < newLength, currentPath.elements.count == 0 { + /// The start of the trim is between this element and the previous, trim. + /// Get previous element. + let previousElement = pathElements[i - 1] + /// Trim it + let trimLength = trim.start - runningLength + let trimResults = element.splitElementAtPosition(fromElement: previousElement, atLength: trimLength) + /// Add the right span start. + currentPath.moveToStartPoint(trimResults.rightSpan.start.vertex) + + pathElements[i] = trimResults.rightSpan.end + pathElements[i - 1] = trimResults.rightSpan.start + runningLength = runningLength + trimResults.leftSpan.end.length + /// Dont increment index or the current length, the end of this path can be within this span. + continue + } + + if trim.start < newLength, newLength < trim.end { + /// Element lies within the trim span. + currentPath.addElement(element) + runningLength = newLength + i = i + 1 + continue + } + + if newLength == trim.end { + /// Element is the end element. + /// The element could have a new length if it's added right after the start node. + currentPath.addElement(element) + /// We are done with this span. + runningLength = newLength + i = i + 1 + /// Allow the path to be finalized. + /// Fall through to finalize path and move to next position + } + + if runningLength < trim.end, trim.end < newLength { + /// New element must be cut for end. + /// Get previous element. + let previousElement = pathElements[i - 1] + /// Trim it + let trimLength = trim.end - runningLength + let trimResults = element.splitElementAtPosition(fromElement: previousElement, atLength: trimLength) + /// Add the left span end. + + currentPath.updateVertex(trimResults.leftSpan.start.vertex, atIndex: currentPath.elements.count - 1, remeasure: false) + currentPath.addElement(trimResults.leftSpan.end) + + pathElements[i] = trimResults.rightSpan.end + pathElements[i - 1] = trimResults.rightSpan.start + runningLength = runningLength + trimResults.leftSpan.end.length + /// Dont increment index or the current length, the start of the next path can be within this span. + /// We are done with this span. + /// Allow the path to be finalized. + /// Fall through to finalize path and move to next position + } + + paths.append(currentPath) + currentPath = BezierPath() + if remainingPositions.count > 0 { + trim = remainingPositions.remove(at: 0) + } else { + finishedTrimming = true + } + } + return paths + } + +} + +// MARK: Codable + +extension BezierPath: Codable { + + // MARK: Lifecycle + + init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer + + if let keyedContainer = try? decoder.container(keyedBy: BezierPath.CodingKeys.self) { + container = keyedContainer + } else { + var unkeyedContainer = try decoder.unkeyedContainer() + container = try unkeyedContainer.nestedContainer(keyedBy: BezierPath.CodingKeys.self) + } + + closed = try container.decodeIfPresent(Bool.self, forKey: .closed) ?? true + + var vertexContainer = try container.nestedUnkeyedContainer(forKey: .vertices) + var inPointsContainer = try container.nestedUnkeyedContainer(forKey: .inPoints) + var outPointsContainer = try container.nestedUnkeyedContainer(forKey: .outPoints) + + guard vertexContainer.count == inPointsContainer.count, inPointsContainer.count == outPointsContainer.count else { + /// Will throw an error if vertex, inpoints, and outpoints are not the same length. + /// This error is to be expected. + throw DecodingError.dataCorruptedError( + forKey: CodingKeys.vertices, + in: container, + debugDescription: "Vertex data does not match In Tangents and Out Tangents") + } + + guard let count = vertexContainer.count, count > 0 else { + length = 0 + elements = [] + return + } + + var decodedElements = [PathElement]() + + /// Create first point + let firstVertex = CurveVertex( + point: try vertexContainer.decode(CGPoint.self), + inTangentRelative: try inPointsContainer.decode(CGPoint.self), + outTangentRelative: try outPointsContainer.decode(CGPoint.self)) + var previousElement = PathElement(vertex: firstVertex) + decodedElements.append(previousElement) + + var totalLength: CGFloat = 0 + while !vertexContainer.isAtEnd { + /// Get the next vertex data. + let vertex = CurveVertex( + point: try vertexContainer.decode(CGPoint.self), + inTangentRelative: try inPointsContainer.decode(CGPoint.self), + outTangentRelative: try outPointsContainer.decode(CGPoint.self)) + let pathElement = previousElement.pathElementTo(vertex) + decodedElements.append(pathElement) + previousElement = pathElement + totalLength = totalLength + pathElement.length + } + if closed { + let closeElement = previousElement.pathElementTo(firstVertex) + decodedElements.append(closeElement) + totalLength = totalLength + closeElement.length + } + length = totalLength + elements = decodedElements + } + + // MARK: Internal + + /// The BezierPath container is encoded and decoded from the JSON format + /// that defines points for a lottie animation. + /// + /// { + /// "c" = Bool + /// "i" = [[Double]], + /// "o" = [[Double]], + /// "v" = [[Double]] + /// } + /// + + enum CodingKeys: String, CodingKey { + case closed = "c" + case inPoints = "i" + case outPoints = "o" + case vertices = "v" + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: BezierPath.CodingKeys.self) + try container.encode(closed, forKey: .closed) + + var vertexContainer = container.nestedUnkeyedContainer(forKey: .vertices) + var inPointsContainer = container.nestedUnkeyedContainer(forKey: .inPoints) + var outPointsContainer = container.nestedUnkeyedContainer(forKey: .outPoints) + + /// If closed path, ignore the final element. + let finalIndex = closed ? elements.endIndex - 1 : elements.endIndex + for i in 0.. 0 else { + length = 0 + elements = [] + return + } + + var decodedElements = [PathElement]() + let firstVertexDictionary = vertexDictionaries.removeFirst() + let firstInPointsDictionary = inPointsDictionaries.removeFirst() + let firstOutPointsDictionary = outPointsDictionaries.removeFirst() + let firstVertex = CurveVertex( + point: try CGPoint(value: firstVertexDictionary), + inTangentRelative: try CGPoint(value: firstInPointsDictionary), + outTangentRelative: try CGPoint(value: firstOutPointsDictionary)) + var previousElement = PathElement(vertex: firstVertex) + decodedElements.append(previousElement) + + var totalLength: CGFloat = 0 + while vertexDictionaries.count > 0 { + let vertexDictionary = vertexDictionaries.removeFirst() + let inPointsDictionary = inPointsDictionaries.removeFirst() + let outPointsDictionary = outPointsDictionaries.removeFirst() + let vertex = CurveVertex( + point: try CGPoint(value: vertexDictionary), + inTangentRelative: try CGPoint(value: inPointsDictionary), + outTangentRelative: try CGPoint(value: outPointsDictionary)) + let pathElement = previousElement.pathElementTo(vertex) + decodedElements.append(pathElement) + previousElement = pathElement + totalLength = totalLength + pathElement.length + } + if closed { + let closeElement = previousElement.pathElementTo(firstVertex) + decodedElements.append(closeElement) + totalLength = totalLength + closeElement.length + } + + length = totalLength + elements = decodedElements + } + +} + +extension BezierPath { + + func cgPath() -> CGPath { + let cgPath = CGMutablePath() + + var previousElement: PathElement? + for element in elements { + if let previous = previousElement { + if previous.vertex.outTangentRelative.isZero && element.vertex.inTangentRelative.isZero { + cgPath.addLine(to: element.vertex.point) + } else { + cgPath.addCurve(to: element.vertex.point, control1: previous.vertex.outTangent, control2: element.vertex.inTangent) + } + } else { + cgPath.move(to: element.vertex.point) + } + previousElement = element + } + if closed { + cgPath.closeSubpath() + } + return cgPath + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CGPointExtension.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CGPointExtension.swift new file mode 100644 index 0000000000..b98b65a468 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CGPointExtension.swift @@ -0,0 +1,35 @@ +// +// CGPointExtension.swift +// Lottie +// +// Created by Marcelo Fabri on 5/5/22. +// + +import CoreGraphics + +extension CGPoint: AnyInitializable { + + // MARK: Lifecycle + + init(value: Any) throws { + if let dictionary = value as? [String: CGFloat] { + let x: CGFloat = try dictionary.value(for: CodingKeys.x) + let y: CGFloat = try dictionary.value(for: CodingKeys.y) + self.init(x: x, y: y) + } else if + let array = value as? [CGFloat], + array.count > 1 + { + self.init(x: array[0], y: array[1]) + } else { + throw InitializableError.invalidInput + } + } + + // MARK: Private + + private enum CodingKeys: String { + case x + case y + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/ColorExtension.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/ColorExtension.swift new file mode 100644 index 0000000000..13c39b0eb9 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/ColorExtension.swift @@ -0,0 +1,112 @@ +// +// Color.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/14/19. +// + +import CoreGraphics +import Foundation + +// MARK: - Color + Codable + +extension Color: Codable { + + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + var r1: Double + if !container.isAtEnd { + r1 = try container.decode(Double.self) + } else { + r1 = 0 + } + + var g1: Double + if !container.isAtEnd { + g1 = try container.decode(Double.self) + } else { + g1 = 0 + } + + var b1: Double + if !container.isAtEnd { + b1 = try container.decode(Double.self) + } else { + b1 = 0 + } + + var a1: Double + if !container.isAtEnd { + a1 = try container.decode(Double.self) + } else { + a1 = 1 + } + if r1 > 1, g1 > 1, b1 > 1, a1 > 1 { + r1 = r1 / 255 + g1 = g1 / 255 + b1 = b1 / 255 + a1 = a1 / 255 + } + r = r1 + g = g1 + b = b1 + a = a1 + } + + // MARK: Public + + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(r) + try container.encode(g) + try container.encode(b) + try container.encode(a) + } + +} + +// MARK: - Color + AnyInitializable + +extension Color: AnyInitializable { + + init(value: Any) throws { + guard var array = value as? [Double] else { + throw InitializableError.invalidInput + } + var r: Double = array.count > 0 ? array.removeFirst() : 0 + var g: Double = array.count > 0 ? array.removeFirst() : 0 + var b: Double = array.count > 0 ? array.removeFirst() : 0 + var a: Double = array.count > 0 ? array.removeFirst() : 1 + if r > 1, g > 1, b > 1, a > 1 { + r /= 255 + g /= 255 + b /= 255 + a /= 255 + } + self.r = r + self.g = g + self.b = b + self.a = a + } + +} + +extension Color { + + static var clearColor: CGColor { + CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0, 0, 0, 0])! + } + + var cgColorValue: CGColor { + // TODO: Fix color spaces + let colorspace = CGColorSpaceCreateDeviceRGB() + return CGColor(colorSpace: colorspace, components: [CGFloat(r), CGFloat(g), CGFloat(b), CGFloat(a)]) ?? Color.clearColor + } + + func cgColorValue(colorSpace: CGColorSpace) -> CGColor { + return CGColor(colorSpace: colorSpace, components: [CGFloat(r), CGFloat(g), CGFloat(b), CGFloat(a)]) ?? Color.clearColor + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CompoundBezierPath.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CompoundBezierPath.cpp new file mode 100644 index 0000000000..31df703956 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CompoundBezierPath.cpp @@ -0,0 +1,5 @@ +#include "CompoundBezierPath.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CompoundBezierPath.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CompoundBezierPath.hpp new file mode 100644 index 0000000000..c9766c93ae --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CompoundBezierPath.hpp @@ -0,0 +1,176 @@ +#ifndef CompoundBezierPath_hpp +#define CompoundBezierPath_hpp + +#include "Lottie/Private/Utility/Primitives/BezierPath.hpp" + +namespace lottie { + +/// A collection of BezierPath objects that can be trimmed and added. +/// +class CompoundBezierPath: public std::enable_shared_from_this { +public: + CompoundBezierPath() : + paths({}) { + } + + CompoundBezierPath(BezierPath const &path) : + paths({ path }) { + } + + CompoundBezierPath(std::vector paths_, std::optional length_) : + paths(paths_), _length(length_) { + } + + CompoundBezierPath(std::vector paths_) : + paths(paths_) { + } + +public: + std::vector paths; + + double length() { + if (_length.has_value()) { + return _length.value(); + } else { + double l = 0.0; + for (auto &path : paths) { + l += path.length(); + } + _length = l; + return l; + } + } + +private: + std::optional _length; + +public: + std::shared_ptr addingPath(BezierPath const &path) const { + auto newPaths = paths; + newPaths.push_back(path); + return std::make_shared(newPaths); + } + + void appendPath(BezierPath const &path) { + paths.push_back(path); + _length.reset(); + } + + std::shared_ptr combine(std::shared_ptr compoundBezier) { + auto newPaths = paths; + for (const auto &path : compoundBezier->paths) { + newPaths.push_back(path); + } + return std::make_shared(newPaths); + } + + std::shared_ptr trim(double fromPosition, double toPosition, double offset) { + if (fromPosition == toPosition) { + return std::make_shared(); + } + + /*bool trimSimultaneously = false; + if (trimSimultaneously) { + /// Trim each path individually. + std::vector newPaths; + for (auto &path : paths) { + auto trimmedPaths = path.trim(fromPosition * path.length(), toPosition * path.length(), offset * path.length()); + for (const auto &trimmedPath : trimmedPaths) { + newPaths.push_back(trimmedPath); + } + } + return std::make_shared(newPaths); + }*/ + + double lengthValue = length(); + + /// Normalize lengths to the curve length. + double startPosition = fmod(fromPosition + offset, 1.0); + double endPosition = fmod(toPosition + offset, 1.0); + + if (startPosition < 0.0) { + startPosition = 1.0 + startPosition; + } + + if (endPosition < 0.0) { + endPosition = 1.0 + endPosition; + } + + if (startPosition == 1.0) { + startPosition = 0.0; + } + if (endPosition == 0.0) { + endPosition = 1.0; + } + + if ((startPosition == 0.0 && endPosition == 1.0) || + startPosition == endPosition || + (startPosition == 1.0 && endPosition == 0.0)) { + /// The trim encompasses the entire path. Return. + return shared_from_this(); + } + + std::vector positions; + if (endPosition < startPosition) { + positions = { + BezierTrimPathPosition(0.0, endPosition * lengthValue), + BezierTrimPathPosition(startPosition * lengthValue, lengthValue) + }; + } else { + positions = { BezierTrimPathPosition(startPosition * lengthValue, endPosition * lengthValue) }; + } + + auto compoundPath = std::make_shared(); + auto trim = positions[0]; + positions.erase(positions.begin()); + double pathStartPosition = 0.0; + + bool finishedTrimming = false; + int i = 0; + + while (!finishedTrimming) { + if (paths.size() <= i) { + /// Rounding errors + finishedTrimming = true; + continue; + } + auto path = paths[i]; + + auto pathEndPosition = pathStartPosition + path.length(); + + if (pathEndPosition < trim.start) { + /// Path is not included in the trim, continue. + pathStartPosition = pathEndPosition; + i = i + 1; + continue; + } else if (trim.start <= pathStartPosition && pathEndPosition <= trim.end) { + /// Full Path is inside of trim. Add full path. + compoundPath = compoundPath->addingPath(path); + } else { + auto trimPaths = path.trim(trim.start > pathStartPosition ? (trim.start - pathStartPosition) : 0, trim.end < pathEndPosition ? (trim.end - pathStartPosition) : path.length(), 0.0); + if (!trimPaths.empty()) { + compoundPath = compoundPath->addingPath(trimPaths[0]); + } + } + + if (trim.end <= pathEndPosition) { + /// We are done with the current trim. + /// Advance trim but remain on the same path in case the next trim overlaps it. + if (positions.size() > 0) { + trim = positions[0]; + positions.erase(positions.begin()); + } else { + finishedTrimming = true; + } + } else { + pathStartPosition = pathEndPosition; + i = i + 1; + } + } + return compoundPath; + } +}; + +} + +#endif /* CompoundBezierPath_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CompoundBezierPath.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CompoundBezierPath.swift new file mode 100644 index 0000000000..4d8a2e6ce5 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CompoundBezierPath.swift @@ -0,0 +1,172 @@ +// +// CompoundBezierPath.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/14/19. +// + +import CoreGraphics +import Foundation + +/// A collection of BezierPath objects that can be trimmed and added. +/// +struct CompoundBezierPath { + + // MARK: Lifecycle + + init() { + paths = [] + length = 0 + } + + init(path: BezierPath) { + paths = [path] + length = path.length + } + + init(paths: [BezierPath], length: CGFloat) { + self.paths = paths + self.length = length + } + + init(paths: [BezierPath]) { + self.paths = paths + var l: CGFloat = 0 + for path in paths { + l = l + path.length + } + length = l + } + + // MARK: Internal + + let paths: [BezierPath] + + let length: CGFloat + + func addPath(path: BezierPath) -> CompoundBezierPath { + var newPaths = paths + newPaths.append(path) + return CompoundBezierPath(paths: newPaths, length: length + path.length) + } + + func combine(_ compoundBezier: CompoundBezierPath) -> CompoundBezierPath { + var newPaths = paths + newPaths.append(contentsOf: compoundBezier.paths) + return CompoundBezierPath(paths: newPaths, length: length + compoundBezier.length) + } + + func trim(fromPosition: CGFloat, toPosition: CGFloat, offset: CGFloat, trimSimultaneously: Bool) -> CompoundBezierPath { + if fromPosition == toPosition { + return CompoundBezierPath() + } + + if trimSimultaneously { + /// Trim each path individually. + var newPaths = [BezierPath]() + for path in paths { + newPaths.append(contentsOf: path.trim( + fromLength: fromPosition * path.length, + toLength: toPosition * path.length, + offsetLength: offset * path.length)) + } + return CompoundBezierPath(paths: newPaths) + } + + /// Normalize lengths to the curve length. + var startPosition = (fromPosition + offset).truncatingRemainder(dividingBy: 1) + + assert(fmod(fromPosition + offset, 1.0) == startPosition) + + var endPosition = (toPosition + offset).truncatingRemainder(dividingBy: 1) + + assert(fmod(toPosition + offset, 1.0) == endPosition) + + if startPosition < 0 { + startPosition = 1 + startPosition + } + + if endPosition < 0 { + endPosition = 1 + endPosition + } + + if startPosition == 1 { + startPosition = 0 + } + if endPosition == 0 { + endPosition = 1 + } + + if + startPosition == 0 && endPosition == 1 || + startPosition == endPosition || + startPosition == 1 && endPosition == 0 + { + /// The trim encompasses the entire path. Return. + return self + } + + var positions: [(start: CGFloat, end: CGFloat)] + if endPosition < startPosition { + positions = [ + (start: 0, end: endPosition * length), + (start: startPosition * length, end: length), + ] + } else { + positions = [(start: startPosition * length, end: endPosition * length)] + } + + var compoundPath = CompoundBezierPath() + var trim = positions.remove(at: 0) + var pathStartPosition: CGFloat = 0 + + var finishedTrimming = false + var i = 0 + + while !finishedTrimming { + if paths.count <= i { + /// Rounding errors + finishedTrimming = true + continue + } + let path = paths[i] + + let pathEndPosition = pathStartPosition + path.length + + if pathEndPosition < trim.start { + /// Path is not included in the trim, continue. + pathStartPosition = pathEndPosition + i = i + 1 + continue + + } else if trim.start <= pathStartPosition, pathEndPosition <= trim.end { + /// Full Path is inside of trim. Add full path. + compoundPath = compoundPath.addPath(path: path) + } else { + if + let trimPath = path.trim( + fromLength: trim.start > pathStartPosition ? (trim.start - pathStartPosition) : 0, + toLength: trim.end < pathEndPosition ? (trim.end - pathStartPosition) : path.length, + offsetLength: 0).first + { + compoundPath = compoundPath.addPath(path: trimPath) + } + } + + if trim.end <= pathEndPosition { + /// We are done with the current trim. + /// Advance trim but remain on the same path in case the next trim overlaps it. + if positions.count > 0 { + trim = positions.remove(at: 0) + } else { + finishedTrimming = true + } + } else { + pathStartPosition = pathEndPosition + i = i + 1 + } + } + return compoundPath + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CoordinateSpace.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CoordinateSpace.cpp new file mode 100644 index 0000000000..695b8dc4e9 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CoordinateSpace.cpp @@ -0,0 +1,5 @@ +#include "CoordinateSpace.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CoordinateSpace.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CoordinateSpace.hpp new file mode 100644 index 0000000000..37a7b7054c --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CoordinateSpace.hpp @@ -0,0 +1,13 @@ +#ifndef CoordinateSpace_hpp +#define CoordinateSpace_hpp + +namespace lottie { + +enum class CoordinateSpace { + Type2d, + Type3d +}; + +} + +#endif /* CoordinateSpace_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CurveVertex.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CurveVertex.cpp new file mode 100644 index 0000000000..5c1d4a0cb0 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CurveVertex.cpp @@ -0,0 +1,5 @@ +#include "CurveVertex.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CurveVertex.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CurveVertex.hpp new file mode 100644 index 0000000000..ef67e0765c --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CurveVertex.hpp @@ -0,0 +1,197 @@ +#ifndef CurveVertex_hpp +#define CurveVertex_hpp + +#include "Lottie/Public/Primitives/Vectors.hpp" +#include "Lottie/Public/Primitives/CGPath.hpp" + +#include + +namespace lottie { + +template +struct CurveVertexSplitResult { + T start; + T trimPoint; + T end; + + explicit CurveVertexSplitResult( + T const &start_, + T const &trimPoint_, + T const &end_ + ) : + start(start_), + trimPoint(trimPoint_), + end(end_) { + } +}; + +/// A single vertex with an in and out tangent +struct CurveVertex { +private: + /// Initializes a curve point with absolute or relative values + explicit CurveVertex(Vector2D const &point_, Vector2D const &inTangent_, Vector2D const &outTangent_, bool isRelative_) : + point(point_), + inTangent(isRelative_ ? (point_ + inTangent_) : inTangent_), + outTangent(isRelative_ ? (point_ + outTangent_) : outTangent_) { + } + +public: + static CurveVertex absolute(Vector2D const &point_, Vector2D const &inTangent_, Vector2D const &outTangent_) { + return CurveVertex(point_, inTangent_, outTangent_, false); + } + + static CurveVertex relative(Vector2D const &point_, Vector2D const &inTangent_, Vector2D const &outTangent_) { + return CurveVertex(point_, inTangent_, outTangent_, true); + } + + Vector2D inTangentRelative() const { + Vector2D result = inTangent - point; + return result; + } + + Vector2D outTangentRelative() const { + Vector2D result = outTangent - point; + return result; + } + + CurveVertex reversed() const { + return CurveVertex(point, outTangent, inTangent, false); + } + + CurveVertex translated(Vector2D const &translation) const { + return CurveVertex(point + translation, inTangent + translation, outTangent + translation, false); + } + + CurveVertex transformed(CATransform3D const &transform) const { + return CurveVertex(transformVector(point, transform), transformVector(inTangent, transform), transformVector(outTangent, transform), false); + } + +public: + Vector2D point = Vector2D::Zero(); + + Vector2D inTangent = Vector2D::Zero(); + Vector2D outTangent = Vector2D::Zero(); + + /// Trims a path defined by two Vertices at a specific position, from 0 to 1 + /// + /// The path can be visualized below. + /// + /// F is fromVertex. + /// V is the vertex of the receiver. + /// P is the position from 0-1. + /// O is the outTangent of fromVertex. + /// F====O=========P=======I====V + /// + /// After trimming the curve can be visualized below. + /// + /// S is the returned Start vertex. + /// E is the returned End vertex. + /// T is the trim point. + /// TI and TO are the new tangents for the trimPoint + /// NO and NI are the new tangents for the startPoint and endPoints + /// S==NO=========TI==T==TO=======NI==E + CurveVertexSplitResult splitCurve(CurveVertex const &toVertex, double position) const { + /// If position is less than or equal to 0, trim at start. + if (position <= 0.0) { + return CurveVertexSplitResult( + CurveVertex(point, inTangentRelative(), Vector2D::Zero(), true), + CurveVertex(point, Vector2D::Zero(), outTangentRelative(), true), + toVertex + ); + } + + /// If position is greater than or equal to 1, trim at end. + if (position >= 1.0) { + return CurveVertexSplitResult( + *this, + CurveVertex(toVertex.point, toVertex.inTangentRelative(), Vector2D::Zero(), true), + CurveVertex(toVertex.point, Vector2D::Zero(), toVertex.outTangentRelative(), true) + ); + } + + if (outTangentRelative().isZero() && toVertex.inTangentRelative().isZero()) { + /// If both tangents are zero, then span to be trimmed is a straight line. + Vector2D trimPoint = interpolate(point, toVertex.point, position); + return CurveVertexSplitResult( + *this, + CurveVertex(trimPoint, Vector2D::Zero(), Vector2D::Zero(), true), + toVertex + ); + } + /// Cutting by amount gives incorrect length.... + /// One option is to cut by a stride until it gets close then edge it down. + /// Measuring a percentage of the spans does not equal the same as measuring a percentage of length. + /// This is where the historical trim path bugs come from. + Vector2D a = interpolate(point, outTangent, position); + Vector2D b = interpolate(outTangent, toVertex.inTangent, position); + Vector2D c = interpolate(toVertex.inTangent, toVertex.point, position); + Vector2D d = interpolate(a, b, position); + Vector2D e = interpolate(b, c, position); + Vector2D f = interpolate(d, e, position); + return CurveVertexSplitResult( + CurveVertex::absolute(point, inTangent, a), + CurveVertex::absolute(f, d, e), + CurveVertex::absolute(toVertex.point, c, toVertex.outTangent) + ); + } + + /// Trims a curve of a known length to a specific length and returns the points. + /// + /// There is not a performant yet accurate way to cut a curve to a specific length. + /// This calls splitCurve(toVertex: position:) to split the curve and then measures + /// the length of the new curve. The function then iterates through the samples, + /// adjusting the position of the cut for a more precise cut. + /// Usually a single iteration is enough to get within 0.5 points of the desired + /// length. + /// + /// This function should probably live in PathElement, since it deals with curve + /// lengths. + CurveVertexSplitResult trimCurve(CurveVertex const &toVertex, double atLength, double curveLength, int maxSamples, double accuracy = 1.0) const { + double currentPosition = atLength / curveLength; + auto results = splitCurve(toVertex, currentPosition); + + if (maxSamples == 0) { + return results; + } + + for (int i = 1; i <= maxSamples; i++) { + auto length = results.start.distanceTo(results.trimPoint); + auto lengthDiff = atLength - length; + /// Check if length is correct. + if (lengthDiff < accuracy) { + return results; + } + auto diffPosition = std::max(std::min((currentPosition / length) * lengthDiff, currentPosition * 0.5), currentPosition * (-0.5)); + currentPosition = diffPosition + currentPosition; + results = splitCurve(toVertex, currentPosition); + } + return results; + } + + /// The distance from the receiver to the provided vertex. + /// + /// For lines (zeroed tangents) the distance between the two points is measured. + /// For curves the curve is iterated over by sample count and the points are measured. + /// This is ~99% accurate at a sample count of 30 + double distanceTo(CurveVertex const &toVertex, int sampleCount = 25) const { + if (outTangentRelative().isZero() && toVertex.inTangentRelative().isZero()) { + /// Return a linear distance. + return point.distanceTo(toVertex.point); + } + + double distance = 0.0; + + auto previousPoint = point; + for (int i = 0; i < sampleCount; i++) { + auto pointOnCurve = splitCurve(toVertex, ((double)(i)) / ((double)(sampleCount))).trimPoint; + distance = distance + previousPoint.distanceTo(pointOnCurve.point); + previousPoint = pointOnCurve.point; + } + distance = distance + previousPoint.distanceTo(toVertex.point); + return distance; + } +}; + +} + +#endif /* CurveVertex_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CurveVertex.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CurveVertex.swift new file mode 100644 index 0000000000..51304a7dfa --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/CurveVertex.swift @@ -0,0 +1,186 @@ +// +// CurveVertex.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/11/19. +// + +import CoreGraphics +import Foundation + +/// A single vertex with an in and out tangent +struct CurveVertex { + + // MARK: Lifecycle + + /// Initializes a curve point with absolute values + init(_ inTangent: CGPoint, _ point: CGPoint, _ outTangent: CGPoint) { + self.point = point + self.inTangent = inTangent + self.outTangent = outTangent + } + + /// Initializes a curve point with relative values + init(point: CGPoint, inTangentRelative: CGPoint, outTangentRelative: CGPoint) { + self.point = point + inTangent = point.add(inTangentRelative) + outTangent = point.add(outTangentRelative) + } + + /// Initializes a curve point with absolute values + init(point: CGPoint, inTangent: CGPoint, outTangent: CGPoint) { + self.point = point + self.inTangent = inTangent + self.outTangent = outTangent + } + + // MARK: Internal + + let point: CGPoint + + let inTangent: CGPoint + let outTangent: CGPoint + + var inTangentRelative: CGPoint { + return inTangent.subtract(point) + } + + var outTangentRelative: CGPoint { + outTangent.subtract(point) + } + + func reversed() -> CurveVertex { + CurveVertex(point: point, inTangent: outTangent, outTangent: inTangent) + } + + func translated(_ translation: CGPoint) -> CurveVertex { + CurveVertex(point: point + translation, inTangent: inTangent + translation, outTangent: outTangent + translation) + } + + /// Trims a path defined by two Vertices at a specific position, from 0 to 1 + /// + /// The path can be visualized below. + /// + /// F is fromVertex. + /// V is the vertex of the receiver. + /// P is the position from 0-1. + /// O is the outTangent of fromVertex. + /// F====O=========P=======I====V + /// + /// After trimming the curve can be visualized below. + /// + /// S is the returned Start vertex. + /// E is the returned End vertex. + /// T is the trim point. + /// TI and TO are the new tangents for the trimPoint + /// NO and NI are the new tangents for the startPoint and endPoints + /// S==NO=========TI==T==TO=======NI==E + func splitCurve(toVertex: CurveVertex, position: CGFloat) -> + (start: CurveVertex, trimPoint: CurveVertex, end: CurveVertex) + { + + /// If position is less than or equal to 0, trim at start. + if position <= 0 { + return ( + start: CurveVertex(point: point, inTangentRelative: inTangentRelative, outTangentRelative: .zero), + trimPoint: CurveVertex(point: point, inTangentRelative: .zero, outTangentRelative: outTangentRelative), + end: toVertex) + } + + /// If position is greater than or equal to 1, trim at end. + if position >= 1 { + return ( + start: self, + trimPoint: CurveVertex( + point: toVertex.point, + inTangentRelative: toVertex.inTangentRelative, + outTangentRelative: .zero), + end: CurveVertex( + point: toVertex.point, + inTangentRelative: .zero, + outTangentRelative: toVertex.outTangentRelative)) + } + + if outTangentRelative.isZero && toVertex.inTangentRelative.isZero { + /// If both tangents are zero, then span to be trimmed is a straight line. + let trimPoint = point.interpolate(to: toVertex.point, amount: position) + return ( + start: self, + trimPoint: CurveVertex(point: trimPoint, inTangentRelative: .zero, outTangentRelative: .zero), + end: toVertex) + } + /// Cutting by amount gives incorrect length.... + /// One option is to cut by a stride until it gets close then edge it down. + /// Measuring a percentage of the spans does not equal the same as measuring a percentage of length. + /// This is where the historical trim path bugs come from. + let a = point.interpolate(to: outTangent, amount: position) + let b = outTangent.interpolate(to: toVertex.inTangent, amount: position) + let c = toVertex.inTangent.interpolate(to: toVertex.point, amount: position) + let d = a.interpolate(to: b, amount: position) + let e = b.interpolate(to: c, amount: position) + let f = d.interpolate(to: e, amount: position) + return ( + start: CurveVertex(point: point, inTangent: inTangent, outTangent: a), + trimPoint: CurveVertex(point: f, inTangent: d, outTangent: e), + end: CurveVertex(point: toVertex.point, inTangent: c, outTangent: toVertex.outTangent)) + } + + /// Trims a curve of a known length to a specific length and returns the points. + /// + /// There is not a performant yet accurate way to cut a curve to a specific length. + /// This calls splitCurve(toVertex: position:) to split the curve and then measures + /// the length of the new curve. The function then iterates through the samples, + /// adjusting the position of the cut for a more precise cut. + /// Usually a single iteration is enough to get within 0.5 points of the desired + /// length. + /// + /// This function should probably live in PathElement, since it deals with curve + /// lengths. + func trimCurve(toVertex: CurveVertex, atLength: CGFloat, curveLength: CGFloat, maxSamples: Int, accuracy: CGFloat = 1) -> + (start: CurveVertex, trimPoint: CurveVertex, end: CurveVertex) + { + var currentPosition = atLength / curveLength + var results = splitCurve(toVertex: toVertex, position: currentPosition) + + if maxSamples == 0 { + return results + } + + for _ in 1...maxSamples { + let length = results.start.distanceTo(results.trimPoint) + let lengthDiff = atLength - length + /// Check if length is correct. + if lengthDiff < accuracy { + return results + } + let diffPosition = max(min((currentPosition / length) * lengthDiff, currentPosition * 0.5), currentPosition * -0.5) + currentPosition = diffPosition + currentPosition + results = splitCurve(toVertex: toVertex, position: currentPosition) + } + return results + } + + /// The distance from the receiver to the provided vertex. + /// + /// For lines (zeroed tangents) the distance between the two points is measured. + /// For curves the curve is iterated over by sample count and the points are measured. + /// This is ~99% accurate at a sample count of 30 + func distanceTo(_ toVertex: CurveVertex, sampleCount: Int = 25) -> CGFloat { + + if outTangentRelative.isZero && toVertex.inTangentRelative.isZero { + /// Return a linear distance. + return point.distanceTo(toVertex.point) + } + + var distance: CGFloat = 0 + + var previousPoint = point + for i in 0.. +struct PathSplitResultSpan { + T start; + T end; + + explicit PathSplitResultSpan(T const &start_, T const &end_) : + start(start_), end(end_) { + } +}; + +template +struct PathSplitResult { + PathSplitResultSpan leftSpan; + PathSplitResultSpan rightSpan; + + explicit PathSplitResult(PathSplitResultSpan const &leftSpan_, PathSplitResultSpan const &rightSpan_) : + leftSpan(leftSpan_), rightSpan(rightSpan_) { + } +}; + +/// A path section, containing one point and its length to the previous point. +/// +/// The relationship between this path element and the previous is implicit. +/// Ideally a path section would be defined by two vertices and a length. +/// We don't do this however, as it would effectively double the memory footprint +/// of path data. +/// +struct PathElement { + /// Initializes a new path with length of 0 + explicit PathElement(CurveVertex const &vertex_) : + vertex(vertex_) { + } + + /// Initializes a new path with length + explicit PathElement(std::optional length_, CurveVertex const &vertex_) : + vertex(vertex_) { + } + + /// The vertex of the element + CurveVertex vertex; + + /// Returns a new path element define the span from the receiver to the new vertex. + PathElement pathElementTo(CurveVertex const &toVertex) const { + return PathElement(std::nullopt, toVertex); + } + + PathElement updateVertex(CurveVertex const &newVertex) const { + return PathElement(newVertex); + } + + /// Splits an element span defined by the receiver and fromElement to a position 0-1 + PathSplitResult splitElementAtPosition(PathElement const &fromElement, double atLength) { + /// Trim the span. Start and trim go into the first, trim and end go into second. + auto trimResults = fromElement.vertex.trimCurve(vertex, atLength, length(fromElement), 3); + + /// Create the elements for the break + auto spanAStart = PathElement( + std::nullopt, + CurveVertex::absolute( + fromElement.vertex.point, + fromElement.vertex.inTangent, + trimResults.start.outTangent + )); + /// Recalculating the length here is a waste as the trimCurve function also accurately calculates this length. + auto spanAEnd = spanAStart.pathElementTo(trimResults.trimPoint); + + auto spanBStart = PathElement(trimResults.trimPoint); + auto spanBEnd = spanBStart.pathElementTo(trimResults.end); + return PathSplitResult( + PathSplitResultSpan(spanAStart, spanAEnd), + PathSplitResultSpan(spanBStart, spanBEnd) + ); + } + + double length(PathElement const &previous) { + double result = previous.vertex.distanceTo(vertex); + return result; + } +}; + +} + +#endif /* PathElement_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/PathElement.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/PathElement.swift new file mode 100644 index 0000000000..68f43b5f25 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/PathElement.swift @@ -0,0 +1,75 @@ +// +// PathElement.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/11/19. +// + +import CoreGraphics +import Foundation + +/// A path section, containing one point and its length to the previous point. +/// +/// The relationship between this path element and the previous is implicit. +/// Ideally a path section would be defined by two vertices and a length. +/// We don't do this however, as it would effectively double the memory footprint +/// of path data. +/// +struct PathElement { + + // MARK: Lifecycle + + /// Initializes a new path with length of 0 + init(vertex: CurveVertex) { + length = 0 + self.vertex = vertex + } + + /// Initializes a new path with length + private init(length: CGFloat, vertex: CurveVertex) { + self.length = length + self.vertex = vertex + } + + // MARK: Internal + + /// The absolute Length of the path element. + let length: CGFloat + + /// The vertex of the element + let vertex: CurveVertex + + /// Returns a new path element define the span from the receiver to the new vertex. + func pathElementTo(_ toVertex: CurveVertex) -> PathElement { + PathElement(length: vertex.distanceTo(toVertex), vertex: toVertex) + } + + func updateVertex(newVertex: CurveVertex) -> PathElement { + PathElement(length: length, vertex: newVertex) + } + + /// Splits an element span defined by the receiver and fromElement to a position 0-1 + func splitElementAtPosition(fromElement: PathElement, atLength: CGFloat) -> + (leftSpan: (start: PathElement, end: PathElement), rightSpan: (start: PathElement, end: PathElement)) + { + /// Trim the span. Start and trim go into the first, trim and end go into second. + let trimResults = fromElement.vertex.trimCurve(toVertex: vertex, atLength: atLength, curveLength: length, maxSamples: 3) + + /// Create the elements for the break + let spanAStart = PathElement( + length: fromElement.length, + vertex: CurveVertex( + point: fromElement.vertex.point, + inTangent: fromElement.vertex.inTangent, + outTangent: trimResults.start.outTangent)) + /// Recalculating the length here is a waste as the trimCurve function also accurately calculates this length. + let spanAEnd = spanAStart.pathElementTo(trimResults.trimPoint) + + let spanBStart = PathElement(vertex: trimResults.trimPoint) + let spanBEnd = spanBStart.pathElementTo(trimResults.end) + return ( + leftSpan: (start: spanAStart, end: spanAEnd), + rightSpan: (start: spanBStart, end: spanBEnd)) + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/VectorsExtensions.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/VectorsExtensions.swift new file mode 100644 index 0000000000..bfdf10021a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Primitives/VectorsExtensions.swift @@ -0,0 +1,345 @@ +// +// Vector.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/7/19. +// + +import CoreGraphics +import Foundation +import QuartzCore + +// MARK: - Vector1D + Codable + +/// Single value container. Needed because lottie sometimes wraps a Double in an array. +extension Vector1D: Codable { + + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + /// Try to decode an array of doubles + do { + var container = try decoder.unkeyedContainer() + value = try container.decode(Double.self) + } catch { + value = try decoder.singleValueContainer().decode(Double.self) + } + } + + // MARK: Public + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(value) + } + + // MARK: Internal + + var cgFloatValue: CGFloat { + CGFloat(value) + } + +} + +// MARK: - Vector1D + AnyInitializable + +extension Vector1D: AnyInitializable { + + init(value: Any) throws { + if + let array = value as? [Double], + let double = array.first + { + self.value = double + } else if let double = value as? Double { + self.value = double + } else { + throw InitializableError.invalidInput + } + } + +} + +extension Double { + var vectorValue: Vector1D { + Vector1D(self) + } +} + +// MARK: - Vector2D + +/// Needed for decoding json {x: y:} to a CGPoint +public struct Vector2D: Codable, Hashable { + + // MARK: Lifecycle + + init(x: Double, y: Double) { + self.x = x + self.y = y + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: Vector2D.CodingKeys.self) + + do { + let xValue: [Double] = try container.decode([Double].self, forKey: .x) + x = xValue[0] + } catch { + x = try container.decode(Double.self, forKey: .x) + } + + do { + let yValue: [Double] = try container.decode([Double].self, forKey: .y) + y = yValue[0] + } catch { + y = try container.decode(Double.self, forKey: .y) + } + } + + // MARK: Public + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: Vector2D.CodingKeys.self) + try container.encode(x, forKey: .x) + try container.encode(y, forKey: .y) + } + + // MARK: Internal + + var x: Double + var y: Double + + var pointValue: CGPoint { + CGPoint(x: x, y: y) + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case x + case y + } +} + +// MARK: AnyInitializable + +extension Vector2D: AnyInitializable { + + init(value: Any) throws { + guard let dictionary = value as? [String: Any] else { + throw InitializableError.invalidInput + } + + if + let array = dictionary[CodingKeys.x.rawValue] as? [Double], + let double = array.first + { + x = double + } else if let double = dictionary[CodingKeys.x.rawValue] as? Double { + x = double + } else { + throw InitializableError.invalidInput + } + if + let array = dictionary[CodingKeys.y.rawValue] as? [Double], + let double = array.first + { + y = double + } else if let double = dictionary[CodingKeys.y.rawValue] as? Double { + y = double + } else { + throw InitializableError.invalidInput + } + } +} + +extension CGPoint { + var vector2dValue: Vector2D { + Vector2D(x: Double(x), y: Double(y)) + } +} + +// MARK: - Vector3D + Codable + +/// A three dimensional vector. +/// These vectors are encoded and decoded from [Double] + +extension Vector3D: Codable { + + // MARK: Lifecycle + + init(x: CGFloat, y: CGFloat, z: CGFloat) { + self.x = Double(x) + self.y = Double(y) + self.z = Double(z) + } + + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + if !container.isAtEnd { + x = try container.decode(Double.self) + } else { + x = 0 + } + + if !container.isAtEnd { + y = try container.decode(Double.self) + } else { + y = 0 + } + + if !container.isAtEnd { + z = try container.decode(Double.self) + } else { + z = 0 + } + } + + // MARK: Public + + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(x) + try container.encode(y) + try container.encode(z) + } + +} + +// MARK: - Vector3D + AnyInitializable + +extension Vector3D: AnyInitializable { + + init(value: Any) throws { + guard var array = value as? [Double] else { + throw InitializableError.invalidInput + } + x = array.count > 0 ? array.removeFirst() : 0 + y = array.count > 0 ? array.removeFirst() : 0 + z = array.count > 0 ? array.removeFirst() : 0 + } + +} + +extension Vector3D { + public var pointValue: CGPoint { + CGPoint(x: x, y: y) + } + + public var sizeValue: CGSize { + CGSize(width: x, height: y) + } +} + +extension CGPoint { + var vector3dValue: Vector3D { + Vector3D(x: x, y: y, z: 0) + } +} + +extension CGSize { + var vector3dValue: Vector3D { + Vector3D(x: width, y: height, z: 1) + } +} + +extension CATransform3D { + + static func makeSkew(skew: CGFloat, skewAxis: CGFloat) -> CATransform3D { + let mCos = cos(skewAxis.toRadians()) + let mSin = sin(skewAxis.toRadians()) + let aTan = tan(skew.toRadians()) + + let transform1 = CATransform3D( + m11: mCos, + m12: mSin, + m13: 0, + m14: 0, + m21: -mSin, + m22: mCos, + m23: 0, + m24: 0, + m31: 0, + m32: 0, + m33: 1, + m34: 0, + m41: 0, + m42: 0, + m43: 0, + m44: 1) + + let transform2 = CATransform3D( + m11: 1, + m12: 0, + m13: 0, + m14: 0, + m21: aTan, + m22: 1, + m23: 0, + m24: 0, + m31: 0, + m32: 0, + m33: 1, + m34: 0, + m41: 0, + m42: 0, + m43: 0, + m44: 1) + + let transform3 = CATransform3D( + m11: mCos, + m12: -mSin, + m13: 0, + m14: 0, + m21: mSin, + m22: mCos, + m23: 0, + m24: 0, + m31: 0, + m32: 0, + m33: 1, + m34: 0, + m41: 0, + m42: 0, + m43: 0, + m44: 1) + return CATransform3DConcat(transform3, CATransform3DConcat(transform2, transform1)) + } + + static func makeTransform( + anchor: CGPoint, + position: CGPoint, + scale: CGSize, + rotation: CGFloat, + skew: CGFloat?, + skewAxis: CGFloat?) + -> CATransform3D + { + let result: CATransform3D + if let skew = skew, let skewAxis = skewAxis { + result = CATransform3DMakeTranslation(position.x, position.y, 0).rotated(rotation).skewed(skew: -skew, skewAxis: skewAxis) + .scaled(scale * 0.01).translated(anchor * -1) + } else { + result = CATransform3DMakeTranslation(position.x, position.y, 0).rotated(rotation).scaled(scale * 0.01).translated(anchor * -1) + } + + return result + } + + func rotated(_ degrees: CGFloat) -> CATransform3D { + CATransform3DRotate(self, degrees.toRadians(), 0, 0, 1) + } + + func translated(_ translation: CGPoint) -> CATransform3D { + CATransform3DTranslate(self, translation.x, translation.y, 0) + } + + func scaled(_ scale: CGSize) -> CATransform3D { + CATransform3DScale(self, scale.width, scale.height, 1) + } + + func skewed(skew: CGFloat, skewAxis: CGFloat) -> CATransform3D { + CATransform3DConcat(CATransform3D.makeSkew(skew: skew, skewAxis: skewAxis), self) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Animation/AnimationPublic.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Animation/AnimationPublic.swift new file mode 100644 index 0000000000..ab5b7a61b7 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Animation/AnimationPublic.swift @@ -0,0 +1,269 @@ +// +// AnimationPublic.swift +// lottie-swift +// +// Created by Brandon Withrow on 2/5/19. +// + +import CoreGraphics +import Foundation + +public extension Animation { + + /// A closure for an Animation download. The closure is passed `nil` if there was an error. + typealias DownloadClosure = (Animation?) -> Void + + /// The duration in seconds of the animation. + var duration: TimeInterval { + Double(endFrame - startFrame) / framerate + } + + /// The natural bounds in points of the animation. + var bounds: CGRect { + CGRect(x: 0, y: 0, width: width, height: height) + } + + /// The natural size in points of the animation. + var size: CGSize { + CGSize(width: width, height: height) + } + + // MARK: Animation (Loading) + + /// Loads an animation model from a bundle by its name. Returns `nil` if an animation is not found. + /// + /// - Parameter name: The name of the json file without the json extension. EG "StarAnimation" + /// - Parameter bundle: The bundle in which the animation is located. Defaults to `Bundle.main` + /// - Parameter subdirectory: A subdirectory in the bundle in which the animation is located. Optional. + /// - Parameter animationCache: A cache for holding loaded animations. Optional. + /// + /// - Returns: Deserialized `Animation`. Optional. + static func named( + _ name: String, + bundle: Bundle = Bundle.main, + subdirectory: String? = nil, + animationCache: AnimationCacheProvider? = nil) + -> Animation? + { + /// Create a cache key for the animation. + let cacheKey = bundle.bundlePath + (subdirectory ?? "") + "/" + name + + /// Check cache for animation + if + let animationCache = animationCache, + let animation = animationCache.animation(forKey: cacheKey) + { + /// If found, return the animation. + return animation + } + + do { + /// Decode animation. + guard let json = try bundle.getAnimationData(name, subdirectory: subdirectory) else { + return nil + } + let animation = try Animation.from(data: json) + animationCache?.setAnimation(animation, forKey: cacheKey) + return animation + } catch { + /// Decoding error. + LottieLogger.shared.warn("Error when decoding animation \"\(name)\": \(error)") + return nil + } + } + + /// Loads an animation from a specific filepath. + /// - Parameter filepath: The absolute filepath of the animation to load. EG "/User/Me/starAnimation.json" + /// - Parameter animationCache: A cache for holding loaded animations. Optional. + /// + /// - Returns: Deserialized `Animation`. Optional. + static func filepath( + _ filepath: String, + animationCache: AnimationCacheProvider? = nil) + -> Animation? + { + + /// Check cache for animation + if + let animationCache = animationCache, + let animation = animationCache.animation(forKey: filepath) + { + return animation + } + + do { + /// Decode the animation. + let json = try Data(contentsOf: URL(fileURLWithPath: filepath)) + let animation = try Animation.from(data: json) + animationCache?.setAnimation(animation, forKey: filepath) + return animation + } catch { + /// Decoding Error. + return nil + } + } + + /// Loads an animation model from the asset catalog by its name. Returns `nil` if an animation is not found. + /// - Parameter name: The name of the json file in the asset catalog. EG "StarAnimation" + /// - Parameter bundle: The bundle in which the animation is located. Defaults to `Bundle.main` + /// - Parameter animationCache: A cache for holding loaded animations. Optional. + /// - Returns: Deserialized `Animation`. Optional. + static func asset( + _ name: String, + bundle: Bundle = Bundle.main, + animationCache: AnimationCacheProvider? = nil) + -> Animation? + { + /// Create a cache key for the animation. + let cacheKey = bundle.bundlePath + "/" + name + + /// Check cache for animation + if + let animationCache = animationCache, + let animation = animationCache.animation(forKey: cacheKey) + { + /// If found, return the animation. + return animation + } + + /// Load jsonData from Asset + guard let json = Data.jsonData(from: name, in: bundle) else { + return nil + } + + do { + /// Decode animation. + let animation = try Animation.from(data: json) + animationCache?.setAnimation(animation, forKey: cacheKey) + return animation + } catch { + /// Decoding error. + return nil + } + } + + /// Loads a Lottie animation from a `Data` object containing a JSON animation. + /// + /// - Parameter data: The object to load the animation from. + /// - Parameter strategy: How the data should be decoded. Defaults to using the strategy set in `LottieConfiguration.shared`. + /// - Returns: Deserialized `Animation`. Optional. + /// + static func from( + data: Data, + strategy: DecodingStrategy = LottieConfiguration.shared.decodingStrategy) throws + -> Animation + { + switch strategy { + case .codable: + return try JSONDecoder().decode(Animation.self, from: data) + case .dictionaryBased: + let json = try JSONSerialization.jsonObject(with: data) + guard let dict = json as? [String: Any] else { + throw InitializableError.invalidInput + } + return try Animation(dictionary: dict) + } + } + + /// Loads a Lottie animation asynchronously from the URL. + /// + /// - Parameter url: The url to load the animation from. + /// - Parameter closure: A closure to be called when the animation has loaded. + /// - Parameter animationCache: A cache for holding loaded animations. + /// + static func loadedFrom( + url: URL, + closure: @escaping Animation.DownloadClosure, + animationCache: AnimationCacheProvider?) + { + + if let animationCache = animationCache, let animation = animationCache.animation(forKey: url.absoluteString) { + closure(animation) + } else { + let task = URLSession.shared.dataTask(with: url) { data, _, error in + guard error == nil, let jsonData = data else { + DispatchQueue.main.async { + closure(nil) + } + return + } + do { + let animation = try Animation.from(data: jsonData) + DispatchQueue.main.async { + animationCache?.setAnimation(animation, forKey: url.absoluteString) + closure(animation) + } + } catch { + DispatchQueue.main.async { + closure(nil) + } + } + + } + task.resume() + } + } + + // MARK: Animation (Helpers) + + /// Markers are a way to describe a point in time by a key name. + /// + /// Markers are encoded into animation JSON. By using markers a designer can mark + /// playback points for a developer to use without having to worry about keeping + /// track of animation frames. If the animation file is updated, the developer + /// does not need to update playback code. + /// + /// Returns the Progress Time for the marker named. Returns nil if no marker found. + func progressTime(forMarker named: String) -> AnimationProgressTime? { + guard let markers = markerMap, let marker = markers[named] else { + return nil + } + return progressTime(forFrame: marker.frameTime) + } + + /// Markers are a way to describe a point in time by a key name. + /// + /// Markers are encoded into animation JSON. By using markers a designer can mark + /// playback points for a developer to use without having to worry about keeping + /// track of animation frames. If the animation file is updated, the developer + /// does not need to update playback code. + /// + /// Returns the Frame Time for the marker named. Returns nil if no marker found. + func frameTime(forMarker named: String) -> AnimationFrameTime? { + guard let markers = markerMap, let marker = markers[named] else { + return nil + } + return marker.frameTime + } + + /// Converts Frame Time (Seconds * Framerate) into Progress Time + /// (optionally clamped to between 0 and 1). + func progressTime( + forFrame frameTime: AnimationFrameTime, + clamped: Bool = true) + -> AnimationProgressTime + { + let progressTime = ((frameTime - startFrame) / (endFrame - startFrame)) + + if clamped { + return progressTime.clamp(0, 1) + } else { + return progressTime + } + } + + /// Converts Progress Time (0 to 1) into Frame Time (Seconds * Framerate) + func frameTime(forProgress progressTime: AnimationProgressTime) -> AnimationFrameTime { + ((endFrame - startFrame) * progressTime) + startFrame + } + + /// Converts Frame Time (Seconds * Framerate) into Time (Seconds) + func time(forFrame frameTime: AnimationFrameTime) -> TimeInterval { + Double(frameTime - startFrame) / framerate + } + + /// Converts Time (Seconds) into Frame Time (Seconds * Framerate) + func frameTime(forTime time: TimeInterval) -> AnimationFrameTime { + CGFloat(time * framerate) + startFrame + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Animation/AnimationView.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Animation/AnimationView.swift new file mode 100644 index 0000000000..700cbfb229 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Animation/AnimationView.swift @@ -0,0 +1,1302 @@ +// +// AnimationView.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/23/19. +// + +import Foundation +import QuartzCore + +// MARK: - LottieBackgroundBehavior + +/// Describes the behavior of an AnimationView when the app is moved to the background. +public enum LottieBackgroundBehavior { + /// Stop the animation and reset it to the beginning of its current play time. The completion block is called. + case stop + + /// Pause the animation in its current state. The completion block is called. + /// - This is the default when using the Main Thread rendering engine. + case pause + + /// Pause the animation and restart it when the application moves to the foreground. The completion block is stored and called when the animation completes. + case pauseAndRestore + + /// Stops the animation and sets it to the end of its current play time. The completion block is called. + case forceFinish + + /// The animation continues playing in the background. + /// - This is the default when using the Core Animation rendering engine. + /// Playing an animation using the Core Animation engine doesn't come with any CPU overhead, + /// so using `.continuePlaying` avoids the need to stop and then resume the animation + /// (which does come with some CPU overhead). + /// - This mode should not be used with the Main Thread rendering engine. + case continuePlaying + + // MARK: Public + + /// The default background behavior, based on the rendering engine being used to play the animation. + /// - Playing an animation using the Main Thread rendering engine comes with CPU overhead, + /// so the animation should be paused or stopped when the `AnimationView` is not visible. + /// - Playing an animation using the Core Animation rendering engine does not come with any + /// CPU overhead, so these animations do not need to be paused in the background. + public static func `default`(for renderingEngine: RenderingEngine) -> LottieBackgroundBehavior { + switch renderingEngine { + case .mainThread: + return .pause + case .coreAnimation: + return .continuePlaying + } + } +} + +// MARK: - LottieLoopMode + +/// Defines animation loop behavior +public enum LottieLoopMode { + /// Animation is played once then stops. + case playOnce + /// Animation will loop from beginning to end until stopped. + case loop + /// Animation will play forward, then backwards and loop until stopped. + case autoReverse + /// Animation will loop from beginning to end up to defined amount of times. + case `repeat`(Float) + /// Animation will play forward, then backwards a defined amount of times. + case repeatBackwards(Float) +} + +// MARK: Equatable + +extension LottieLoopMode: Equatable { + public static func == (lhs: LottieLoopMode, rhs: LottieLoopMode) -> Bool { + switch (lhs, rhs) { + case (.repeat(let lhsAmount), .repeat(let rhsAmount)), + (.repeatBackwards(let lhsAmount), .repeatBackwards(let rhsAmount)): + return lhsAmount == rhsAmount + case (.playOnce, .playOnce), + (.loop, .loop), + (.autoReverse, .autoReverse): + return true + default: + return false + } + } +} + +// MARK: - AnimationView + +@IBDesignable +final public class AnimationView: AnimationViewBase { + + // MARK: Lifecycle + + // MARK: - Public (Initializers) + + /// Initializes an AnimationView with an animation. + public init( + animation: Animation?, + imageProvider: AnimationImageProvider? = nil, + textProvider: AnimationTextProvider = DefaultTextProvider(), + fontProvider: AnimationFontProvider = DefaultFontProvider(), + configuration: LottieConfiguration = .shared) + { + self.animation = animation + self.imageProvider = imageProvider ?? BundleImageProvider(bundle: Bundle.main, searchPath: nil) + self.textProvider = textProvider + self.fontProvider = fontProvider + self.configuration = configuration + super.init(frame: .zero) + commonInit() + makeAnimationLayer(usingEngine: configuration.renderingEngine) + if let animation = animation { + frame = animation.bounds + } + } + + public init(configuration: LottieConfiguration = .shared) { + animation = nil + imageProvider = BundleImageProvider(bundle: Bundle.main, searchPath: nil) + textProvider = DefaultTextProvider() + fontProvider = DefaultFontProvider() + self.configuration = configuration + super.init(frame: .zero) + commonInit() + } + + public override init(frame: CGRect) { + animation = nil + imageProvider = BundleImageProvider(bundle: Bundle.main, searchPath: nil) + textProvider = DefaultTextProvider() + fontProvider = DefaultFontProvider() + configuration = .shared + super.init(frame: frame) + commonInit() + } + + required public init?(coder aDecoder: NSCoder) { + imageProvider = BundleImageProvider(bundle: Bundle.main, searchPath: nil) + textProvider = DefaultTextProvider() + fontProvider = DefaultFontProvider() + configuration = .shared + super.init(coder: aDecoder) + commonInit() + } + + // MARK: Public + + /// The configuration that this `AnimationView` uses when playing its animation + public let configuration: LottieConfiguration + + /// Value Providers that have been registered using `setValueProvider(_:keypath:)` + public private(set) var valueProviders = [AnimationKeypath: AnyValueProvider]() + + /// Describes the behavior of an AnimationView when the app is moved to the background. + /// + /// The default for the Main Thread animation engine is `pause`, + /// which pauses the animation when the application moves to + /// the background. This prevents the animation from consuming CPU + /// resources when not on-screen. The completion block is called with + /// `false` for completed. + /// + /// The default for the Core Animation engine is `continuePlaying`, + /// since the Core Animation engine does not have any CPU overhead. + public var backgroundBehavior: LottieBackgroundBehavior { + get { + let currentBackgroundBehavior = _backgroundBehavior ?? .default(for: currentRenderingEngine ?? .mainThread) + + if + currentRenderingEngine == .mainThread, + _backgroundBehavior == .continuePlaying + { + LottieLogger.shared.assertionFailure(""" + `LottieBackgroundBehavior.continuePlaying` should not be used with the Main Thread + rendering engine, since this would waste CPU resources on playing an animation + that is not visible. Consider using a different background mode, or switching to + the Core Animation rendering engine (which does not have any CPU overhead). + """) + } + + return currentBackgroundBehavior + } + set { + _backgroundBehavior = newValue + } + } + + /// Sets the animation backing the animation view. Setting this will clear the + /// view's contents, completion blocks and current state. The new animation will + /// be loaded up and set to the beginning of its timeline. + public var animation: Animation? { + didSet { + makeAnimationLayer(usingEngine: configuration.renderingEngine) + } + } + + /// Sets the image provider for the animation view. An image provider provides the + /// animation with its required image data. + /// + /// Setting this will cause the animation to reload its image contents. + public var imageProvider: AnimationImageProvider { + didSet { + animationLayer?.imageProvider = imageProvider.cachedImageProvider + reloadImages() + } + } + + /// Sets the text provider for animation view. A text provider provides the + /// animation with values for text layers + public var textProvider: AnimationTextProvider { + didSet { + animationLayer?.textProvider = textProvider + } + } + + /// Sets the text provider for animation view. A text provider provides the + /// animation with values for text layers + public var fontProvider: AnimationFontProvider { + didSet { + animationLayer?.fontProvider = fontProvider + } + } + + /// Returns `true` if the animation is currently playing. + public var isAnimationPlaying: Bool { + guard let animationLayer = animationLayer else { + return false + } + + if let valueFromLayer = animationLayer.isAnimationPlaying { + return valueFromLayer + } else { + return animationLayer.animation(forKey: activeAnimationName) != nil + } + } + + /// Returns `true` if the animation will start playing when this view is added to a window. + public var isAnimationQueued: Bool { + animationContext != nil && waitingToPlayAnimation + } + + /// Sets the loop behavior for `play` calls. Defaults to `playOnce` + public var loopMode: LottieLoopMode = .playOnce { + didSet { + updateInFlightAnimation() + } + } + + /// When `true` the animation view will rasterize its contents when not animating. + /// Rasterizing will improve performance of static animations. + /// + /// Note: this will not produce crisp results at resolutions above the animations natural resolution. + /// + /// Defaults to `false` + public var shouldRasterizeWhenIdle = false { + didSet { + updateRasterizationState() + } + } + + /// Sets the current animation time with a Progress Time + /// + /// Note: Setting this will stop the current animation, if any. + /// Note 2: If `animation` is nil, setting this will fallback to 0 + public var currentProgress: AnimationProgressTime { + set { + if let animation = animation { + currentFrame = animation.frameTime(forProgress: newValue) + } else { + currentFrame = 0 + } + } + get { + if let animation = animation { + return animation.progressTime(forFrame: currentFrame) + } else { + return 0 + } + } + } + + /// Sets the current animation time with a time in seconds. + /// + /// Note: Setting this will stop the current animation, if any. + /// Note 2: If `animation` is nil, setting this will fallback to 0 + public var currentTime: TimeInterval { + set { + if let animation = animation { + currentFrame = animation.frameTime(forTime: newValue) + } else { + currentFrame = 0 + } + } + get { + if let animation = animation { + return animation.time(forFrame: currentFrame) + } else { + return 0 + } + } + } + + /// Sets the current animation time with a frame in the animations framerate. + /// + /// Note: Setting this will stop the current animation, if any. + public var currentFrame: AnimationFrameTime { + set { + removeCurrentAnimationIfNecessary() + updateAnimationFrame(newValue) + } + get { + animationLayer?.currentFrame ?? 0 + } + } + + /// Returns the current animation frame while an animation is playing. + public var realtimeAnimationFrame: AnimationFrameTime { + isAnimationPlaying ? animationLayer?.presentation()?.currentFrame ?? currentFrame : currentFrame + } + + /// Returns the current animation frame while an animation is playing. + public var realtimeAnimationProgress: AnimationProgressTime { + if let animation = animation { + return animation.progressTime(forFrame: realtimeAnimationFrame) + } + return 0 + } + + /// Sets the speed of the animation playback. Defaults to 1 + public var animationSpeed: CGFloat = 1 { + didSet { + updateInFlightAnimation() + } + } + + /// When `true` the animation will play back at the framerate encoded in the + /// `Animation` model. When `false` the animation will play at the framerate + /// of the device. + /// + /// Defaults to false + public var respectAnimationFrameRate = false { + didSet { + animationLayer?.respectAnimationFrameRate = respectAnimationFrameRate + } + } + + /// Controls the cropping of an Animation. Setting this property will crop the animation + /// to the current views bounds by the viewport frame. The coordinate space is specified + /// in the animation's coordinate space. + /// + /// Animatable. + public var viewportFrame: CGRect? = nil { + didSet { + + // This is really ugly, but is needed to trigger a layout pass within an animation block. + // Typically this happens automatically, when layout objects are UIView based. + // The animation layer is a CALayer which will not implicitly grab the animation + // duration of a UIView animation block. + // + // By setting bounds and then resetting bounds the UIView animation block's + // duration and curve are captured and added to the layer. This is used in the + // layout block to animate the animationLayer's position and size. + let rect = bounds + self.bounds = CGRect.zero + self.bounds = rect + self.setNeedsLayout() + } + } + + override public var intrinsicContentSize: CGSize { + if let animation = animation { + return animation.bounds.size + } + return .zero + } + + /// The rendering engine currently being used by this view. + /// - This will only be `nil` in cases where the configuration is `automatic` + /// but a `RootAnimationLayer` hasn't been constructed yet + public var currentRenderingEngine: RenderingEngine? { + switch configuration.renderingEngine { + case .specific(let engine): + return engine + + case .automatic: + guard let animationLayer = animationLayer else { + return nil + } + + if animationLayer is CoreAnimationLayer { + return .coreAnimation + } else { + return .mainThread + } + } + } + + /// Plays the animation from its current state to the end. + /// + /// - Parameter completion: An optional completion closure to be called when the animation completes playing. + public func play(completion: LottieCompletionBlock? = nil) { + guard let animation = animation else { + return + } + + /// Build a context for the animation. + let context = AnimationContext( + playFrom: CGFloat(animation.startFrame), + playTo: CGFloat(animation.endFrame), + closure: completion) + removeCurrentAnimationIfNecessary() + addNewAnimationForContext(context) + } + + /// Plays the animation from a progress (0-1) to a progress (0-1). + /// + /// - Parameter fromProgress: The start progress of the animation. If `nil` the animation will start at the current progress. + /// - Parameter toProgress: The end progress of the animation. + /// - Parameter loopMode: The loop behavior of the animation. If `nil` the view's `loopMode` property will be used. + /// - Parameter completion: An optional completion closure to be called when the animation stops. + public func play( + fromProgress: AnimationProgressTime? = nil, + toProgress: AnimationProgressTime, + loopMode: LottieLoopMode? = nil, + completion: LottieCompletionBlock? = nil) + { + guard let animation = animation else { + return + } + + removeCurrentAnimationIfNecessary() + if let loopMode = loopMode { + /// Set the loop mode, if one was supplied + self.loopMode = loopMode + } + let context = AnimationContext( + playFrom: animation.frameTime(forProgress: fromProgress ?? currentProgress), + playTo: animation.frameTime(forProgress: toProgress), + closure: completion) + addNewAnimationForContext(context) + } + + /// Plays the animation from a start frame to an end frame in the animation's framerate. + /// + /// - Parameter fromFrame: The start frame of the animation. If `nil` the animation will start at the current frame. + /// - Parameter toFrame: The end frame of the animation. + /// - Parameter loopMode: The loop behavior of the animation. If `nil` the view's `loopMode` property will be used. + /// - Parameter completion: An optional completion closure to be called when the animation stops. + public func play( + fromFrame: AnimationFrameTime? = nil, + toFrame: AnimationFrameTime, + loopMode: LottieLoopMode? = nil, + completion: LottieCompletionBlock? = nil) + { + removeCurrentAnimationIfNecessary() + if let loopMode = loopMode { + /// Set the loop mode, if one was supplied + self.loopMode = loopMode + } + + let context = AnimationContext( + playFrom: fromFrame ?? currentProgress, + playTo: toFrame, + closure: completion) + addNewAnimationForContext(context) + } + + /// Plays the animation from a named marker to another marker. + /// + /// Markers are point in time that are encoded into the Animation data and assigned + /// a name. + /// + /// NOTE: If markers are not found the play command will exit. + /// + /// - Parameter fromMarker: The start marker for the animation playback. If `nil` the + /// animation will start at the current progress. + /// - Parameter toMarker: The end marker for the animation playback. + /// - Parameter loopMode: The loop behavior of the animation. If `nil` the view's `loopMode` property will be used. + /// - Parameter completion: An optional completion closure to be called when the animation stops. + public func play( + fromMarker: String? = nil, + toMarker: String, + loopMode: LottieLoopMode? = nil, + completion: LottieCompletionBlock? = nil) + { + + guard let animation = animation, let markers = animation.markerMap, let to = markers[toMarker] else { + return + } + + removeCurrentAnimationIfNecessary() + if let loopMode = loopMode { + /// Set the loop mode, if one was supplied + self.loopMode = loopMode + } + + let fromTime: CGFloat + if let fromName = fromMarker, let from = markers[fromName] { + fromTime = CGFloat(from.frameTime) + } else { + fromTime = currentFrame + } + + let context = AnimationContext( + playFrom: fromTime, + playTo: CGFloat(to.frameTime), + closure: completion) + addNewAnimationForContext(context) + } + + /// Stops the animation and resets the view to its start frame. + /// + /// The completion closure will be called with `false` + public func stop() { + removeCurrentAnimation() + currentFrame = 0 + } + + /// Pauses the animation in its current state. + /// + /// The completion closure will be called with `false` + public func pause() { + removeCurrentAnimation() + } + + /// Reloads the images supplied to the animation from the `imageProvider` + public func reloadImages() { + animationLayer?.reloadImages() + } + + /// Forces the AnimationView to redraw its contents. + public func forceDisplayUpdate() { + animationLayer?.forceDisplayUpdate() + } + + /// Sets a ValueProvider for the specified keypath. The value provider will be set + /// on all properties that match the keypath. + /// + /// Nearly all properties of a Lottie animation can be changed at runtime using a + /// combination of `Animation Keypaths` and `Value Providers`. + /// Setting a ValueProvider on a keypath will cause the animation to update its + /// contents and read the new Value Provider. + /// + /// A value provider provides a typed value on a frame by frame basis. + /// + /// - Parameter valueProvider: The new value provider for the properties. + /// - Parameter keypath: The keypath used to search for properties. + /// + /// Example: + /// ``` + /// /// A keypath that finds the color value for all `Fill 1` nodes. + /// let fillKeypath = AnimationKeypath(keypath: "**.Fill 1.Color") + /// /// A Color Value provider that returns a reddish color. + /// let redValueProvider = ColorValueProvider(Color(r: 1, g: 0.2, b: 0.3, a: 1)) + /// /// Set the provider on the animationView. + /// animationView.setValueProvider(redValueProvider, keypath: fillKeypath) + /// ``` + public func setValueProvider(_ valueProvider: AnyValueProvider, keypath: AnimationKeypath) { + guard let animationLayer = animationLayer else { return } + + valueProviders[keypath] = valueProvider + animationLayer.setValueProvider(valueProvider, keypath: keypath) + } + + /// Reads the value of a property specified by the Keypath. + /// Returns nil if no property is found. + /// + /// - Parameter for: The keypath used to search for the property. + /// - Parameter atFrame: The Frame Time of the value to query. If nil then the current frame is used. + public func getValue(for keypath: AnimationKeypath, atFrame: AnimationFrameTime?) -> Any? { + animationLayer?.getValue(for: keypath, atFrame: atFrame) + } + + /// Reads the original value of a property specified by the Keypath. + /// This will ignore any value providers and can be useful when implementing a value providers that makes change to the original value from the animation. + /// Returns nil if no property is found. + /// + /// - Parameter for: The keypath used to search for the property. + /// - Parameter atFrame: The Frame Time of the value to query. If nil then the current frame is used. + public func getOriginalValue(for keypath: AnimationKeypath, atFrame: AnimationFrameTime?) -> Any? { + animationLayer?.getOriginalValue(for: keypath, atFrame: atFrame) + } + + /// Logs all child keypaths. + public func logHierarchyKeypaths() { + animationLayer?.logHierarchyKeypaths() + } + + /// Searches for the nearest child layer to the first Keypath and adds the subview + /// to that layer. The subview will move and animate with the child layer. + /// Furthermore the subview will be in the child layers coordinate space. + /// + /// Note: if no layer is found for the keypath, then nothing happens. + /// + /// - Parameter subview: The subview to add to the found animation layer. + /// - Parameter keypath: The keypath used to find the animation layer. + /// + /// Example: + /// ``` + /// /// A keypath that finds `Layer 1` + /// let layerKeypath = AnimationKeypath(keypath: "Layer 1") + /// + /// /// Wrap the custom view in an `AnimationSubview` + /// let subview = AnimationSubview() + /// subview.addSubview(customView) + /// + /// /// Set the provider on the animationView. + /// animationView.addSubview(subview, forLayerAt: layerKeypath) + /// ``` + public func addSubview(_ subview: AnimationSubview, forLayerAt keypath: AnimationKeypath) { + guard let sublayer = animationLayer?.layer(for: keypath) else { + return + } + setNeedsLayout() + layoutIfNeeded() + forceDisplayUpdate() + addSubview(subview) + if let subViewLayer = subview.viewLayer { + sublayer.addSublayer(subViewLayer) + } + } + + /// Converts a CGRect from the AnimationView's coordinate space into the + /// coordinate space of the layer found at Keypath. + /// + /// If no layer is found, nil is returned + /// + /// - Parameter rect: The CGRect to convert. + /// - Parameter toLayerAt: The keypath used to find the layer. + public func convert(_ rect: CGRect, toLayerAt keypath: AnimationKeypath?) -> CGRect? { + guard let animationLayer = animationLayer else { return nil } + guard let keypath = keypath else { + return viewLayer?.convert(rect, to: animationLayer) + } + guard let sublayer = animationLayer.layer(for: keypath) else { + return nil + } + setNeedsLayout() + layoutIfNeeded() + forceDisplayUpdate() + return animationLayer.convert(rect, to: sublayer) + } + + /// Converts a CGPoint from the AnimationView's coordinate space into the + /// coordinate space of the layer found at Keypath. + /// + /// If no layer is found, nil is returned + /// + /// - Parameter point: The CGPoint to convert. + /// - Parameter toLayerAt: The keypath used to find the layer. + public func convert(_ point: CGPoint, toLayerAt keypath: AnimationKeypath?) -> CGPoint? { + guard let animationLayer = animationLayer else { return nil } + guard let keypath = keypath else { + return viewLayer?.convert(point, to: animationLayer) + } + guard let sublayer = animationLayer.layer(for: keypath) else { + return nil + } + setNeedsLayout() + layoutIfNeeded() + forceDisplayUpdate() + return animationLayer.convert(point, to: sublayer) + } + + /// Sets the enabled state of all animator nodes found with the keypath search. + /// This can be used to interactively enable / disable parts of the animation. + /// + /// - Parameter isEnabled: When true the animator nodes affect the rendering tree. When false the node is removed from the tree. + /// - Parameter keypath: The keypath used to find the node(s). + public func setNodeIsEnabled(isEnabled: Bool, keypath: AnimationKeypath) { + guard let animationLayer = animationLayer else { return } + let nodes = animationLayer.animatorNodes(for: keypath) + if let nodes = nodes { + for node in nodes { + node.isEnabled = isEnabled + } + forceDisplayUpdate() + } + } + + /// Markers are a way to describe a point in time by a key name. + /// + /// Markers are encoded into animation JSON. By using markers a designer can mark + /// playback points for a developer to use without having to worry about keeping + /// track of animation frames. If the animation file is updated, the developer + /// does not need to update playback code. + /// + /// Returns the Progress Time for the marker named. Returns nil if no marker found. + public func progressTime(forMarker named: String) -> AnimationProgressTime? { + guard let animation = animation else { + return nil + } + return animation.progressTime(forMarker: named) + } + + /// Markers are a way to describe a point in time by a key name. + /// + /// Markers are encoded into animation JSON. By using markers a designer can mark + /// playback points for a developer to use without having to worry about keeping + /// track of animation frames. If the animation file is updated, the developer + /// does not need to update playback code. + /// + /// Returns the Frame Time for the marker named. Returns nil if no marker found. + public func frameTime(forMarker named: String) -> AnimationFrameTime? { + guard let animation = animation else { + return nil + } + return animation.frameTime(forMarker: named) + } + + // MARK: Internal + + var animationLayer: RootAnimationLayer? = nil + + /// Set animation name from Interface Builder + @IBInspectable var animationName: String? { + didSet { + self.animation = animationName.flatMap { + Animation.named($0, animationCache: nil) + } + } + } + + override func layoutAnimation() { + guard let animation = animation, let animationLayer = animationLayer else { return } + var position = animation.bounds.center + let xform: CATransform3D + var shouldForceUpdates = false + + if let viewportFrame = viewportFrame { + shouldForceUpdates = contentMode == .redraw + + let compAspect = viewportFrame.size.width / viewportFrame.size.height + let viewAspect = bounds.size.width / bounds.size.height + let dominantDimension = compAspect > viewAspect ? bounds.size.width : bounds.size.height + let compDimension = compAspect > viewAspect ? viewportFrame.size.width : viewportFrame.size.height + let scale = dominantDimension / compDimension + + let viewportOffset = animation.bounds.center - viewportFrame.center + xform = CATransform3DTranslate(CATransform3DMakeScale(scale, scale, 1), viewportOffset.x, viewportOffset.y, 0) + position = bounds.center + } else { + switch contentMode { + case .scaleToFill: + position = bounds.center + xform = CATransform3DMakeScale( + bounds.size.width / animation.size.width, + bounds.size.height / animation.size.height, + 1); + case .scaleAspectFit: + position = bounds.center + let compAspect = animation.size.width / animation.size.height + let viewAspect = bounds.size.width / bounds.size.height + let dominantDimension = compAspect > viewAspect ? bounds.size.width : bounds.size.height + let compDimension = compAspect > viewAspect ? animation.size.width : animation.size.height + let scale = dominantDimension / compDimension + xform = CATransform3DMakeScale(scale, scale, 1) + case .scaleAspectFill: + position = bounds.center + let compAspect = animation.size.width / animation.size.height + let viewAspect = bounds.size.width / bounds.size.height + let scaleWidth = compAspect < viewAspect + let dominantDimension = scaleWidth ? bounds.size.width : bounds.size.height + let compDimension = scaleWidth ? animation.size.width : animation.size.height + let scale = dominantDimension / compDimension + xform = CATransform3DMakeScale(scale, scale, 1) + case .redraw: + shouldForceUpdates = true + xform = CATransform3DIdentity + case .center: + position = bounds.center + xform = CATransform3DIdentity + case .top: + position.x = bounds.center.x + xform = CATransform3DIdentity + case .bottom: + position.x = bounds.center.x + position.y = bounds.maxY - animation.bounds.midY + xform = CATransform3DIdentity + case .left: + position.y = bounds.center.y + xform = CATransform3DIdentity + case .right: + position.y = bounds.center.y + position.x = bounds.maxX - animation.bounds.midX + xform = CATransform3DIdentity + case .topLeft: + xform = CATransform3DIdentity + case .topRight: + position.x = bounds.maxX - animation.bounds.midX + xform = CATransform3DIdentity + case .bottomLeft: + position.y = bounds.maxY - animation.bounds.midY + xform = CATransform3DIdentity + case .bottomRight: + position.x = bounds.maxX - animation.bounds.midX + position.y = bounds.maxY - animation.bounds.midY + xform = CATransform3DIdentity + + #if os(iOS) || os(tvOS) + @unknown default: + LottieLogger.shared.assertionFailure("unsupported contentMode: \(contentMode.rawValue)") + xform = CATransform3DIdentity + #endif + } + } + + // UIView Animation does not implicitly set CAAnimation time or timing fuctions. + // If layout is changed in an animation we must get the current animation duration + // and timing function and then manually create a CAAnimation to match the UIView animation. + // If layout is changed without animation, explicitly set animation duration to 0.0 + // inside CATransaction to avoid unwanted artifacts. + /// Check if any animation exist on the view's layer, and match it. + if let key = viewLayer?.animationKeys()?.first, let animation = viewLayer?.animation(forKey: key) { + // The layout is happening within an animation block. Grab the animation data. + + let positionKey = "LayoutPositionAnimation" + let transformKey = "LayoutTransformAnimation" + animationLayer.removeAnimation(forKey: positionKey) + animationLayer.removeAnimation(forKey: transformKey) + + let positionAnimation = animation.copy() as? CABasicAnimation ?? CABasicAnimation(keyPath: "position") + positionAnimation.keyPath = "position" + positionAnimation.isAdditive = false + positionAnimation.fromValue = (animationLayer.presentation() ?? animationLayer).position + positionAnimation.toValue = position + positionAnimation.isRemovedOnCompletion = true + + let xformAnimation = animation.copy() as? CABasicAnimation ?? CABasicAnimation(keyPath: "transform") + xformAnimation.keyPath = "transform" + xformAnimation.isAdditive = false + xformAnimation.fromValue = (animationLayer.presentation() ?? animationLayer).transform + xformAnimation.toValue = xform + xformAnimation.isRemovedOnCompletion = true + + animationLayer.position = position + animationLayer.transform = xform + #if os(OSX) + animationLayer.anchorPoint = layer?.anchorPoint ?? CGPoint.zero + #else + animationLayer.anchorPoint = layer.anchorPoint + #endif + animationLayer.add(positionAnimation, forKey: positionKey) + animationLayer.add(xformAnimation, forKey: transformKey) + } else { + // In performance tests, we have to wrap the animation view setup + // in a `CATransaction` in order for the layers to be deallocated at + // the correct time. The `CATransaction`s in this method interfere + // with the ones managed by the performance test, and aren't actually + // necessary in a headless environment, so we disable them. + if TestHelpers.performanceTestsAreRunning { + animationLayer.position = position + animationLayer.transform = xform + } else { + CATransaction.begin() + CATransaction.setAnimationDuration(0.0) + CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .linear)) + animationLayer.position = position + animationLayer.transform = xform + CATransaction.commit() + } + } + + if shouldForceUpdates { + animationLayer.forceDisplayUpdate() + } + } + + func updateRasterizationState() { + if isAnimationPlaying { + animationLayer?.shouldRasterize = false + } else { + animationLayer?.shouldRasterize = shouldRasterizeWhenIdle + } + } + + /// Updates the animation frame. Does not affect any current animations + func updateAnimationFrame(_ newFrame: CGFloat) { + // In performance tests, we have to wrap the animation view setup + // in a `CATransaction` in order for the layers to be deallocated at + // the correct time. The `CATransaction`s in this method interfere + // with the ones managed by the performance test, and aren't actually + // necessary in a headless environment, so we disable them. + if TestHelpers.performanceTestsAreRunning { + animationLayer?.currentFrame = newFrame + animationLayer?.forceDisplayUpdate() + return + } + + CATransaction.begin() + CATransaction.setCompletionBlock { + self.animationLayer?.forceDisplayUpdate() + } + CATransaction.setDisableActions(true) + animationLayer?.currentFrame = newFrame + CATransaction.commit() + } + + @objc + override func animationWillMoveToBackground() { + updateAnimationForBackgroundState() + } + + @objc + override func animationWillEnterForeground() { + updateAnimationForForegroundState() + } + + override func animationMovedToWindow() { + /// Don't update any state if the `superview` is `nil` + /// When A viewA owns superViewB, it removes the superViewB from the window. At this point, viewA still owns superViewB and triggers the viewA method: -didmovetowindow + guard superview != nil else { return } + + if window != nil { + updateAnimationForForegroundState() + } else { + updateAnimationForBackgroundState() + } + } + + // MARK: Fileprivate + + fileprivate var animationContext: AnimationContext? + fileprivate var _activeAnimationName: String = AnimationView.animationName + fileprivate var animationID = 0 + + fileprivate var waitingToPlayAnimation = false + + fileprivate var activeAnimationName: String { + switch animationLayer?.primaryAnimationKey { + case .specific(let animationKey): + return animationKey + case .managed, nil: + return _activeAnimationName + } + } + + fileprivate func makeAnimationLayer(usingEngine renderingEngine: RenderingEngineOption) { + + /// Remove current animation if any + removeCurrentAnimation() + + if let oldAnimation = animationLayer { + oldAnimation.removeFromSuperlayer() + } + + invalidateIntrinsicContentSize() + + guard let animation = animation else { + return + } + + let rootAnimationLayer: RootAnimationLayer? + switch renderingEngine { + case .automatic: + rootAnimationLayer = makeAutomaticEngineLayer(for: animation) + case .specific(.coreAnimation): + rootAnimationLayer = makeCoreAnimationLayer(for: animation) + case .specific(.mainThread): + rootAnimationLayer = makeMainThreadAnimationLayer(for: animation) + } + + guard let animationLayer = rootAnimationLayer else { + return + } + + animationLayer.renderScale = screenScale + + viewLayer?.addSublayer(animationLayer) + self.animationLayer = animationLayer + reloadImages() + animationLayer.setNeedsDisplay() + setNeedsLayout() + currentFrame = CGFloat(animation.startFrame) + } + + fileprivate func makeMainThreadAnimationLayer(for animation: Animation) -> MainThreadAnimationLayer { + MainThreadAnimationLayer( + animation: animation, + imageProvider: imageProvider.cachedImageProvider, + textProvider: textProvider, + fontProvider: fontProvider) + } + + fileprivate func makeCoreAnimationLayer(for animation: Animation) -> CoreAnimationLayer? { + do { + let coreAnimationLayer = try CoreAnimationLayer( + animation: animation, + imageProvider: imageProvider.cachedImageProvider, + fontProvider: fontProvider, + compatibilityTrackerMode: .track) + + coreAnimationLayer.didSetUpAnimation = { compatibilityIssues in + LottieLogger.shared.assert( + compatibilityIssues.isEmpty, + "Encountered Core Animation compatibility issues while setting up animation:\n" + + compatibilityIssues.map { $0.description }.joined(separator: "\n") + "\n\n" + + """ + This animation cannot be rendered correctly by the Core Animation engine. + To resolve this issue, you can use `RenderingEngineOption.automatic`, which automatically falls back + to the Main Thread rendering engine when necessary, or just use `RenderingEngineOption.mainThread`. + + """) + } + + return coreAnimationLayer + } catch { + // This should never happen, because we initialize the `CoreAnimationLayer` with + // `CompatibilityTracker.Mode.track` (which reports errors in `didSetUpAnimation`, + // not by throwing). + LottieLogger.shared.assertionFailure("Encountered unexpected error \(error)") + return nil + } + } + + fileprivate func makeAutomaticEngineLayer(for animation: Animation) -> CoreAnimationLayer? { + do { + // Attempt to set up the Core Animation layer. This can either throw immediately in `init`, + // or throw an error later in `CALayer.display()` that will be reported in `didSetUpAnimation`. + let coreAnimationLayer = try CoreAnimationLayer( + animation: animation, + imageProvider: imageProvider.cachedImageProvider, + fontProvider: fontProvider, + compatibilityTrackerMode: .abort) + + coreAnimationLayer.didSetUpAnimation = { [weak self] issues in + self?.automaticEngineLayerDidSetUpAnimation(issues) + } + + return coreAnimationLayer + } catch { + if case CompatibilityTracker.Error.encounteredCompatibilityIssue(let compatibilityIssue) = error { + automaticEngineLayerDidSetUpAnimation([compatibilityIssue]) + } else { + // This should never happen, because we expect `CoreAnimationLayer` to only throw + // `CompatibilityTracker.Error.encounteredCompatibilityIssue` errors. + LottieLogger.shared.assertionFailure("Encountered unexpected error \(error)") + automaticEngineLayerDidSetUpAnimation([]) + } + + return nil + } + } + + // Handles any compatibility issues with the Core Animation engine + // by falling back to the Main Thread engine + fileprivate func automaticEngineLayerDidSetUpAnimation(_ compatibilityIssues: [CompatibilityIssue]) { + // If there weren't any compatibility issues, then there's nothing else to do + if compatibilityIssues.isEmpty { + return + } + + LottieLogger.shared.warn( + "Encountered Core Animation compatibility issue while setting up animation:\n" + + compatibilityIssues.map { $0.description }.joined(separator: "\n") + "\n" + + """ + This animation may have additional compatibility issues, but animation setup was cancelled early to avoid wasted work. + + Automatically falling back to Main Thread rendering engine. This fallback comes with some additional performance + overhead, which can be reduced by manually specifying that this animation should always use the Main Thread engine. + + """) + + let animationContext = self.animationContext + let currentFrame = self.currentFrame + + makeAnimationLayer(usingEngine: .mainThread) + + // Set up the Main Thread animation layer using the same configuration that + // was being used by the previous Core Animation layer + self.currentFrame = currentFrame + + if let animationContext = animationContext { + // `AnimationContext.closure` (`AnimationCompletionDelegate`) is a reference type + // that is the animation layer's `CAAnimationDelegate`, and holds a reference to + // the animation layer. Reusing a single instance across different animation layers + // can cause the animation setup to fail, so we create a copy of the `animationContext`: + addNewAnimationForContext(AnimationContext( + playFrom: animationContext.playFrom, + playTo: animationContext.playTo, + closure: animationContext.closure.completionBlock)) + } + } + + fileprivate func updateAnimationForBackgroundState() { + if let currentContext = animationContext { + switch backgroundBehavior { + case .stop: + removeCurrentAnimation() + updateAnimationFrame(currentContext.playFrom) + case .pause: + removeCurrentAnimation() + case .pauseAndRestore: + currentContext.closure.ignoreDelegate = true + removeCurrentAnimation() + /// Keep the stale context around for when the app enters the foreground. + animationContext = currentContext + case .forceFinish: + removeCurrentAnimation() + updateAnimationFrame(currentContext.playTo) + case .continuePlaying: + break + } + } + } + + fileprivate func updateAnimationForForegroundState() { + if let currentContext = animationContext { + if waitingToPlayAnimation { + waitingToPlayAnimation = false + addNewAnimationForContext(currentContext) + } else if backgroundBehavior == .pauseAndRestore { + /// Restore animation from saved state + updateInFlightAnimation() + } + } + } + + /// Removes the current animation and pauses the animation at the current frame + /// if necessary before setting up a new animation. + /// - This is not necessary with the Core Animation engine, and skipping + /// this step lets us avoid building the animations twice (once paused + /// and once again playing) + fileprivate func removeCurrentAnimationIfNecessary() { + switch currentRenderingEngine { + case .mainThread: + removeCurrentAnimation() + case .coreAnimation, nil: + break + } + } + + /// Stops the current in flight animation and freezes the animation in its current state. + fileprivate func removeCurrentAnimation() { + guard animationContext != nil else { return } + let pauseFrame = realtimeAnimationFrame + animationLayer?.removeAnimation(forKey: activeAnimationName) + updateAnimationFrame(pauseFrame) + animationContext = nil + } + + /// Updates an in flight animation. + fileprivate func updateInFlightAnimation() { + guard let animationContext = animationContext else { return } + + guard animationContext.closure.animationState != .complete else { + // Tried to re-add an already completed animation. Cancel. + self.animationContext = nil + return + } + + /// Tell existing context to ignore its closure + animationContext.closure.ignoreDelegate = true + + /// Make a new context, stealing the completion block from the previous. + let newContext = AnimationContext( + playFrom: animationContext.playFrom, + playTo: animationContext.playTo, + closure: animationContext.closure.completionBlock) + + /// Remove current animation, and freeze the current frame. + let pauseFrame = realtimeAnimationFrame + animationLayer?.removeAnimation(forKey: activeAnimationName) + animationLayer?.currentFrame = pauseFrame + + addNewAnimationForContext(newContext) + } + + /// Adds animation to animation layer and sets the delegate. If animation layer or animation are nil, exits. + fileprivate func addNewAnimationForContext(_ animationContext: AnimationContext) { + guard let animationlayer = animationLayer, let animation = animation else { + return + } + + self.animationContext = animationContext + + switch currentRenderingEngine { + case .mainThread: + guard window != nil else { + waitingToPlayAnimation = true + return + } + + case .coreAnimation, nil: + // The Core Animation engine automatically batches animation setup to happen + // in `CALayer.display()`, which won't be called until the layer is on-screen, + // so we don't need to defer animation setup at this layer. + break + } + + animationID = animationID + 1 + _activeAnimationName = AnimationView.animationName + String(animationID) + + if let coreAnimationLayer = animationlayer as? CoreAnimationLayer { + var animationContext = animationContext + var timingConfiguration = CoreAnimationLayer.CAMediaTimingConfiguration( + autoreverses: loopMode.caAnimationConfiguration.autoreverses, + repeatCount: loopMode.caAnimationConfiguration.repeatCount, + speed: Float(animationSpeed)) + + // The animation should start playing from the `currentFrame`, + // if `currentFrame` is included in the time range being played. + let lowerBoundTime = min(animationContext.playFrom, animationContext.playTo) + let upperBoundTime = max(animationContext.playFrom, animationContext.playTo) + if (lowerBoundTime ..< upperBoundTime).contains(round(currentFrame)) { + // We have to configure this differently depending on the loop mode: + switch loopMode { + // When playing exactly once (and not looping), we can just set the + // `playFrom` time to be the `currentFrame`. Since the animation duration + // is based on `playFrom` and `playTo`, this automatically truncates the + // duration (so the animation stops playing at `playFrom`). + case .playOnce: + animationContext.playFrom = currentFrame + + // When looping, we specifically _don't_ want to affect the duration of the animation, + // since that would affect the duration of all subsequent loops. We just want to adjust + // the duration of the _first_ loop. Instead of setting `playFrom`, we just add a `timeOffset` + // so the first loop begins at `currentTime` but all subsequent loops are the standard duration. + default: + timingConfiguration.timeOffset = currentTime - animation.time(forFrame: animationContext.playFrom) + } + } + + coreAnimationLayer.playAnimation( + context: animationContext, + timingConfiguration: timingConfiguration) + + return + } + + /// At this point there is no animation on animationLayer and its state is set. + + let framerate = animation.framerate + + let playFrom = animationContext.playFrom.clamp(animation.startFrame, animation.endFrame) + let playTo = animationContext.playTo.clamp(animation.startFrame, animation.endFrame) + + let duration = ((max(playFrom, playTo) - min(playFrom, playTo)) / CGFloat(framerate)) + + let playingForward: Bool = + ( + (animationSpeed > 0 && playFrom < playTo) || + (animationSpeed < 0 && playTo < playFrom)) + + var startFrame = currentFrame.clamp(min(playFrom, playTo), max(playFrom, playTo)) + if startFrame == playTo { + startFrame = playFrom + } + + let timeOffset: TimeInterval = playingForward + ? Double(startFrame - min(playFrom, playTo)) / framerate + : Double(max(playFrom, playTo) - startFrame) / framerate + + let layerAnimation = CABasicAnimation(keyPath: "currentFrame") + layerAnimation.fromValue = playFrom + layerAnimation.toValue = playTo + layerAnimation.speed = Float(animationSpeed) + layerAnimation.duration = TimeInterval(duration) + layerAnimation.fillMode = CAMediaTimingFillMode.both + layerAnimation.repeatCount = loopMode.caAnimationConfiguration.repeatCount + layerAnimation.autoreverses = loopMode.caAnimationConfiguration.autoreverses + + layerAnimation.isRemovedOnCompletion = false + if timeOffset != 0 { + let currentLayerTime = viewLayer?.convertTime(CACurrentMediaTime(), from: nil) ?? 0 + layerAnimation.beginTime = currentLayerTime - (timeOffset * 1 / Double(abs(animationSpeed))) + } + layerAnimation.delegate = animationContext.closure + animationContext.closure.animationLayer = animationlayer + animationContext.closure.animationKey = activeAnimationName + + animationlayer.add(layerAnimation, forKey: activeAnimationName) + updateRasterizationState() + } + + // MARK: Private + + static private let animationName = "Lottie" + + /// The `LottieBackgroundBehavior` that was specified manually by setting `self.backgroundBehavior` + private var _backgroundBehavior: LottieBackgroundBehavior? + +} + +// MARK: - LottieLoopMode + caAnimationConfiguration + +extension LottieLoopMode { + /// The `CAAnimation` configuration that reflects this mode + var caAnimationConfiguration: (repeatCount: Float, autoreverses: Bool) { + switch self { + case .playOnce: + return (repeatCount: 1, autoreverses: false) + case .loop: + return (repeatCount: .greatestFiniteMagnitude, autoreverses: false) + case .autoReverse: + return (repeatCount: .greatestFiniteMagnitude, autoreverses: true) + case .repeat(let amount): + return (repeatCount: amount, autoreverses: false) + case .repeatBackwards(let amount): + return (repeatCount: amount, autoreverses: true) + } + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Animation/AnimationViewInitializers.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Animation/AnimationViewInitializers.swift new file mode 100644 index 0000000000..848d8e8bb3 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Animation/AnimationViewInitializers.swift @@ -0,0 +1,107 @@ +// +// AnimationViewInitializers.swift +// lottie-swift-iOS +// +// Created by Brandon Withrow on 2/6/19. +// + +import Foundation + +extension AnimationView { + + // MARK: Lifecycle + + /// Loads a Lottie animation from a JSON file in the supplied bundle. + /// + /// - Parameter name: The string name of the lottie animation with no file + /// extension provided. + /// - Parameter bundle: The bundle in which the animation is located. + /// Defaults to the Main bundle. + /// - Parameter imageProvider: An image provider for the animation's image data. + /// If none is supplied Lottie will search in the supplied bundle for images. + public convenience init( + name: String, + bundle: Bundle = Bundle.main, + imageProvider: AnimationImageProvider? = nil, + animationCache: AnimationCacheProvider? = LRUAnimationCache.sharedCache) + { + let animation = Animation.named(name, bundle: bundle, subdirectory: nil, animationCache: animationCache) + let provider = imageProvider ?? BundleImageProvider(bundle: bundle, searchPath: nil) + self.init(animation: animation, imageProvider: provider) + } + + /// Loads a Lottie animation from a JSON file in a specific path on disk. + /// + /// - Parameter name: The absolute path of the Lottie Animation. + /// - Parameter imageProvider: An image provider for the animation's image data. + /// If none is supplied Lottie will search in the supplied filepath for images. + public convenience init( + filePath: String, + imageProvider: AnimationImageProvider? = nil, + animationCache: AnimationCacheProvider? = LRUAnimationCache.sharedCache) + { + let animation = Animation.filepath(filePath, animationCache: animationCache) + let provider = imageProvider ?? + FilepathImageProvider(filepath: URL(fileURLWithPath: filePath).deletingLastPathComponent().path) + self.init(animation: animation, imageProvider: provider) + } + + /// Loads a Lottie animation asynchronously from the URL + /// + /// - Parameter url: The url to load the animation from. + /// - Parameter imageProvider: An image provider for the animation's image data. + /// If none is supplied Lottie will search in the main bundle for images. + /// - Parameter closure: A closure to be called when the animation has loaded. + public convenience init( + url: URL, + imageProvider: AnimationImageProvider? = nil, + closure: @escaping AnimationView.DownloadClosure, + animationCache: AnimationCacheProvider? = LRUAnimationCache.sharedCache) + { + + if let animationCache = animationCache, let animation = animationCache.animation(forKey: url.absoluteString) { + self.init(animation: animation, imageProvider: imageProvider) + closure(nil) + } else { + + self.init(animation: nil, imageProvider: imageProvider) + + Animation.loadedFrom(url: url, closure: { animation in + if let animation = animation { + self.animation = animation + closure(nil) + } else { + closure(LottieDownloadError.downloadFailed) + } + }, animationCache: animationCache) + } + } + + /// Loads a Lottie animation from a JSON file located in the Asset catalog of the supplied bundle. + /// - Parameter name: The string name of the lottie animation in the asset catalog. + /// - Parameter bundle: The bundle in which the animation is located. + /// Defaults to the Main bundle. + /// - Parameter imageProvider: An image provider for the animation's image data. + /// If none is supplied Lottie will search in the supplied bundle for images. + public convenience init( + asset name: String, + bundle: Bundle = Bundle.main, + imageProvider: AnimationImageProvider? = nil, + animationCache: AnimationCacheProvider? = LRUAnimationCache.sharedCache) + { + let animation = Animation.asset(name, bundle: bundle, animationCache: animationCache) + let provider = imageProvider ?? BundleImageProvider(bundle: bundle, searchPath: nil) + self.init(animation: animation, imageProvider: provider) + } + + // MARK: Public + + public typealias DownloadClosure = (Error?) -> Void + +} + +// MARK: - LottieDownloadError + +enum LottieDownloadError: Error { + case downloadFailed +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/AnimationCache/AnimationCacheProvider.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/AnimationCache/AnimationCacheProvider.swift new file mode 100644 index 0000000000..bdf189dec4 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/AnimationCache/AnimationCacheProvider.swift @@ -0,0 +1,22 @@ +// +// AnimationCacheProvider.swift +// lottie-swift +// +// Created by Brandon Withrow on 2/5/19. +// + +import Foundation +/// `AnimationCacheProvider` is a protocol that describes an Animation Cache. +/// Animation Cache is used when loading `Animation` models. Using an Animation Cache +/// can increase performance when loading an animation multiple times. +/// +/// Lottie comes with a prebuilt LRU Animation Cache. +public protocol AnimationCacheProvider { + + func animation(forKey: String) -> Animation? + + func setAnimation(_ animation: Animation, forKey: String) + + func clearCache() + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/AnimationCache/LRUAnimationCache.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/AnimationCache/LRUAnimationCache.swift new file mode 100644 index 0000000000..d48ce8ff5d --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/AnimationCache/LRUAnimationCache.swift @@ -0,0 +1,61 @@ +// +// LRUAnimationCache.swift +// lottie-swift +// +// Created by Brandon Withrow on 2/5/19. +// + +import Foundation + +/// An Animation Cache that will store animations up to `cacheSize`. +/// +/// Once `cacheSize` is reached, the least recently used animation will be ejected. +/// The default size of the cache is 100. +public class LRUAnimationCache: AnimationCacheProvider { + + // MARK: Lifecycle + + public init() { } + + // MARK: Public + + /// The global shared Cache. + public static let sharedCache = LRUAnimationCache() + + /// The size of the cache. + public var cacheSize = 100 + + /// Clears the Cache. + public func clearCache() { + cacheMap.removeAll() + lruList.removeAll() + } + + public func animation(forKey: String) -> Animation? { + guard let animation = cacheMap[forKey] else { + return nil + } + if let index = lruList.firstIndex(of: forKey) { + lruList.remove(at: index) + lruList.append(forKey) + } + return animation + } + + public func setAnimation(_ animation: Animation, forKey: String) { + cacheMap[forKey] = animation + lruList.append(forKey) + if lruList.count > cacheSize { + let removed = lruList.remove(at: 0) + if removed != forKey { + cacheMap[removed] = nil + } + } + } + + // MARK: Fileprivate + + fileprivate var cacheMap: [String: Animation] = [:] + fileprivate var lruList: [String] = [] + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/AnimationKeypath.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/AnimationKeypath.cpp new file mode 100644 index 0000000000..8f90bfc95e --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/AnimationKeypath.cpp @@ -0,0 +1,5 @@ +#include "AnimationKeypath.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/AnimationKeypath.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/AnimationKeypath.hpp new file mode 100644 index 0000000000..8679106d12 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/AnimationKeypath.hpp @@ -0,0 +1,51 @@ +#ifndef AnimationKeypath_hpp +#define AnimationKeypath_hpp + +#include +#include + +namespace lottie { + +/// `AnimationKeypath` is an object that describes a keypath search for nodes in the +/// animation JSON. `AnimationKeypath` matches views and properties inside of `AnimationView` +/// to their backing `Animation` model by name. +/// +/// A keypath can be used to set properties on an existing animation, or can be validated +/// with an existing `Animation`. +/// +/// `AnimationKeypath` can describe a specific object, or can use wildcards for fuzzy matching +/// of objects. Acceptable wildcards are either "*" (star) or "**" (double star). +/// Single star will search a single depth for the next object. +/// Double star will search any depth. +/// +/// Read More at https://airbnb.io/lottie/#/ios?id=dynamic-animation-properties +/// +/// EG: +/// @"Layer.Shape Group.Stroke 1.Color" +/// Represents a specific color node on a specific stroke. +/// +/// @"**.Stroke 1.Color" +/// Represents the color node for every Stroke named "Stroke 1" in the animation. +class AnimationKeypath { +public: + /// Creates a keypath from a dot-separated string. The string is separated by "." + /*public init(keypath: String) { + keys = keypath.components(separatedBy: ".") + }*/ + + /// Creates a keypath from a list of strings. + AnimationKeypath(std::vector const &keys) : + _keys(keys) { + } + + std::vector const &keys() const { + return _keys; + } + +private: + std::vector _keys; +}; + +} + +#endif /* AnimationKeypath_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/AnimationKeypath.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/AnimationKeypath.swift new file mode 100644 index 0000000000..f1b6748331 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/AnimationKeypath.swift @@ -0,0 +1,49 @@ +// +// AnimationKeypath.swift +// lottie-swift +// +// Created by Brandon Withrow on 2/4/19. +// + +import Foundation + +/// `AnimationKeypath` is an object that describes a keypath search for nodes in the +/// animation JSON. `AnimationKeypath` matches views and properties inside of `AnimationView` +/// to their backing `Animation` model by name. +/// +/// A keypath can be used to set properties on an existing animation, or can be validated +/// with an existing `Animation`. +/// +/// `AnimationKeypath` can describe a specific object, or can use wildcards for fuzzy matching +/// of objects. Acceptable wildcards are either "*" (star) or "**" (double star). +/// Single star will search a single depth for the next object. +/// Double star will search any depth. +/// +/// Read More at https://airbnb.io/lottie/#/ios?id=dynamic-animation-properties +/// +/// EG: +/// @"Layer.Shape Group.Stroke 1.Color" +/// Represents a specific color node on a specific stroke. +/// +/// @"**.Stroke 1.Color" +/// Represents the color node for every Stroke named "Stroke 1" in the animation. +public struct AnimationKeypath: Hashable, ExpressibleByStringLiteral { + + /// Creates a keypath from a dot-separated string. The string is separated by "." + public init(keypath: String) { + keys = keypath.components(separatedBy: ".") + } + + /// Creates a keypath from a dot-separated string + public init(stringLiteral: String) { + self.init(keypath: stringLiteral) + } + + /// Creates a keypath from a list of strings. + public init(keys: [String]) { + self.keys = keys + } + + var keys: [String] + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/AnyValueProvider.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/AnyValueProvider.cpp new file mode 100644 index 0000000000..f56124c957 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/AnyValueProvider.cpp @@ -0,0 +1,5 @@ +#include "AnyValueProvider.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/AnyValueProvider.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/AnyValueProvider.hpp new file mode 100644 index 0000000000..a9b708afbd --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/AnyValueProvider.hpp @@ -0,0 +1,38 @@ +#ifndef AnyValueProvider_hpp +#define AnyValueProvider_hpp + +#include "Lottie/Public/Primitives/AnimationTime.hpp" +#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" +#include "Lottie/Public/Primitives/AnyValue.hpp" + +#include +#include + +namespace lottie { + +/// `AnyValueProvider` is a protocol that return animation data for a property at a +/// given time. Every frame an `AnimationView` queries all of its properties and asks +/// if their ValueProvider has an update. If it does the AnimationView will read the +/// property and update that portion of the animation. +/// +/// Value Providers can be used to dynamically set animation properties at run time. +class AnyValueProvider { +public: + /// The Type of the value provider + virtual AnyValue::Type valueType() const = 0; + + /// Asks the provider if it has an update for the given frame. + virtual bool hasUpdate(AnimationFrameTime frame) const = 0; +}; + +/// A base protocol for strongly-typed Value Providers +template +class ValueProvider: public AnyValueProvider { +public: + /// Asks the provider to update the container with its value for the frame. + virtual T value(AnimationFrameTime frame) = 0; +}; + +} + +#endif /* AnyValueProvider_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/AnyValueProvider.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/AnyValueProvider.swift new file mode 100644 index 0000000000..deded9368e --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/AnyValueProvider.swift @@ -0,0 +1,132 @@ +// +// AnyValueProvider.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/30/19. +// + +import CoreGraphics +import Foundation + +// MARK: - AnyValueProvider + +/// `AnyValueProvider` is a protocol that return animation data for a property at a +/// given time. Every frame an `AnimationView` queries all of its properties and asks +/// if their ValueProvider has an update. If it does the AnimationView will read the +/// property and update that portion of the animation. +/// +/// Value Providers can be used to dynamically set animation properties at run time. +public protocol AnyValueProvider { + + /// The Type of the value provider + var valueType: Any.Type { get } + + /// The type-erased storage for this Value Provider + var typeErasedStorage: AnyValueProviderStorage { get } + + /// Asks the provider if it has an update for the given frame. + func hasUpdate(frame: AnimationFrameTime) -> Bool + +} + +extension AnyValueProvider { + /// Asks the provider to update the container with its value for the frame. + public func value(frame: AnimationFrameTime) -> Any { + typeErasedStorage.value(frame: frame) + } +} + +// MARK: - ValueProvider + +/// A base protocol for strongly-typed Value Providers +protocol ValueProvider: AnyValueProvider { + associatedtype Value: AnyInterpolatable + + /// The strongly-typed storage for this Value Provider + var storage: ValueProviderStorage { get } +} + +extension ValueProvider { + public var typeErasedStorage: AnyValueProviderStorage { + switch storage { + case .closure(let typedClosure): + return .closure(typedClosure) + + case .singleValue(let typedValue): + return .singleValue(typedValue) + + case .keyframes(let keyframes): + return .keyframes( + keyframes.map { keyframe in + keyframe.withValue(keyframe.value as Any) + }, + interpolate: storage.value(frame:)) + } + } +} + +// MARK: - ValueProviderStorage + +/// The underlying storage of a `ValueProvider` +public enum ValueProviderStorage { + /// The value provider stores a single value that is used on all frames + case singleValue(T) + + /// The value provider stores a group of keyframes + /// - The main-thread rendering engine interpolates values in these keyframes + /// using `T`'s `Interpolatable` implementation. + /// - The Core Animation rendering engine constructs a `CAKeyframeAnimation` + /// using these keyframes. The Core Animation render server performs + /// the interpolation, without calling `T`'s `Interpolatable` implementation. + case keyframes([Keyframe]) + + /// The value provider stores a closure that is invoked on every frame + /// - This is only supported by the main-thread rendering engine + case closure((AnimationFrameTime) -> T) + + // MARK: Internal + + func value(frame: AnimationFrameTime) -> T { + switch self { + case .singleValue(let value): + return value + + case .closure(let closure): + return closure(frame) + + case .keyframes(let keyframes): + return KeyframeInterpolator(keyframes: ContiguousArray(keyframes)).storage.value(frame: frame) + } + } +} + +// MARK: - AnyValueProviderStorage + +/// A type-erased representation of `ValueProviderStorage` +public enum AnyValueProviderStorage { + /// The value provider stores a single value that is used on all frames + case singleValue(Any) + + /// The value provider stores a group of keyframes + /// - Since we can't interpolate a type-erased `KeyframeGroup`, + /// the interpolation has to be performed in the `interpolate` closure. + case keyframes([Keyframe], interpolate: (AnimationFrameTime) -> Any) + + /// The value provider stores a closure that is invoked on every frame + case closure((AnimationFrameTime) -> Any) + + // MARK: Internal + + func value(frame: AnimationFrameTime) -> Any { + switch self { + case .singleValue(let value): + return value + + case .closure(let closure): + return closure(frame) + + case .keyframes(_, let valueForFrame): + return valueForFrame(frame) + } + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/ValueProviders/ColorValueProvider.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/ValueProviders/ColorValueProvider.swift new file mode 100644 index 0000000000..a991b1966a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/ValueProviders/ColorValueProvider.swift @@ -0,0 +1,84 @@ +// +// ColorValueProvider.swift +// lottie-swift +// +// Created by Brandon Withrow on 2/4/19. +// + +import CoreGraphics +import Foundation + +/// A `ValueProvider` that returns a CGColor Value +public final class ColorValueProvider: ValueProvider { + + // MARK: Lifecycle + + /// Initializes with a block provider + public init(block: @escaping ColorValueBlock) { + self.block = block + color = Color(r: 0, g: 0, b: 0, a: 1) + keyframes = nil + } + + /// Initializes with a single color. + public init(_ color: Color) { + self.color = color + block = nil + keyframes = nil + hasUpdate = true + } + + /// Initializes with multiple colors, with timing information + public init(_ keyframes: [Keyframe]) { + self.keyframes = keyframes + color = Color(r: 0, g: 0, b: 0, a: 1) + block = nil + hasUpdate = true + } + + // MARK: Public + + /// Returns a Color for a CGColor(Frame Time) + public typealias ColorValueBlock = (CGFloat) -> Color + + /// The color value of the provider. + public var color: Color { + didSet { + hasUpdate = true + } + } + + // MARK: ValueProvider Protocol + + public var valueType: Any.Type { + Color.self + } + + public var storage: ValueProviderStorage { + if let block = block { + return .closure { frame in + self.hasUpdate = false + return block(frame) + } + } else if let keyframes = keyframes { + return .keyframes(keyframes) + } else { + hasUpdate = false + return .singleValue(color) + } + } + + public func hasUpdate(frame _: CGFloat) -> Bool { + if block != nil { + return true + } + return hasUpdate + } + + // MARK: Private + + private var hasUpdate = true + + private var block: ColorValueBlock? + private var keyframes: [Keyframe]? +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/ValueProviders/FloatValueProvider.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/ValueProviders/FloatValueProvider.swift new file mode 100644 index 0000000000..0e60979d04 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/ValueProviders/FloatValueProvider.swift @@ -0,0 +1,70 @@ +// +// DoubleValueProvider.swift +// lottie-swift +// +// Created by Brandon Withrow on 2/4/19. +// + +import CoreGraphics +import Foundation + +/// A `ValueProvider` that returns a CGFloat Value +public final class FloatValueProvider: ValueProvider { + + // MARK: Lifecycle + + /// Initializes with a block provider + public init(block: @escaping CGFloatValueBlock) { + self.block = block + float = 0 + } + + /// Initializes with a single float. + public init(_ float: CGFloat) { + self.float = float + block = nil + hasUpdate = true + } + + // MARK: Public + + /// Returns a CGFloat for a CGFloat(Frame Time) + public typealias CGFloatValueBlock = (CGFloat) -> CGFloat + + public var float: CGFloat { + didSet { + hasUpdate = true + } + } + + // MARK: ValueProvider Protocol + + public var valueType: Any.Type { + Vector1D.self + } + + public var storage: ValueProviderStorage { + if let block = block { + return .closure { frame in + self.hasUpdate = false + return Vector1D(Double(block(frame))) + } + } else { + hasUpdate = false + return .singleValue(Vector1D(Double(float))) + } + } + + public func hasUpdate(frame _: CGFloat) -> Bool { + if block != nil { + return true + } + return hasUpdate + } + + // MARK: Private + + private var hasUpdate = true + + private var block: CGFloatValueBlock? +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/ValueProviders/GradientValueProvider.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/ValueProviders/GradientValueProvider.swift new file mode 100644 index 0000000000..22b4886037 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/ValueProviders/GradientValueProvider.swift @@ -0,0 +1,125 @@ +// +// GradientValueProvider.swift +// lottie-swift +// +// Created by Enrique Bermúdez on 10/27/19. +// + +import CoreGraphics +import Foundation + +/// A `ValueProvider` that returns a Gradient Color Value. +public final class GradientValueProvider: ValueProvider { + + // MARK: Lifecycle + + /// Initializes with a block provider. + public init( + block: @escaping ColorsValueBlock, + locations: ColorLocationsBlock? = nil) + { + self.block = block + locationsBlock = locations + colors = [] + self.locations = [] + } + + /// Initializes with an array of colors. + public init( + _ colors: [Color], + locations: [Double] = []) + { + self.colors = colors + self.locations = locations + updateValueArray() + hasUpdate = true + } + + // MARK: Public + + /// Returns a [Color] for a CGFloat(Frame Time). + public typealias ColorsValueBlock = (CGFloat) -> [Color] + /// Returns a [Double](Color locations) for a CGFloat(Frame Time). + public typealias ColorLocationsBlock = (CGFloat) -> [Double] + + /// The colors values of the provider. + public var colors: [Color] { + didSet { + updateValueArray() + hasUpdate = true + } + } + + /// The color location values of the provider. + public var locations: [Double] { + didSet { + updateValueArray() + hasUpdate = true + } + } + + // MARK: ValueProvider Protocol + + public var valueType: Any.Type { + [Double].self + } + + public var storage: ValueProviderStorage<[Double]> { + .closure { [self] frame in + hasUpdate = false + + if let block = block { + let newColors = block(frame) + let newLocations = locationsBlock?(frame) ?? [] + value = value(from: newColors, locations: newLocations) + } + + return value + } + } + + public func hasUpdate(frame _: CGFloat) -> Bool { + if block != nil || locationsBlock != nil { + return true + } + return hasUpdate + } + + // MARK: Private + + private var hasUpdate = true + + private var block: ColorsValueBlock? + private var locationsBlock: ColorLocationsBlock? + private var value: [Double] = [] + + private func value(from colors: [Color], locations: [Double]) -> [Double] { + + var colorValues = [Double]() + var alphaValues = [Double]() + var shouldAddAlphaValues = false + + for i in 0.. i + ? locations[i] + : (Double(i) / Double(colors.count - 1)) + + colorValues.append(location) + colorValues.append(colors[i].r) + colorValues.append(colors[i].g) + colorValues.append(colors[i].b) + + alphaValues.append(location) + alphaValues.append(colors[i].a) + } + + return colorValues + (shouldAddAlphaValues ? alphaValues : []) + } + + private func updateValueArray() { + value = value(from: colors, locations: locations) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/ValueProviders/PointValueProvider.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/ValueProviders/PointValueProvider.swift new file mode 100644 index 0000000000..7e2d165cff --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/ValueProviders/PointValueProvider.swift @@ -0,0 +1,69 @@ +// +// PointValueProvider.swift +// lottie-swift +// +// Created by Brandon Withrow on 2/4/19. +// + +import CoreGraphics +import Foundation +/// A `ValueProvider` that returns a CGPoint Value +public final class PointValueProvider: ValueProvider { + + // MARK: Lifecycle + + /// Initializes with a block provider + public init(block: @escaping PointValueBlock) { + self.block = block + point = .zero + } + + /// Initializes with a single point. + public init(_ point: CGPoint) { + self.point = point + block = nil + hasUpdate = true + } + + // MARK: Public + + /// Returns a CGPoint for a CGFloat(Frame Time) + public typealias PointValueBlock = (CGFloat) -> CGPoint + + public var point: CGPoint { + didSet { + hasUpdate = true + } + } + + // MARK: ValueProvider Protocol + + public var valueType: Any.Type { + Vector3D.self + } + + public var storage: ValueProviderStorage { + if let block = block { + return .closure { frame in + self.hasUpdate = false + return block(frame).vector3dValue + } + } else { + hasUpdate = false + return .singleValue(point.vector3dValue) + } + } + + public func hasUpdate(frame _: CGFloat) -> Bool { + if block != nil { + return true + } + return hasUpdate + } + + // MARK: Private + + private var hasUpdate = true + + private var block: PointValueBlock? +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/ValueProviders/SizeValueProvider.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/ValueProviders/SizeValueProvider.swift new file mode 100644 index 0000000000..e1829403b7 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/DynamicProperties/ValueProviders/SizeValueProvider.swift @@ -0,0 +1,70 @@ +// +// SizeValueProvider.swift +// lottie-swift +// +// Created by Brandon Withrow on 2/4/19. +// + +import CoreGraphics +import Foundation + +/// A `ValueProvider` that returns a CGSize Value +public final class SizeValueProvider: ValueProvider { + + // MARK: Lifecycle + + /// Initializes with a block provider + public init(block: @escaping SizeValueBlock) { + self.block = block + size = .zero + } + + /// Initializes with a single size. + public init(_ size: CGSize) { + self.size = size + block = nil + hasUpdate = true + } + + // MARK: Public + + /// Returns a CGSize for a CGFloat(Frame Time) + public typealias SizeValueBlock = (CGFloat) -> CGSize + + public var size: CGSize { + didSet { + hasUpdate = true + } + } + + // MARK: ValueProvider Protocol + + public var valueType: Any.Type { + Vector3D.self + } + + public var storage: ValueProviderStorage { + if let block = block { + return .closure { frame in + self.hasUpdate = false + return block(frame).vector3dValue + } + } else { + hasUpdate = false + return .singleValue(size.vector3dValue) + } + } + + public func hasUpdate(frame _: CGFloat) -> Bool { + if block != nil { + return true + } + return hasUpdate + } + + // MARK: Private + + private var hasUpdate = true + + private var block: SizeValueBlock? +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/FontProvider/AnimationFontProvider.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/FontProvider/AnimationFontProvider.cpp new file mode 100644 index 0000000000..a6a4774df7 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/FontProvider/AnimationFontProvider.cpp @@ -0,0 +1,5 @@ +#include "AnimationFontProvider.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/FontProvider/AnimationFontProvider.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/FontProvider/AnimationFontProvider.hpp new file mode 100644 index 0000000000..2f11b5639a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/FontProvider/AnimationFontProvider.hpp @@ -0,0 +1,31 @@ +#ifndef AnimationFontProvider_hpp +#define AnimationFontProvider_hpp + +#include "Lottie/Public/Primitives/CTFont.hpp" + +#include + +namespace lottie { + +/// Font provider is a protocol that is used to supply fonts to `AnimationView`. +/// +class AnimationFontProvider { +public: + virtual std::shared_ptr fontFor(std::string const &family, double size) = 0; +}; + +/// Default Font provider. +class DefaultFontProvider: public AnimationFontProvider { +public: + DefaultFontProvider() { + } + + virtual std::shared_ptr fontFor(std::string const &family, double size) override { + //CTFontCreateWithName(family as CFString, size, nil) + return nullptr; + } +}; + +} + +#endif /* AnimationFontProvider_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/FontProvider/AnimationFontProvider.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/FontProvider/AnimationFontProvider.swift new file mode 100644 index 0000000000..a908c73f71 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/FontProvider/AnimationFontProvider.swift @@ -0,0 +1,35 @@ +// +// AnimationFontProvider.swift +// Lottie +// +// Created by Brandon Withrow on 8/5/20. +// Copyright © 2020 YurtvilleProds. All rights reserved. +// + +import CoreGraphics +import CoreText +import Foundation + +// MARK: - AnimationFontProvider + +/// Font provider is a protocol that is used to supply fonts to `AnimationView`. +/// +public protocol AnimationFontProvider { + func fontFor(family: String, size: CGFloat) -> CTFont? +} + +// MARK: - DefaultFontProvider + +/// Default Font provider. +public final class DefaultFontProvider: AnimationFontProvider { + + // MARK: Lifecycle + + public init() {} + + // MARK: Public + + public func fontFor(family: String, size: CGFloat) -> CTFont? { + CTFontCreateWithName(family as CFString, size, nil) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/ImageProvider/AnimationImageProvider.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/ImageProvider/AnimationImageProvider.hpp new file mode 100644 index 0000000000..3c55ffd944 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/ImageProvider/AnimationImageProvider.hpp @@ -0,0 +1,16 @@ +#ifndef AnimationImageProvider_hpp +#define AnimationImageProvider_hpp + +#include "Lottie/Public/Primitives/CALayer.hpp" +#include "Lottie/Private/Model/Assets/ImageAsset.hpp" + +namespace lottie { + +class AnimationImageProvider { +public: + virtual std::shared_ptr imageForAsset(ImageAsset const &imageAsset) = 0; +}; + +} + +#endif /* AnimationImageProvider_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/ImageProvider/AnimationImageProvider.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/ImageProvider/AnimationImageProvider.swift new file mode 100644 index 0000000000..c565921e0e --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/ImageProvider/AnimationImageProvider.swift @@ -0,0 +1,21 @@ +// +// LottieImageProvider.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/25/19. +// + +import CoreGraphics +import Foundation + +/// Image provider is a protocol that is used to supply images to `AnimationView`. +/// +/// Some animations require a reference to an image. The image provider loads and +/// provides those images to the `AnimationView`. Lottie includes a couple of +/// prebuilt Image Providers that supply images from a Bundle, or from a FilePath. +/// +/// Additionally custom Image Providers can be made to load images from a URL, +/// or to Cache images. +public protocol AnimationImageProvider { + func imageForAsset(asset: ImageAsset) -> CGImage? +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/Interpolatable.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/Interpolatable.cpp new file mode 100644 index 0000000000..8165d87d71 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/Interpolatable.cpp @@ -0,0 +1,15 @@ +#include "Interpolatable.hpp" + +namespace lottie { + +double remapDouble(double value, double fromLow, double fromHigh, double toLow, double toHigh) { + return toLow + (value - fromLow) * (toHigh - toLow) / (fromHigh - fromLow); +} + +double clampDouble(double value, double a, double b) { + double minValue = a <= b ? a : b; + double maxValue = a <= b ? b : a; + return std::max(std::min(value, maxValue), minValue); +} + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/Interpolatable.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/Interpolatable.hpp new file mode 100644 index 0000000000..2bfabdca07 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/Interpolatable.hpp @@ -0,0 +1,14 @@ +#ifndef Interpolatable_hpp +#define Interpolatable_hpp + +#include + +namespace lottie { + +double remapDouble(double value, double fromLow, double fromHigh, double toLow, double toHigh); + +double clampDouble(double value, double a, double b); + +} + +#endif /* Interpolatable_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/Interpolatable.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/Interpolatable.swift new file mode 100644 index 0000000000..2a7ab5b08e --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/Interpolatable.swift @@ -0,0 +1,253 @@ +// Created by Cal Stephens on 1/24/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +import CoreGraphics + +// MARK: - Interpolatable + +/// A type that can be interpolated between two values +public protocol Interpolatable: AnyInterpolatable { + /// Interpolates the `self` to the given number by `amount`. + /// - Parameter to: The number to interpolate to. + /// - Parameter amount: The amount to interpolate, + /// relative to 0.0 (self) and 1.0 (to). + /// `amount` can be greater than one and less than zero, + /// and interpolation should not be clamped. + /// + /// ``` + /// let number = 5 + /// let interpolated = number.interpolateTo(10, amount: 0.5) + /// print(interpolated) // 7.5 + /// ``` + /// + /// ``` + /// let number = 5 + /// let interpolated = number.interpolateTo(10, amount: 1.5) + /// print(interpolated) // 12.5 + /// ``` + func interpolate(to: Self, amount: CGFloat) -> Self +} + +// MARK: - SpatialInterpolatable + +/// A type that can be interpolated between two values, +/// additionally using optional `spatialOutTangent` and `spatialInTangent` values. +/// - If your implementation doesn't use the `spatialOutTangent` and `spatialInTangent` +/// parameters, prefer implementing the simpler `Interpolatable` protocol. +public protocol SpatialInterpolatable: AnyInterpolatable { + /// Interpolates the `self` to the given number by `amount`. + /// - Parameter to: The number to interpolate to. + /// - Parameter amount: The amount to interpolate, + /// relative to 0.0 (self) and 1.0 (to). + /// `amount` can be greater than one and less than zero, + /// and interpolation should not be clamped. + func interpolate( + to: Self, + amount: CGFloat, + spatialOutTangent: CGPoint?, + spatialInTangent: CGPoint?) + -> Self +} + +// MARK: - AnyInterpolatable + +/// The base protocol that is implemented by both `Interpolatable` and `SpatialInterpolatable` +/// Types should not directly implement this protocol. +public protocol AnyInterpolatable { + /// Interpolates by calling either `Interpolatable.interpolate` + /// or `SpatialInterpolatable.interpolate`. + /// Should not be implemented or called by consumers. + func _interpolate( + to: Self, + amount: CGFloat, + spatialOutTangent: CGPoint?, + spatialInTangent: CGPoint?) + -> Self +} + +extension Interpolatable { + public func _interpolate( + to: Self, + amount: CGFloat, + spatialOutTangent _: CGPoint?, + spatialInTangent _: CGPoint?) + -> Self + { + interpolate(to: to, amount: amount) + } +} + +extension SpatialInterpolatable { + /// Helper that interpolates this `SpatialInterpolatable` + /// with `nil` spatial in/out tangents + public func interpolate(to: Self, amount: CGFloat) -> Self { + interpolate( + to: to, + amount: amount, + spatialOutTangent: nil, + spatialInTangent: nil) + } + + public func _interpolate( + to: Self, + amount: CGFloat, + spatialOutTangent: CGPoint?, + spatialInTangent: CGPoint?) + -> Self + { + interpolate( + to: to, + amount: amount, + spatialOutTangent: spatialOutTangent, + spatialInTangent: spatialInTangent) + } +} + +// MARK: - Double + Interpolatable + +extension Double: Interpolatable { } + +// MARK: - CGFloat + Interpolatable + +extension CGFloat: Interpolatable { } + +// MARK: - Float + Interpolatable + +extension Float: Interpolatable { } + +extension Interpolatable where Self: BinaryFloatingPoint { + public func interpolate(to: Self, amount: CGFloat) -> Self { + self + ((to - self) * Self(amount)) + } +} + +// MARK: - CGRect + Interpolatable + +extension CGRect: Interpolatable { + public func interpolate(to: CGRect, amount: CGFloat) -> CGRect { + CGRect( + x: origin.x.interpolate(to: to.origin.x, amount: amount), + y: origin.y.interpolate(to: to.origin.y, amount: amount), + width: width.interpolate(to: to.width, amount: amount), + height: height.interpolate(to: to.height, amount: amount)) + } +} + +// MARK: - CGSize + Interpolatable + +extension CGSize: Interpolatable { + public func interpolate(to: CGSize, amount: CGFloat) -> CGSize { + CGSize( + width: width.interpolate(to: to.width, amount: amount), + height: height.interpolate(to: to.height, amount: amount)) + } +} + +// MARK: - CGPoint + SpatialInterpolatable + +extension CGPoint: SpatialInterpolatable { + public func interpolate( + to: CGPoint, + amount: CGFloat, + spatialOutTangent: CGPoint?, + spatialInTangent: CGPoint?) + -> CGPoint + { + guard + let outTan = spatialOutTangent, + let inTan = spatialInTangent + else { + return CGPoint( + x: x.interpolate(to: to.x, amount: amount), + y: y.interpolate(to: to.y, amount: amount)) + } + + let cp1 = self + outTan + let cp2 = to + inTan + return interpolate(to, outTangent: cp1, inTangent: cp2, amount: amount) + } +} + +// MARK: - Color + Interpolatable + +extension Color: Interpolatable { + public func interpolate(to: Color, amount: CGFloat) -> Color { + Color( + r: r.interpolate(to: to.r, amount: amount), + g: g.interpolate(to: to.g, amount: amount), + b: b.interpolate(to: to.b, amount: amount), + a: a.interpolate(to: to.a, amount: amount)) + } +} + +// MARK: - Vector1D + Interpolatable + +extension Vector1D: Interpolatable { + public func interpolate(to: Vector1D, amount: CGFloat) -> Vector1D { + value.interpolate(to: to.value, amount: amount).vectorValue + } +} + +// MARK: - Vector2D + SpatialInterpolatable + +extension Vector2D: SpatialInterpolatable { + public func interpolate( + to: Vector2D, + amount: CGFloat, + spatialOutTangent: CGPoint?, + spatialInTangent: CGPoint?) + -> Vector2D + { + pointValue.interpolate( + to: to.pointValue, + amount: amount, + spatialOutTangent: spatialOutTangent, + spatialInTangent: spatialInTangent) + .vector2dValue + } +} + +// MARK: - Vector3D + SpatialInterpolatable + +extension Vector3D: SpatialInterpolatable { + public func interpolate( + to: Vector3D, + amount: CGFloat, + spatialOutTangent: CGPoint?, + spatialInTangent: CGPoint?) + -> Vector3D + { + if spatialInTangent != nil || spatialOutTangent != nil { + // TODO Support third dimension spatial interpolation + let point = pointValue.interpolate( + to: to.pointValue, + amount: amount, + spatialOutTangent: spatialOutTangent, + spatialInTangent: spatialInTangent) + + return Vector3D( + x: point.x, + y: point.y, + z: CGFloat(z.interpolate(to: to.z, amount: amount))) + } + + return Vector3D( + x: x.interpolate(to: to.x, amount: amount), + y: y.interpolate(to: to.y, amount: amount), + z: z.interpolate(to: to.z, amount: amount)) + } +} + +// MARK: - Array + Interpolatable, AnyInterpolatable + +extension Array: Interpolatable, AnyInterpolatable where Element: Interpolatable { + public func interpolate(to: [Element], amount: CGFloat) -> [Element] { + LottieLogger.shared.assert( + count == to.count, + "When interpolating Arrays, both array sound have the same element count.") + + return zip(self, to).map { lhs, rhs in + lhs.interpolate(to: rhs, amount: amount) + } + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/Keyframe.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/Keyframe.cpp new file mode 100644 index 0000000000..e699e6daf8 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/Keyframe.cpp @@ -0,0 +1,5 @@ +#include "Keyframe.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/Keyframe.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/Keyframe.hpp new file mode 100644 index 0000000000..f219032d0c --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/Keyframe.hpp @@ -0,0 +1,256 @@ +#ifndef Keyframe_hpp +#define Keyframe_hpp + +#include "Lottie/Public/Primitives/AnimationTime.hpp" +#include "Lottie/Public/Primitives/Vectors.hpp" +#include "Lottie/Public/Keyframes/Interpolatable.hpp" +#include "Lottie/Public/Keyframes/ValueInterpolators.hpp" + +#include + +namespace lottie { + +/// A keyframe with a single value, and timing information +/// about when the value should be displayed and how it +/// should be interpolated. +template +class Keyframe { +public: + /// Initialize a value-only keyframe with no time data. + Keyframe( + T const &value_, + std::optional spatialInTangent_, + std::optional spatialOutTangent_ + ) : + value(value_), + spatialInTangent(spatialInTangent_), + spatialOutTangent(spatialOutTangent_), + time(0), + isHold(true), + inTangent(std::nullopt), + outTangent(std::nullopt) { + } + + /// Initialize a keyframe + Keyframe( + T value_, + AnimationFrameTime time_, + bool isHold_, + std::optional inTangent_, + std::optional outTangent_, + std::optional spatialInTangent_, + std::optional spatialOutTangent_ + ) : + value(value_), + time(time_), + isHold(isHold_), + inTangent(inTangent_), + outTangent(outTangent_), + spatialInTangent(spatialInTangent_), + spatialOutTangent(spatialOutTangent_) { + } + + bool operator==(Keyframe const &rhs) { + return value == rhs.value + && time == rhs.time + && isHold == rhs.isHold + && inTangent == rhs.inTangent + && outTangent == rhs.outTangent + && spatialInTangent == rhs.spatialInTangent + && spatialOutTangent == rhs.spatialOutTangent; + } + + bool operator!=(Keyframe const &rhs) { + return !(*this == rhs); + } + +public: + T interpolate(Keyframe const &to, double progress) { + std::optional spatialOutTangent2d; + if (spatialOutTangent) { + spatialOutTangent2d = Vector2D(spatialOutTangent->x, spatialOutTangent->y); + } + std::optional spatialInTangent2d; + if (to.spatialInTangent) { + spatialInTangent2d = Vector2D(to.spatialInTangent->x, to.spatialInTangent->y); + } + return ValueInterpolator::interpolate(value, to.value, progress, spatialOutTangent2d, spatialInTangent2d); + } + + /// Interpolates the keyTime into a value from 0-1 + double interpolatedProgress(Keyframe const &to, double keyTime) { + double startTime = time; + double endTime = to.time; + if (keyTime <= startTime) { + return 0.0; + } + if (endTime <= keyTime) { + return 1.0; + } + + if (isHold) { + return 0.0; + } + + Vector2D outTanPoint = Vector2D::Zero(); + if (outTangent.has_value()) { + outTanPoint = outTangent.value(); + } + Vector2D inTanPoint = Vector2D(1.0, 1.0); + if (to.inTangent.has_value()) { + inTanPoint = to.inTangent.value(); + } + double progress = remapDouble(keyTime, startTime, endTime, 0.0, 1.0); + if (!outTanPoint.isZero() || inTanPoint != Vector2D(1.0, 1.0)) { + /// Cubic interpolation + progress = cubicBezierInterpolate(progress, Vector2D::Zero(), outTanPoint, inTanPoint, Vector2D(1.0, 1.0)); + } + return progress; + } + +public: + /// The value of the keyframe + T value; + /// The time in frames of the keyframe. + AnimationFrameTime time; + /// A hold keyframe freezes interpolation until the next keyframe that is not a hold. + bool isHold; + /// The in tangent for the time interpolation curve. + std::optional inTangent; + /// The out tangent for the time interpolation curve. + std::optional outTangent; + + /// The spatial in tangent of the vector. + std::optional spatialInTangent; + /// The spatial out tangent of the vector. + std::optional spatialOutTangent; +}; + +template +class KeyframeData { +public: + KeyframeData( + std::optional startValue_, + std::optional endValue_, + std::optional time_, + std::optional hold_, + std::optional inTangent_, + std::optional outTangent_, + std::optional spatialInTangent_, + std::optional spatialOutTangent_ + ) : + startValue(startValue_), + endValue(endValue_), + time(time_), + hold(hold_), + inTangent(inTangent_), + outTangent(outTangent_), + spatialInTangent(spatialInTangent_), + spatialOutTangent(spatialOutTangent_) { + } + + explicit KeyframeData(json11::Json const &json) noexcept(false) { + if (!json.is_object()) { + throw LottieParsingException(); + } + + if (const auto startValueData = getOptionalAny(json.object_items(), "s")) { + startValue = T(startValueData.value()); + } + + if (const auto endValueData = getOptionalAny(json.object_items(), "e")) { + endValue = T(endValueData.value()); + } + + time = getOptionalDouble(json.object_items(), "t"); + hold = getOptionalInt(json.object_items(), "h"); + + if (const auto inTangentData = getOptionalObject(json.object_items(), "i")) { + inTangent = Vector2D(inTangentData.value()); + } + + if (const auto outTangentData = getOptionalObject(json.object_items(), "o")) { + outTangent = Vector2D(outTangentData.value()); + } + + if (const auto spatialInTangentData = getOptionalAny(json.object_items(), "ti")) { + spatialInTangent = Vector3D(spatialInTangentData.value()); + } + + if (const auto spatialOutTangentData = getOptionalAny(json.object_items(), "to")) { + spatialOutTangent = Vector3D(spatialOutTangentData.value()); + } + + if (const auto nDataValue = getOptionalAny(json.object_items(), "n")) { + nData = nDataValue.value(); + } + } + + json11::Json::object toJson() const { + json11::Json::object result; + + if (startValue.has_value()) { + result.insert(std::make_pair("s", startValue->toJson())); + } + if (endValue.has_value()) { + result.insert(std::make_pair("e", endValue->toJson())); + } + if (time.has_value()) { + result.insert(std::make_pair("t", time.value())); + } + if (hold.has_value()) { + result.insert(std::make_pair("h", hold.value())); + } + if (inTangent.has_value()) { + result.insert(std::make_pair("i", inTangent->toJson())); + } + if (outTangent.has_value()) { + result.insert(std::make_pair("o", outTangent->toJson())); + } + if (spatialInTangent.has_value()) { + result.insert(std::make_pair("ti", spatialInTangent->toJson())); + } + if (spatialOutTangent.has_value()) { + result.insert(std::make_pair("to", spatialOutTangent->toJson())); + } + if (nData.has_value()) { + result.insert(std::make_pair("n", nData.value())); + } + + return result; + } + +public: + /// The start value of the keyframe + std::optional startValue; + /// The End value of the keyframe. Note: Newer versions animation json do not have this field. + std::optional endValue; + /// The time in frames of the keyframe. + std::optional time; + /// A hold keyframe freezes interpolation until the next keyframe that is not a hold. + std::optional hold; + + /// The in tangent for the time interpolation curve. + std::optional inTangent; + /// The out tangent for the time interpolation curve. + std::optional outTangent; + + /// The spacial in tangent of the vector. + std::optional spatialInTangent; + /// The spacial out tangent of the vector. + std::optional spatialOutTangent; + + std::optional nData; + + bool isHold() const { + if (hold.has_value()) { + return hold.value() > 0; + } else { + return false; + } + } +}; + +} + +#endif /* Keyframe_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/Keyframe.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/Keyframe.swift new file mode 100644 index 0000000000..2e3befc53c --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/Keyframe.swift @@ -0,0 +1,92 @@ +// Created by Cal Stephens on 1/24/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +// MARK: - Keyframe + +/// A keyframe with a single value, and timing information +/// about when the value should be displayed and how it +/// should be interpolated. +public final class Keyframe { + + // MARK: Lifecycle + + /// Initialize a value-only keyframe with no time data. + public init( + _ value: T, + spatialInTangent: Vector3D? = nil, + spatialOutTangent: Vector3D? = nil) + { + self.value = value + time = 0 + isHold = true + inTangent = nil + outTangent = nil + self.spatialInTangent = spatialInTangent + self.spatialOutTangent = spatialOutTangent + } + + /// Initialize a keyframe + public init( + value: T, + time: AnimationFrameTime, + isHold: Bool = false, + inTangent: Vector2D? = nil, + outTangent: Vector2D? = nil, + spatialInTangent: Vector3D? = nil, + spatialOutTangent: Vector3D? = nil) + { + self.value = value + self.time = time + self.isHold = isHold + self.outTangent = outTangent + self.inTangent = inTangent + self.spatialInTangent = spatialInTangent + self.spatialOutTangent = spatialOutTangent + } + + // MARK: Public + + /// The value of the keyframe + public let value: T + /// The time in frames of the keyframe. + public let time: AnimationFrameTime + /// A hold keyframe freezes interpolation until the next keyframe that is not a hold. + public let isHold: Bool + /// The in tangent for the time interpolation curve. + public let inTangent: Vector2D? + /// The out tangent for the time interpolation curve. + public let outTangent: Vector2D? + + /// The spatial in tangent of the vector. + public let spatialInTangent: Vector3D? + /// The spatial out tangent of the vector. + public let spatialOutTangent: Vector3D? +} + +// MARK: Equatable + +extension Keyframe: Equatable where T: Equatable { + public static func == (lhs: Keyframe, rhs: Keyframe) -> Bool { + lhs.value == rhs.value + && lhs.time == rhs.time + && lhs.isHold == rhs.isHold + && lhs.inTangent == rhs.inTangent + && lhs.outTangent == rhs.outTangent + && lhs.spatialInTangent == rhs.spatialOutTangent + && lhs.spatialOutTangent == rhs.spatialOutTangent + } +} + +// MARK: Hashable + +extension Keyframe: Hashable where T: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(value) + hasher.combine(time) + hasher.combine(isHold) + hasher.combine(inTangent) + hasher.combine(outTangent) + hasher.combine(spatialInTangent) + hasher.combine(spatialOutTangent) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/ValueInterpolators.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/ValueInterpolators.cpp new file mode 100644 index 0000000000..55e4fac688 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/ValueInterpolators.cpp @@ -0,0 +1,5 @@ +#include "ValueInterpolators.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/ValueInterpolators.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/ValueInterpolators.hpp new file mode 100644 index 0000000000..b2c063dccc --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Keyframes/ValueInterpolators.hpp @@ -0,0 +1,231 @@ +#ifndef ValueInterpolators_hpp +#define ValueInterpolators_hpp + +#include "Lottie/Public/Primitives/Vectors.hpp" +#include "Lottie/Public/Primitives/Color.hpp" +#include "Lottie/Private/Utility/Primitives/BezierPath.hpp" +#include "Lottie/Private/Model/Text/TextDocument.hpp" +#include "Lottie/Public/Primitives/GradientColorSet.hpp" +#include "Lottie/Public/Primitives/DashPattern.hpp" + +#include +#include + +namespace lottie { + +template +struct ValueInterpolator { +}; + +template<> +struct ValueInterpolator { +public: + static double interpolate(double value, double to, double amount, std::optional spatialOutTangent, std::optional spatialInTangent) { + return value + ((to - value) * amount); + } +}; + +template<> +struct ValueInterpolator { +public: + static Vector1D interpolate(Vector1D const &value, Vector1D const &to, double amount, std::optional spatialOutTangent, std::optional spatialInTangent) { + return Vector1D(ValueInterpolator::interpolate(value.value, to.value, amount, spatialOutTangent, spatialInTangent)); + } +}; + +template<> +struct ValueInterpolator { +public: + static Vector2D interpolate(Vector2D const &value, Vector2D const &to, double amount, Vector2D spatialOutTangent, Vector2D spatialInTangent) { + auto cp1 = value + spatialOutTangent; + auto cp2 = to + spatialInTangent; + + return value.interpolate(to, cp1, cp2, amount); + } + + static Vector2D interpolate(Vector2D const &value, Vector2D const &to, double amount) { + return value.interpolate(to, amount); + } +}; + +template<> +struct ValueInterpolator { +public: + static Vector3D interpolate(Vector3D const &value, Vector3D const &to, double amount, std::optional spatialOutTangent, std::optional spatialInTangent) { + if (spatialOutTangent && spatialInTangent) { + Vector2D from2d(value.x, value.y); + Vector2D to2d(to.x, to.y); + + auto cp1 = from2d + spatialOutTangent.value(); + auto cp2 = to2d + spatialInTangent.value(); + + Vector2D result2d = from2d.interpolate(to2d, cp1, cp2, amount); + + return Vector3D( + result2d.x, + result2d.y, + ValueInterpolator::interpolate(value.z, to.z, amount, spatialOutTangent, spatialInTangent) + ); + } + + return Vector3D( + ValueInterpolator::interpolate(value.x, to.x, amount, spatialOutTangent, spatialInTangent), + ValueInterpolator::interpolate(value.y, to.y, amount, spatialOutTangent, spatialInTangent), + ValueInterpolator::interpolate(value.z, to.z, amount, spatialOutTangent, spatialInTangent) + ); + } +}; + +template<> +struct ValueInterpolator { +public: + static Color interpolate(Color const &value, Color const &to, double amount, std::optional spatialOutTangent, std::optional spatialInTangent) { + return Color( + ValueInterpolator::interpolate(value.r, to.r, amount, spatialOutTangent, spatialInTangent), + ValueInterpolator::interpolate(value.g, to.g, amount, spatialOutTangent, spatialInTangent), + ValueInterpolator::interpolate(value.b, to.b, amount, spatialOutTangent, spatialInTangent), + ValueInterpolator::interpolate(value.a, to.a, amount, spatialOutTangent, spatialInTangent) + ); + } +}; + +template<> +struct ValueInterpolator { +public: + static CurveVertex interpolate(CurveVertex const &value, CurveVertex const &to, double amount, Vector2D spatialOutTangent, Vector2D spatialInTangent) { + return CurveVertex::absolute( + ValueInterpolator::interpolate(value.point, to.point, amount, spatialOutTangent, spatialInTangent), + ValueInterpolator::interpolate(value.inTangent, to.inTangent, amount, spatialOutTangent, spatialInTangent), + ValueInterpolator::interpolate(value.outTangent, to.outTangent, amount, spatialOutTangent, spatialInTangent) + ); + } + + static CurveVertex interpolate(CurveVertex const &value, CurveVertex const &to, double amount) { + return CurveVertex::absolute( + ValueInterpolator::interpolate(value.point, to.point, amount), + ValueInterpolator::interpolate(value.inTangent, to.inTangent, amount), + ValueInterpolator::interpolate(value.outTangent, to.outTangent, amount) + ); + } +}; + +template<> +struct ValueInterpolator { +public: + static BezierPath interpolate(BezierPath const &value, BezierPath const &to, double amount, std::optional spatialOutTangent, std::optional spatialInTangent) { + BezierPath newPath; + newPath.reserveCapacity(std::max(value.elements().size(), to.elements().size())); + //TODO:probably a bug in the upstream code, uncomment + //newPath.setClosed(value.closed()); + size_t elementCount = std::min(value.elements().size(), to.elements().size()); + + if (spatialInTangent && spatialOutTangent) { + Vector2D spatialInTangentValue = spatialInTangent.value(); + Vector2D spatialOutTangentValue = spatialOutTangent.value(); + + for (size_t i = 0; i < elementCount; i++) { + const auto &fromVertex = value.elements()[i].vertex; + const auto &toVertex = to.elements()[i].vertex; + + newPath.addVertex(ValueInterpolator::interpolate(fromVertex, toVertex, amount, spatialOutTangentValue, spatialInTangentValue)); + } + } else { + for (size_t i = 0; i < elementCount; i++) { + const auto &fromVertex = value.elements()[i].vertex; + const auto &toVertex = to.elements()[i].vertex; + + newPath.addVertex(ValueInterpolator::interpolate(fromVertex, toVertex, amount)); + } + } + return newPath; + } + + static void setInplace(BezierPath const &value, BezierPath &resultPath) { + resultPath.reserveCapacity(value.elements().size()); + resultPath.setElementCount(value.elements().size()); + resultPath.invalidateLength(); + + memcpy(resultPath.mutableElements().data(), value.elements().data(), value.elements().size() * sizeof(PathElement)); + } + + static void interpolateInplace(BezierPath const &value, BezierPath const &to, double amount, std::optional spatialOutTangent, std::optional spatialInTangent, BezierPath &resultPath) { + /*if (value.elements().size() != to.elements().size()) { + return to; + }*/ + + //TODO:probably a bug in the upstream code, uncomment + //newPath.setClosed(value.closed()); + int elementCount = (int)std::min(value.elements().size(), to.elements().size()); + + resultPath.reserveCapacity(std::max(value.elements().size(), to.elements().size())); + resultPath.setElementCount(elementCount); + resultPath.invalidateLength(); + + if (spatialInTangent && spatialOutTangent) { + Vector2D spatialInTangentValue = spatialInTangent.value(); + Vector2D spatialOutTangentValue = spatialOutTangent.value(); + + for (int i = 0; i < elementCount; i++) { + const auto &fromVertex = value.elements()[i].vertex; + const auto &toVertex = to.elements()[i].vertex; + + auto vertex = ValueInterpolator::interpolate(fromVertex, toVertex, amount, spatialOutTangentValue, spatialInTangentValue); + + resultPath.updateVertex(vertex, i, false); + } + } else { + for (int i = 0; i < elementCount; i++) { + const auto &fromVertex = value.elements()[i].vertex; + const auto &toVertex = to.elements()[i].vertex; + + auto vertex = ValueInterpolator::interpolate(fromVertex, toVertex, amount); + + resultPath.updateVertex(vertex, i, false); + } + } + } +}; + +template<> +struct ValueInterpolator { +public: + static TextDocument interpolate(TextDocument const &value, TextDocument const &to, double amount, std::optional spatialOutTangent, std::optional spatialInTangent) { + if (amount == 1.0) { + return to; + } else { + return value; + } + } +}; + +template<> +struct ValueInterpolator { +public: + static GradientColorSet interpolate(GradientColorSet const &value, GradientColorSet const &to, double amount, std::optional spatialOutTangent, std::optional spatialInTangent) { + assert(value.colors.size() == to.colors.size()); + std::vector colors; + size_t colorCount = std::min(value.colors.size(), to.colors.size()); + for (size_t i = 0; i < colorCount; i++) { + colors.push_back(ValueInterpolator::interpolate(value.colors[i], to.colors[i], amount, spatialOutTangent, spatialInTangent)); + } + return GradientColorSet(colors); + } +}; + +template<> +struct ValueInterpolator { +public: + static DashPattern interpolate(DashPattern const &value, DashPattern const &to, double amount, std::optional spatialOutTangent, std::optional spatialInTangent) { + assert(value.values.size() == to.values.size()); + std::vector values; + size_t colorCount = std::min(value.values.size(), to.values.size()); + for (size_t i = 0; i < colorCount; i++) { + values.push_back(ValueInterpolator::interpolate(value.values[i], to.values[i], amount, spatialOutTangent, spatialInTangent)); + } + return DashPattern(std::move(values)); + } +}; + +} + +#endif /* ValueInterpolators_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Logging/LottieLogger.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Logging/LottieLogger.swift new file mode 100644 index 0000000000..983eb23b86 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Logging/LottieLogger.swift @@ -0,0 +1,108 @@ +// Created by eric_horacek on 12/9/20. +// Copyright © 2020 Airbnb Inc. All rights reserved. + +// MARK: - LottieLogger + +/// A shared logger that allows consumers to intercept Lottie assertions and warning messages to pipe +/// into their own logging systems. +public final class LottieLogger { + + // MARK: Lifecycle + + public init( + assert: @escaping Assert = Swift.assert, + assertionFailure: @escaping AssertionFailure = Swift.assertionFailure, + warn: @escaping Warn = { message, _, _ in + #if DEBUG + // swiftlint:disable:next no_direct_standard_out_logs + print(message()) + #endif + }) + { + _assert = assert + _assertionFailure = assertionFailure + _warn = warn + } + + // MARK: Public + + /// Logs that an assertion occurred. + public typealias Assert = ( + _ condition: @autoclosure () -> Bool, + _ message: @autoclosure () -> String, + _ fileID: StaticString, + _ line: UInt) + -> Void + + /// Logs that an assertion failure occurred. + public typealias AssertionFailure = ( + _ message: @autoclosure () -> String, + _ fileID: StaticString, + _ line: UInt) + -> Void + + /// Logs a warning message. + public typealias Warn = ( + _ message: @autoclosure () -> String, + _ fileID: StaticString, + _ line: UInt) + -> Void + + /// The shared instance used to log Lottie assertions and warnings. + /// + /// Set this to a new logger instance to intercept assertions and warnings logged by Lottie. + public static var shared = LottieLogger() + + /// Logs that an assertion occurred. + public func assert( + _ condition: @autoclosure () -> Bool, + _ message: @autoclosure () -> String = String(), + fileID: StaticString = #fileID, + line: UInt = #line) + { + _assert(condition(), message(), fileID, line) + } + + /// Logs that an assertion failure occurred. + public func assertionFailure( + _ message: @autoclosure () -> String = String(), + fileID: StaticString = #fileID, + line: UInt = #line) + { + _assertionFailure(message(), fileID, line) + } + + /// Logs a warning message. + public func warn( + _ message: @autoclosure () -> String = String(), + fileID: StaticString = #fileID, + line: UInt = #line) + { + _warn(message(), fileID, line) + } + + // MARK: Private + + private let _assert: Assert + private let _assertionFailure: AssertionFailure + private let _warn: Warn + +} + +// MARK: - LottieLogger + printToConsole + +extension LottieLogger { + /// A `LottieLogger` instance that always prints to the console (by calling `print`) + /// instead of calling `assert` / `assertionFailure`, which halt execution in debug builds. + public static var printToConsole: LottieLogger { + LottieLogger( + assert: { condition, message, _, _ in + if !condition() { + print(message()) + } + }, + assertionFailure: { message, _, _ in + print(message()) + }) + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/LottieConfiguration.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/LottieConfiguration.swift new file mode 100644 index 0000000000..188bd371a9 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/LottieConfiguration.swift @@ -0,0 +1,143 @@ +// Created by Cal Stephens on 12/13/21. +// Copyright © 2021 Airbnb Inc. All rights reserved. + +// MARK: - LottieConfiguration + +/// Global configuration options for Lottie animations +public struct LottieConfiguration: Hashable { + + public init( + renderingEngine: RenderingEngineOption = .mainThread, + decodingStrategy: DecodingStrategy = .codable) + { + self.renderingEngine = renderingEngine + self.decodingStrategy = decodingStrategy + } + + /// The global configuration of Lottie, + /// which applies to all `AnimationView`s by default. + public static var shared = LottieConfiguration() + + /// The rendering engine implementation to use when displaying an animation + public var renderingEngine: RenderingEngineOption + + /// The decoding implementation to use when parsing an animation JSON file + public var decodingStrategy: DecodingStrategy + +} + +// MARK: - RenderingEngineOption + +public enum RenderingEngineOption: Hashable { + /// Uses the Core Animation engine for supported animations, and falls back to using + /// the Main Thread engine for animations that use features not supported by the + /// Core Animation engine. + case automatic + + /// Uses the specified rendering engine + case specific(RenderingEngine) + + /// The Main Thread rendering engine, which supports all Lottie features + /// but runs on the main thread, which comes with some CPU overhead and + /// can cause the animation to play at a low framerate when the CPU is busy. + public static var mainThread: RenderingEngineOption { .specific(.mainThread) } + + /// The Core Animation rendering engine, that animates using Core Animation + /// and has better performance characteristics than the Main Thread engine, + /// but doesn't support all Lottie features. + public static var coreAnimation: RenderingEngineOption { .specific(.coreAnimation) } +} + +// MARK: - RenderingEngine + +/// The rendering engine implementation to use when displaying an animation +public enum RenderingEngine: Hashable { + /// The Main Thread rendering engine, which supports all Lottie features + /// but runs on the main thread, which comes with some CPU overhead and + /// can cause the animation to play at a low framerate when the CPU is busy. + case mainThread + + /// The Core Animation rendering engine, that animates using Core Animation + /// and has better performance characteristics than the Main Thread engine, + /// but doesn't support all Lottie features. + case coreAnimation +} + +// MARK: - RenderingEngineOption + RawRepresentable, CustomStringConvertible + +extension RenderingEngineOption: RawRepresentable, CustomStringConvertible { + + // MARK: Lifecycle + + public init?(rawValue: String) { + if rawValue == "Automatic" { + self = .automatic + } else if let engine = RenderingEngine(rawValue: rawValue) { + self = .specific(engine) + } else { + return nil + } + } + + // MARK: Public + + public var rawValue: String { + switch self { + case .automatic: + return "Automatic" + case .specific(let engine): + return engine.rawValue + } + } + + public var description: String { + rawValue + } + +} + +// MARK: - RenderingEngine + RawRepresentable, CustomStringConvertible + +extension RenderingEngine: RawRepresentable, CustomStringConvertible { + + // MARK: Lifecycle + + public init?(rawValue: String) { + switch rawValue { + case "Main Thread": + self = .mainThread + case "Core Animation": + self = .coreAnimation + default: + return nil + } + } + + // MARK: Public + + public var rawValue: String { + switch self { + case .mainThread: + return "Main Thread" + case .coreAnimation: + return "Core Animation" + } + } + + public var description: String { + rawValue + } +} + +// MARK: - DecodingStrategy + +/// How animation files should be decoded +public enum DecodingStrategy: Hashable { + /// Use Codable. This is the default strategy introduced on Lottie 3. + case codable + + /// Manually deserialize a dictionary into an Animation. + /// This should be at least 2-3x faster than using Codable, + /// but since it's manually implemented, there might be issues while it's experimental. + case dictionaryBased +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/AnimationTime.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/AnimationTime.cpp new file mode 100644 index 0000000000..c804b75b10 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/AnimationTime.cpp @@ -0,0 +1,5 @@ +#include "AnimationTime.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/AnimationTime.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/AnimationTime.hpp new file mode 100644 index 0000000000..02a3e558d3 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/AnimationTime.hpp @@ -0,0 +1,14 @@ +#ifndef AnimationTime_hpp +#define AnimationTime_hpp + +namespace lottie { + +/// Defines animation time in Frames (Seconds * Framerate). +typedef double AnimationFrameTime; + +/// Defines animation time by a progress from 0 (beginning of the animation) to 1 (end of the animation) +typedef double AnimationProgressTime; + +} + +#endif /* AnimationTime_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/AnimationTime.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/AnimationTime.swift new file mode 100644 index 0000000000..2c33e2b452 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/AnimationTime.swift @@ -0,0 +1,15 @@ +// +// AnimationTime.swift +// lottie-swift-iOS +// +// Created by Brandon Withrow on 2/6/19. +// + +import CoreGraphics +import Foundation + +/// Defines animation time in Frames (Seconds * Framerate). +public typealias AnimationFrameTime = CGFloat + +/// Defines animation time by a progress from 0 (beginning of the animation) to 1 (end of the animation) +public typealias AnimationProgressTime = CGFloat diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/AnyValue.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/AnyValue.cpp new file mode 100644 index 0000000000..1af508b3de --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/AnyValue.cpp @@ -0,0 +1,5 @@ +#include "AnyValue.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/AnyValue.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/AnyValue.hpp new file mode 100644 index 0000000000..516b2e7d10 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/AnyValue.hpp @@ -0,0 +1,245 @@ +#ifndef AnyValue_hpp +#define AnyValue_hpp + +#include "Lottie/Public/Primitives/Vectors.hpp" +#include "Lottie/Public/Primitives/Color.hpp" +#include "Lottie/Private/Utility/Primitives/BezierPath.hpp" +#include "Lottie/Private/Model/Text/TextDocument.hpp" +#include "Lottie/Private/Model/ShapeItems/GradientFill.hpp" +#include "Lottie/Private/Model/Objects/DashElement.hpp" + +#include +#include + +namespace lottie { + +class AnyValue { +public: + enum class Type { + Double, + Vector1D, + Vector2D, + Vector3D, + Color, + BezierPath, + TextDocument, + GradientColorSet, + DashPattern + }; + +public: + AnyValue(double value) : + _type(Type::Double), + _doubleValue(value) { + } + + AnyValue(Vector1D const &value) : + _type(Type::Vector1D), + _vector1DValue(value) { + } + + AnyValue(Vector2D const &value) : + _type(Type::Vector2D), + _vector2DValue(value) { + } + + AnyValue(Vector3D const & value) : + _type(Type::Vector3D), + _vector3DValue(value) { + } + + AnyValue(Color const &value) : + _type(Type::Color), + _colorValue(value) { + } + + AnyValue(BezierPath const &value) : + _type(Type::BezierPath), + _bezierPathValue(value) { + } + + AnyValue(TextDocument const &value) : + _type(Type::TextDocument), + _textDocumentValue(value) { + } + + AnyValue(GradientColorSet const &value) : + _type(Type::GradientColorSet), + _gradientColorSetValue(value) { + } + + AnyValue(DashPattern const &value) : + _type(Type::DashPattern), + _dashPatternValue(value) { + } + + template::value>> + double get() { + return asDouble(); + } + + template::value>> + Vector1D get() { + return asVector1D(); + } + + template::value>> + Vector2D get() { + return asVector2D(); + } + + template::value>> + Vector3D get() { + return asVector3D(); + } + + template::value>> + Color get() { + return asColor(); + } + + template::value>> + BezierPath get() { + return asBezierPath(); + } + + template::value>> + TextDocument get() { + return asTextDocument(); + } + + template::value>> + GradientColorSet get() { + return asGradientColorSet(); + } + + template::value>> + DashPattern get() { + return asDashPattern(); + } + +public: + Type type() { + return _type; + } + + double asDouble() { + return _doubleValue.value(); + } + + Vector1D asVector1D() { + return _vector1DValue.value(); + } + + Vector2D asVector2D() { + return _vector2DValue.value(); + } + + Vector3D asVector3D() { + return _vector3DValue.value(); + } + + Color asColor() { + return _colorValue.value(); + } + + BezierPath asBezierPath() { + return _bezierPathValue.value(); + } + + TextDocument asTextDocument() { + return _textDocumentValue.value(); + } + + GradientColorSet asGradientColorSet() { + return _gradientColorSetValue.value(); + } + + DashPattern asDashPattern() { + return _dashPatternValue.value(); + } + +private: + Type _type; + + std::optional _doubleValue; + std::optional _vector1DValue; + std::optional _vector2DValue; + std::optional _vector3DValue; + std::optional _colorValue; + std::optional _bezierPathValue; + std::optional _textDocumentValue; + std::optional _gradientColorSetValue; + std::optional _dashPatternValue; +}; + +template +struct AnyValueType { +}; + +template<> +struct AnyValueType { + static AnyValue::Type type() { + return AnyValue::Type::Double; + } +}; + +template<> +struct AnyValueType { + static AnyValue::Type type() { + return AnyValue::Type::Vector1D; + } +}; + +template<> +struct AnyValueType { + static AnyValue::Type type() { + return AnyValue::Type::Vector2D; + } +}; + +template<> +struct AnyValueType { + static AnyValue::Type type() { + return AnyValue::Type::Vector3D; + } +}; + +template<> +struct AnyValueType { + static AnyValue::Type type() { + return AnyValue::Type::Color; + } +}; + +template<> +struct AnyValueType { + static AnyValue::Type type() { + return AnyValue::Type::BezierPath; + } +}; + +template<> +struct AnyValueType { + static AnyValue::Type type() { + return AnyValue::Type::TextDocument; + } +}; + +template<> +struct AnyValueType { + static AnyValue::Type type() { + return AnyValue::Type::GradientColorSet; + } +}; + +template<> +struct AnyValueType { + static AnyValue::Type type() { + return AnyValue::Type::DashPattern; + } +}; + +} + +#endif /* AnyValue_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CALayer.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CALayer.hpp new file mode 100644 index 0000000000..92cb586508 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CALayer.hpp @@ -0,0 +1,760 @@ +#ifndef CALayer_hpp +#define CALayer_hpp + +#include "Lottie/Public/Primitives/Color.hpp" +#include "Lottie/Public/Primitives/Vectors.hpp" +#include "Lottie/Public/Primitives/CGPath.hpp" +#include "Lottie/Private/Model/ShapeItems/Fill.hpp" +#include "Lottie/Private/Model/Layers/LayerModel.hpp" +#include "Lottie/Public/Primitives/DrawingAttributes.hpp" +#include "Lottie/Private/Model/ShapeItems/GradientFill.hpp" + +#include +#include +#include + +namespace lottie { + +enum class CGBlendMode { + Normal, + DestinationIn, + DestinationOut +}; + +class CGImage { +public: + virtual ~CGImage() = default; +}; + +class CGGradient { +public: + CGGradient(std::vector const &colors, std::vector const &locations) : + _colors(colors), + _locations(locations) { + assert(_colors.size() == _locations.size()); + } + + std::vector const &colors() const { + return _colors; + } + + std::vector const &locations() const { + return _locations; + } + +private: + std::vector _colors; + std::vector _locations; +}; + +class CGContext { +public: + virtual ~CGContext() = default; + + virtual int width() const = 0; + virtual int height() const = 0; + + virtual std::shared_ptr makeLayer(int width, int height) = 0; + + virtual void saveState() = 0; + virtual void restoreState() = 0; + + virtual void fillPath(std::shared_ptr const &path, FillRule fillRule, Color const &color) = 0; + virtual void linearGradientFillPath(std::shared_ptr const &path, FillRule fillRule, CGGradient const &gradient, Vector2D const &start, Vector2D const &end) = 0; + virtual void radialGradientFillPath(std::shared_ptr const &path, FillRule fillRule, CGGradient const &gradient, Vector2D const &startCenter, double startRadius, Vector2D const &endCenter, double endRadius) = 0; + + virtual void strokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, Color const &color) = 0; + virtual void linearGradientStrokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, CGGradient const &gradient, Vector2D const &start, Vector2D const &end) = 0; + virtual void radialGradientStrokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, CGGradient const &gradient, Vector2D const &startCenter, double startRadius, Vector2D const &endCenter, double endRadius) = 0; + + virtual void fill(CGRect const &rect, Color const &fillColor) = 0; + virtual void setBlendMode(CGBlendMode blendMode) = 0; + + virtual void setAlpha(double alpha) = 0; + + virtual void concatenate(CATransform3D const &transform) = 0; + + virtual void draw(std::shared_ptr const &other, CGRect const &rect) = 0; +}; + +class RenderableItem { +public: + enum class Type { + Shape, + GradientFill + }; + +public: + RenderableItem() { + } + + virtual ~RenderableItem() = default; + + virtual Type type() const = 0; + virtual CGRect boundingRect() const = 0; + + virtual bool isEqual(std::shared_ptr rhs) const = 0; +}; + +class ShapeRenderableItem: public RenderableItem { +public: + struct Fill { + Color color; + FillRule rule; + + Fill(Color color_, FillRule rule_) : + color(color_), rule(rule_) { + } + + bool operator==(Fill const &rhs) const { + if (color != rhs.color) { + return false; + } + if (rule != rhs.rule) { + return false; + } + return true; + } + + bool operator!=(Fill const &rhs) const { + return !(*this == rhs); + } + }; + + struct Stroke { + Color color; + double lineWidth = 0.0; + LineJoin lineJoin = LineJoin::Round; + LineCap lineCap = LineCap::Square; + double dashPhase = 0.0; + std::vector dashPattern; + + Stroke( + Color color_, + double lineWidth_, + LineJoin lineJoin_, + LineCap lineCap_, + double dashPhase_, + std::vector dashPattern_ + ) : + color(color_), + lineWidth(lineWidth_), + lineJoin(lineJoin_), + lineCap(lineCap_), + dashPhase(dashPhase_), + dashPattern(dashPattern_) { + } + + bool operator==(Stroke const &rhs) const { + if (color != rhs.color) { + return false; + } + if (lineWidth != rhs.lineWidth) { + return false; + } + if (lineJoin != rhs.lineJoin) { + return false; + } + if (lineCap != rhs.lineCap) { + return false; + } + if (dashPhase != rhs.dashPhase) { + return false; + } + if (dashPattern != rhs.dashPattern) { + return false; + } + return true; + } + + bool operator!=(Stroke const &rhs) const { + return !(*this == rhs); + } + }; + +public: + ShapeRenderableItem( + std::shared_ptr path_, + std::optional const &fill_, + std::optional const &stroke_ + ) : + path(path_), + fill(fill_), + stroke(stroke_) { + } + + virtual Type type() const override { + return Type::Shape; + } + + virtual CGRect boundingRect() const override { + if (path) { + CGRect shapeBounds = path->boundingBox(); + if (stroke) { + shapeBounds = shapeBounds.insetBy(-stroke->lineWidth / 2.0, -stroke->lineWidth / 2.0); + } + return shapeBounds; + } else { + return CGRect(0.0, 0.0, 0.0, 0.0); + } + } + + virtual bool isEqual(std::shared_ptr rhs) const override { + if (rhs->type() != type()) { + return false; + } + ShapeRenderableItem *other = (ShapeRenderableItem *)rhs.get(); + if ((path == nullptr) != (other->path == nullptr)) { + return false; + } else if (path) { + if (!path->isEqual(other->path.get())) { + return false; + } + } + if (fill != other->fill) { + return false; + } + if (stroke != other->stroke) { + return false; + } + return false; + } + +public: + std::shared_ptr path; + std::optional fill; + std::optional stroke; +}; + +class GradientFillRenderableItem: public RenderableItem { +public: + GradientFillRenderableItem( + std::shared_ptr path_, + FillRule pathFillRule_, + GradientType gradientType_, + std::vector const &colors_, + std::vector const &locations_, + Vector2D const &start_, + Vector2D const &end_, + CGRect bounds_ + ) : + path(path_), + pathFillRule(pathFillRule_), + gradientType(gradientType_), + colors(colors_), + locations(locations_), + start(start_), + end(end_), + bounds(bounds_) { + } + + virtual Type type() const override { + return Type::GradientFill; + } + + virtual CGRect boundingRect() const override { + return bounds; + } + + virtual bool isEqual(std::shared_ptr rhs) const override { + if (rhs->type() != type()) { + return false; + } + GradientFillRenderableItem *other = (GradientFillRenderableItem *)rhs.get(); + + if (gradientType != other->gradientType) { + return false; + } + if (colors != other->colors) { + return false; + } + if (locations != other->locations) { + return false; + } + if (start != other->start) { + return false; + } + if (end != other->end) { + return false; + } + if (bounds != other->bounds) { + return false; + } + + return true; + } + +public: + std::shared_ptr path; + FillRule pathFillRule; + GradientType gradientType; + std::vector colors; + std::vector locations; + Vector2D start; + Vector2D end; + CGRect bounds; +}; + +class RenderTreeNodeContent { +public: + enum class ShadingType { + Solid, + Gradient + }; + + class Shading { + public: + Shading() { + } + + virtual ~Shading() = default; + + virtual ShadingType type() const = 0; + }; + + class SolidShading: public Shading { + public: + SolidShading(Color const &color_, double opacity_) : + color(color_), + opacity(opacity_) { + } + + virtual ShadingType type() const override { + return ShadingType::Solid; + } + + public: + Color color; + double opacity = 0.0; + }; + + class GradientShading: public Shading { + public: + GradientShading( + double opacity_, + GradientType gradientType_, + std::vector const &colors_, + std::vector const &locations_, + Vector2D const &start_, + Vector2D const &end_ + ) : + opacity(opacity_), + gradientType(gradientType_), + colors(colors_), + locations(locations_), + start(start_), + end(end_) { + } + + virtual ShadingType type() const override { + return ShadingType::Gradient; + } + + public: + double opacity = 0.0; + GradientType gradientType; + std::vector colors; + std::vector locations; + Vector2D start; + Vector2D end; + }; + + struct Stroke { + std::shared_ptr shading; + double lineWidth = 0.0; + LineJoin lineJoin = LineJoin::Round; + LineCap lineCap = LineCap::Square; + double miterLimit = 4.0; + double dashPhase = 0.0; + std::vector dashPattern; + + Stroke( + std::shared_ptr shading_, + double lineWidth_, + LineJoin lineJoin_, + LineCap lineCap_, + double miterLimit_, + double dashPhase_, + std::vector dashPattern_ + ) : + shading(shading_), + lineWidth(lineWidth_), + lineJoin(lineJoin_), + lineCap(lineCap_), + miterLimit(miterLimit_), + dashPhase(dashPhase_), + dashPattern(dashPattern_) { + } + }; + + struct Fill { + std::shared_ptr shading; + FillRule rule; + + Fill( + std::shared_ptr shading_, + FillRule rule_ + ) : + shading(shading_), + rule(rule_) { + } + }; + +public: + RenderTreeNodeContent( + std::vector paths_, + std::shared_ptr stroke_, + std::shared_ptr fill_ + ) : + paths(paths_), + stroke(stroke_), + fill(fill_) { + } + +public: + std::vector paths; + std::shared_ptr stroke; + std::shared_ptr fill; +}; + +class RenderTreeNode { +public: + RenderTreeNode( + CGRect bounds_, + Vector2D position_, + CATransform3D transform_, + double alpha_, + bool masksToBounds_, + bool isHidden_, + std::shared_ptr content_, + std::vector> subnodes_, + std::shared_ptr mask_, + bool invertMask_ + ) : + _bounds(bounds_), + _position(position_), + _transform(transform_), + _alpha(alpha_), + _masksToBounds(masksToBounds_), + _isHidden(isHidden_), + _content(content_), + _subnodes(subnodes_), + _mask(mask_), + _invertMask(invertMask_) { + } + + ~RenderTreeNode() { + } + +public: + CGRect const &bounds() const { + return _bounds; + } + + Vector2D const &position() const { + return _position; + } + + CATransform3D const &transform() const { + return _transform; + } + + double alpha() const { + return _alpha; + } + + bool masksToBounds() const { + return _masksToBounds; + } + + bool isHidden() const { + return _isHidden; + } + + std::shared_ptr const &content() const { + return _content; + } + + std::vector> const &subnodes() const { + return _subnodes; + } + + std::shared_ptr const &mask() const { + return _mask; + } + + bool invertMask() const { + return _invertMask; + } + +public: + CGRect _bounds; + Vector2D _position; + CATransform3D _transform = CATransform3D::identity(); + double _alpha = 1.0; + bool _masksToBounds = false; + bool _isHidden = false; + std::shared_ptr _content; + std::vector> _subnodes; + std::shared_ptr _mask; + bool _invertMask = false; +}; + +class CALayer: public std::enable_shared_from_this { +public: + CALayer() { + } + + void addSublayer(std::shared_ptr layer) { + if (layer->_superlayer) { + layer->_superlayer->removeSublayer(layer.get()); + } + layer->_superlayer = this; + _sublayers.push_back(layer); + } + + void insertSublayer(std::shared_ptr layer, int index) { + if (layer->_superlayer) { + layer->_superlayer->removeSublayer(layer.get()); + } + layer->_superlayer = this; + _sublayers.insert(_sublayers.begin() + index, layer); + } + + void removeFromSuperlayer() { + if (_superlayer) { + _superlayer->removeSublayer(this); + } + } + + bool needsDisplay() const { + return _needsDisplay; + } + void setNeedsDisplay(bool needsDisplay) { + _needsDisplay = true; + } + + virtual bool implementsDraw() const { + return false; + } + + virtual bool isInvertedMatte() const { + return false; + } + + virtual void draw(std::shared_ptr const &context) { + } + + virtual std::shared_ptr renderableItem() { + return nullptr; + } + + bool isHidden() const { + return _isHidden; + } + void setIsHidden(bool isHidden) { + _isHidden = isHidden; + } + + float opacity() const { + return _opacity; + } + void setOpacity(float opacity) { + _opacity = opacity; + } + + Vector2D const &position() const { + return _position; + } + void setPosition(Vector2D const &position) { + _position = position; + } + + CGRect const &bounds() const { + return _bounds; + } + void setBounds(CGRect const &bounds) { + _bounds = bounds; + } + + virtual CGRect effectiveBounds() const { + return bounds(); + } + + CATransform3D const &transform() const { + return _transform; + } + void setTransform(CATransform3D const &transform) { + _transform = transform; + } + + std::shared_ptr const &mask() const { + return _mask; + } + void setMask(std::shared_ptr mask) { + _mask = mask; + } + + bool masksToBounds() const { + return _masksToBounds; + } + void setMasksToBounds(bool masksToBounds) { + _masksToBounds = masksToBounds; + } + + std::vector> const &sublayers() const { + return _sublayers; + } + + std::optional const &compositingFilter() const { + return _compositingFilter; + } + void setCompositingFilter(std::optional const &compositingFilter) { + _compositingFilter = compositingFilter; + } + + std::shared_ptr const &contents() const { + return _contents; + } + void setContents(std::shared_ptr contents) { + _contents = contents; + } + +protected: + template + std::shared_ptr shared_from_base() { + return std::static_pointer_cast(shared_from_this()); + } + +private: + void removeSublayer(CALayer *layer) { + for (auto it = _sublayers.begin(); it != _sublayers.end(); it++) { + if (it->get() == layer) { + layer->_superlayer = nullptr; + _sublayers.erase(it); + break; + } + } + } + +private: + CALayer *_superlayer = nullptr; + std::vector> _sublayers; + bool _needsDisplay = false; + bool _isHidden = false; + float _opacity = 1.0; + Vector2D _position = Vector2D(0.0, 0.0); + CGRect _bounds = CGRect(0.0, 0.0, 0.0, 0.0); + CATransform3D _transform = CATransform3D::identity(); + std::shared_ptr _mask; + bool _masksToBounds = false; + std::optional _compositingFilter; + std::shared_ptr _contents; +}; + +class CAShapeLayer: public CALayer { +public: + CAShapeLayer() { + } + + std::optional const &strokeColor() { + return _strokeColor; + } + void setStrokeColor(std::optional const &strokeColor) { + _strokeColor = strokeColor; + } + + std::optional const &fillColor() { + return _fillColor; + } + void setFillColor(std::optional const &fillColor) { + _fillColor = fillColor; + } + + FillRule fillRule() { + return _fillRule; + } + void setFillRule(FillRule fillRule) { + _fillRule = fillRule; + } + + std::shared_ptr const &path() const { + return _path; + } + void setPath(std::shared_ptr const &path) { + _path = path; + } + + double lineWidth() const { + return _lineWidth; + } + void setLineWidth(double lineWidth) { + _lineWidth = lineWidth; + } + + LineJoin lineJoin() const { + return _lineJoin; + } + void setLineJoin(LineJoin lineJoin) { + _lineJoin = lineJoin; + } + + LineCap lineCap() const { + return _lineCap; + } + void setLineCap(LineCap lineCap) { + _lineCap = lineCap; + } + + double lineDashPhase() const { + return _lineDashPhase; + } + void setLineDashPhase(double lineDashPhase) { + _lineDashPhase = lineDashPhase; + } + + std::vector const &dashPattern() const { + return _dashPattern; + } + void setDashPattern(std::vector const &dashPattern) { + _dashPattern = dashPattern; + } + + virtual CGRect effectiveBounds() const override { + if (_path) { + CGRect boundingBox = _path->boundingBox(); + if (_strokeColor) { + boundingBox.x -= _lineWidth / 2.0; + boundingBox.y -= _lineWidth / 2.0; + boundingBox.width += _lineWidth; + boundingBox.height += _lineWidth; + } + return boundingBox; + } else { + return CGRect(0.0, 0.0, 0.0, 0.0); + } + } + + /*virtual bool implementsDraw() const override { + return true; + } + + virtual void draw(std::shared_ptr const &context) override;*/ + + std::shared_ptr renderableItem() override; + +private: + std::optional _strokeColor; + std::optional _fillColor = Color(0.0, 0.0, 0.0, 1.0); + FillRule _fillRule = FillRule::NonZeroWinding; + std::shared_ptr _path; + double _lineWidth = 1.0; + LineJoin _lineJoin = LineJoin::Miter; + LineCap _lineCap = LineCap::Butt; + double _lineDashPhase = 0.0; + std::vector _dashPattern; +}; + +} + +#endif /* CALayer_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CALayer.mm b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CALayer.mm similarity index 100% rename from submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CALayer.mm rename to Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CALayer.mm diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CALayerCocoa.h b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CALayerCocoa.h similarity index 100% rename from submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CALayerCocoa.h rename to Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CALayerCocoa.h diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGContextSkiaImpl.h b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGContextSkiaImpl.h new file mode 100644 index 0000000000..ef22613483 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGContextSkiaImpl.h @@ -0,0 +1,51 @@ +#ifndef CGContextSkiaImpl_h +#define CGContextSkiaImpl_h + +#include "Lottie/Public/Primitives/CALayer.hpp" + +#include "include/core/SkCanvas.h" +#include "include/core/SkSurface.h" + +namespace lottie { + +class CGContextSkiaImpl: public CGContext { +public: + CGContextSkiaImpl(int width, int height); + virtual ~CGContextSkiaImpl(); + + virtual int width() const override; + virtual int height() const override; + + virtual std::shared_ptr makeLayer(int width, int height) override; + + virtual void saveState() override; + virtual void restoreState() override; + + virtual void fillPath(std::shared_ptr const &path, FillRule fillRule, Color const &color) override; + virtual void linearGradientFillPath(std::shared_ptr const &path, FillRule fillRule, CGGradient const &gradient, Vector2D const &start, Vector2D const &end) override; + virtual void radialGradientFillPath(std::shared_ptr const &path, FillRule fillRule, CGGradient const &gradient, Vector2D const &startCenter, double startRadius, Vector2D const &endCenter, double endRadius) override; + virtual void strokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, Color const &color) override; + virtual void fill(CGRect const &rect, Color const &fillColor) override; + + virtual void setBlendMode(CGBlendMode blendMode) override; + + virtual void setAlpha(double alpha) override; + + virtual void concatenate(CATransform3D const &transform) override; + + virtual void draw(std::shared_ptr const &other, CGRect const &rect) override; + + sk_sp surface() const; + +private: + int _width = 0; + int _height = 0; + sk_sp _surface; + SkCanvas *_canvas = nullptr; + SkBlendMode _blendMode = SkBlendMode::kSrcOver; + double _alpha = 1.0; +}; + +} + +#endif diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGContextSkiaImpl.mm b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGContextSkiaImpl.mm new file mode 100644 index 0000000000..422327c18e --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGContextSkiaImpl.mm @@ -0,0 +1,286 @@ +#include "Lottie/Public/Primitives/CGContextSkiaImpl.h" + +#include "include/core/SkCanvas.h" +#include "include/core/SkColor.h" +#include "include/core/SkFont.h" +#include "include/core/SkFontTypes.h" +#include "include/core/SkGraphics.h" +#include "include/core/SkPaint.h" +#include "include/core/SkPoint.h" +#include "include/core/SkRect.h" +#include "include/core/SkShader.h" +#include "include/core/SkString.h" +#include "include/core/SkSurface.h" +#include "include/core/SkTileMode.h" +#include "include/core/SkPath.h" +#include "include/core/SkPathEffect.h" +#include "include/effects/SkDashPathEffect.h" +#include "include/effects/SkGradientShader.h" + +#include "Lottie/Public/Primitives/CALayerCocoa.h" +#include "Lottie/Public/Primitives/CGPathCocoa.h" + +namespace lottie { + +namespace { + +SkColor skColor(Color const &color) { + return SkColorSetARGB((uint8_t)(color.a * 255.0), (uint8_t)(color.r * 255.0), (uint8_t)(color.g * 255.0), (uint8_t)(color.b * 255.0)); +} + +void skPath(std::shared_ptr const &path, SkPath &nativePath) { + path->enumerate([&nativePath](CGPathItem const &item) { + switch (item.type) { + case CGPathItem::Type::MoveTo: { + nativePath.moveTo(item.points[0].x, item.points[0].y); + break; + } + case CGPathItem::Type::LineTo: { + nativePath.lineTo(item.points[0].x, item.points[0].y); + break; + } + case CGPathItem::Type::CurveTo: { + nativePath.cubicTo(item.points[0].x, item.points[0].y, item.points[1].x, item.points[1].y, item.points[2].x, item.points[2].y); + break; + } + case CGPathItem::Type::Close: { + nativePath.close(); + break; + } + } + }); +} + +} + +CGContextSkiaImpl::CGContextSkiaImpl(int width, int height) : +_width(width), _height(height) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + SkGraphics::Init(); + }); + + //static sk_sp sharedSurface = SkSurface::MakeRasterN32Premul(width, height); + //_surface = sharedSurface; + + _surface = SkSurface::MakeRasterN32Premul(width, height); + + _canvas = _surface->getCanvas(); + _canvas->resetMatrix(); + _canvas->clear(SkColorSetARGB(0, 0, 0, 0)); +} + +CGContextSkiaImpl::~CGContextSkiaImpl() { +} + +int CGContextSkiaImpl::width() const { + return _width; +} + +int CGContextSkiaImpl::height() const { + return _height; +} + +std::shared_ptr CGContextSkiaImpl::makeLayer(int width, int height) { + return std::make_shared(width, height); +} + +void CGContextSkiaImpl::saveState() { + _canvas->save(); +} + +void CGContextSkiaImpl::restoreState() { + _canvas->restore(); +} + +void CGContextSkiaImpl::fillPath(std::shared_ptr const &path, FillRule fillRule, Color const &color) { + SkPaint paint; + paint.setColor(skColor(color)); + paint.setAlphaf(_alpha); + paint.setAntiAlias(true); + paint.setBlendMode(_blendMode); + + SkPath nativePath; + skPath(path, nativePath); + nativePath.setFillType(fillRule == FillRule::EvenOdd ? SkPathFillType::kEvenOdd : SkPathFillType::kWinding); + + _canvas->drawPath(nativePath, paint); +} + +void CGContextSkiaImpl::linearGradientFillPath(std::shared_ptr const &path, FillRule fillRule, CGGradient const &gradient, Vector2D const &start, Vector2D const &end) { + SkPaint paint; + paint.setAntiAlias(true); + paint.setBlendMode(_blendMode); + paint.setDither(false); + paint.setStyle(SkPaint::Style::kFill_Style); + + SkPoint linearPoints[2] = { + SkPoint::Make(start.x, start.y), + SkPoint::Make(end.x, end.y) + }; + + std::vector colors; + for (const auto &color : gradient.colors()) { + colors.push_back(skColor(Color(color.r, color.g, color.b, color.a * _alpha))); + } + + std::vector locations; + for (auto location : gradient.locations()) { + locations.push_back(location); + } + + paint.setShader(SkGradientShader::MakeLinear(linearPoints, colors.data(), locations.data(), (int)colors.size(), SkTileMode::kMirror)); + + SkPath nativePath; + skPath(path, nativePath); + nativePath.setFillType(fillRule == FillRule::EvenOdd ? SkPathFillType::kEvenOdd : SkPathFillType::kWinding); + + _canvas->drawPath(nativePath, paint); +} + +void CGContextSkiaImpl::radialGradientFillPath(std::shared_ptr const &path, FillRule fillRule, CGGradient const &gradient, Vector2D const &startCenter, double startRadius, Vector2D const &endCenter, double endRadius) { + SkPaint paint; + paint.setAntiAlias(true); + paint.setBlendMode(_blendMode); + paint.setDither(false); + paint.setStyle(SkPaint::Style::kFill_Style); + + std::vector colors; + for (const auto &color : gradient.colors()) { + colors.push_back(skColor(Color(color.r, color.g, color.b, color.a * _alpha))); + } + + std::vector locations; + for (auto location : gradient.locations()) { + locations.push_back(location); + } + + paint.setShader(SkGradientShader::MakeRadial(SkPoint::Make(startCenter.x, startCenter.y), endRadius, colors.data(), locations.data(), (int)colors.size(), SkTileMode::kMirror)); + + SkPath nativePath; + skPath(path, nativePath); + nativePath.setFillType(fillRule == FillRule::EvenOdd ? SkPathFillType::kEvenOdd : SkPathFillType::kWinding); + + _canvas->drawPath(nativePath, paint); +} + +void CGContextSkiaImpl::strokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, Color const &color) { + SkPaint paint; + paint.setAntiAlias(true); + paint.setBlendMode(_blendMode); + paint.setColor(skColor(color)); + paint.setAlphaf(_alpha); + paint.setStyle(SkPaint::Style::kStroke_Style); + + paint.setStrokeWidth(lineWidth); + switch (lineJoin) { + case LineJoin::Miter: { + paint.setStrokeJoin(SkPaint::Join::kMiter_Join); + break; + } + case LineJoin::Round: { + paint.setStrokeJoin(SkPaint::Join::kRound_Join); + break; + } + case LineJoin::Bevel: { + paint.setStrokeJoin(SkPaint::Join::kBevel_Join); + break; + } + default: { + paint.setStrokeJoin(SkPaint::Join::kBevel_Join); + break; + } + } + + switch (lineCap) { + case LineCap::Butt: { + paint.setStrokeCap(SkPaint::Cap::kButt_Cap); + break; + } + case LineCap::Round: { + paint.setStrokeCap(SkPaint::Cap::kRound_Cap); + break; + } + case LineCap::Square: { + paint.setStrokeCap(SkPaint::Cap::kSquare_Cap); + break; + } + default: { + paint.setStrokeCap(SkPaint::Cap::kSquare_Cap); + break; + } + } + + if (!dashPattern.empty()) { + std::vector intervals; + intervals.reserve(dashPattern.size()); + for (auto value : dashPattern) { + intervals.push_back(value); + } + paint.setPathEffect(SkDashPathEffect::Make(intervals.data(), (int)intervals.size(), dashPhase)); + } + + SkPath nativePath; + skPath(path, nativePath); + + _canvas->drawPath(nativePath, paint); +} + +void CGContextSkiaImpl::fill(CGRect const &rect, Color const &fillColor) { + SkPaint paint; + paint.setAntiAlias(true); + paint.setColor(skColor(fillColor)); + paint.setAlphaf(_alpha); + paint.setBlendMode(_blendMode); + + _canvas->drawRect(SkRect::MakeXYWH(rect.x, rect.y, rect.width, rect.height), paint); +} + +void CGContextSkiaImpl::setBlendMode(CGBlendMode blendMode) { + switch (blendMode) { + case CGBlendMode::Normal: { + _blendMode = SkBlendMode::kSrcOver; + break; + } + case CGBlendMode::DestinationIn: { + _blendMode = SkBlendMode::kDstIn; + break; + } + case CGBlendMode::DestinationOut: { + _blendMode = SkBlendMode::kDstOut; + break; + } + default: { + _blendMode = SkBlendMode::kSrcOver; + break; + } + } +} + +void CGContextSkiaImpl::setAlpha(double alpha) { + _alpha = alpha; +} + +void CGContextSkiaImpl::concatenate(CATransform3D const &transform) { + _canvas->concat(SkM44( + transform.m11, transform.m21, transform.m31, transform.m41, + transform.m12, transform.m22, transform.m32, transform.m42, + transform.m13, transform.m23, transform.m33, transform.m43, + transform.m14, transform.m24, transform.m34, transform.m44 + )); +} + +void CGContextSkiaImpl::draw(std::shared_ptr const &other, CGRect const &rect) { + CGContextSkiaImpl *impl = (CGContextSkiaImpl *)other.get(); + auto image = impl->surface()->makeImageSnapshot(); + SkPaint paint; + paint.setBlendMode(_blendMode); + paint.setAlphaf(_alpha); + _canvas->drawImageRect(image.get(), SkRect::MakeXYWH(rect.x, rect.y, rect.width, rect.height), SkSamplingOptions(SkFilterMode::kLinear), &paint); +} + +sk_sp CGContextSkiaImpl::surface() const { + return _surface; +} + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGContextTVGImpl.h b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGContextTVGImpl.h new file mode 100644 index 0000000000..c6a50cb381 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGContextTVGImpl.h @@ -0,0 +1,65 @@ +#ifndef CGContextTVGImpl_h +#define CGContextTVGImpl_h + +#include "Lottie/Public/Primitives/CALayer.hpp" + +#include "thorvg.h" + +namespace lottie { + +class CGContextTVGImpl: public CGContext { +public: + CGContextTVGImpl(int width, int height); + virtual ~CGContextTVGImpl(); + + virtual int width() const override; + virtual int height() const override; + + virtual std::shared_ptr makeLayer(int width, int height) override; + + virtual void saveState() override; + virtual void restoreState() override; + + virtual void fillPath(std::shared_ptr const &path, FillRule fillRule, Color const &color) override; + virtual void linearGradientFillPath(std::shared_ptr const &path, FillRule fillRule, CGGradient const &gradient, Vector2D const &start, Vector2D const &end) override; + virtual void radialGradientFillPath(std::shared_ptr const &path, FillRule fillRule, CGGradient const &gradient, Vector2D const &startCenter, double startRadius, Vector2D const &endCenter, double endRadius) override; + virtual void strokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, Color const &color) override; + virtual void linearGradientStrokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, CGGradient const &gradient, Vector2D const &start, Vector2D const &end) override; + virtual void radialGradientStrokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, CGGradient const &gradient, Vector2D const &startCenter, double startRadius, Vector2D const &endCenter, double endRadius) override; + virtual void fill(CGRect const &rect, Color const &fillColor) override; + + virtual void setBlendMode(CGBlendMode blendMode) override; + + virtual void setAlpha(double alpha) override; + + virtual void concatenate(CATransform3D const &transform) override; + + virtual void draw(std::shared_ptr const &other, CGRect const &rect) override; + + uint32_t *backingData() { + return _backingData; + } + + int bytesPerRow() const { + return _bytesPerRow; + } + + void flush(); + +private: + int _width = 0; + int _height = 0; + std::unique_ptr _canvas; + + //SkBlendMode _blendMode = SkBlendMode::kSrcOver; + double _alpha = 1.0; + CATransform3D _transform; + std::vector _stateStack; + int _bytesPerRow = 0; + uint32_t *_backingData = nullptr; + int _statsNumStrokes = 0; +}; + +} + +#endif diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGContextTVGImpl.mm b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGContextTVGImpl.mm new file mode 100644 index 0000000000..5db9f3578e --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGContextTVGImpl.mm @@ -0,0 +1,293 @@ +#include "Lottie/Public/Primitives/CGContextTVGImpl.h" + +#include "Lottie/Public/Primitives/CALayerCocoa.h" +#include "Lottie/Public/Primitives/VectorsCocoa.h" + +namespace lottie { + +namespace { + +void tvgPath(std::shared_ptr const &path, tvg::Shape *shape) { + path->enumerate([shape](CGPathItem const &item) { + switch (item.type) { + case CGPathItem::Type::MoveTo: { + shape->moveTo(item.points[0].x, item.points[0].y); + break; + } + case CGPathItem::Type::LineTo: { + shape->lineTo(item.points[0].x, item.points[0].y); + break; + } + case CGPathItem::Type::CurveTo: { + shape->cubicTo(item.points[0].x, item.points[0].y, item.points[1].x, item.points[1].y, item.points[2].x, item.points[2].y); + break; + } + case CGPathItem::Type::Close: { + shape->close(); + break; + } + } + }); +} + +tvg::Matrix tvgTransform(CATransform3D const &transform) { + CGAffineTransform affineTransform = CATransform3DGetAffineTransform(nativeTransform(transform)); + tvg::Matrix result; + result.e11 = affineTransform.a; + result.e21 = affineTransform.b; + result.e31 = 0.0f; + result.e12 = affineTransform.c; + result.e22 = affineTransform.d; + result.e32 = 0.0f; + result.e13 = affineTransform.tx; + result.e23 = affineTransform.ty; + result.e33 = 1.0f; + return result; +} + +} + +CGContextTVGImpl::CGContextTVGImpl(int width, int height) : +_width(width), _height(height), _transform(CATransform3D::identity()) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + tvg::Initializer::init(tvg::CanvasEngine::Sw, 0); + }); + + _canvas = tvg::SwCanvas::gen(); + + _bytesPerRow = width * 4; + + static uint32_t *sharedBackingData = (uint32_t *)malloc(_bytesPerRow * height); + _backingData = sharedBackingData; + + _canvas->target(_backingData, _bytesPerRow / 4, width, height, tvg::SwCanvas::ARGB8888); +} + +CGContextTVGImpl::~CGContextTVGImpl() { +} + +int CGContextTVGImpl::width() const { + return _width; +} + +int CGContextTVGImpl::height() const { + return _height; +} + +std::shared_ptr CGContextTVGImpl::makeLayer(int width, int height) { + return std::make_shared(width, height); +} + +void CGContextTVGImpl::saveState() { + _stateStack.push_back(_transform); +} + +void CGContextTVGImpl::restoreState() { + if (_stateStack.empty()) { + assert(false); + return; + } + _transform = _stateStack[_stateStack.size() - 1]; + _stateStack.pop_back(); +} + +void CGContextTVGImpl::fillPath(std::shared_ptr const &path, FillRule fillRule, Color const &color) { + auto shape = tvg::Shape::gen(); + tvgPath(path, shape.get()); + + shape->transform(tvgTransform(_transform)); + + shape->fill((int)(color.r * 255.0), (int)(color.g * 255.0), (int)(color.b * 255.0), (int)(color.a * _alpha * 255.0)); + shape->fill(fillRule == FillRule::EvenOdd ? tvg::FillRule::EvenOdd : tvg::FillRule::Winding); + + _canvas->push(std::move(shape)); +} + +void CGContextTVGImpl::linearGradientFillPath(std::shared_ptr const &path, FillRule fillRule, CGGradient const &gradient, Vector2D const &start, Vector2D const &end) { + auto shape = tvg::Shape::gen(); + tvgPath(path, shape.get()); + + shape->transform(tvgTransform(_transform)); + + auto fill = tvg::LinearGradient::gen(); + fill->linear(start.x, start.y, end.x, end.y); + + std::vector colors; + for (size_t i = 0; i < gradient.colors().size(); i++) { + const auto &color = gradient.colors()[i]; + tvg::Fill::ColorStop colorStop; + colorStop.offset = gradient.locations()[i]; + colorStop.r = (int)(color.r * 255.0); + colorStop.g = (int)(color.g * 255.0); + colorStop.b = (int)(color.b * 255.0); + colorStop.a = (int)(color.a * _alpha * 255.0); + colors.push_back(colorStop); + } + fill->colorStops(colors.data(), (uint32_t)colors.size()); + shape->fill(std::move(fill)); + + shape->fill(fillRule == FillRule::EvenOdd ? tvg::FillRule::EvenOdd : tvg::FillRule::Winding); + + _canvas->push(std::move(shape)); +} + +void CGContextTVGImpl::radialGradientFillPath(std::shared_ptr const &path, FillRule fillRule, CGGradient const &gradient, Vector2D const &startCenter, double startRadius, Vector2D const &endCenter, double endRadius) { + auto shape = tvg::Shape::gen(); + tvgPath(path, shape.get()); + + shape->transform(tvgTransform(_transform)); + + auto fill = tvg::RadialGradient::gen(); + fill->radial(startCenter.x, startCenter.y, endRadius); + + std::vector colors; + for (size_t i = 0; i < gradient.colors().size(); i++) { + const auto &color = gradient.colors()[i]; + tvg::Fill::ColorStop colorStop; + colorStop.offset = gradient.locations()[i]; + colorStop.r = (int)(color.r * 255.0); + colorStop.g = (int)(color.g * 255.0); + colorStop.b = (int)(color.b * 255.0); + colorStop.a = (int)(color.a * _alpha * 255.0); + colors.push_back(colorStop); + } + fill->colorStops(colors.data(), (uint32_t)colors.size()); + shape->fill(std::move(fill)); + + shape->fill(fillRule == FillRule::EvenOdd ? tvg::FillRule::EvenOdd : tvg::FillRule::Winding); + + _canvas->push(std::move(shape)); +} + +void CGContextTVGImpl::strokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, Color const &color) { + auto shape = tvg::Shape::gen(); + tvgPath(path, shape.get()); + + shape->transform(tvgTransform(_transform)); + + shape->stroke((int)(color.r * 255.0), (int)(color.g * 255.0), (int)(color.b * 255.0), (int)(color.a * _alpha * 255.0)); + shape->stroke(lineWidth); + + switch (lineJoin) { + case LineJoin::Miter: { + shape->stroke(tvg::StrokeJoin::Miter); + break; + } + case LineJoin::Round: { + shape->stroke(tvg::StrokeJoin::Round); + break; + } + case LineJoin::Bevel: { + shape->stroke(tvg::StrokeJoin::Bevel); + break; + } + default: { + shape->stroke(tvg::StrokeJoin::Bevel); + break; + } + } + + switch (lineCap) { + case LineCap::Butt: { + shape->stroke(tvg::StrokeCap::Butt); + break; + } + case LineCap::Round: { + shape->stroke(tvg::StrokeCap::Round); + break; + } + case LineCap::Square: { + shape->stroke(tvg::StrokeCap::Square); + break; + } + default: { + shape->stroke(tvg::StrokeCap::Square); + break; + } + } + + if (!dashPattern.empty()) { + std::vector intervals; + intervals.reserve(dashPattern.size()); + for (auto value : dashPattern) { + intervals.push_back(value); + } + shape->stroke(intervals.data(), (uint32_t)intervals.size()); + //TODO:phase + } + + _canvas->push(std::move(shape)); +} + +void CGContextTVGImpl::linearGradientStrokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, CGGradient const &gradient, Vector2D const &start, Vector2D const &end) { + assert(false); +} + +void CGContextTVGImpl::radialGradientStrokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, CGGradient const &gradient, Vector2D const &startCenter, double startRadius, Vector2D const &endCenter, double endRadius) { + assert(false); +} + +void CGContextTVGImpl::fill(CGRect const &rect, Color const &fillColor) { + auto shape = tvg::Shape::gen(); + shape->appendRect(rect.x, rect.y, rect.width, rect.height, 0.0f, 0.0f); + + shape->transform(tvgTransform(_transform)); + + shape->fill((int)(fillColor.r * 255.0), (int)(fillColor.g * 255.0), (int)(fillColor.b * 255.0), (int)(fillColor.a * _alpha * 255.0)); + + _canvas->push(std::move(shape)); +} + +void CGContextTVGImpl::setBlendMode(CGBlendMode blendMode) { + /*switch (blendMode) { + case CGBlendMode::Normal: { + _blendMode = SkBlendMode::kSrcOver; + break; + } + case CGBlendMode::DestinationIn: { + _blendMode = SkBlendMode::kDstIn; + break; + } + case CGBlendMode::DestinationOut: { + _blendMode = SkBlendMode::kDstOut; + break; + } + default: { + _blendMode = SkBlendMode::kSrcOver; + break; + } + }*/ +} + +void CGContextTVGImpl::setAlpha(double alpha) { + _alpha = alpha; +} + +void CGContextTVGImpl::concatenate(CATransform3D const &transform) { + _transform = transform * _transform; + /*_canvas->concat(SkM44( + transform.m11, transform.m21, transform.m31, transform.m41, + transform.m12, transform.m22, transform.m32, transform.m42, + transform.m13, transform.m23, transform.m33, transform.m43, + transform.m14, transform.m24, transform.m34, transform.m44 + ));*/ +} + +void CGContextTVGImpl::draw(std::shared_ptr const &other, CGRect const &rect) { + /*CGContextTVGImpl *impl = (CGContextTVGImpl *)other.get(); + auto image = impl->surface()->makeImageSnapshot(); + SkPaint paint; + paint.setBlendMode(_blendMode); + paint.setAlphaf(_alpha); + _canvas->drawImageRect(image.get(), SkRect::MakeXYWH(rect.x, rect.y, rect.width, rect.height), SkSamplingOptions(SkFilterMode::kLinear), &paint);*/ +} + +void CGContextTVGImpl::flush() { + _canvas->draw(); + _canvas->sync(); + + _statsNumStrokes = 0; +} + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGPath.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGPath.cpp new file mode 100644 index 0000000000..7b352cf13b --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGPath.cpp @@ -0,0 +1,194 @@ +#include "CGPath.hpp" + +#include + +namespace lottie { + +namespace { + +void addPointToBoundingRect(bool *isFirst, CGRect *rect, Vector2D const *point) { + if (*isFirst) { + *isFirst = false; + + rect->x = point->x; + rect->y = point->y; + rect->width = 0.0; + rect->height = 0.0; + + return; + } + if (point->x > rect->x + rect->width) { + rect->width = point->x - rect->x; + } + if (point->y > rect->y + rect->height) { + rect->height = point->y - rect->y; + } + if (point->x < rect->x) { + rect->width += rect->x - point->x; + rect->x = point->x; + } + if (point->y < rect->y) { + rect->height += rect->y - point->y; + rect->y = point->y; + } +} + +} + +Vector2D transformVector(Vector2D const &v, CATransform3D const &m) { + return Vector2D( + m.m11 * v.x + m.m21 * v.y + m.m41 * 1.0, + m.m12 * v.x + m.m22 * v.y + m.m42 * 1.0 + ); +} + +class CGPathImpl: public CGPath { +public: + CGPathImpl(); + virtual ~CGPathImpl(); + + virtual CGRect boundingBox() const override; + + virtual bool empty() const override; + + virtual std::shared_ptr copyUsingTransform(CATransform3D const &transform) const override; + + virtual void addLineTo(Vector2D const &point) override; + virtual void addCurveTo(Vector2D const &point, Vector2D const &control1, Vector2D const &control2) override; + virtual void moveTo(Vector2D const &point) override; + virtual void closeSubpath() override; + virtual void addRect(CGRect const &rect) override; + virtual void addPath(std::shared_ptr const &path) override; + virtual bool isEqual(CGPath *other) const override; + virtual void enumerate(std::function) override; + +private: + std::vector _items; +}; + +CGPathImpl::CGPathImpl() { +} + +CGPathImpl::~CGPathImpl() { +} + +CGRect CGPathImpl::boundingBox() const { + bool isFirst = true; + CGRect result(0.0, 0.0, 0.0, 0.0); + + for (const auto &item : _items) { + switch (item.type) { + case CGPathItem::Type::MoveTo: { + addPointToBoundingRect(&isFirst, &result, &item.points[0]); + break; + } + case CGPathItem::Type::LineTo: { + addPointToBoundingRect(&isFirst, &result, &item.points[0]); + break; + } + case CGPathItem::Type::CurveTo: { + addPointToBoundingRect(&isFirst, &result, &item.points[0]); + addPointToBoundingRect(&isFirst, &result, &item.points[1]); + addPointToBoundingRect(&isFirst, &result, &item.points[2]); + break; + } + case CGPathItem::Type::Close: { + break; + } + default: { + break; + } + } + } + + return result; +} + +bool CGPathImpl::empty() const { + return _items.empty(); +} + +std::shared_ptr CGPathImpl::copyUsingTransform(CATransform3D const &transform) const { + auto result = std::make_shared(); + + if (transform == CATransform3D::identity()) { + result->_items = _items; + return result; + } + + result->_items.reserve(_items.capacity()); + for (auto &sourceItem : _items) { + CGPathItem &item = result->_items.emplace_back(sourceItem.type); + item.points[0] = transformVector(sourceItem.points[0], transform); + item.points[1] = transformVector(sourceItem.points[1], transform); + item.points[2] = transformVector(sourceItem.points[2], transform); + } + + return result; +} + +void CGPathImpl::addLineTo(Vector2D const &point) { + CGPathItem &item = _items.emplace_back(CGPathItem::Type::LineTo); + item.points[0] = point; +} + +void CGPathImpl::addCurveTo(Vector2D const &point, Vector2D const &control1, Vector2D const &control2) { + CGPathItem &item = _items.emplace_back(CGPathItem::Type::CurveTo); + item.points[0] = control1; + item.points[1] = control2; + item.points[2] = point; +} + +void CGPathImpl::moveTo(Vector2D const &point) { + CGPathItem &item = _items.emplace_back(CGPathItem::Type::MoveTo); + item.points[0] = point; +} + +void CGPathImpl::closeSubpath() { + _items.emplace_back(CGPathItem::Type::Close); +} + +void CGPathImpl::addRect(CGRect const &rect) { + assert(false); + //CGPathAddRect(_path, nil, ::CGRectMake(rect.x, rect.y, rect.width, rect.height)); +} + +void CGPathImpl::addPath(std::shared_ptr const &path) { + if (_items.size() == 0) { + _items = std::static_pointer_cast(path)->_items; + } else { + size_t totalItemCount = _items.size() + std::static_pointer_cast(path)->_items.size(); + if (_items.capacity() < totalItemCount) { + _items.reserve(totalItemCount); + } + for (const auto &item : std::static_pointer_cast(path)->_items) { + _items.push_back(item); + } + } +} + +bool CGPathImpl::isEqual(CGPath *other) const { + if (_items.size() != ((CGPathImpl *)other)->_items.size()) { + return false; + } + + for (size_t i = 0; i < _items.size(); i++) { + if (_items[i] != ((CGPathImpl *)other)->_items[i]) { + return false; + } + } + + return true; +} + +void CGPathImpl::enumerate(std::function f) { + for (const auto &item : _items) { + f(item); + } +} + +std::shared_ptr CGPath::makePath() { + return std::static_pointer_cast(std::make_shared()); +} + +} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CGPath.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGPath.hpp similarity index 100% rename from submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CGPath.hpp rename to Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGPath.hpp diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGPath.mm b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGPath.mm new file mode 100644 index 0000000000..a7798daa9a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGPath.mm @@ -0,0 +1,240 @@ +#include "CGPath.hpp" +#include "Lottie/Public/Primitives/CGPathCocoa.h" + +#import + +namespace { + +void addPointToBoundingRect(bool *isFirst, CGRect *rect, CGPoint *point) { + if (*isFirst) { + *isFirst = false; + + rect->origin.x = point->x; + rect->origin.y = point->y; + rect->size.width = 0.0; + rect->size.height = 0.0; + + return; + } + if (point->x > rect->origin.x + rect->size.width) { + rect->size.width = point->x - rect->origin.x; + } + if (point->y > rect->origin.y + rect->size.height) { + rect->size.height = point->y - rect->origin.y; + } + if (point->x < rect->origin.x) { + rect->size.width += rect->origin.x - point->x; + rect->origin.x = point->x; + } + if (point->y < rect->origin.y) { + rect->size.height += rect->origin.y - point->y; + rect->origin.y = point->y; + } +} + +} + +CGRect calculatePathBoundingBox(CGPathRef path) { + __block CGRect result = CGRectMake(0.0, 0.0, 0.0, 0.0); + __block bool isFirst = true; + + CGPathApplyWithBlock(path, ^(const CGPathElement * _Nonnull element) { + switch (element->type) { + case kCGPathElementMoveToPoint: { + addPointToBoundingRect(&isFirst, &result, &element->points[0]); + break; + } + case kCGPathElementAddLineToPoint: { + addPointToBoundingRect(&isFirst, &result, &element->points[0]); + break; + } + case kCGPathElementAddCurveToPoint: { + addPointToBoundingRect(&isFirst, &result, &element->points[0]); + addPointToBoundingRect(&isFirst, &result, &element->points[1]); + addPointToBoundingRect(&isFirst, &result, &element->points[2]); + break; + } + case kCGPathElementAddQuadCurveToPoint: { + addPointToBoundingRect(&isFirst, &result, &element->points[0]); + addPointToBoundingRect(&isFirst, &result, &element->points[1]); + break; + } + case kCGPathElementCloseSubpath: { + break; + } + } + }); + + return result; +} + +namespace lottie { + +CGPathCocoaImpl::CGPathCocoaImpl() { + _path = CGPathCreateMutable(); +} + +CGPathCocoaImpl::CGPathCocoaImpl(CGMutablePathRef path) { + CFRetain(path); + _path = path; +} + +CGPathCocoaImpl::~CGPathCocoaImpl() { + CGPathRelease(_path); +} + +CGRect CGPathCocoaImpl::boundingBox() const { + auto rect = calculatePathBoundingBox(_path); + return CGRect(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height); +} + +bool CGPathCocoaImpl::empty() const { + return CGPathIsEmpty(_path); +} + +std::shared_ptr CGPathCocoaImpl::copyUsingTransform(CATransform3D const &transform) const { + ::CATransform3D nativeTransform; + nativeTransform.m11 = transform.m11; + nativeTransform.m12 = transform.m12; + nativeTransform.m13 = transform.m13; + nativeTransform.m14 = transform.m14; + + nativeTransform.m21 = transform.m21; + nativeTransform.m22 = transform.m22; + nativeTransform.m23 = transform.m23; + nativeTransform.m24 = transform.m24; + + nativeTransform.m31 = transform.m31; + nativeTransform.m32 = transform.m32; + nativeTransform.m33 = transform.m33; + nativeTransform.m34 = transform.m34; + + nativeTransform.m41 = transform.m41; + nativeTransform.m42 = transform.m42; + nativeTransform.m43 = transform.m43; + nativeTransform.m44 = transform.m44; + + auto affineTransform = CATransform3DGetAffineTransform(nativeTransform); + + CGPathRef resultPath = CGPathCreateCopyByTransformingPath(_path, &affineTransform); + if (resultPath == nil) { + return nullptr; + } + + CGMutablePathRef resultMutablePath = CGPathCreateMutableCopy(resultPath); + CGPathRelease(resultPath); + auto result = std::make_shared(resultMutablePath); + CGPathRelease(resultMutablePath); + + return result; +} + +void CGPathCocoaImpl::addLineTo(Vector2D const &point) { + CGPathAddLineToPoint(_path, nil, point.x, point.y); +} + +void CGPathCocoaImpl::addCurveTo(Vector2D const &point, Vector2D const &control1, Vector2D const &control2) { + CGPathAddCurveToPoint(_path, nil, control1.x, control1.y, control2.x, control2.y, point.x, point.y); +} + +void CGPathCocoaImpl::moveTo(Vector2D const &point) { + CGPathMoveToPoint(_path, nil, point.x, point.y); +} + +void CGPathCocoaImpl::closeSubpath() { + CGPathCloseSubpath(_path); +} + +void CGPathCocoaImpl::addRect(CGRect const &rect) { + CGPathAddRect(_path, nil, ::CGRectMake(rect.x, rect.y, rect.width, rect.height)); +} + +void CGPathCocoaImpl::addPath(std::shared_ptr const &path) { + if (CGPathIsEmpty(_path)) { + _path = CGPathCreateMutableCopy(std::static_pointer_cast(path)->_path); + } else { + CGPathAddPath(_path, nil, std::static_pointer_cast(path)->_path); + } +} + +CGPathRef CGPathCocoaImpl::nativePath() const { + return _path; +} + +bool CGPathCocoaImpl::isEqual(CGPath *other) const { + CGPathCocoaImpl *otherImpl = (CGPathCocoaImpl *)other; + return CGPathEqualToPath(_path, otherImpl->_path); +} + +void CGPathCocoaImpl::enumerate(std::function f) { + CGPathApplyWithBlock(_path, ^(const CGPathElement * _Nonnull element) { + CGPathItem item(CGPathItem::Type::MoveTo); + + switch (element->type) { + case kCGPathElementMoveToPoint: { + item.type = CGPathItem::Type::MoveTo; + item.points[0] = Vector2D(element->points[0].x, element->points[0].y); + f(item); + break; + } + case kCGPathElementAddLineToPoint: { + item.type = CGPathItem::Type::LineTo; + item.points[0] = Vector2D(element->points[0].x, element->points[0].y); + f(item); + break; + } + case kCGPathElementAddCurveToPoint: { + item.type = CGPathItem::Type::CurveTo; + item.points[0] = Vector2D(element->points[0].x, element->points[0].y); + item.points[1] = Vector2D(element->points[1].x, element->points[1].y); + item.points[2] = Vector2D(element->points[2].x, element->points[2].y); + f(item); + break; + } + case kCGPathElementAddQuadCurveToPoint: { + break; + } + case kCGPathElementCloseSubpath: { + item.type = CGPathItem::Type::Close; + f(item); + break; + } + } + }); +} + +void CGPathCocoaImpl::withNativePath(std::shared_ptr const &path, std::function f) { + CGMutablePathRef result = CGPathCreateMutable(); + + path->enumerate([result](CGPathItem const &element) { + switch (element.type) { + case CGPathItem::Type::MoveTo: { + CGPathMoveToPoint(result, nullptr, element.points[0].x, element.points[0].y); + break; + } + case CGPathItem::Type::LineTo: { + CGPathAddLineToPoint(result, nullptr, element.points[0].x, element.points[0].y); + break; + } + case CGPathItem::Type::CurveTo: { + CGPathAddCurveToPoint(result, nullptr, element.points[0].x, element.points[0].y, element.points[1].x, element.points[1].y, element.points[2].x, element.points[2].y); + break; + } + case CGPathItem::Type::Close: { + CGPathCloseSubpath(result); + break; + } + default: + break; + } + }); + + f(result); + CFRelease(result); +} + +/*std::shared_ptr CGPath::makePath() { + return std::static_pointer_cast(std::make_shared()); +}*/ + +} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CGPathCocoa.h b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGPathCocoa.h similarity index 100% rename from submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CGPathCocoa.h rename to Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CGPathCocoa.h diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CTFont.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CTFont.cpp new file mode 100644 index 0000000000..24022559bf --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CTFont.cpp @@ -0,0 +1,5 @@ +#include "CTFont.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CTFont.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CTFont.hpp new file mode 100644 index 0000000000..351d4bfc22 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/CTFont.hpp @@ -0,0 +1,12 @@ +#ifndef CTFont_hpp +#define CTFont_hpp + +namespace lottie { + +class CTFont { + +}; + +} + +#endif /* CTFont_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/Color.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/Color.cpp new file mode 100644 index 0000000000..c1ecb86f45 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/Color.cpp @@ -0,0 +1,29 @@ +#include "Color.hpp" + +#include + +namespace lottie { + +Color Color::fromString(std::string const &string) { + if (string.empty()) { + return Color(0.0, 0.0, 0.0, 0.0); + } + + std::string workString = string; + if (workString[0] == '#') { + workString.erase(workString.begin()); + } + + std::istringstream converter(workString); + uint32_t rgbValue; + converter >> std::hex >> rgbValue; + + return Color( + ((double)((rgbValue & 0xFF0000) >> 16)) / 255.0, + ((double)((rgbValue & 0x00FF00) >> 8)) / 255.0, + ((double)(rgbValue & 0x0000FF)) / 255.0, + 1.0 + ); +} + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/Color.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/Color.hpp new file mode 100644 index 0000000000..8fdbdea8ae --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/Color.hpp @@ -0,0 +1,144 @@ +#ifndef Color_hpp +#define Color_hpp + +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +#include + +namespace lottie { + +enum class ColorFormatDenominator { + One, + OneHundred, + TwoFiftyFive +}; + +struct Color { + double r; + double g; + double b; + double a; + + bool operator==(Color const &rhs) const { + if (r != rhs.r) { + return false; + } + if (g != rhs.g) { + return false; + } + if (b != rhs.b) { + return false; + } + if (a != rhs.a) { + return false; + } + return true; + } + + bool operator!=(Color const &rhs) const { + return !(*this == rhs); + } + + explicit Color(double r_, double g_, double b_, double a_, ColorFormatDenominator denominator = ColorFormatDenominator::One) { + double denominatorValue = 1.0; + switch (denominator) { + case ColorFormatDenominator::One: { + denominatorValue = 1.0; + break; + } + case ColorFormatDenominator::OneHundred: { + denominatorValue = 100.0; + break; + } + case ColorFormatDenominator::TwoFiftyFive: { + denominatorValue = 255.0; + break; + } + } + + r = r_ / denominatorValue; + g = g_ / denominatorValue; + b = b_ / denominatorValue; + a = a_ / denominatorValue; + } + + explicit Color(json11::Json const &jsonAny) noexcept(false) : + r(0.0), g(0.0), b(0.0), a(0.0) { + if (!jsonAny.is_array()) { + throw LottieParsingException(); + } + + for (const auto &item : jsonAny.array_items()) { + if (!item.is_number()) { + throw LottieParsingException(); + } + } + + size_t index = 0; + + double r1 = 0.0; + if (index < jsonAny.array_items().size()) { + if (!jsonAny.array_items()[index].is_number()) { + throw LottieParsingException(); + } + r1 = jsonAny.array_items()[index].number_value(); + index++; + } + + double g1 = 0.0; + if (index < jsonAny.array_items().size()) { + if (!jsonAny.array_items()[index].is_number()) { + throw LottieParsingException(); + } + g1 = jsonAny.array_items()[index].number_value(); + index++; + } + + double b1 = 0.0; + if (index < jsonAny.array_items().size()) { + if (!jsonAny.array_items()[index].is_number()) { + throw LottieParsingException(); + } + b1 = jsonAny.array_items()[index].number_value(); + index++; + } + + double a1 = 0.0; + if (index < jsonAny.array_items().size()) { + if (!jsonAny.array_items()[index].is_number()) { + throw LottieParsingException(); + } + a1 = jsonAny.array_items()[index].number_value(); + index++; + } + + if (r1 > 1.0 && r1 > 1.0 && b1 > 1.0 && a1 > 1.0) { + r1 = r1 / 255.0; + g1 = g1 / 255.0; + b1 = b1 / 255.0; + a1 = a1 / 255.0; + } + + r = r1; + g = g1; + b = b1; + a = a1; + } + + json11::Json toJson() const { + json11::Json::array result; + + result.push_back(json11::Json(r)); + result.push_back(json11::Json(g)); + result.push_back(json11::Json(b)); + result.push_back(json11::Json(a)); + + return result; + } + + static Color fromString(std::string const &string); +}; + +} + +#endif /* Color_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/Color.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/Color.swift new file mode 100644 index 0000000000..948b77e911 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/Color.swift @@ -0,0 +1,45 @@ +// +// Color.swift +// lottie-swift +// +// Created by Brandon Withrow on 2/4/19. +// + +import Foundation + +// MARK: - ColorFormatDenominator + +public enum ColorFormatDenominator: Hashable { + case One + case OneHundred + case TwoFiftyFive + + var value: Double { + switch self { + case .One: + return 1.0 + case .OneHundred: + return 100.0 + case .TwoFiftyFive: + return 255.0 + } + } +} + +// MARK: - Color + +public struct Color: Hashable { + + public var r: Double + public var g: Double + public var b: Double + public var a: Double + + public init(r: Double, g: Double, b: Double, a: Double, denominator: ColorFormatDenominator = .One) { + self.r = r / denominator.value + self.g = g / denominator.value + self.b = b / denominator.value + self.a = a / denominator.value + } + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/DashPattern.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/DashPattern.cpp new file mode 100644 index 0000000000..33426de338 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/DashPattern.cpp @@ -0,0 +1,5 @@ +#include "DashPattern.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/DashPattern.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/DashPattern.hpp new file mode 100644 index 0000000000..d22ee1e4d8 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/DashPattern.hpp @@ -0,0 +1,18 @@ +#ifndef DashPattern_hpp +#define DashPattern_hpp + +#include + +namespace lottie { + +struct DashPattern { + DashPattern(std::vector &&values_) : + values(std::move(values_)) { + } + + std::vector values; +}; + +} + +#endif /* DashPattern_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/DrawingAttributes.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/DrawingAttributes.hpp new file mode 100644 index 0000000000..7b43581e65 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/DrawingAttributes.hpp @@ -0,0 +1,22 @@ +#ifndef DrawingAttributes_hpp +#define DrawingAttributes_hpp + +namespace lottie { + +enum class LineCap: int { + None = 0, + Butt = 1, + Round = 2, + Square = 3 +}; + +enum class LineJoin: int { + None = 0, + Miter = 1, + Round = 2, + Bevel = 3 +}; + +} + +#endif /* DrawingAttributes_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/GradientColorSet.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/GradientColorSet.cpp new file mode 100644 index 0000000000..3a92ce0d41 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/GradientColorSet.cpp @@ -0,0 +1,5 @@ +#include "GradientColorSet.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/GradientColorSet.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/GradientColorSet.hpp new file mode 100644 index 0000000000..e1db01a15f --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/GradientColorSet.hpp @@ -0,0 +1,42 @@ +#ifndef GradientColorSet_hpp +#define GradientColorSet_hpp + +#include "Lottie/Private/Parsing/JsonParsing.hpp" + +#include + +namespace lottie { + +struct GradientColorSet { + GradientColorSet() { + } + + explicit GradientColorSet(json11::Json const &jsonAny) noexcept(false) { + if (!jsonAny.is_array()) { + throw LottieParsingException(); + } + + for (const auto &item : jsonAny.array_items()) { + if (!item.is_number()) { + throw LottieParsingException(); + } + colors.push_back(item.number_value()); + } + } + + json11::Json toJson() const { + json11::Json::array result; + + for (auto value : colors) { + result.push_back(value); + } + + return result; + } + + std::vector colors; +}; + +} + +#endif /* GradientColorSet_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/RenderTree.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/RenderTree.cpp new file mode 100644 index 0000000000..eec19c1596 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/RenderTree.cpp @@ -0,0 +1,140 @@ +#include "RenderTree.hpp" + +namespace lottie { + +BoundingBoxNode::BoundingBoxNode( + LayerParams const &layer_, + CGRect const &globalRect_, + CGRect const &localRect_, + CATransform3D const &globalTransform_, + bool drawsContent_, + std::shared_ptr renderableItem_, + bool isInvertedMatte_, + std::vector> const &subnodes_, + std::shared_ptr const &mask_ +) : +layer(layer_), +globalRect(globalRect_), +localRect(localRect_), +globalTransform(globalTransform_), +drawsContent(drawsContent_), +renderableItem(renderableItem_), +isInvertedMatte(isInvertedMatte_), +subnodes(subnodes_), +mask(mask_) { +} + +std::shared_ptr boundingBoxTree(std::shared_ptr const &layer, Vector2D const &globalSize, CATransform3D const &parentTransform) { + if (layer->isHidden() || layer->opacity() == 0.0f) { + return nullptr; + } + + if (layer->masksToBounds()) { + if (layer->bounds().empty()) { + return nullptr; + } + } + + auto currentTransform = parentTransform; + + currentTransform = currentTransform.translated(Vector2D(layer->position().x, layer->position().y)); + currentTransform = currentTransform.translated(Vector2D(-layer->bounds().x, -layer->bounds().y)); + currentTransform = layer->transform() * currentTransform; + + if (!currentTransform.isInvertible()) { + return nullptr; + } + + std::optional effectiveLocalBounds; + + auto renderableItem = layer->renderableItem(); + if (renderableItem) { + effectiveLocalBounds = renderableItem->boundingRect(); + } else if (layer->implementsDraw()) { + effectiveLocalBounds = layer->bounds(); + } + + bool isInvertedMatte = layer->isInvertedMatte(); + if (isInvertedMatte) { + effectiveLocalBounds = layer->bounds(); + } + + if (effectiveLocalBounds && effectiveLocalBounds->empty()) { + effectiveLocalBounds = std::nullopt; + } + + std::vector> subnodes; + std::optional subnodesGlobalRect; + + for (const auto &sublayer : layer->sublayers()) { + if (const auto subnode = boundingBoxTree(sublayer, globalSize, currentTransform)) { + subnodes.push_back(subnode); + + if (subnodesGlobalRect) { + subnodesGlobalRect = subnodesGlobalRect->unionWith(subnode->globalRect); + } else { + subnodesGlobalRect = subnode->globalRect; + } + } + } + + std::optional fuzzyGlobalRect; + + if (effectiveLocalBounds) { + CGRect effectiveGlobalBounds = effectiveLocalBounds->applyingTransform(currentTransform); + if (fuzzyGlobalRect) { + fuzzyGlobalRect = fuzzyGlobalRect->unionWith(effectiveGlobalBounds); + } else { + fuzzyGlobalRect = effectiveGlobalBounds; + } + } + + if (subnodesGlobalRect) { + if (fuzzyGlobalRect) { + fuzzyGlobalRect = fuzzyGlobalRect->unionWith(subnodesGlobalRect.value()); + } else { + fuzzyGlobalRect = subnodesGlobalRect; + } + } + + if (!fuzzyGlobalRect) { + return nullptr; + } + + CGRect globalRect( + std::floor(fuzzyGlobalRect->x), + std::floor(fuzzyGlobalRect->y), + std::ceil(fuzzyGlobalRect->width + fuzzyGlobalRect->x - floor(fuzzyGlobalRect->x)), + std::ceil(fuzzyGlobalRect->height + fuzzyGlobalRect->y - floor(fuzzyGlobalRect->y)) + ); + + if (!CGRect(0.0, 0.0, globalSize.x, globalSize.y).intersects(globalRect)) { + return nullptr; + } + + std::shared_ptr maskNode; + if (layer->mask()) { + if (const auto maskNodeValue = boundingBoxTree(layer->mask(), globalSize, currentTransform)) { + if (!maskNodeValue->globalRect.intersects(globalRect)) { + return nullptr; + } + maskNode = maskNodeValue; + } else { + return nullptr; + } + } + + return std::make_shared( + layer, + globalRect, + CGRect(0.0, 0.0, 0.0, 0.0), + currentTransform, + effectiveLocalBounds.has_value(), + renderableItem, + isInvertedMatte, + subnodes, + maskNode + ); +} + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/RenderTree.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/RenderTree.hpp new file mode 100644 index 0000000000..1ab374e226 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/RenderTree.hpp @@ -0,0 +1,172 @@ +#ifndef RenderTree_hpp +#define RenderTree_hpp + +#include + +#include "Lottie/Public/Primitives/Vectors.hpp" +#include "Lottie/Public/Primitives/CALayer.hpp" + +namespace lottie { + +struct BoundingBoxNode { + struct LayerParams { + CGRect _bounds; + Vector2D _position; + CATransform3D _transform; + double _opacity; + bool _masksToBounds; + bool _isHidden; + + LayerParams( + CGRect bounds_, + Vector2D position_, + CATransform3D transform_, + double opacity_, + bool masksToBounds_, + bool isHidden_ + ) : + _bounds(bounds_), + _position(position_), + _transform(transform_), + _opacity(opacity_), + _masksToBounds(masksToBounds_), + _isHidden(isHidden_) { + } + + LayerParams(std::shared_ptr const &layer) : + _bounds(layer->bounds()), + _position(layer->position()), + _transform(layer->transform()), + _opacity(layer->opacity()), + _masksToBounds(layer->masksToBounds()), + _isHidden(layer->isHidden()) { + } + + bool operator==(LayerParams const &rhs) const { + if (_bounds != rhs._bounds) { + return false; + } + if (_position != rhs._position) { + return false; + } + if (_transform != rhs._transform) { + return false; + } + if (_opacity != rhs._opacity) { + return false; + } + if (_masksToBounds != rhs._masksToBounds) { + return false; + } + if (_isHidden != rhs._isHidden) { + return false; + } + return true; + } + + bool operator!=(LayerParams const &rhs) const { + return !(*this == rhs); + } + + CGRect bounds() const { + return _bounds; + } + + Vector2D position() const { + return _position; + } + + CATransform3D transform() const { + return _transform; + } + + double opacity() const { + return _opacity; + } + + bool masksToBounds() const { + return _masksToBounds; + } + + bool isHidden() const { + return _isHidden; + } + }; + + LayerParams layer; + CGRect globalRect; + CGRect localRect; + CATransform3D globalTransform; + bool drawsContent; + std::shared_ptr renderableItem; + bool isInvertedMatte; + std::vector> subnodes; + std::shared_ptr mask; + + explicit BoundingBoxNode( + LayerParams const &layer_, + CGRect const &globalRect_, + CGRect const &localRect_, + CATransform3D const &globalTransform_, + bool drawsContent_, + std::shared_ptr renderableItem_, + bool isInvertedMatte_, + std::vector> const &subnodes_, + std::shared_ptr const &mask_ + ); + + bool operator==(BoundingBoxNode const &rhs) const { + if (layer != rhs.layer) { + return false; + } + if (globalRect != rhs.globalRect) { + return false; + } + if (localRect != rhs.localRect) { + return false; + } + if (globalTransform != rhs.globalTransform) { + return false; + } + if (drawsContent != rhs.drawsContent) { + return false; + } + if ((renderableItem == nullptr) != (rhs.renderableItem == nullptr)) { + return false; + } else if (renderableItem) { + if (!renderableItem->isEqual(rhs.renderableItem)) { + return false; + } + } + if (isInvertedMatte != rhs.isInvertedMatte) { + return false; + } + if (subnodes.size() != rhs.subnodes.size()) { + return false; + } else { + for (size_t i = 0; i < subnodes.size(); i++) { + if ((*subnodes[i].get()) != (*rhs.subnodes[i].get())) { + return false; + } + } + } + if ((mask == nullptr) != (rhs.mask == nullptr)) { + return false; + } else if (mask) { + if ((*mask.get()) != *(rhs.mask.get())) { + return false; + } + } + return true; + } + + bool operator!=(BoundingBoxNode const &rhs) const { + return !(*this == rhs); + } +}; + +std::shared_ptr boundingBoxTree(std::shared_ptr const &layer, Vector2D const &globalSize, CATransform3D const &parentTransform); + +} + +#endif /* RenderTree_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Vectors.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/Vectors.hpp similarity index 94% rename from submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Vectors.hpp rename to Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/Vectors.hpp index 41eb1c5667..9cdff038d1 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Vectors.hpp +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/Vectors.hpp @@ -18,7 +18,7 @@ struct Vector1D { value(value_) { } - explicit Vector1D(lottiejson11::Json const &json) noexcept(false) { + explicit Vector1D(json11::Json const &json) noexcept(false) { if (json.is_number()) { value = json.number_value(); } else if (json.is_array()) { @@ -34,8 +34,8 @@ struct Vector1D { } } - lottiejson11::Json toJson() const { - return lottiejson11::Json(value); + json11::Json toJson() const { + return json11::Json(value); } double value; @@ -68,7 +68,7 @@ struct Vector2D { y(y_) { } - explicit Vector2D(lottiejson11::Json const &json) noexcept(false) { + explicit Vector2D(json11::Json const &json) noexcept(false) { x = 0.0; y = 0.0; @@ -121,13 +121,13 @@ struct Vector2D { } } - lottiejson11::Json toJson() const { - lottiejson11::Json::object result; + json11::Json toJson() const { + json11::Json::object result; result.insert(std::make_pair("x", x)); result.insert(std::make_pair("y", y)); - return lottiejson11::Json(result); + return json11::Json(result); } double x; @@ -200,7 +200,7 @@ struct Vector3D { z(z_) { } - explicit Vector3D(lottiejson11::Json const &json) noexcept(false) { + explicit Vector3D(json11::Json const &json) noexcept(false) { if (!json.is_array()) { throw LottieParsingException(); } @@ -236,14 +236,14 @@ struct Vector3D { } } - lottiejson11::Json toJson() const { - lottiejson11::Json::array result; + json11::Json toJson() const { + json11::Json::array result; - result.push_back(lottiejson11::Json(x)); - result.push_back(lottiejson11::Json(y)); - result.push_back(lottiejson11::Json(z)); + result.push_back(json11::Json(x)); + result.push_back(json11::Json(y)); + result.push_back(json11::Json(z)); - return lottiejson11::Json(result); + return json11::Json(result); } double x = 0.0; diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/Vectors.mm b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/Vectors.mm new file mode 100644 index 0000000000..e37b668c02 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/Vectors.mm @@ -0,0 +1,518 @@ +#include "Vectors.hpp" + +#include "VectorsCocoa.h" + +#include "Lottie/Public/Keyframes/Interpolatable.hpp" + +#include + +#import + +#import + +namespace lottie { + +CATransform3D CATransform3D::_identity = CATransform3D( + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0 +); + +double interpolate(double value, double to, double amount) { + return value + ((to - value) * amount); +} + +Vector1D interpolate( + Vector1D const &from, + Vector1D const &to, + double amount +) { + return Vector1D(interpolate(from.value, to.value, amount)); +} + +Vector2D interpolate( + Vector2D const &from, + Vector2D const &to, + double amount +) { + return Vector2D(interpolate(from.x, to.x, amount), interpolate(from.y, to.y, amount)); +} + + +Vector3D interpolate( + Vector3D const &from, + Vector3D const &to, + double amount +) { + return Vector3D(interpolate(from.x, to.x, amount), interpolate(from.y, to.y, amount), interpolate(from.z, to.z, amount)); +} + +static double cubicRoot(double value) { + return pow(value, 1.0 / 3.0); +} + +static double SolveQuadratic(double a, double b, double c) { + double result = (-b + sqrt((b * b) - 4 * a * c)) / (2 * a); + if (isInRangeOrEqual(result, 0.0, 1.0)) { + return result; + } + + result = (-b - sqrt((b * b) - 4 * a * c)) / (2 * a); + if (isInRangeOrEqual(result, 0.0, 1.0)) { + return result; + } + + return -1.0; +} + +static double SolveCubic(double a, double b, double c, double d) { + if (a == 0.0) { + return SolveQuadratic(b, c, d); + } + if (d == 0.0) { + return 0.0; + } + b /= a; + c /= a; + d /= a; + double q = (3.0 * c - (b * b)) / 9.0; + double r = (-27.0 * d + b * (9.0 * c - 2.0 * (b * b))) / 54.0; + double disc = (q * q * q) + (r * r); + double term1 = b / 3.0; + + if (disc > 0.0) { + double s = r + sqrt(disc); + s = (s < 0) ? -cubicRoot(-s) : cubicRoot(s); + double t = r - sqrt(disc); + t = (t < 0) ? -cubicRoot(-t) : cubicRoot(t); + + double result = -term1 + s + t; + if (isInRangeOrEqual(result, 0.0, 1.0)) { + return result; + } + } else if (disc == 0) { + double r13 = (r < 0) ? -cubicRoot(-r) : cubicRoot(r); + + double result = -term1 + 2.0 * r13; + if (isInRangeOrEqual(result, 0.0, 1.0)) { + return result; + } + + result = -(r13 + term1); + if (isInRangeOrEqual(result, 0.0, 1.0)) { + return result; + } + } else { + q = -q; + double dum1 = q * q * q; + dum1 = acos(r / sqrt(dum1)); + double r13 = 2.0 * sqrt(q); + + double result = -term1 + r13 * cos(dum1 / 3.0); + if (isInRangeOrEqual(result, 0.0, 1.0)) { + return result; + } + result = -term1 + r13 * cos((dum1 + 2.0 * M_PI) / 3.0); + if (isInRangeOrEqual(result, 0.0, 1.0)) { + return result; + } + result = -term1 + r13 * cos((dum1 + 4.0 * M_PI) / 3.0); + if (isInRangeOrEqual(result, 0.0, 1.0)) { + return result; + } + } + + return -1; +} + +double cubicBezierInterpolate(double value, Vector2D const &P0, Vector2D const &P1, Vector2D const &P2, Vector2D const &P3) { + double t = 0.0; + if (value == P0.x) { + // Handle corner cases explicitly to prevent rounding errors + t = 0.0; + } else if (value == P3.x) { + t = 1.0; + } else { + // Calculate t + double a = -P0.x + 3 * P1.x - 3 * P2.x + P3.x; + double b = 3 * P0.x - 6 * P1.x + 3 * P2.x; + double c = -3 * P0.x + 3 * P1.x; + double d = P0.x - value; + double tTemp = SolveCubic(a, b, c, d); + if (tTemp == -1.0) { + return -1.0; + } + t = tTemp; + } + + // Calculate y from t + double oneMinusT = 1.0 - t; + return (oneMinusT * oneMinusT * oneMinusT) * P0.y + 3 * t * (oneMinusT * oneMinusT) * P1.y + 3 * (t * t) * (1 - t) * P2.y + (t * t * t) * P3.y; +} + +struct InterpolationPoint2D { + InterpolationPoint2D(Vector2D const point_, double distance_) : + point(point_), distance(distance_) { + } + + Vector2D point; + double distance; +}; + +namespace { + double interpolateDouble(double value, double to, double amount) { + return value + ((to - value) * amount); + } +} + +Vector2D Vector2D::pointOnPath(Vector2D const &to, Vector2D const &outTangent, Vector2D const &inTangent, double amount) const { + auto a = interpolate(outTangent, amount); + auto b = outTangent.interpolate(inTangent, amount); + auto c = inTangent.interpolate(to, amount); + auto d = a.interpolate(b, amount); + auto e = b.interpolate(c, amount); + auto f = d.interpolate(e, amount); + return f; +} + +Vector2D Vector2D::interpolate(Vector2D const &to, double amount) const { + return Vector2D( + interpolateDouble(x, to.x, amount), + interpolateDouble(y, to.y, amount) + ); +} + +Vector2D Vector2D::interpolate( + Vector2D const &to, + Vector2D const &outTangent, + Vector2D const &inTangent, + double amount, + int maxIterations, + int samples, + double accuracy +) const { + if (amount == 0.0) { + return *this; + } + if (amount == 1.0) { + return to; + } + + if (colinear(outTangent, inTangent) && outTangent.colinear(inTangent, to)) { + return interpolate(to, amount); + } + + double step = 1.0 / (double)samples; + + std::vector points; + points.push_back(InterpolationPoint2D(*this, 0.0)); + double totalLength = 0.0; + + Vector2D previousPoint = *this; + double previousAmount = 0.0; + + int closestPoint = 0; + + while (previousAmount < 1.0) { + previousAmount = previousAmount + step; + + if (previousAmount < amount) { + closestPoint = closestPoint + 1; + } + + auto newPoint = pointOnPath(to, outTangent, inTangent, previousAmount); + auto distance = previousPoint.distanceTo(newPoint); + totalLength = totalLength + distance; + points.push_back(InterpolationPoint2D(newPoint, totalLength)); + previousPoint = newPoint; + } + + double accurateDistance = amount * totalLength; + auto point = points[closestPoint]; + + bool foundPoint = false; + + double pointAmount = ((double)closestPoint) * step; + double nextPointAmount = pointAmount + step; + + int refineIterations = 0; + while (!foundPoint) { + refineIterations = refineIterations + 1; + /// First see if the next point is still less than the projected length. + auto nextPoint = points[closestPoint + 1]; + if (nextPoint.distance < accurateDistance) { + point = nextPoint; + closestPoint = closestPoint + 1; + pointAmount = ((double)closestPoint) * step; + nextPointAmount = pointAmount + step; + if (closestPoint == (int)points.size()) { + foundPoint = true; + } + continue; + } + if (accurateDistance < point.distance) { + closestPoint = closestPoint - 1; + if (closestPoint < 0) { + foundPoint = true; + continue; + } + point = points[closestPoint]; + pointAmount = ((double)closestPoint) * step; + nextPointAmount = pointAmount + step; + continue; + } + + /// Now we are certain the point is the closest point under the distance + auto pointDiff = nextPoint.distance - point.distance; + auto proposedPointAmount = remapDouble((accurateDistance - point.distance) / pointDiff, 0.0, 1.0, pointAmount, nextPointAmount); + + auto newPoint = pointOnPath(to, outTangent, inTangent, proposedPointAmount); + auto newDistance = point.distance + point.point.distanceTo(newPoint); + pointAmount = proposedPointAmount; + point = InterpolationPoint2D(newPoint, newDistance); + if (accurateDistance - newDistance <= accuracy || + newDistance - accurateDistance <= accuracy) { + foundPoint = true; + } + + if (refineIterations == maxIterations) { + foundPoint = true; + } + } + return point.point; +} + +::CATransform3D nativeTransform(CATransform3D const &value) { + ::CATransform3D result; + + result.m11 = value.m11; + result.m12 = value.m12; + result.m13 = value.m13; + result.m14 = value.m14; + + result.m21 = value.m21; + result.m22 = value.m22; + result.m23 = value.m23; + result.m24 = value.m24; + + result.m31 = value.m31; + result.m32 = value.m32; + result.m33 = value.m33; + result.m34 = value.m34; + + result.m41 = value.m41; + result.m42 = value.m42; + result.m43 = value.m43; + result.m44 = value.m44; + + return result; +} + +CATransform3D fromNativeTransform(::CATransform3D const &value) { + CATransform3D result = CATransform3D::identity(); + + result.m11 = value.m11; + result.m12 = value.m12; + result.m13 = value.m13; + result.m14 = value.m14; + + result.m21 = value.m21; + result.m22 = value.m22; + result.m23 = value.m23; + result.m24 = value.m24; + + result.m31 = value.m31; + result.m32 = value.m32; + result.m33 = value.m33; + result.m34 = value.m34; + + result.m41 = value.m41; + result.m42 = value.m42; + result.m43 = value.m43; + result.m44 = value.m44; + + return result; +} + +CATransform3D CATransform3D::makeRotation(double radians, double x, double y, double z) { + return fromNativeTransform(CATransform3DMakeRotation(radians, x, y, z)); + + /*if (x == 0.0 && y == 0.0 && z == 0.0) { + return CATransform3D::identity(); + } + + float s = sin(radians); + float c = cos(radians); + + float len = sqrt(x*x + y*y + z*z); + x /= len; y /= len; z /= len; + + CATransform3D returnValue = CATransform3D::identity(); + + returnValue.m11 = c + (1-c) * x*x; + returnValue.m12 = (1-c) * x*y + s*z; + returnValue.m13 = (1-c) * x*z - s*y; + returnValue.m14 = 0; + + returnValue.m21 = (1-c) * y*x - s*z; + returnValue.m22 = c + (1-c) * y*y; + returnValue.m23 = (1-c) * y*z + s*x; + returnValue.m24 = 0; + + returnValue.m31 = (1-c) * z*x + s*y; + returnValue.m32 = (1-c) * y*z - s*x; + returnValue.m33 = c + (1-c) * z*z; + returnValue.m34 = 0; + + returnValue.m41 = 0; + returnValue.m42 = 0; + returnValue.m43 = 0; + returnValue.m44 = 1; + + return returnValue;*/ +} + +CATransform3D CATransform3D::rotated(double degrees) const { + return fromNativeTransform(CATransform3DRotate(nativeTransform(*this), degreesToRadians(degrees), 0.0, 0.0, 1.0)); + //return CATransform3D::makeRotation(degreesToRadians(degrees), 0.0, 0.0, 1.0) * (*this); +} + +CATransform3D CATransform3D::translated(Vector2D const &translation) const { + return fromNativeTransform(CATransform3DTranslate(nativeTransform(*this), translation.x, translation.y, 0.0)); +} + +CATransform3D CATransform3D::scaled(Vector2D const &scale) const { + return fromNativeTransform(CATransform3DScale(nativeTransform(*this), scale.x, scale.y, 1.0)); + //return CATransform3D::makeScale(scale.x, scale.y, 1.0) * (*this); +} + +CATransform3D CATransform3D::operator*(CATransform3D const &b) const { + if (isIdentity()) { + return b; + } + if (b.isIdentity()) { + return *this; + } + + const CATransform3D lhs = b; + const CATransform3D &rhs = *this; + CATransform3D result = CATransform3D::identity(); + + result.m11 = (lhs.m11*rhs.m11)+(lhs.m21*rhs.m12)+(lhs.m31*rhs.m13)+(lhs.m41*rhs.m14); + result.m12 = (lhs.m12*rhs.m11)+(lhs.m22*rhs.m12)+(lhs.m32*rhs.m13)+(lhs.m42*rhs.m14); + result.m13 = (lhs.m13*rhs.m11)+(lhs.m23*rhs.m12)+(lhs.m33*rhs.m13)+(lhs.m43*rhs.m14); + result.m14 = (lhs.m14*rhs.m11)+(lhs.m24*rhs.m12)+(lhs.m34*rhs.m13)+(lhs.m44*rhs.m14); + + result.m21 = (lhs.m11*rhs.m21)+(lhs.m21*rhs.m22)+(lhs.m31*rhs.m23)+(lhs.m41*rhs.m24); + result.m22 = (lhs.m12*rhs.m21)+(lhs.m22*rhs.m22)+(lhs.m32*rhs.m23)+(lhs.m42*rhs.m24); + result.m23 = (lhs.m13*rhs.m21)+(lhs.m23*rhs.m22)+(lhs.m33*rhs.m23)+(lhs.m43*rhs.m24); + result.m24 = (lhs.m14*rhs.m21)+(lhs.m24*rhs.m22)+(lhs.m34*rhs.m23)+(lhs.m44*rhs.m24); + + result.m31 = (lhs.m11*rhs.m31)+(lhs.m21*rhs.m32)+(lhs.m31*rhs.m33)+(lhs.m41*rhs.m34); + result.m32 = (lhs.m12*rhs.m31)+(lhs.m22*rhs.m32)+(lhs.m32*rhs.m33)+(lhs.m42*rhs.m34); + result.m33 = (lhs.m13*rhs.m31)+(lhs.m23*rhs.m32)+(lhs.m33*rhs.m33)+(lhs.m43*rhs.m34); + result.m34 = (lhs.m14*rhs.m31)+(lhs.m24*rhs.m32)+(lhs.m34*rhs.m33)+(lhs.m44*rhs.m34); + + result.m41 = (lhs.m11*rhs.m41)+(lhs.m21*rhs.m42)+(lhs.m31*rhs.m43)+(lhs.m41*rhs.m44); + result.m42 = (lhs.m12*rhs.m41)+(lhs.m22*rhs.m42)+(lhs.m32*rhs.m43)+(lhs.m42*rhs.m44); + result.m43 = (lhs.m13*rhs.m41)+(lhs.m23*rhs.m42)+(lhs.m33*rhs.m43)+(lhs.m43*rhs.m44); + result.m44 = (lhs.m14*rhs.m41)+(lhs.m24*rhs.m42)+(lhs.m34*rhs.m43)+(lhs.m44*rhs.m44); + + return result; +} + +bool CATransform3D::isInvertible() const { + return std::abs(m11 * m22 - m12 * m21) >= 0.00000001; +} + +CATransform3D CATransform3D::inverted() const { + return fromNativeTransform(CATransform3DMakeAffineTransform(CGAffineTransformInvert(CATransform3DGetAffineTransform(nativeTransform(*this))))); +} + +bool CGRect::intersects(CGRect const &other) const { + return CGRectIntersectsRect(CGRectMake(x, y, width, height), CGRectMake(other.x, other.y, other.width, other.height)); +} + +bool CGRect::contains(CGRect const &other) const { + return CGRectContainsRect(CGRectMake(x, y, width, height), CGRectMake(other.x, other.y, other.width, other.height)); +} + +CGRect CGRect::intersection(CGRect const &other) const { + auto result = CGRectIntersection(CGRectMake(x, y, width, height), CGRectMake(other.x, other.y, other.width, other.height)); + return CGRect(result.origin.x, result.origin.y, result.size.width, result.size.height); +} + +CGRect CGRect::unionWith(CGRect const &other) const { + auto result = CGRectUnion(CGRectMake(x, y, width, height), CGRectMake(other.x, other.y, other.width, other.height)); + return CGRect(result.origin.x, result.origin.y, result.size.width, result.size.height); +} + +static inline Vector2D applyingTransformToPoint(CATransform3D const &transform, Vector2D const &point) { + double newX = point.x * transform.m11 + point.y * transform.m21 + transform.m41; + double newY = point.x * transform.m12 + point.y * transform.m22 + transform.m42; + double newW = point.x * transform.m14 + point.y * transform.m24 + transform.m44; + + return Vector2D(newX / newW, newY / newW); +} + +CGRect CGRect::applyingTransform(CATransform3D const &transform) const { + if (transform.isIdentity()) { + return *this; + } + + Vector2D topLeft = applyingTransformToPoint(transform, Vector2D(x, y)); + Vector2D topRight = applyingTransformToPoint(transform, Vector2D(x + width, y)); + Vector2D bottomLeft = applyingTransformToPoint(transform, Vector2D(x, y + height)); + Vector2D bottomRight = applyingTransformToPoint(transform, Vector2D(x + width, y + height)); + + double minX = topLeft.x; + if (topRight.x < minX) { + minX = topRight.x; + } + if (bottomLeft.x < minX) { + minX = bottomLeft.x; + } + if (bottomRight.x < minX) { + minX = bottomRight.x; + } + + double minY = topLeft.y; + if (topRight.y < minY) { + minY = topRight.y; + } + if (bottomLeft.y < minY) { + minY = bottomLeft.y; + } + if (bottomRight.y < minY) { + minY = bottomRight.y; + } + + double maxX = topLeft.x; + if (topRight.x > maxX) { + maxX = topRight.x; + } + if (bottomLeft.x > maxX) { + maxX = bottomLeft.x; + } + if (bottomRight.x > maxX) { + maxX = bottomRight.x; + } + + double maxY = topLeft.y; + if (topRight.y > maxY) { + maxY = topRight.y; + } + if (bottomLeft.y > maxY) { + maxY = bottomLeft.y; + } + if (bottomRight.y > maxY) { + maxY = bottomRight.y; + } + + CGRect result(minX, minY, maxX - minX, maxY - minY); + + return result; +} + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/Vectors.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/Vectors.swift new file mode 100644 index 0000000000..9d20ee8eae --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/Vectors.swift @@ -0,0 +1,38 @@ +// +// Vectors.swift +// lottie-swift +// +// Created by Brandon Withrow on 2/4/19. +// + +import Foundation + +// MARK: - Vector1D + +public struct Vector1D: Hashable { + + public init(_ value: Double) { + self.value = value + } + + public let value: Double + +} + +// MARK: - Vector3D + +/// A three dimensional vector. +/// These vectors are encoded and decoded from [Double] +public struct Vector3D: Hashable { + + public let x: Double + public let y: Double + public let z: Double + + public init(x: Double, y: Double, z: Double) { + self.x = x + self.y = y + self.z = z + } + +} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/VectorsCocoa.h b/Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/VectorsCocoa.h similarity index 100% rename from submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/VectorsCocoa.h rename to Tests/LottieMetalTest/LottieSwift/Sources/Public/Primitives/VectorsCocoa.h diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/TextProvider/AnimationTextProvider.cpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/TextProvider/AnimationTextProvider.cpp new file mode 100644 index 0000000000..6ba1e020c8 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/TextProvider/AnimationTextProvider.cpp @@ -0,0 +1,5 @@ +#include "AnimationTextProvider.hpp" + +namespace lottie { + +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/TextProvider/AnimationTextProvider.hpp b/Tests/LottieMetalTest/LottieSwift/Sources/Public/TextProvider/AnimationTextProvider.hpp new file mode 100644 index 0000000000..d93089c623 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/TextProvider/AnimationTextProvider.hpp @@ -0,0 +1,48 @@ +#ifndef AnimationTextProvider_hpp +#define AnimationTextProvider_hpp + +#include +#include + +namespace lottie { + +/// Text provider is a protocol that is used to supply text to `AnimationView`. +class AnimationTextProvider { +public: + virtual std::string textFor(std::string const &keypathName, std::string const &sourceText) = 0; +}; + +/// Text provider that simply map values from dictionary +class DictionaryTextProvider: public AnimationTextProvider { +public: + DictionaryTextProvider(std::map const &values) : + _values(values) { + } + + virtual std::string textFor(std::string const &keypathName, std::string const &sourceText) override { + const auto it = _values.find(keypathName); + if (it != _values.end()) { + return it->second; + } else { + return sourceText; + } + } + +private: + std::map _values; +}; + +/// Default text provider. Uses text in the animation file +class DefaultTextProvider: public AnimationTextProvider { +public: + DefaultTextProvider() { + } + + virtual std::string textFor(std::string const &keypathName, std::string const &sourceText) override { + return sourceText; + } +}; + +} + +#endif /* AnimationTextProvider_hpp */ diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/TextProvider/AnimationTextProvider.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/TextProvider/AnimationTextProvider.swift new file mode 100644 index 0000000000..b448dbb1df --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/TextProvider/AnimationTextProvider.swift @@ -0,0 +1,53 @@ +// +// AnimationImageProvider.swift +// Lottie_iOS +// +// Created by Alexandr Goncharov on 07/06/2019. +// + +import Foundation + +// MARK: - AnimationTextProvider + +/// Text provider is a protocol that is used to supply text to `AnimationView`. +public protocol AnimationTextProvider: AnyObject { + func textFor(keypathName: String, sourceText: String) -> String +} + +// MARK: - DictionaryTextProvider + +/// Text provider that simply map values from dictionary +public final class DictionaryTextProvider: AnimationTextProvider { + + // MARK: Lifecycle + + public init(_ values: [String: String]) { + self.values = values + } + + // MARK: Public + + public func textFor(keypathName: String, sourceText: String) -> String { + values[keypathName] ?? sourceText + } + + // MARK: Internal + + let values: [String: String] +} + +// MARK: - DefaultTextProvider + +/// Default text provider. Uses text in the animation file +public final class DefaultTextProvider: AnimationTextProvider { + + // MARK: Lifecycle + + public init() {} + + // MARK: Public + + public func textFor(keypathName _: String, sourceText: String) -> String { + sourceText + } +} diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/AnimatedButton.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/AnimatedButton.swift new file mode 100644 index 0000000000..7aa0346bed --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/AnimatedButton.swift @@ -0,0 +1,78 @@ +// +// AnimatedButton.swift +// lottie-swift +// +// Created by Brandon Withrow on 2/4/19. +// + +import Foundation +#if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst) +import UIKit +/// An interactive button that plays an animation when pressed. +open class AnimatedButton: AnimatedControl { + + // MARK: Lifecycle + + public override init( + animation: Animation, + configuration: LottieConfiguration = .shared) + { + super.init(animation: animation, configuration: configuration) + accessibilityTraits = UIAccessibilityTraits.button + } + + public override init() { + super.init() + accessibilityTraits = UIAccessibilityTraits.button + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + // MARK: Public + + /// Sets the play range for the given UIControlEvent. + public func setPlayRange(fromProgress: AnimationProgressTime, toProgress: AnimationProgressTime, event: UIControl.Event) { + rangesForEvents[event.rawValue] = (from: fromProgress, to: toProgress) + } + + /// Sets the play range for the given UIControlEvent. + public func setPlayRange(fromMarker fromName: String, toMarker toName: String, event: UIControl.Event) { + if + let start = animationView.progressTime(forMarker: fromName), + let end = animationView.progressTime(forMarker: toName) + { + rangesForEvents[event.rawValue] = (from: start, to: end) + } + } + + public override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + let _ = super.beginTracking(touch, with: event) + let touchEvent = UIControl.Event.touchDown + if let playrange = rangesForEvents[touchEvent.rawValue] { + animationView.play(fromProgress: playrange.from, toProgress: playrange.to, loopMode: LottieLoopMode.playOnce) + } + return true + } + + public override func endTracking(_ touch: UITouch?, with event: UIEvent?) { + super.endTracking(touch, with: event) + let touchEvent: UIControl.Event + if let touch = touch, bounds.contains(touch.location(in: self)) { + touchEvent = UIControl.Event.touchUpInside + } else { + touchEvent = UIControl.Event.touchUpOutside + } + + if let playrange = rangesForEvents[touchEvent.rawValue] { + animationView.play(fromProgress: playrange.from, toProgress: playrange.to, loopMode: LottieLoopMode.playOnce) + } + } + + // MARK: Fileprivate + + fileprivate var rangesForEvents: [UInt : (from: CGFloat, to: CGFloat)] = + [UIControl.Event.touchUpInside.rawValue : (from: 0, to: 1)] +} +#endif diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/AnimatedControl.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/AnimatedControl.swift new file mode 100644 index 0000000000..a0d568afc5 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/AnimatedControl.swift @@ -0,0 +1,177 @@ +// +// AnimatedControl.swift +// lottie-swift +// +// Created by Brandon Withrow on 2/4/19. +// + +import Foundation +#if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst) +import UIKit + +/// Lottie comes prepacked with a two Animated Controls, `AnimatedSwitch` and +/// `AnimatedButton`. Both of these controls are built on top of `AnimatedControl` +/// +/// `AnimatedControl` is a subclass of `UIControl` that provides an interactive +/// mechanism for controlling the visual state of an animation in response to +/// user actions. +/// +/// The `AnimatedControl` will show and hide layers depending on the current +/// `UIControl.State` of the control. +/// +/// Users of `AnimationControl` can set a Layer Name for each `UIControl.State`. +/// When the state is change the `AnimationControl` will change the visibility +/// of its layers. +/// +/// NOTE: Do not initialize directly. This is intended to be subclassed. +open class AnimatedControl: UIControl { + + // MARK: Lifecycle + + // MARK: Initializers + + public init( + animation: Animation, + configuration: LottieConfiguration = .shared) + { + animationView = AnimationView( + animation: animation, + configuration: configuration) + + super.init(frame: animation.bounds) + commonInit() + } + + public init() { + animationView = AnimationView() + super.init(frame: .zero) + commonInit() + } + + required public init?(coder aDecoder: NSCoder) { + animationView = AnimationView() + super.init(coder: aDecoder) + commonInit() + } + + // MARK: Open + + // MARK: UIControl Overrides + + open override var isEnabled: Bool { + didSet { + updateForState() + } + } + + open override var isSelected: Bool { + didSet { + updateForState() + } + } + + open override var isHighlighted: Bool { + didSet { + updateForState() + } + } + + open override var intrinsicContentSize: CGSize { + animationView.intrinsicContentSize + } + + open override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + updateForState() + return super.beginTracking(touch, with: event) + } + + open override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + updateForState() + return super.continueTracking(touch, with: event) + } + + open override func endTracking(_ touch: UITouch?, with event: UIEvent?) { + updateForState() + return super.endTracking(touch, with: event) + } + + open override func cancelTracking(with event: UIEvent?) { + updateForState() + super.cancelTracking(with: event) + } + + open func animationDidSet() { + + } + + // MARK: Public + + /// The animation view in which the animation is rendered. + public let animationView: AnimationView + + /// The animation backing the animated control. + public var animation: Animation? { + didSet { + animationView.animation = animation + animationView.bounds = animation?.bounds ?? .zero + setNeedsLayout() + updateForState() + animationDidSet() + } + } + + /// The speed of the animation playback. Defaults to 1 + public var animationSpeed: CGFloat { + set { animationView.animationSpeed = newValue } + get { animationView.animationSpeed } + } + + /// Sets which Animation Layer should be visible for the given state. + public func setLayer(named: String, forState: UIControl.State) { + stateMap[forState.rawValue] = named + updateForState() + } + + /// Sets a ValueProvider for the specified keypath + public func setValueProvider(_ valueProvider: AnyValueProvider, keypath: AnimationKeypath) { + animationView.setValueProvider(valueProvider, keypath: keypath) + } + + // MARK: Internal + + var stateMap: [UInt: String] = [:] + + func updateForState() { + guard let animationLayer = animationView.animationLayer else { return } + if + let layerName = stateMap[state.rawValue], + let stateLayer = animationLayer.layer(for: AnimationKeypath(keypath: layerName)) + { + for layer in animationLayer._animationLayers { + layer.isHidden = true + } + stateLayer.isHidden = false + } else { + for layer in animationLayer._animationLayers { + layer.isHidden = false + } + } + } + + // MARK: Fileprivate + + fileprivate func commonInit() { + animationView.clipsToBounds = false + clipsToBounds = true + animationView.translatesAutoresizingMaskIntoConstraints = false + animationView.backgroundBehavior = .forceFinish + addSubview(animationView) + animationView.contentMode = .scaleAspectFit + animationView.isUserInteractionEnabled = false + animationView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true + animationView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true + animationView.topAnchor.constraint(equalTo: topAnchor).isActive = true + animationView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + } +} +#endif diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/AnimatedSwitch.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/AnimatedSwitch.swift new file mode 100644 index 0000000000..19e1edd6b7 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/AnimatedSwitch.swift @@ -0,0 +1,225 @@ +// +// AnimatedSwitch.swift +// lottie-swift +// +// Created by Brandon Withrow on 2/4/19. +// + +import Foundation +#if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst) +import UIKit + +/// An interactive switch with an 'On' and 'Off' state. When the user taps on the +/// switch the state is toggled and the appropriate animation is played. +/// +/// Both the 'On' and 'Off' have an animation play range associated with their state. +open class AnimatedSwitch: AnimatedControl { + + // MARK: Lifecycle + + public override init( + animation: Animation, + configuration: LottieConfiguration = .shared) + { + /// Generate a haptic generator if available. + #if os(iOS) + if #available(iOS 10.0, *) { + self.hapticGenerator = HapticGenerator() + } else { + hapticGenerator = NullHapticGenerator() + } + #else + hapticGenerator = NullHapticGenerator() + #endif + super.init(animation: animation, configuration: configuration) + updateOnState(isOn: _isOn, animated: false, shouldFireHaptics: false) + accessibilityTraits = UIAccessibilityTraits.button + } + + public override init() { + /// Generate a haptic generator if available. + #if os(iOS) + if #available(iOS 10.0, *) { + self.hapticGenerator = HapticGenerator() + } else { + hapticGenerator = NullHapticGenerator() + } + #else + hapticGenerator = NullHapticGenerator() + #endif + super.init() + updateOnState(isOn: _isOn, animated: false, shouldFireHaptics: false) + accessibilityTraits = UIAccessibilityTraits.button + } + + required public init?(coder aDecoder: NSCoder) { + /// Generate a haptic generator if available. + #if os(iOS) + if #available(iOS 10.0, *) { + self.hapticGenerator = HapticGenerator() + } else { + hapticGenerator = NullHapticGenerator() + } + #else + hapticGenerator = NullHapticGenerator() + #endif + super.init(coder: aDecoder) + accessibilityTraits = UIAccessibilityTraits.button + } + + // MARK: Public + + /// Defines what happens when the user taps the switch while an + /// animation is still in flight + public enum CancelBehavior { + case reverse // default - plays the current animation in reverse + case none // does not update the animation when canceled + } + + /// The cancel behavior for the switch. See CancelBehavior for options + public var cancelBehavior: CancelBehavior = .reverse + + /// The current state of the switch. + public var isOn: Bool { + set { + /// This is forwarded to a private variable because the animation needs to be updated without animation when set externally and with animation when set internally. + guard _isOn != newValue else { return } + updateOnState(isOn: newValue, animated: false, shouldFireHaptics: false) + } + get { + _isOn + } + } + + /// Set the state of the switch and specify animation and haptics + public func setIsOn(_ isOn: Bool, animated: Bool, shouldFireHaptics: Bool = true) { + guard isOn != _isOn else { return } + updateOnState(isOn: isOn, animated: animated, shouldFireHaptics: shouldFireHaptics) + } + + /// Sets the play range for the given state. When the switch is toggled, the animation range is played. + public func setProgressForState( + fromProgress: AnimationProgressTime, + toProgress: AnimationProgressTime, + forOnState: Bool) + { + if forOnState { + onStartProgress = fromProgress + onEndProgress = toProgress + } else { + offStartProgress = fromProgress + offEndProgress = toProgress + } + + updateOnState(isOn: _isOn, animated: false, shouldFireHaptics: false) + } + + public override func endTracking(_ touch: UITouch?, with event: UIEvent?) { + super.endTracking(touch, with: event) + updateOnState(isOn: !_isOn, animated: true, shouldFireHaptics: true) + sendActions(for: .valueChanged) + } + + public override func animationDidSet() { + updateOnState(isOn: _isOn, animated: true, shouldFireHaptics: false) + } + + // MARK: Internal + + // MARK: Animation State + + func updateOnState(isOn: Bool, animated: Bool, shouldFireHaptics: Bool) { + _isOn = isOn + var startProgress = isOn ? onStartProgress : offStartProgress + var endProgress = isOn ? onEndProgress : offEndProgress + let finalProgress = endProgress + + if cancelBehavior == .reverse { + let realtimeProgress = animationView.realtimeAnimationProgress + + let previousStateStart = isOn ? offStartProgress : onStartProgress + let previousStateEnd = isOn ? offEndProgress : onEndProgress + if + realtimeProgress.isInRange( + min(previousStateStart, previousStateEnd), + max(previousStateStart, previousStateEnd)) + { + /// Animation is currently in the previous time range. Reverse the previous play. + startProgress = previousStateEnd + endProgress = previousStateStart + } + } + + updateAccessibilityLabel() + + guard animated == true else { + animationView.currentProgress = finalProgress + return + } + + if shouldFireHaptics { + hapticGenerator.generateImpact() + } + + animationView.play( + fromProgress: startProgress, + toProgress: endProgress, + loopMode: LottieLoopMode.playOnce, + completion: { [weak self] finished in + guard let self = self else { return } + + // For the Main Thread rendering engine, we freeze the animation at the expected final progress + // once the animation is complete. This isn't necessary on the Core Animation engine. + if finished, !(self.animationView.animationLayer is CoreAnimationLayer) { + self.animationView.currentProgress = finalProgress + } + }) + } + + // MARK: Fileprivate + + fileprivate var onStartProgress: CGFloat = 0 + fileprivate var onEndProgress: CGFloat = 1 + fileprivate var offStartProgress: CGFloat = 1 + fileprivate var offEndProgress: CGFloat = 0 + fileprivate var _isOn = false + fileprivate var hapticGenerator: ImpactGenerator + + // MARK: Private + + private func updateAccessibilityLabel() { + accessibilityValue = _isOn ? NSLocalizedString("On", comment: "On") : NSLocalizedString("Off", comment: "Off") + } + +} +#endif + +// MARK: - ImpactGenerator + +protocol ImpactGenerator { + func generateImpact() +} + +// MARK: - NullHapticGenerator + +class NullHapticGenerator: ImpactGenerator { + func generateImpact() { + + } +} + +#if os(iOS) +@available(iOS 10.0, *) +class HapticGenerator: ImpactGenerator { + + // MARK: Internal + + func generateImpact() { + impact.impactOccurred() + } + + // MARK: Fileprivate + + fileprivate let impact = UIImpactFeedbackGenerator(style: .light) +} +#endif diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/AnimationSubview.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/AnimationSubview.swift new file mode 100644 index 0000000000..bc4df54db4 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/AnimationSubview.swift @@ -0,0 +1,20 @@ +// +// AnimationSubview.swift +// lottie-swift +// +// Created by Brandon Withrow on 2/4/19. +// + +import Foundation +#if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst) +import UIKit + +/// A view that can be added to a keypath of an AnimationView +public final class AnimationSubview: UIView { + + var viewLayer: CALayer? { + layer + } + +} +#endif diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/AnimationViewBase.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/AnimationViewBase.swift new file mode 100644 index 0000000000..adf48dc393 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/AnimationViewBase.swift @@ -0,0 +1,78 @@ +// +// AnimationViewBase.swift +// lottie-swift-iOS +// +// Created by Brandon Withrow on 2/6/19. +// + +#if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst) +import UIKit + +/// The base view for `AnimationView` on iOS, tvOS, watchOS, and macCatalyst. +/// +/// Enables the `AnimationView` implementation to be shared across platforms. +public class AnimationViewBase: UIView { + + // MARK: Public + + public override var contentMode: UIView.ContentMode { + didSet { + setNeedsLayout() + } + } + + public override func didMoveToWindow() { + super.didMoveToWindow() + animationMovedToWindow() + } + + public override func layoutSubviews() { + super.layoutSubviews() + layoutAnimation() + } + + // MARK: Internal + + var viewLayer: CALayer? { + layer + } + + var screenScale: CGFloat { + UIScreen.main.scale + } + + func layoutAnimation() { + // Implemented by subclasses. + } + + func animationMovedToWindow() { + // Implemented by subclasses. + } + + func commonInit() { + contentMode = .scaleAspectFit + clipsToBounds = true + NotificationCenter.default.addObserver( + self, + selector: #selector(animationWillEnterForeground), + name: UIApplication.willEnterForegroundNotification, + object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(animationWillMoveToBackground), + name: UIApplication.didEnterBackgroundNotification, + object: nil) + } + + @objc + func animationWillMoveToBackground() { + // Implemented by subclasses. + } + + @objc + func animationWillEnterForeground() { + // Implemented by subclasses. + } + +} +#endif diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/BundleImageProvider.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/BundleImageProvider.swift new file mode 100644 index 0000000000..512d07af7c --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/BundleImageProvider.swift @@ -0,0 +1,90 @@ +// +// LottieBundleImageProvider.swift +// lottie-swift +// +// Created by Brandon Withrow on 1/25/19. +// + +import CoreGraphics +import Foundation +#if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst) +import UIKit + +/// An `AnimationImageProvider` that provides images by name from a specific bundle. +/// The BundleImageProvider is initialized with a bundle and an optional searchPath. +public class BundleImageProvider: AnimationImageProvider { + + // MARK: Lifecycle + + /// Initializes an image provider with a bundle and an optional subpath. + /// + /// Provides images for an animation from a bundle. Additionally the provider can + /// search a specific subpath for the images. + /// + /// - Parameter bundle: The bundle containing images for the provider. + /// - Parameter searchPath: The subpath is a path within the bundle to search for image assets. + /// + public init(bundle: Bundle, searchPath: String?) { + self.bundle = bundle + self.searchPath = searchPath + } + + // MARK: Public + + public func imageForAsset(asset: ImageAsset) -> CGImage? { + + if + let data = Data(imageAsset: asset), + let image = UIImage(data: data) + { + return image.cgImage + } + + let imagePath: String? + /// Try to find the image in the bundle. + if let searchPath = searchPath { + /// Search in the provided search path for the image + var directoryPath = URL(fileURLWithPath: searchPath) + directoryPath.appendPathComponent(asset.directory) + + if let path = bundle.path(forResource: asset.name, ofType: nil, inDirectory: directoryPath.path) { + /// First search for the image in the asset provided sub directory. + imagePath = path + } else if let path = bundle.path(forResource: asset.name, ofType: nil, inDirectory: searchPath) { + /// Try finding the image in the search path. + imagePath = path + } else { + imagePath = bundle.path(forResource: asset.name, ofType: nil) + } + } else { + if let path = bundle.path(forResource: asset.name, ofType: nil, inDirectory: asset.directory) { + /// First search for the image in the asset provided sub directory. + imagePath = path + } else { + /// First search for the image in bundle. + imagePath = bundle.path(forResource: asset.name, ofType: nil) + } + } + + if imagePath == nil { + guard let image = UIImage(named: asset.name, in: bundle, compatibleWith: nil) else { + LottieLogger.shared.assertionFailure("Could not find image \"\(asset.name)\" in bundle") + return nil + } + return image.cgImage + } + + guard let foundPath = imagePath, let image = UIImage(contentsOfFile: foundPath) else { + /// No image found. + LottieLogger.shared.assertionFailure("Could not find image \"\(asset.name)\" in bundle") + return nil + } + return image.cgImage + } + + // MARK: Internal + + let bundle: Bundle + let searchPath: String? +} +#endif diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/Compatibility/CompatibleAnimationKeypath.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/Compatibility/CompatibleAnimationKeypath.swift new file mode 100644 index 0000000000..73963557f9 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/Compatibility/CompatibleAnimationKeypath.swift @@ -0,0 +1,33 @@ +// +// CompatibleAnimationKeypath.swift +// Lottie_iOS +// +// Created by Tyler Hedrick on 3/6/19. +// + +import Foundation +#if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst) + +/// An Objective-C compatible wrapper around Lottie's AnimationKeypath +@objc +public final class CompatibleAnimationKeypath: NSObject { + + // MARK: Lifecycle + + /// Creates a keypath from a dot separated string. The string is separated by "." + @objc + public init(keypath: String) { + animationKeypath = AnimationKeypath(keypath: keypath) + } + + /// Creates a keypath from a list of strings. + @objc + public init(keys: [String]) { + animationKeypath = AnimationKeypath(keys: keys) + } + + // MARK: Public + + public let animationKeypath: AnimationKeypath +} +#endif diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/Compatibility/CompatibleAnimationView.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/Compatibility/CompatibleAnimationView.swift new file mode 100644 index 0000000000..b1e358ce8a --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/Compatibility/CompatibleAnimationView.swift @@ -0,0 +1,323 @@ +// +// CompatibleAnimationView.swift +// Lottie_iOS +// +// Created by Tyler Hedrick on 3/6/19. +// + +import Foundation +#if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst) +import UIKit + +/// An Objective-C compatible wrapper around Lottie's Animation class. +/// Use in tandem with CompatibleAnimationView when using Lottie in Objective-C +@objc +public final class CompatibleAnimation: NSObject { + + // MARK: Lifecycle + + @objc + public init(name: String, bundle: Bundle = Bundle.main) { + self.name = name + self.bundle = bundle + super.init() + } + + // MARK: Internal + + internal var animation: Animation? { + Animation.named(name, bundle: bundle) + } + + @objc + static func named(_ name: String) -> CompatibleAnimation { + CompatibleAnimation(name: name) + } + + // MARK: Private + + private let name: String + private let bundle: Bundle +} + +/// An Objective-C compatible wrapper around Lottie's AnimationView. +@objc +public final class CompatibleAnimationView: UIView { + + // MARK: Lifecycle + + @objc + public init(compatibleAnimation: CompatibleAnimation) { + animationView = AnimationView(animation: compatibleAnimation.animation) + self.compatibleAnimation = compatibleAnimation + super.init(frame: .zero) + commonInit() + } + + @objc + public override init(frame: CGRect) { + animationView = AnimationView() + super.init(frame: frame) + commonInit() + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Public + + @objc + public var compatibleAnimation: CompatibleAnimation? { + didSet { + animationView.animation = compatibleAnimation?.animation + } + } + + @objc + public var loopAnimationCount: CGFloat = 0 { + didSet { + animationView.loopMode = loopAnimationCount == -1 ? .loop : .repeat(Float(loopAnimationCount)) + } + } + + @objc + public override var contentMode: UIView.ContentMode { + set { animationView.contentMode = newValue } + get { animationView.contentMode } + } + + @objc + public var shouldRasterizeWhenIdle: Bool { + set { animationView.shouldRasterizeWhenIdle = newValue } + get { animationView.shouldRasterizeWhenIdle } + } + + @objc + public var currentProgress: CGFloat { + set { animationView.currentProgress = newValue } + get { animationView.currentProgress } + } + + @objc + public var currentTime: TimeInterval { + set { animationView.currentTime = newValue } + get { animationView.currentTime } + } + + @objc + public var currentFrame: CGFloat { + set { animationView.currentFrame = newValue } + get { animationView.currentFrame } + } + + @objc + public var realtimeAnimationFrame: CGFloat { + animationView.realtimeAnimationFrame + } + + @objc + public var realtimeAnimationProgress: CGFloat { + animationView.realtimeAnimationProgress + } + + @objc + public var animationSpeed: CGFloat { + set { animationView.animationSpeed = newValue } + get { animationView.animationSpeed } + } + + @objc + public var respectAnimationFrameRate: Bool { + set { animationView.respectAnimationFrameRate = newValue } + get { animationView.respectAnimationFrameRate } + } + + @objc + public var isAnimationPlaying: Bool { + animationView.isAnimationPlaying + } + + @objc + public func play() { + play(completion: nil) + } + + @objc + public func play(completion: ((Bool) -> Void)?) { + animationView.play(completion: completion) + } + + @objc + public func play( + fromProgress: CGFloat, + toProgress: CGFloat, + completion: ((Bool) -> Void)? = nil) + { + animationView.play( + fromProgress: fromProgress, + toProgress: toProgress, + loopMode: nil, + completion: completion) + } + + @objc + public func play( + fromFrame: CGFloat, + toFrame: CGFloat, + completion: ((Bool) -> Void)? = nil) + { + animationView.play( + fromFrame: fromFrame, + toFrame: toFrame, + loopMode: nil, + completion: completion) + } + + @objc + public func play( + fromMarker: String, + toMarker: String, + completion: ((Bool) -> Void)? = nil) + { + animationView.play( + fromMarker: fromMarker, + toMarker: toMarker, + completion: completion) + } + + @objc + public func stop() { + animationView.stop() + } + + @objc + public func pause() { + animationView.pause() + } + + @objc + public func reloadImages() { + animationView.reloadImages() + } + + @objc + public func forceDisplayUpdate() { + animationView.forceDisplayUpdate() + } + + @objc + public func getValue( + for keypath: CompatibleAnimationKeypath, + atFrame: CGFloat) + -> Any? + { + animationView.getValue( + for: keypath.animationKeypath, + atFrame: atFrame) + } + + @objc + public func logHierarchyKeypaths() { + animationView.logHierarchyKeypaths() + } + + @objc + public func setColorValue(_ color: UIColor, forKeypath keypath: CompatibleAnimationKeypath) { + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + // TODO: Fix color spaces + let colorspace = CGColorSpaceCreateDeviceRGB() + + let convertedColor = color.cgColor.converted(to: colorspace, intent: .defaultIntent, options: nil) + + if let components = convertedColor?.components, components.count == 4 { + red = components[0] + green = components[1] + blue = components[2] + alpha = components[3] + } else { + color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) + } + + let valueProvider = ColorValueProvider(Color(r: Double(red), g: Double(green), b: Double(blue), a: Double(alpha))) + animationView.setValueProvider(valueProvider, keypath: keypath.animationKeypath) + } + + @objc + public func getColorValue(for keypath: CompatibleAnimationKeypath, atFrame: CGFloat) -> UIColor? { + let value = animationView.getValue(for: keypath.animationKeypath, atFrame: atFrame) + guard let colorValue = value as? Color else { + return nil; + } + + return UIColor( + red: CGFloat(colorValue.r), + green: CGFloat(colorValue.g), + blue: CGFloat(colorValue.b), + alpha: CGFloat(colorValue.a)) + } + + @objc + public func addSubview( + _ subview: AnimationSubview, + forLayerAt keypath: CompatibleAnimationKeypath) + { + animationView.addSubview( + subview, + forLayerAt: keypath.animationKeypath) + } + + @objc + public func convert( + rect: CGRect, + toLayerAt keypath: CompatibleAnimationKeypath?) + -> CGRect + { + animationView.convert( + rect, + toLayerAt: keypath?.animationKeypath) ?? .zero + } + + @objc + public func convert( + point: CGPoint, + toLayerAt keypath: CompatibleAnimationKeypath?) + -> CGPoint + { + animationView.convert( + point, + toLayerAt: keypath?.animationKeypath) ?? .zero + } + + @objc + public func progressTime(forMarker named: String) -> CGFloat { + animationView.progressTime(forMarker: named) ?? 0 + } + + @objc + public func frameTime(forMarker named: String) -> CGFloat { + animationView.frameTime(forMarker: named) ?? 0 + } + + // MARK: Private + + private let animationView: AnimationView + + private func commonInit() { + translatesAutoresizingMaskIntoConstraints = false + setUpViews() + } + + private func setUpViews() { + animationView.translatesAutoresizingMaskIntoConstraints = false + addSubview(animationView) + animationView.topAnchor.constraint(equalTo: topAnchor).isActive = true + animationView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true + animationView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true + animationView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + } +} +#endif diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/FilepathImageProvider.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/FilepathImageProvider.swift new file mode 100644 index 0000000000..4fe3db0d75 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/FilepathImageProvider.swift @@ -0,0 +1,60 @@ +// +// FilepathImageProvider.swift +// lottie-swift +// +// Created by Brandon Withrow on 2/1/19. +// + +import Foundation +#if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst) +import UIKit + +/// Provides an image for a lottie animation from a provided Bundle. +public class FilepathImageProvider: AnimationImageProvider { + + // MARK: Lifecycle + + /// Initializes an image provider with a specific filepath. + /// + /// - Parameter filepath: The absolute filepath containing the images. + /// + public init(filepath: String) { + self.filepath = URL(fileURLWithPath: filepath) + } + + public init(filepath: URL) { + self.filepath = filepath + } + + // MARK: Public + + public func imageForAsset(asset: ImageAsset) -> CGImage? { + + if + asset.name.hasPrefix("data:"), + let url = URL(string: asset.name), + let data = try? Data(contentsOf: url), + let image = UIImage(data: data) + { + return image.cgImage + } + + let directPath = filepath.appendingPathComponent(asset.name).path + if FileManager.default.fileExists(atPath: directPath) { + return UIImage(contentsOfFile: directPath)?.cgImage + } + + let pathWithDirectory = filepath.appendingPathComponent(asset.directory).appendingPathComponent(asset.name).path + if FileManager.default.fileExists(atPath: pathWithDirectory) { + return UIImage(contentsOfFile: pathWithDirectory)?.cgImage + } + + LottieLogger.shared.assertionFailure("Could not find image \"\(asset.name)\" in bundle") + return nil + } + + // MARK: Internal + + let filepath: URL +} +#endif diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/UIColorExtension.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/UIColorExtension.swift new file mode 100644 index 0000000000..4cf335e059 --- /dev/null +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Public/iOS/UIColorExtension.swift @@ -0,0 +1,21 @@ +// +// UIColorExtension.swift +// lottie-swift +// +// Created by Brandon Withrow on 2/4/19. +// + +import Foundation +#if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst) +import UIKit + +extension UIColor { + + public var lottieColorValue: Color { + var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 + getRed(&r, green: &g, blue: &b, alpha: &a) + return Color(r: Double(r), g: Double(g), b: Double(b), a: Double(a)) + } + +} +#endif diff --git a/Tests/LottieMetalTest/QOILoader/BUILD b/Tests/LottieMetalTest/QOILoader/BUILD new file mode 100644 index 0000000000..0c3f6ac18e --- /dev/null +++ b/Tests/LottieMetalTest/QOILoader/BUILD @@ -0,0 +1,33 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +objc_library( + name = "QOILoader", + enable_modules = True, + module_name = "QOILoader", + srcs = glob([ + "Sources/**/*.m", + "Sources/**/*.mm", + "Sources/**/*.h", + "Sources/**/*.c", + "Sources/**/*.cpp", + "Sources/**/*.hpp", + ]), + copts = [ + "-Werror", + "-I{}/Sources".format(package_name()), + ], + hdrs = glob([ + "PublicHeaders/**/*.h", + ]), + includes = [ + "PublicHeaders", + ], + deps = [ + ], + sdk_frameworks = [ + "Foundation", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Tests/LottieMetalTest/QOILoader/PublicHeaders/QOILoader/QOILoader.h b/Tests/LottieMetalTest/QOILoader/PublicHeaders/QOILoader/QOILoader.h new file mode 100644 index 0000000000..c2c92fb90e --- /dev/null +++ b/Tests/LottieMetalTest/QOILoader/PublicHeaders/QOILoader/QOILoader.h @@ -0,0 +1,10 @@ +#ifndef QOILoader_h +#define QOILoader_h + +#import +#import + +NSData * _Nullable encodeImageQOI(UIImage * _Nonnull image); +UIImage * _Nullable decodeImageQOI(NSData * _Nonnull data); + +#endif /* QOILoader_h */ diff --git a/Tests/LottieMetalTest/QOILoader/Sources/QOILoader.m b/Tests/LottieMetalTest/QOILoader/Sources/QOILoader.m new file mode 100644 index 0000000000..6bc35b87a6 --- /dev/null +++ b/Tests/LottieMetalTest/QOILoader/Sources/QOILoader.m @@ -0,0 +1,64 @@ +#import + +#define QOI_IMPLEMENTATION +#import "qoi.h" + +NSData * _Nullable encodeImageQOI(UIImage * _Nonnull image) { + if (image == nil) { + return nil; + } + CGImageRef cgImage = image.CGImage; + if (cgImage == nil) { + return nil; + } + CGDataProviderRef dataProvider = CGImageGetDataProvider(cgImage); + if (dataProvider == nil) { + return nil; + } + NSData *data = (__bridge_transfer NSData *)CGDataProviderCopyData(dataProvider); + if (data == nil) { + return nil; + } + if (CGImageGetBitsPerPixel(cgImage) / CGImageGetBitsPerComponent(cgImage) != 4) { + return nil; + } + + int outLength = 0; + void *outBytes = qoi_encode(data.bytes, &(qoi_desc){ + .width = (unsigned int)CGImageGetWidth(cgImage), + .height = (unsigned int)CGImageGetHeight(cgImage), + .channels = 4, + .colorspace = QOI_SRGB + }, &outLength); + if (outBytes == nil) { + return nil; + } + return [[NSData alloc] initWithBytesNoCopy:outBytes length:outLength freeWhenDone:true]; +} + +static void releaseMallocedMemory(void *info, const void *data, size_t size) { + free((void *)data); +} + +UIImage * _Nullable decodeImageQOI(NSData * _Nonnull data) { + qoi_desc desc; + void *outPixels = qoi_decode(data.bytes, (int)data.length, &desc, 4); + if (outPixels == nil) { + return nil; + } + + CGDataProviderRef dataProvider = CGDataProviderCreateWithData(nil, outPixels, desc.width * 4 * desc.height, releaseMallocedMemory); + if (dataProvider == nil) { + free(outPixels); + return nil; + } + + CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst; + CGImageRef cgImage = CGImageCreate(desc.width, desc.height, 8, 8 * 4, desc.width * 4, CGColorSpaceCreateDeviceRGB(), bitmapInfo, dataProvider, nil, false, kCGRenderingIntentDefault); + CGDataProviderRelease(dataProvider); + + UIImage *image = [[UIImage alloc] initWithCGImage:cgImage]; + CGImageRelease(cgImage); + + return image; +} diff --git a/Tests/LottieMetalTest/QOILoader/Sources/qoi.h b/Tests/LottieMetalTest/QOILoader/Sources/qoi.h new file mode 100644 index 0000000000..3c38fd4de2 --- /dev/null +++ b/Tests/LottieMetalTest/QOILoader/Sources/qoi.h @@ -0,0 +1,649 @@ +/* + +Copyright (c) 2021, Dominic Szablewski - https://phoboslab.org +SPDX-License-Identifier: MIT + + +QOI - The "Quite OK Image" format for fast, lossless image compression + +-- About + +QOI encodes and decodes images in a lossless format. Compared to stb_image and +stb_image_write QOI offers 20x-50x faster encoding, 3x-4x faster decoding and +20% better compression. + + +-- Synopsis + +// Define `QOI_IMPLEMENTATION` in *one* C/C++ file before including this +// library to create the implementation. + +#define QOI_IMPLEMENTATION +#include "qoi.h" + +// Encode and store an RGBA buffer to the file system. The qoi_desc describes +// the input pixel data. +qoi_write("image_new.qoi", rgba_pixels, &(qoi_desc){ + .width = 1920, + .height = 1080, + .channels = 4, + .colorspace = QOI_SRGB +}); + +// Load and decode a QOI image from the file system into a 32bbp RGBA buffer. +// The qoi_desc struct will be filled with the width, height, number of channels +// and colorspace read from the file header. +qoi_desc desc; +void *rgba_pixels = qoi_read("image.qoi", &desc, 4); + + + +-- Documentation + +This library provides the following functions; +- qoi_read -- read and decode a QOI file +- qoi_decode -- decode the raw bytes of a QOI image from memory +- qoi_write -- encode and write a QOI file +- qoi_encode -- encode an rgba buffer into a QOI image in memory + +See the function declaration below for the signature and more information. + +If you don't want/need the qoi_read and qoi_write functions, you can define +QOI_NO_STDIO before including this library. + +This library uses malloc() and free(). To supply your own malloc implementation +you can define QOI_MALLOC and QOI_FREE before including this library. + +This library uses memset() to zero-initialize the index. To supply your own +implementation you can define QOI_ZEROARR before including this library. + + +-- Data Format + +A QOI file has a 14 byte header, followed by any number of data "chunks" and an +8-byte end marker. + +struct qoi_header_t { + char magic[4]; // magic bytes "qoif" + uint32_t width; // image width in pixels (BE) + uint32_t height; // image height in pixels (BE) + uint8_t channels; // 3 = RGB, 4 = RGBA + uint8_t colorspace; // 0 = sRGB with linear alpha, 1 = all channels linear +}; + +Images are encoded row by row, left to right, top to bottom. The decoder and +encoder start with {r: 0, g: 0, b: 0, a: 255} as the previous pixel value. An +image is complete when all pixels specified by width * height have been covered. + +Pixels are encoded as + - a run of the previous pixel + - an index into an array of previously seen pixels + - a difference to the previous pixel value in r,g,b + - full r,g,b or r,g,b,a values + +The color channels are assumed to not be premultiplied with the alpha channel +("un-premultiplied alpha"). + +A running array[64] (zero-initialized) of previously seen pixel values is +maintained by the encoder and decoder. Each pixel that is seen by the encoder +and decoder is put into this array at the position formed by a hash function of +the color value. In the encoder, if the pixel value at the index matches the +current pixel, this index position is written to the stream as QOI_OP_INDEX. +The hash function for the index is: + + index_position = (r * 3 + g * 5 + b * 7 + a * 11) % 64 + +Each chunk starts with a 2- or 8-bit tag, followed by a number of data bits. The +bit length of chunks is divisible by 8 - i.e. all chunks are byte aligned. All +values encoded in these data bits have the most significant bit on the left. + +The 8-bit tags have precedence over the 2-bit tags. A decoder must check for the +presence of an 8-bit tag first. + +The byte stream's end is marked with 7 0x00 bytes followed a single 0x01 byte. + + +The possible chunks are: + + +.- QOI_OP_INDEX ----------. +| Byte[0] | +| 7 6 5 4 3 2 1 0 | +|-------+-----------------| +| 0 0 | index | +`-------------------------` +2-bit tag b00 +6-bit index into the color index array: 0..63 + +A valid encoder must not issue 2 or more consecutive QOI_OP_INDEX chunks to the +same index. QOI_OP_RUN should be used instead. + + +.- QOI_OP_DIFF -----------. +| Byte[0] | +| 7 6 5 4 3 2 1 0 | +|-------+-----+-----+-----| +| 0 1 | dr | dg | db | +`-------------------------` +2-bit tag b01 +2-bit red channel difference from the previous pixel between -2..1 +2-bit green channel difference from the previous pixel between -2..1 +2-bit blue channel difference from the previous pixel between -2..1 + +The difference to the current channel values are using a wraparound operation, +so "1 - 2" will result in 255, while "255 + 1" will result in 0. + +Values are stored as unsigned integers with a bias of 2. E.g. -2 is stored as +0 (b00). 1 is stored as 3 (b11). + +The alpha value remains unchanged from the previous pixel. + + +.- QOI_OP_LUMA -------------------------------------. +| Byte[0] | Byte[1] | +| 7 6 5 4 3 2 1 0 | 7 6 5 4 3 2 1 0 | +|-------+-----------------+-------------+-----------| +| 1 0 | green diff | dr - dg | db - dg | +`---------------------------------------------------` +2-bit tag b10 +6-bit green channel difference from the previous pixel -32..31 +4-bit red channel difference minus green channel difference -8..7 +4-bit blue channel difference minus green channel difference -8..7 + +The green channel is used to indicate the general direction of change and is +encoded in 6 bits. The red and blue channels (dr and db) base their diffs off +of the green channel difference and are encoded in 4 bits. I.e.: + dr_dg = (cur_px.r - prev_px.r) - (cur_px.g - prev_px.g) + db_dg = (cur_px.b - prev_px.b) - (cur_px.g - prev_px.g) + +The difference to the current channel values are using a wraparound operation, +so "10 - 13" will result in 253, while "250 + 7" will result in 1. + +Values are stored as unsigned integers with a bias of 32 for the green channel +and a bias of 8 for the red and blue channel. + +The alpha value remains unchanged from the previous pixel. + + +.- QOI_OP_RUN ------------. +| Byte[0] | +| 7 6 5 4 3 2 1 0 | +|-------+-----------------| +| 1 1 | run | +`-------------------------` +2-bit tag b11 +6-bit run-length repeating the previous pixel: 1..62 + +The run-length is stored with a bias of -1. Note that the run-lengths 63 and 64 +(b111110 and b111111) are illegal as they are occupied by the QOI_OP_RGB and +QOI_OP_RGBA tags. + + +.- QOI_OP_RGB ------------------------------------------. +| Byte[0] | Byte[1] | Byte[2] | Byte[3] | +| 7 6 5 4 3 2 1 0 | 7 .. 0 | 7 .. 0 | 7 .. 0 | +|-------------------------+---------+---------+---------| +| 1 1 1 1 1 1 1 0 | red | green | blue | +`-------------------------------------------------------` +8-bit tag b11111110 +8-bit red channel value +8-bit green channel value +8-bit blue channel value + +The alpha value remains unchanged from the previous pixel. + + +.- QOI_OP_RGBA ---------------------------------------------------. +| Byte[0] | Byte[1] | Byte[2] | Byte[3] | Byte[4] | +| 7 6 5 4 3 2 1 0 | 7 .. 0 | 7 .. 0 | 7 .. 0 | 7 .. 0 | +|-------------------------+---------+---------+---------+---------| +| 1 1 1 1 1 1 1 1 | red | green | blue | alpha | +`-----------------------------------------------------------------` +8-bit tag b11111111 +8-bit red channel value +8-bit green channel value +8-bit blue channel value +8-bit alpha channel value + +*/ + + +/* ----------------------------------------------------------------------------- +Header - Public functions */ + +#ifndef QOI_H +#define QOI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* A pointer to a qoi_desc struct has to be supplied to all of qoi's functions. +It describes either the input format (for qoi_write and qoi_encode), or is +filled with the description read from the file header (for qoi_read and +qoi_decode). + +The colorspace in this qoi_desc is an enum where + 0 = sRGB, i.e. gamma scaled RGB channels and a linear alpha channel + 1 = all channels are linear +You may use the constants QOI_SRGB or QOI_LINEAR. The colorspace is purely +informative. It will be saved to the file header, but does not affect +how chunks are en-/decoded. */ + +#define QOI_SRGB 0 +#define QOI_LINEAR 1 + +typedef struct { + unsigned int width; + unsigned int height; + unsigned char channels; + unsigned char colorspace; +} qoi_desc; + +#ifndef QOI_NO_STDIO + +/* Encode raw RGB or RGBA pixels into a QOI image and write it to the file +system. The qoi_desc struct must be filled with the image width, height, +number of channels (3 = RGB, 4 = RGBA) and the colorspace. + +The function returns 0 on failure (invalid parameters, or fopen or malloc +failed) or the number of bytes written on success. */ + +int qoi_write(const char *filename, const void *data, const qoi_desc *desc); + + +/* Read and decode a QOI image from the file system. If channels is 0, the +number of channels from the file header is used. If channels is 3 or 4 the +output format will be forced into this number of channels. + +The function either returns NULL on failure (invalid data, or malloc or fopen +failed) or a pointer to the decoded pixels. On success, the qoi_desc struct +will be filled with the description from the file header. + +The returned pixel data should be free()d after use. */ + +void *qoi_read(const char *filename, qoi_desc *desc, int channels); + +#endif /* QOI_NO_STDIO */ + + +/* Encode raw RGB or RGBA pixels into a QOI image in memory. + +The function either returns NULL on failure (invalid parameters or malloc +failed) or a pointer to the encoded data on success. On success the out_len +is set to the size in bytes of the encoded data. + +The returned qoi data should be free()d after use. */ + +void *qoi_encode(const void *data, const qoi_desc *desc, int *out_len); + + +/* Decode a QOI image from memory. + +The function either returns NULL on failure (invalid parameters or malloc +failed) or a pointer to the decoded pixels. On success, the qoi_desc struct +is filled with the description from the file header. + +The returned pixel data should be free()d after use. */ + +void *qoi_decode(const void *data, int size, qoi_desc *desc, int channels); + + +#ifdef __cplusplus +} +#endif +#endif /* QOI_H */ + + +/* ----------------------------------------------------------------------------- +Implementation */ + +#ifdef QOI_IMPLEMENTATION +#include +#include + +#ifndef QOI_MALLOC + #define QOI_MALLOC(sz) malloc(sz) + #define QOI_FREE(p) free(p) +#endif +#ifndef QOI_ZEROARR + #define QOI_ZEROARR(a) memset((a),0,sizeof(a)) +#endif + +#define QOI_OP_INDEX 0x00 /* 00xxxxxx */ +#define QOI_OP_DIFF 0x40 /* 01xxxxxx */ +#define QOI_OP_LUMA 0x80 /* 10xxxxxx */ +#define QOI_OP_RUN 0xc0 /* 11xxxxxx */ +#define QOI_OP_RGB 0xfe /* 11111110 */ +#define QOI_OP_RGBA 0xff /* 11111111 */ + +#define QOI_MASK_2 0xc0 /* 11000000 */ + +#define QOI_COLOR_HASH(C) (C.rgba.r*3 + C.rgba.g*5 + C.rgba.b*7 + C.rgba.a*11) +#define QOI_MAGIC \ + (((unsigned int)'q') << 24 | ((unsigned int)'o') << 16 | \ + ((unsigned int)'i') << 8 | ((unsigned int)'f')) +#define QOI_HEADER_SIZE 14 + +/* 2GB is the max file size that this implementation can safely handle. We guard +against anything larger than that, assuming the worst case with 5 bytes per +pixel, rounded down to a nice clean value. 400 million pixels ought to be +enough for anybody. */ +#define QOI_PIXELS_MAX ((unsigned int)400000000) + +typedef union { + struct { unsigned char r, g, b, a; } rgba; + unsigned int v; +} qoi_rgba_t; + +static const unsigned char qoi_padding[8] = {0,0,0,0,0,0,0,1}; + +static void qoi_write_32(unsigned char *bytes, int *p, unsigned int v) { + bytes[(*p)++] = (0xff000000 & v) >> 24; + bytes[(*p)++] = (0x00ff0000 & v) >> 16; + bytes[(*p)++] = (0x0000ff00 & v) >> 8; + bytes[(*p)++] = (0x000000ff & v); +} + +static unsigned int qoi_read_32(const unsigned char *bytes, int *p) { + unsigned int a = bytes[(*p)++]; + unsigned int b = bytes[(*p)++]; + unsigned int c = bytes[(*p)++]; + unsigned int d = bytes[(*p)++]; + return a << 24 | b << 16 | c << 8 | d; +} + +void *qoi_encode(const void *data, const qoi_desc *desc, int *out_len) { + int i, max_size, p, run; + int px_len, px_end, px_pos, channels; + unsigned char *bytes; + const unsigned char *pixels; + qoi_rgba_t index[64]; + qoi_rgba_t px, px_prev; + + if ( + data == NULL || out_len == NULL || desc == NULL || + desc->width == 0 || desc->height == 0 || + desc->channels < 3 || desc->channels > 4 || + desc->colorspace > 1 || + desc->height >= QOI_PIXELS_MAX / desc->width + ) { + return NULL; + } + + max_size = + desc->width * desc->height * (desc->channels + 1) + + QOI_HEADER_SIZE + sizeof(qoi_padding); + + p = 0; + bytes = (unsigned char *) QOI_MALLOC(max_size); + if (!bytes) { + return NULL; + } + + qoi_write_32(bytes, &p, QOI_MAGIC); + qoi_write_32(bytes, &p, desc->width); + qoi_write_32(bytes, &p, desc->height); + bytes[p++] = desc->channels; + bytes[p++] = desc->colorspace; + + + pixels = (const unsigned char *)data; + + QOI_ZEROARR(index); + + run = 0; + px_prev.rgba.r = 0; + px_prev.rgba.g = 0; + px_prev.rgba.b = 0; + px_prev.rgba.a = 255; + px = px_prev; + + px_len = desc->width * desc->height * desc->channels; + px_end = px_len - desc->channels; + channels = desc->channels; + + for (px_pos = 0; px_pos < px_len; px_pos += channels) { + px.rgba.r = pixels[px_pos + 0]; + px.rgba.g = pixels[px_pos + 1]; + px.rgba.b = pixels[px_pos + 2]; + + if (channels == 4) { + px.rgba.a = pixels[px_pos + 3]; + } + + if (px.v == px_prev.v) { + run++; + if (run == 62 || px_pos == px_end) { + bytes[p++] = QOI_OP_RUN | (run - 1); + run = 0; + } + } + else { + int index_pos; + + if (run > 0) { + bytes[p++] = QOI_OP_RUN | (run - 1); + run = 0; + } + + index_pos = QOI_COLOR_HASH(px) % 64; + + if (index[index_pos].v == px.v) { + bytes[p++] = QOI_OP_INDEX | index_pos; + } + else { + index[index_pos] = px; + + if (px.rgba.a == px_prev.rgba.a) { + signed char vr = px.rgba.r - px_prev.rgba.r; + signed char vg = px.rgba.g - px_prev.rgba.g; + signed char vb = px.rgba.b - px_prev.rgba.b; + + signed char vg_r = vr - vg; + signed char vg_b = vb - vg; + + if ( + vr > -3 && vr < 2 && + vg > -3 && vg < 2 && + vb > -3 && vb < 2 + ) { + bytes[p++] = QOI_OP_DIFF | (vr + 2) << 4 | (vg + 2) << 2 | (vb + 2); + } + else if ( + vg_r > -9 && vg_r < 8 && + vg > -33 && vg < 32 && + vg_b > -9 && vg_b < 8 + ) { + bytes[p++] = QOI_OP_LUMA | (vg + 32); + bytes[p++] = (vg_r + 8) << 4 | (vg_b + 8); + } + else { + bytes[p++] = QOI_OP_RGB; + bytes[p++] = px.rgba.r; + bytes[p++] = px.rgba.g; + bytes[p++] = px.rgba.b; + } + } + else { + bytes[p++] = QOI_OP_RGBA; + bytes[p++] = px.rgba.r; + bytes[p++] = px.rgba.g; + bytes[p++] = px.rgba.b; + bytes[p++] = px.rgba.a; + } + } + } + px_prev = px; + } + + for (i = 0; i < (int)sizeof(qoi_padding); i++) { + bytes[p++] = qoi_padding[i]; + } + + *out_len = p; + return bytes; +} + +void *qoi_decode(const void *data, int size, qoi_desc *desc, int channels) { + const unsigned char *bytes; + unsigned int header_magic; + unsigned char *pixels; + qoi_rgba_t index[64]; + qoi_rgba_t px; + int px_len, chunks_len, px_pos; + int p = 0, run = 0; + + if ( + data == NULL || desc == NULL || + (channels != 0 && channels != 3 && channels != 4) || + size < QOI_HEADER_SIZE + (int)sizeof(qoi_padding) + ) { + return NULL; + } + + bytes = (const unsigned char *)data; + + header_magic = qoi_read_32(bytes, &p); + desc->width = qoi_read_32(bytes, &p); + desc->height = qoi_read_32(bytes, &p); + desc->channels = bytes[p++]; + desc->colorspace = bytes[p++]; + + if ( + desc->width == 0 || desc->height == 0 || + desc->channels < 3 || desc->channels > 4 || + desc->colorspace > 1 || + header_magic != QOI_MAGIC || + desc->height >= QOI_PIXELS_MAX / desc->width + ) { + return NULL; + } + + if (channels == 0) { + channels = desc->channels; + } + + px_len = desc->width * desc->height * channels; + pixels = (unsigned char *) QOI_MALLOC(px_len); + if (!pixels) { + return NULL; + } + + QOI_ZEROARR(index); + px.rgba.r = 0; + px.rgba.g = 0; + px.rgba.b = 0; + px.rgba.a = 255; + + chunks_len = size - (int)sizeof(qoi_padding); + for (px_pos = 0; px_pos < px_len; px_pos += channels) { + if (run > 0) { + run--; + } + else if (p < chunks_len) { + int b1 = bytes[p++]; + + if (b1 == QOI_OP_RGB) { + px.rgba.r = bytes[p++]; + px.rgba.g = bytes[p++]; + px.rgba.b = bytes[p++]; + } + else if (b1 == QOI_OP_RGBA) { + px.rgba.r = bytes[p++]; + px.rgba.g = bytes[p++]; + px.rgba.b = bytes[p++]; + px.rgba.a = bytes[p++]; + } + else if ((b1 & QOI_MASK_2) == QOI_OP_INDEX) { + px = index[b1]; + } + else if ((b1 & QOI_MASK_2) == QOI_OP_DIFF) { + px.rgba.r += ((b1 >> 4) & 0x03) - 2; + px.rgba.g += ((b1 >> 2) & 0x03) - 2; + px.rgba.b += ( b1 & 0x03) - 2; + } + else if ((b1 & QOI_MASK_2) == QOI_OP_LUMA) { + int b2 = bytes[p++]; + int vg = (b1 & 0x3f) - 32; + px.rgba.r += vg - 8 + ((b2 >> 4) & 0x0f); + px.rgba.g += vg; + px.rgba.b += vg - 8 + (b2 & 0x0f); + } + else if ((b1 & QOI_MASK_2) == QOI_OP_RUN) { + run = (b1 & 0x3f); + } + + index[QOI_COLOR_HASH(px) % 64] = px; + } + + pixels[px_pos + 0] = px.rgba.r; + pixels[px_pos + 1] = px.rgba.g; + pixels[px_pos + 2] = px.rgba.b; + + if (channels == 4) { + pixels[px_pos + 3] = px.rgba.a; + } + } + + return pixels; +} + +#ifndef QOI_NO_STDIO +#include + +int qoi_write(const char *filename, const void *data, const qoi_desc *desc) { + FILE *f = fopen(filename, "wb"); + int size, err; + void *encoded; + + if (!f) { + return 0; + } + + encoded = qoi_encode(data, desc, &size); + if (!encoded) { + fclose(f); + return 0; + } + + fwrite(encoded, 1, size, f); + fflush(f); + err = ferror(f); + fclose(f); + + QOI_FREE(encoded); + return err ? 0 : size; +} + +void *qoi_read(const char *filename, qoi_desc *desc, int channels) { + FILE *f = fopen(filename, "rb"); + int size, bytes_read; + void *pixels, *data; + + if (!f) { + return NULL; + } + + fseek(f, 0, SEEK_END); + size = (int)ftell(f); + if (size <= 0 || fseek(f, 0, SEEK_SET) != 0) { + fclose(f); + return NULL; + } + + data = QOI_MALLOC(size); + if (!data) { + fclose(f); + return NULL; + } + + bytes_read = (int)fread(data, 1, size, f); + fclose(f); + pixels = (bytes_read != size) ? NULL : qoi_decode(data, bytes_read, desc, channels); + QOI_FREE(data); + return pixels; +} + +#endif /* QOI_NO_STDIO */ +#endif /* QOI_IMPLEMENTATION */ diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/BUILD b/Tests/LottieMetalTest/SoftwareLottieRenderer/BUILD new file mode 100644 index 0000000000..1b46d73bdf --- /dev/null +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/BUILD @@ -0,0 +1,34 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +objc_library( + name = "SoftwareLottieRenderer", + enable_modules = True, + module_name = "SoftwareLottieRenderer", + srcs = glob([ + "Sources/**/*.m", + "Sources/**/*.mm", + "Sources/**/*.h", + "Sources/**/*.c", + "Sources/**/*.cpp", + "Sources/**/*.hpp", + ]), + copts = [ + "-Werror", + "-I{}/Sources".format(package_name()), + ], + hdrs = glob([ + "PublicHeaders/**/*.h", + ]), + includes = [ + "PublicHeaders", + ], + deps = [ + "//submodules/TelegramUI/Components/LottieCpp", + ], + sdk_frameworks = [ + "Foundation", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/PublicHeaders/SoftwareLottieRenderer/SoftwareLottieRenderer.h b/Tests/LottieMetalTest/SoftwareLottieRenderer/PublicHeaders/SoftwareLottieRenderer/SoftwareLottieRenderer.h new file mode 100644 index 0000000000..7a3dded9ce --- /dev/null +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/PublicHeaders/SoftwareLottieRenderer/SoftwareLottieRenderer.h @@ -0,0 +1,20 @@ +#ifndef SoftwareLottieRenderer_h +#define SoftwareLottieRenderer_h + +#import +#import + +#import + +#ifdef __cplusplus +extern "C" { +#endif + +CGRect getPathNativeBoundingBox(CGPathRef _Nonnull path); +UIImage * _Nullable renderLottieAnimationContainer(LottieAnimationContainer * _Nonnull animationContainer, CGSize size, bool useReferenceRendering); + +#ifdef __cplusplus +} +#endif + +#endif /* QOILoader_h */ diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/Canvas.h b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/Canvas.h new file mode 100644 index 0000000000..28b5e69fb9 --- /dev/null +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/Canvas.h @@ -0,0 +1,128 @@ +#ifndef Canvas_h +#define Canvas_h + +#include + +#include +#include +#include + +namespace lottieRendering { + +struct Color { + double r; + double g; + double b; + double a; + + Color(double r_, double g_, double b_, double a_) : + r(r_), g(g_), b(b_), a(a_) { + } + + bool operator==(Color const &rhs) const { + if (r != rhs.r) { + return false; + } + if (g != rhs.g) { + return false; + } + if (b != rhs.b) { + return false; + } + if (a != rhs.a) { + return false; + } + return true; + } + + bool operator!=(Color const &rhs) const { + return !(*this == rhs); + } +}; + +enum class BlendMode { + Normal, + DestinationIn, + DestinationOut +}; + +enum class FillRule: int { + None = 0, + NonZeroWinding = 1, + EvenOdd = 2 +}; + +enum class LineCap: int { + None = 0, + Butt = 1, + Round = 2, + Square = 3 +}; + +enum class LineJoin: int { + None = 0, + Miter = 1, + Round = 2, + Bevel = 3 +}; + +class Image { +public: + virtual ~Image() = default; +}; + +class Gradient { +public: + Gradient(std::vector const &colors, std::vector const &locations) : + _colors(colors), + _locations(locations) { + assert(_colors.size() == _locations.size()); + } + + std::vector const &colors() const { + return _colors; + } + + std::vector const &locations() const { + return _locations; + } + +private: + std::vector _colors; + std::vector _locations; +}; + +class Canvas { +public: + virtual ~Canvas() = default; + + virtual int width() const = 0; + virtual int height() const = 0; + + virtual std::shared_ptr makeLayer(int width, int height) = 0; + + virtual void saveState() = 0; + virtual void restoreState() = 0; + + virtual void fillPath(std::shared_ptr const &path, FillRule fillRule, Color const &color) = 0; + virtual void linearGradientFillPath(std::shared_ptr const &path, FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) = 0; + virtual void radialGradientFillPath(std::shared_ptr const &path, FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &startCenter, double startRadius, lottie::Vector2D const &endCenter, double endRadius) = 0; + + virtual void strokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, Color const &color) = 0; + virtual void linearGradientStrokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) = 0; + virtual void radialGradientStrokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &startCenter, double startRadius, lottie::Vector2D const &endCenter, double endRadius) = 0; + + virtual void fill(lottie::CGRect const &rect, Color const &fillColor) = 0; + virtual void setBlendMode(BlendMode blendMode) = 0; + + virtual void setAlpha(double alpha) = 0; + + virtual void concatenate(lottie::CATransform3D const &transform) = 0; + + virtual void draw(std::shared_ptr const &other, lottie::CGRect const &rect) = 0; +}; + +} + +#endif + diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.h b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.h new file mode 100644 index 0000000000..fc572306cd --- /dev/null +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.h @@ -0,0 +1,72 @@ +#ifndef CoreGraphicsCanvasImpl_h +#define CoreGraphicsCanvasImpl_h + +#include "Canvas.h" + +namespace lottieRendering { + +class ImageImpl: public Image { +public: + ImageImpl(::CGImageRef image); + virtual ~ImageImpl(); + ::CGImageRef nativeImage() const; + +private: + CGImageRef _image = nil; +}; + +class CanvasImpl: public Canvas { +public: + CanvasImpl(int width, int height); + CanvasImpl(CGContextRef context, int width, int height); + virtual ~CanvasImpl(); + + virtual int width() const override; + virtual int height() const override; + + std::shared_ptr makeLayer(int width, int height) override; + + virtual void saveState() override; + virtual void restoreState() override; + + virtual void fillPath(std::shared_ptr const &path, FillRule fillRule, Color const &color) override; + virtual void linearGradientFillPath(std::shared_ptr const &path, FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) override; + virtual void radialGradientFillPath(std::shared_ptr const &path, FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &startCenter, double startRadius, lottie::Vector2D const &endCenter, double endRadius) override; + + virtual void strokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, Color const &color) override; + virtual void linearGradientStrokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) override; + virtual void radialGradientStrokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &startCenter, double startRadius, lottie::Vector2D const &endCenter, double endRadius) override; + + virtual void fill(lottie::CGRect const &rect, Color const &fillColor) override; + virtual void setBlendMode(BlendMode blendMode) override; + virtual void setAlpha(double alpha) override; + virtual void concatenate(lottie::CATransform3D const &transform) override; + + virtual std::shared_ptr makeImage() const; + virtual void draw(std::shared_ptr const &other, lottie::CGRect const &rect) override; + + CGContextRef nativeContext() const { + return _context; + } + + std::vector &backingData() { + return _backingData; + } + + int bytesPerRow() { + return _bytesPerRow; + } + +private: + int _width = 0; + int _height = 0; + int _bytesPerRow = 0; + std::vector _backingData; + CGContextRef _context = nil; + CGContextRef _topContext = nil; + CGLayerRef _layer = nil; +}; + +} + +#endif diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.mm b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.mm new file mode 100644 index 0000000000..5a915888c6 --- /dev/null +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.mm @@ -0,0 +1,462 @@ +#include "CoreGraphicsCanvasImpl.h" + + + +namespace lottieRendering { + +namespace { + +int alignUp(int size, int align) { + assert(((align - 1) & align) == 0); + + int alignmentMask = align - 1; + return (size + alignmentMask) & ~alignmentMask; +} + +} + +ImageImpl::ImageImpl(::CGImageRef image) { + _image = CGImageRetain(image); +} + +ImageImpl::~ImageImpl() { + CFRelease(_image); +} + +::CGImageRef ImageImpl::nativeImage() const { + return _image; +} + + +CanvasImpl::CanvasImpl(int width, int height) { + _width = width; + _height = height; + _bytesPerRow = alignUp(width * 4, 16); + _backingData.resize(_bytesPerRow * _height); + memset(_backingData.data(), 0, _backingData.size()); + + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + + CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst; + _context = CGBitmapContextCreate(_backingData.data(), _width, _height, 8, _bytesPerRow, colorSpace, bitmapInfo); + + CGContextClearRect(_context, CGRectMake(0.0, 0.0, _width, _height)); + + //CGContextSetInterpolationQuality(_context, kCGInterpolationLow); + //CGContextSetAllowsAntialiasing(_context, true); + //CGContextSetShouldAntialias(_context, true); + + CFRelease(colorSpace); + + _topContext = CGContextRetain(_context); +} + +CanvasImpl::CanvasImpl(CGContextRef context, int width, int height) { + _topContext = CGContextRetain(context); + _layer = CGLayerCreateWithContext(context, CGSizeMake(width, height), nil); + _context = CGContextRetain(CGLayerGetContext(_layer)); + _width = width; + _height = height; +} + +CanvasImpl::~CanvasImpl() { + CFRelease(_context); + if (_topContext) { + CFRelease(_topContext); + } + if (_layer) { + CFRelease(_layer); + } +} + +int CanvasImpl::width() const { + return _width; +} + +int CanvasImpl::height() const { + return _height; +} + +std::shared_ptr CanvasImpl::makeLayer(int width, int height) { + return std::make_shared(_topContext, width, height); +} + +void CanvasImpl::saveState() { + CGContextSaveGState(_context); +} + +void CanvasImpl::restoreState() { + CGContextRestoreGState(_context); +} + +void CanvasImpl::fillPath(std::shared_ptr const &path, FillRule fillRule, Color const &color) { + CGContextBeginPath(_context); + lottie::CGPathCocoaImpl::withNativePath(path, [context = _context](CGPathRef nativePath) { + CGContextAddPath(context, nativePath); + }); + + CGFloat components[4] = { color.r, color.g, color.b, color.a }; + CGColorRef nativeColor = CGColorCreate(CGBitmapContextGetColorSpace(_topContext), components); + CGContextSetFillColorWithColor(_context, nativeColor); + CFRelease(nativeColor); + + switch (fillRule) { + case FillRule::EvenOdd: { + CGContextEOFillPath(_context); + break; + } + default: { + CGContextFillPath(_context); + break; + } + } +} + +void CanvasImpl::linearGradientFillPath(std::shared_ptr const &path, FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) { + CGContextSaveGState(_context); + CGContextBeginPath(_context); + lottie::CGPathCocoaImpl::withNativePath(path, [context = _context](CGPathRef nativePath) { + CGContextAddPath(context, nativePath); + }); + + switch (fillRule) { + case FillRule::EvenOdd: { + CGContextEOClip(_context); + break; + } + default: { + CGContextClip(_context); + break; + } + } + + std::vector components; + components.reserve(gradient.colors().size() + 4); + + for (const auto &color : gradient.colors()) { + components.push_back(color.r); + components.push_back(color.g); + components.push_back(color.b); + components.push_back(color.a); + } + + assert(gradient.colors().size() == gradient.locations().size()); + + CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(_topContext), components.data(), gradient.locations().data(), gradient.locations().size()); + if (nativeGradient) { + CGContextDrawLinearGradient(_context, nativeGradient, CGPointMake(start.x, start.y), CGPointMake(end.x, end.y), kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); + CFRelease(nativeGradient); + } + + CGContextResetClip(_context); + CGContextRestoreGState(_context); +} + +void CanvasImpl::radialGradientFillPath(std::shared_ptr const &path, FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &startCenter, double startRadius, lottie::Vector2D const &endCenter, double endRadius) { + CGContextSaveGState(_context); + CGContextBeginPath(_context); + lottie::CGPathCocoaImpl::withNativePath(path, [context = _context](CGPathRef nativePath) { + CGContextAddPath(context, nativePath); + }); + + switch (fillRule) { + case FillRule::EvenOdd: { + CGContextEOClip(_context); + break; + } + default: { + CGContextClip(_context); + break; + } + } + + std::vector components; + components.reserve(gradient.colors().size() + 4); + + for (const auto &color : gradient.colors()) { + components.push_back(color.r); + components.push_back(color.g); + components.push_back(color.b); + components.push_back(color.a); + } + + assert(gradient.colors().size() == gradient.locations().size()); + + CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(_topContext), components.data(), gradient.locations().data(), gradient.locations().size()); + if (nativeGradient) { + CGContextDrawRadialGradient(_context, nativeGradient, CGPointMake(startCenter.x, startCenter.y), startRadius, CGPointMake(endCenter.x, endCenter.y), endRadius, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); + CFRelease(nativeGradient); + } + + CGContextResetClip(_context); + CGContextRestoreGState(_context); +} + +void CanvasImpl::strokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, Color const &color) { + CGContextBeginPath(_context); + lottie::CGPathCocoaImpl::withNativePath(path, [context = _context](CGPathRef nativePath) { + CGContextAddPath(context, nativePath); + }); + + CGFloat components[4] = { color.r, color.g, color.b, color.a }; + CGColorRef nativeColor = CGColorCreate(CGBitmapContextGetColorSpace(_topContext), components); + CGContextSetStrokeColorWithColor(_context, nativeColor); + CFRelease(nativeColor); + + CGContextSetLineWidth(_context, lineWidth); + + switch (lineJoin) { + case LineJoin::Miter: { + CGContextSetLineJoin(_context, kCGLineJoinMiter); + break; + } + case LineJoin::Round: { + CGContextSetLineJoin(_context, kCGLineJoinRound); + break; + } + case LineJoin::Bevel: { + CGContextSetLineJoin(_context, kCGLineJoinBevel); + break; + } + default: { + CGContextSetLineJoin(_context, kCGLineJoinBevel); + break; + } + } + + switch (lineCap) { + case LineCap::Butt: { + CGContextSetLineCap(_context, kCGLineCapButt); + break; + } + case LineCap::Round: { + CGContextSetLineCap(_context, kCGLineCapRound); + break; + } + case LineCap::Square: { + CGContextSetLineCap(_context, kCGLineCapSquare); + break; + } + default: { + CGContextSetLineCap(_context, kCGLineCapSquare); + break; + } + } + + if (!dashPattern.empty()) { + CGContextSetLineDash(_context, dashPhase, dashPattern.data(), dashPattern.size()); + } + CGContextStrokePath(_context); +} + +void CanvasImpl::linearGradientStrokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) { + CGContextSaveGState(_context); + CGContextBeginPath(_context); + lottie::CGPathCocoaImpl::withNativePath(path, [context = _context](CGPathRef nativePath) { + CGContextAddPath(context, nativePath); + }); + + CGContextSetLineWidth(_context, lineWidth); + + switch (lineJoin) { + case LineJoin::Miter: { + CGContextSetLineJoin(_context, kCGLineJoinMiter); + break; + } + case LineJoin::Round: { + CGContextSetLineJoin(_context, kCGLineJoinRound); + break; + } + case LineJoin::Bevel: { + CGContextSetLineJoin(_context, kCGLineJoinBevel); + break; + } + default: { + CGContextSetLineJoin(_context, kCGLineJoinBevel); + break; + } + } + + switch (lineCap) { + case LineCap::Butt: { + CGContextSetLineCap(_context, kCGLineCapButt); + break; + } + case LineCap::Round: { + CGContextSetLineCap(_context, kCGLineCapRound); + break; + } + case LineCap::Square: { + CGContextSetLineCap(_context, kCGLineCapSquare); + break; + } + default: { + CGContextSetLineCap(_context, kCGLineCapSquare); + break; + } + } + + if (!dashPattern.empty()) { + CGContextSetLineDash(_context, dashPhase, dashPattern.data(), dashPattern.size()); + } + + CGContextReplacePathWithStrokedPath(_context); + CGContextClip(_context); + + std::vector components; + components.reserve(gradient.colors().size() + 4); + + for (const auto &color : gradient.colors()) { + components.push_back(color.r); + components.push_back(color.g); + components.push_back(color.b); + components.push_back(color.a); + } + + assert(gradient.colors().size() == gradient.locations().size()); + + CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(_topContext), components.data(), gradient.locations().data(), gradient.locations().size()); + if (nativeGradient) { + CGContextDrawLinearGradient(_context, nativeGradient, CGPointMake(start.x, start.y), CGPointMake(end.x, end.y), kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); + CFRelease(nativeGradient); + } + + CGContextResetClip(_context); + CGContextRestoreGState(_context); +} + +void CanvasImpl::radialGradientStrokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &startCenter, double startRadius, lottie::Vector2D const &endCenter, double endRadius) { + CGContextSaveGState(_context); + CGContextBeginPath(_context); + lottie::CGPathCocoaImpl::withNativePath(path, [context = _context](CGPathRef nativePath) { + CGContextAddPath(context, nativePath); + }); + + CGContextSetLineWidth(_context, lineWidth); + + switch (lineJoin) { + case LineJoin::Miter: { + CGContextSetLineJoin(_context, kCGLineJoinMiter); + break; + } + case LineJoin::Round: { + CGContextSetLineJoin(_context, kCGLineJoinRound); + break; + } + case LineJoin::Bevel: { + CGContextSetLineJoin(_context, kCGLineJoinBevel); + break; + } + default: { + CGContextSetLineJoin(_context, kCGLineJoinBevel); + break; + } + } + + switch (lineCap) { + case LineCap::Butt: { + CGContextSetLineCap(_context, kCGLineCapButt); + break; + } + case LineCap::Round: { + CGContextSetLineCap(_context, kCGLineCapRound); + break; + } + case LineCap::Square: { + CGContextSetLineCap(_context, kCGLineCapSquare); + break; + } + default: { + CGContextSetLineCap(_context, kCGLineCapSquare); + break; + } + } + + if (!dashPattern.empty()) { + CGContextSetLineDash(_context, dashPhase, dashPattern.data(), dashPattern.size()); + } + + CGContextReplacePathWithStrokedPath(_context); + CGContextClip(_context); + + std::vector components; + components.reserve(gradient.colors().size() + 4); + + for (const auto &color : gradient.colors()) { + components.push_back(color.r); + components.push_back(color.g); + components.push_back(color.b); + components.push_back(color.a); + } + + assert(gradient.colors().size() == gradient.locations().size()); + + CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(_topContext), components.data(), gradient.locations().data(), gradient.locations().size()); + if (nativeGradient) { + CGContextDrawRadialGradient(_context, nativeGradient, CGPointMake(startCenter.x, startCenter.y), startRadius, CGPointMake(endCenter.x, endCenter.y), endRadius, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); + CFRelease(nativeGradient); + } + + CGContextResetClip(_context); + CGContextRestoreGState(_context); +} + +void CanvasImpl::fill(lottie::CGRect const &rect, Color const &fillColor) { + CGFloat components[4] = { fillColor.r, fillColor.g, fillColor.b, fillColor.a }; + CGColorRef nativeColor = CGColorCreate(CGBitmapContextGetColorSpace(_topContext), components); + CGContextSetFillColorWithColor(_context, nativeColor); + CFRelease(nativeColor); + + CGContextFillRect(_context, CGRectMake(rect.x, rect.y, rect.width, rect.height)); +} + +void CanvasImpl::setBlendMode(BlendMode blendMode) { + ::CGBlendMode nativeMode = kCGBlendModeNormal; + switch (blendMode) { + case BlendMode::Normal: { + nativeMode = kCGBlendModeNormal; + break; + } + case BlendMode::DestinationIn: { + nativeMode = kCGBlendModeDestinationIn; + break; + } + case BlendMode::DestinationOut: { + nativeMode = kCGBlendModeDestinationOut; + break; + } + } + CGContextSetBlendMode(_context, nativeMode); +} + +void CanvasImpl::setAlpha(double alpha) { + CGContextSetAlpha(_context, alpha); +} + +void CanvasImpl::concatenate(lottie::CATransform3D const &transform) { + CGContextConcatCTM(_context, CATransform3DGetAffineTransform(nativeTransform(transform))); +} + +std::shared_ptr CanvasImpl::makeImage() const { + ::CGImageRef nativeImage = CGBitmapContextCreateImage(_context); + if (nativeImage) { + auto image = std::make_shared(nativeImage); + CFRelease(nativeImage); + return image; + } else { + return nil; + } +} + +void CanvasImpl::draw(std::shared_ptr const &other, lottie::CGRect const &rect) { + CanvasImpl *impl = (CanvasImpl *)other.get(); + if (impl->_layer) { + CGContextDrawLayerInRect(_context, CGRectMake(rect.x, rect.y, rect.width, rect.height), impl->_layer); + } else { + auto image = impl->makeImage(); + CGContextDrawImage(_context, CGRectMake(rect.x, rect.y, rect.width, rect.height), ((ImageImpl *)image.get())->nativeImage()); + } +} + +} + diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm new file mode 100644 index 0000000000..3d5b3ce137 --- /dev/null +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm @@ -0,0 +1,261 @@ +#import + +#import "Canvas.h" +#import "CoreGraphicsCanvasImpl.h" + +namespace { + +static void drawLottieRenderableItem(std::shared_ptr context, LottieRenderContent * _Nonnull item) { + if (item.path == nil) { + return; + } + + std::shared_ptr path = lottie::CGPath::makePath(); + [item.path enumerateItems:^(LottiePathItem * _Nonnull pathItem) { + switch (pathItem->type) { + case LottiePathItemTypeMoveTo: { + path->moveTo(lottie::Vector2D(pathItem->points[0].x, pathItem->points[0].y)); + break; + } + case LottiePathItemTypeLineTo: { + path->addLineTo(lottie::Vector2D(pathItem->points[0].x, pathItem->points[0].y)); + break; + } + case LottiePathItemTypeCurveTo: { + path->addCurveTo(lottie::Vector2D(pathItem->points[2].x, pathItem->points[2].y), lottie::Vector2D(pathItem->points[0].x, pathItem->points[0].y), lottie::Vector2D(pathItem->points[1].x, pathItem->points[1].y)); + break; + } + case LottiePathItemTypeClose: { + path->closeSubpath(); + break; + } + default: { + break; + } + } + }]; + + if (item.stroke != nil) { + if ([item.stroke.shading isKindOfClass:[LottieRenderContentSolidShading class]]) { + LottieRenderContentSolidShading *solidShading = (LottieRenderContentSolidShading *)item.stroke.shading; + + lottieRendering::LineJoin lineJoin = lottieRendering::LineJoin::Bevel; + switch (item.stroke.lineJoin) { + case kCGLineJoinBevel: { + lineJoin = lottieRendering::LineJoin::Bevel; + break; + } + case kCGLineJoinRound: { + lineJoin = lottieRendering::LineJoin::Round; + break; + } + case kCGLineJoinMiter: { + lineJoin = lottieRendering::LineJoin::Miter; + break; + } + default: { + break; + } + } + + lottieRendering::LineCap lineCap = lottieRendering::LineCap::Square; + switch (item.stroke.lineCap) { + case kCGLineCapButt: { + lineCap = lottieRendering::LineCap::Butt; + break; + } + case kCGLineCapRound: { + lineCap = lottieRendering::LineCap::Round; + break; + } + case kCGLineCapSquare: { + lineCap = lottieRendering::LineCap::Square; + break; + } + default: { + break; + } + } + + std::vector dashPattern; + if (item.stroke.dashPattern != nil) { + for (NSNumber *value in item.stroke.dashPattern) { + dashPattern.push_back([value doubleValue]); + } + } + + context->strokePath(path, item.stroke.lineWidth, lineJoin, lineCap, item.stroke.dashPhase, dashPattern, lottieRendering::Color(solidShading.color.r, solidShading.color.g, solidShading.color.b, solidShading.color.a)); + } else if ([item.stroke.shading isKindOfClass:[LottieRenderContentGradientShading class]]) { + __unused LottieRenderContentGradientShading *gradientShading = (LottieRenderContentGradientShading *)item.stroke.shading; + } + } else if (item.fill != nil) { + lottieRendering::FillRule rule = lottieRendering::FillRule::NonZeroWinding; + switch (item.fill.fillRule) { + case LottieFillRuleEvenOdd: { + rule = lottieRendering::FillRule::EvenOdd; + break; + } + case LottieFillRuleWinding: { + rule = lottieRendering::FillRule::NonZeroWinding; + break; + } + default: { + break; + } + } + + if ([item.fill.shading isKindOfClass:[LottieRenderContentSolidShading class]]) { + LottieRenderContentSolidShading *solidShading = (LottieRenderContentSolidShading *)item.fill.shading; + + context->fillPath(path, rule, lottieRendering::Color(solidShading.color.r, solidShading.color.g, solidShading.color.b, solidShading.color.a)); + } else if ([item.fill.shading isKindOfClass:[LottieRenderContentGradientShading class]]) { + LottieRenderContentGradientShading *gradientShading = (LottieRenderContentGradientShading *)item.fill.shading; + + std::vector colors; + std::vector locations; + for (LottieColorStop *colorStop in gradientShading.colorStops) { + colors.push_back(lottieRendering::Color(colorStop.color.r, colorStop.color.g, colorStop.color.b, colorStop.color.a)); + locations.push_back(colorStop.location); + } + + lottieRendering::Gradient gradient(colors, locations); + lottie::Vector2D start(gradientShading.start.x, gradientShading.start.y); + lottie::Vector2D end(gradientShading.end.x, gradientShading.end.y); + + switch (gradientShading.gradientType) { + case LottieGradientTypeLinear: { + context->linearGradientFillPath(path, rule, gradient, start, end); + break; + } + case LottieGradientTypeRadial: { + context->radialGradientFillPath(path, rule, gradient, start, 0.0, start, start.distanceTo(end)); + break; + } + default: { + break; + } + } + } + } +} + +static void renderLottieRenderNode(LottieRenderNode * _Nonnull node, std::shared_ptr parentContext, lottie::Vector2D const &globalSize, double parentAlpha) { + float normalizedOpacity = node.opacity; + double layerAlpha = ((double)normalizedOpacity) * parentAlpha; + + if (node.isHidden || normalizedOpacity == 0.0f) { + return; + } + + parentContext->saveState(); + + std::shared_ptr maskContext; + std::shared_ptr currentContext; + std::shared_ptr tempContext; + + bool needsTempContext = false; + if (node.mask != nil) { + needsTempContext = true; + } else { + needsTempContext = layerAlpha != 1.0 || node.masksToBounds; + } + + if (needsTempContext) { + if (node.mask != nil || node.masksToBounds) { + auto maskBackingStorage = parentContext->makeLayer((int)(node.globalRect.size.width), (int)(node.globalRect.size.height)); + + maskBackingStorage->concatenate(lottie::CATransform3D::identity().translated(lottie::Vector2D(-node.globalRect.origin.x, -node.globalRect.origin.y))); + maskBackingStorage->concatenate(lottie::fromNativeTransform(node.globalTransform)); + + if (node.masksToBounds) { + maskBackingStorage->fill(lottie::CGRect(node.bounds.origin.x, node.bounds.origin.y, node.bounds.size.width, node.bounds.size.height), lottieRendering::Color(1.0, 1.0, 1.0, 1.0)); + } + if (node.mask != nil) { + renderLottieRenderNode(node.mask, maskBackingStorage, globalSize, 1.0); + } + + maskContext = maskBackingStorage; + } + + auto tempContextValue = parentContext->makeLayer((int)(node.globalRect.size.width), (int)(node.globalRect.size.height)); + tempContext = tempContextValue; + + currentContext = tempContextValue; + currentContext->concatenate(lottie::CATransform3D::identity().translated(lottie::Vector2D(-node.globalRect.origin.x, -node.globalRect.origin.y))); + + currentContext->saveState(); + currentContext->concatenate(lottie::fromNativeTransform(node.globalTransform)); + } else { + currentContext = parentContext; + } + + parentContext->concatenate(lottie::CATransform3D::identity().translated(lottie::Vector2D(node.position.x, node.position.y))); + parentContext->concatenate(lottie::CATransform3D::identity().translated(lottie::Vector2D(-node.bounds.origin.x, -node.bounds.origin.y))); + parentContext->concatenate(lottie::fromNativeTransform(node.transform)); + + double renderAlpha = 1.0; + if (tempContext) { + renderAlpha = 1.0; + } else { + renderAlpha = layerAlpha; + } + + currentContext->setAlpha(renderAlpha); + + if (node.renderContent != nil) { + drawLottieRenderableItem(currentContext, node.renderContent); + } + + if (node.isInvertedMatte) { + currentContext->fill(lottie::CGRect(node.bounds.origin.x, node.bounds.origin.y, node.bounds.size.width, node.bounds.size.height), lottieRendering::Color(0.0, 0.0, 0.0, 1.0)); + currentContext->setBlendMode(lottieRendering::BlendMode::DestinationOut); + } + + for (LottieRenderNode *subnode in node.subnodes) { + renderLottieRenderNode(subnode, currentContext, globalSize, renderAlpha); + } + + if (tempContext) { + tempContext->restoreState(); + + if (maskContext) { + tempContext->setBlendMode(lottieRendering::BlendMode::DestinationIn); + tempContext->draw(maskContext, lottie::CGRect(node.globalRect.origin.x, node.globalRect.origin.y, node.globalRect.size.width, node.globalRect.size.height)); + } + + parentContext->concatenate(lottie::fromNativeTransform(node.globalTransform).inverted()); + parentContext->setAlpha(layerAlpha); + parentContext->draw(tempContext, lottie::CGRect(node.globalRect.origin.x, node.globalRect.origin.y, node.globalRect.size.width, node.globalRect.size.height)); + } + + parentContext->restoreState(); +} + +} + +CGRect getPathNativeBoundingBox(CGPathRef _Nonnull path) { + auto rect = calculatePathBoundingBox(path); + return CGRectMake(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height); +} + +UIImage * _Nullable renderLottieAnimationContainer(LottieAnimationContainer * _Nonnull animationContainer, CGSize size, bool useReferenceRendering) { + LottieAnimation *animation = animationContainer.animation; + LottieRenderNode *lottieNode = [animationContainer getCurrentRenderTreeForSize:size]; + + if (useReferenceRendering) { + auto context = std::make_shared((int)size.width, (int)size.height); + + if (lottieNode) { + CGPoint scale = CGPointMake(size.width / (CGFloat)animation.size.width, size.height / (CGFloat)animation.size.height); + context->concatenate(lottie::CATransform3D::makeScale(scale.x, scale.y, 1.0)); + + renderLottieRenderNode(lottieNode, context, lottie::Vector2D(context->width(), context->height()), 1.0); + } + + auto image = context->makeImage(); + + return [[UIImage alloc] initWithCGImage:std::static_pointer_cast(image)->nativeImage()]; + } else { + return nil; + } +} diff --git a/Tests/LottieMesh/Sources/AppDelegate.swift b/Tests/LottieMetalTest/Sources/AppDelegate.swift similarity index 100% rename from Tests/LottieMesh/Sources/AppDelegate.swift rename to Tests/LottieMetalTest/Sources/AppDelegate.swift diff --git a/Tests/LottieMetalTest/Sources/CompareToReferenceRendering.swift b/Tests/LottieMetalTest/Sources/CompareToReferenceRendering.swift new file mode 100644 index 0000000000..33f52ac8a4 --- /dev/null +++ b/Tests/LottieMetalTest/Sources/CompareToReferenceRendering.swift @@ -0,0 +1,317 @@ +import Foundation +import UIKit +import LottieMetal +import LottieCpp +import RLottieBinding +import Display +import Accelerate +import QOILoader +import SoftwareLottieRenderer +import LottieSwift + +@available(iOS 13.0, *) +func areImagesEqual(_ lhs: UIImage, _ rhs: UIImage) -> UIImage? { + let lhsBuffer = try! vImage_Buffer(cgImage: lhs.cgImage!) + let rhsBuffer = try! vImage_Buffer(cgImage: rhs.cgImage!) + + let maxDifferenceCount = Int((Double(Int(lhs.size.width) * Int(lhs.size.height)) * 0.01)) + + var foundDifferenceCount = 0 + + outer: for y in 0 ..< Int(lhs.size.height) { + let lhsRowPixels = lhsBuffer.data.assumingMemoryBound(to: UInt8.self).advanced(by: y * lhsBuffer.rowBytes) + let rhsRowPixels = rhsBuffer.data.assumingMemoryBound(to: UInt8.self).advanced(by: y * lhsBuffer.rowBytes) + + for x in 0 ..< Int(lhs.size.width) { + let lhs0 = lhsRowPixels.advanced(by: x * 4 + 0).pointee + let lhs1 = lhsRowPixels.advanced(by: x * 4 + 1).pointee + let lhs2 = lhsRowPixels.advanced(by: x * 4 + 2).pointee + let lhs3 = lhsRowPixels.advanced(by: x * 4 + 3).pointee + + let rhs0 = rhsRowPixels.advanced(by: x * 4 + 0).pointee + let rhs1 = rhsRowPixels.advanced(by: x * 4 + 1).pointee + let rhs2 = rhsRowPixels.advanced(by: x * 4 + 2).pointee + let rhs3 = rhsRowPixels.advanced(by: x * 4 + 3).pointee + + let maxDiff = 25 + if abs(Int(lhs0) - Int(rhs0)) > maxDiff || abs(Int(lhs1) - Int(rhs1)) > maxDiff || abs(Int(lhs2) - Int(rhs2)) > maxDiff || abs(Int(lhs3) - Int(rhs3)) > maxDiff { + + /*if false { + lhsRowPixels.advanced(by: x * 4 + 0).pointee = 255 + lhsRowPixels.advanced(by: x * 4 + 1).pointee = 0 + lhsRowPixels.advanced(by: x * 4 + 2).pointee = 0 + lhsRowPixels.advanced(by: x * 4 + 3).pointee = 255 + }*/ + + foundDifferenceCount += 1 + } + } + } + + lhsBuffer.free() + rhsBuffer.free() + + if foundDifferenceCount > maxDifferenceCount { + let colorSpace = Unmanaged.passRetained(lhs.cgImage!.colorSpace!) + let diffImage = try! lhsBuffer.createCGImage(format: vImage_CGImageFormat(bitsPerComponent: 8, bitsPerPixel: 32, colorSpace: colorSpace, bitmapInfo: lhs.cgImage!.bitmapInfo, version: 0, decode: nil, renderingIntent: .defaultIntent), flags: .doNotTile) + return UIImage(cgImage: diffImage) + } else { + return nil + } +} + +@available(iOS 13.0, *) +func processDrawAnimation(baseCachePath: String, path: String, name: String, size: CGSize, alwaysDraw: Bool, updateImage: @escaping (UIImage?, UIImage?) -> Void) async -> Bool { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { + print("Could not load \(path)") + return false + } + + guard let animation = LottieAnimation(data: data) else { + print("Could not parse animation at \(path)") + return false + } + + let layer = LottieAnimationContainer(animation: animation) + + let cacheFolderPath = cacheReferenceFolderPath(baseCachePath: baseCachePath, width: Int(size.width), name: name) + if !FileManager.default.fileExists(atPath: cacheFolderPath) { + let _ = await cacheReferenceAnimation(baseCachePath: baseCachePath, width: Int(size.width), path: path, name: name) + } + + for i in 0 ..< min(100000, animation.frameCount) { + let frameResult = autoreleasepool { + let frameIndex = i % animation.frameCount + + let referenceImageData = try! Data(contentsOf: URL(fileURLWithPath: cacheFolderPath + "/frame\(frameIndex)")) + let referenceImage = decompressImageFrame(data: referenceImageData) + + layer.update(frameIndex) + let image = renderLottieAnimationContainer(layer, size, true)! + + if let diffImage = areImagesEqual(image, referenceImage) { + updateImage(diffImage, referenceImage) + + print("Mismatch in frame \(frameIndex)") + return false + } else { + if alwaysDraw { + updateImage(image, referenceImage) + } + return true + } + } + if !frameResult { + return false + } + } + + return true +} + +private func processAnimationFolder(basePath: String, path: String, stopOnFailure: Bool, process: (String, String) -> Bool) -> Bool { + let directoryPath = "\(basePath)\(path.isEmpty ? "" : "/")/\(path)" + for fileName in try! FileManager.default.contentsOfDirectory(atPath: directoryPath) { + let filePath = directoryPath + "/" + fileName + var isDirectory = ObjCBool(false) + if FileManager.default.fileExists(atPath: filePath, isDirectory: &isDirectory) { + if isDirectory.boolValue { + if !processAnimationFolder(basePath: basePath, path: "\(path)\(path.isEmpty ? "" : "/")/\(fileName)", stopOnFailure: stopOnFailure, process: process) { + if stopOnFailure { + return false + } + } + } else if fileName.hasSuffix("json") { + var processAnimationResult = false + autoreleasepool { + processAnimationResult = process(filePath, fileName) + } + if !processAnimationResult { + print("Error processing \(path)\(path.isEmpty ? "" : "/")\(fileName)") + if stopOnFailure { + return false + } + } else { + print("[OK] processing \(path)\(path.isEmpty ? "" : "/")\(fileName)") + } + } + } + } + return true +} + +func buildAnimationFolderItems(basePath: String, path: String) -> [(String, String)] { + var result: [(String, String)] = [] + + let directoryPath = "\(basePath)\(path.isEmpty ? "" : "/")/\(path)" + for fileName in try! FileManager.default.contentsOfDirectory(atPath: directoryPath) { + let filePath = directoryPath + "/" + fileName + var isDirectory = ObjCBool(false) + if FileManager.default.fileExists(atPath: filePath, isDirectory: &isDirectory) { + if isDirectory.boolValue { + result.append(contentsOf: buildAnimationFolderItems(basePath: basePath, path: "\(path)\(path.isEmpty ? "" : "/")/\(fileName)")) + } else if fileName.hasSuffix("json") { + result.append((filePath, fileName)) + } + } + } + + return result +} + +private func processAnimationFolderItems(items: [(String, String)], countPerBucket: Int, stopOnFailure: Bool, process: @escaping (String, String, Bool) async -> Bool) async -> Bool { + let bucketCount = items.count / countPerBucket + var buckets: [[(String, String)]] = [] + for item in items { + if buckets.isEmpty { + buckets.append([]) + } + if buckets[buckets.count - 1].count < bucketCount { + buckets[buckets.count - 1].append(item) + } else { + buckets.append([item]) + } + } + + var count = 0 + for bucket in buckets { + for (filePath, fileName) in bucket { + var processAnimationResult = false + processAnimationResult = await process(filePath, fileName, true) + if !processAnimationResult { + print("Error processing \(fileName)") + if stopOnFailure { + return false + } + } else { + count += 1 + print("[OK \(count) / \(items.count)] processing \(fileName)") + } + } + } + + return true +} + +@available(iOS 13.0, *) +private func processAnimationFolderItemsParallel(items: [(String, String)], stopOnFailure: Bool, process: @escaping (String, String, Bool) async -> Bool) async -> Bool { + let bucketCount = items.count / 7 + var buckets: [[(String, String)]] = [] + for item in items { + if buckets.isEmpty { + buckets.append([]) + } + if buckets[buckets.count - 1].count < bucketCount { + buckets[buckets.count - 1].append(item) + } else { + buckets.append([item]) + } + } + + class AtomicCounter { + var value: Int = 0 + } + let count = AtomicCounter() + let itemCount = items.count + let countQueue = DispatchQueue(label: "com.example.count-queue") + + let result = await withTaskGroup(of: Bool.self, body: { group in + var alwaysDraw = true + for bucket in buckets { + let alwaysDrawValue = alwaysDraw + alwaysDraw = false + group.addTask(operation: { + for (filePath, fileName) in bucket { + var processAnimationResult = false + processAnimationResult = await process(filePath, fileName, alwaysDrawValue) + if !processAnimationResult { + print("Error processing \(fileName)") + if stopOnFailure { + return false + } + } else { + countQueue.async { + count.value += 1 + print("[OK \(count.value) / \(itemCount)] processing \(fileName)") + } + } + } + return true + }) + } + + for await result in group { + if !result { + return false + } + } + return true + }) + + return result +} + +func processAnimationFolderAsync(basePath: String, path: String, stopOnFailure: Bool, process: @escaping (String, String, Bool) async -> Bool) async -> Bool { + let items = buildAnimationFolderItems(basePath: basePath, path: path) + return await processAnimationFolderItems(items: items, countPerBucket: 1, stopOnFailure: stopOnFailure, process: process) +} + +func processAnimationFolderParallel(basePath: String, path: String, stopOnFailure: Bool, process: @escaping (String, String, Bool) async -> Bool) async -> Bool { + let items = buildAnimationFolderItems(basePath: basePath, path: path) + return await processAnimationFolderItems(items: items, countPerBucket: 16, stopOnFailure: stopOnFailure, process: process) +} + +func cacheReferenceFolderPath(baseCachePath: String, width: Int, name: String) -> String { + return baseCachePath + "/" + name + "_\(width)" +} + +func compressImageFrame(image: UIImage) -> Data { + return encodeImageQOI(image)! +} + +func decompressImageFrame(data: Data) -> UIImage { + return decodeImageQOI(data)! +} + +@MainActor +func cacheReferenceAnimation(baseCachePath: String, width: Int, path: String, name: String) -> String { + let targetFolderPath = cacheReferenceFolderPath(baseCachePath: baseCachePath, width: width, name: name) + if FileManager.default.fileExists(atPath: targetFolderPath) { + return targetFolderPath + } + + guard let referenceAnimation = Animation.filepath(path) else { + preconditionFailure("Could not parse reference animation at \(path)") + } + let referenceLayer = MainThreadAnimationLayer(animation: referenceAnimation, imageProvider: BlankImageProvider(), textProvider: DefaultTextProvider(), fontProvider: DefaultFontProvider()) + + let cacheFolderPath = NSTemporaryDirectory() + "\(UInt64.random(in: 0 ... UInt64.max))" + let _ = try? FileManager.default.createDirectory(atPath: cacheFolderPath, withIntermediateDirectories: true) + + let frameCount = Int(referenceAnimation.endFrame - referenceAnimation.startFrame) + + let size = CGSize(width: CGFloat(width), height: CGFloat(width)) + + for i in 0 ..< min(100000, frameCount) { + let frameIndex = i % frameCount + + referenceLayer.currentFrame = CGFloat(frameIndex) + referenceLayer.displayUpdate() + referenceLayer.position = referenceAnimation.bounds.center + + referenceLayer.isOpaque = false + referenceLayer.backgroundColor = nil + let referenceContext = ImageContext(width: width, height: width) + referenceContext.context.clear(CGRect(origin: CGPoint(), size: size)) + referenceContext.context.scaleBy(x: size.width / CGFloat(referenceAnimation.width), y: size.height / CGFloat(referenceAnimation.height)) + + referenceLayer.render(in: referenceContext.context) + + let referenceImage = referenceContext.makeImage() + try! compressImageFrame(image: referenceImage).write(to: URL(fileURLWithPath: cacheFolderPath + "/frame\(i)")) + } + + let _ = try! FileManager.default.moveItem(atPath: cacheFolderPath, toPath: targetFolderPath) + + return targetFolderPath +} diff --git a/Tests/LottieMetalTest/Sources/ImageUtils.swift b/Tests/LottieMetalTest/Sources/ImageUtils.swift new file mode 100644 index 0000000000..b52764bb69 --- /dev/null +++ b/Tests/LottieMetalTest/Sources/ImageUtils.swift @@ -0,0 +1,41 @@ +import Foundation +import UIKit + +private func alignUp(size: Int, align: Int) -> Int { + precondition(((align - 1) & align) == 0, "Align must be a power of two") + + let alignmentMask = align - 1 + return (size + alignmentMask) & ~alignmentMask +} + +final class ImageContext { + let context: CGContext + + init(width: Int, height: Int, isMask: Bool = false) { + let bytesPerRow: Int + let colorSpace: CGColorSpace + let bitmapInfo: CGBitmapInfo + + if isMask { + bytesPerRow = alignUp(size: width, align: 16) + colorSpace = CGColorSpaceCreateDeviceGray() + bitmapInfo = CGBitmapInfo(rawValue: 0) + } else { + bytesPerRow = alignUp(size: width * 4, align: 16) + colorSpace = CGColorSpaceCreateDeviceRGB() + bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue) + } + + self.context = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)! + + self.context.clear(CGRect(origin: CGPoint(), size: CGSize(width: width, height: height))) + + //context.interpolationQuality = .none + //context.setShouldAntialias(false) + //context.setAllowsAntialiasing(false) + } + + func makeImage() -> UIImage { + return UIImage(cgImage: context.makeImage()!) + } +} diff --git a/Tests/LottieMetalTest/Sources/ViewController.swift b/Tests/LottieMetalTest/Sources/ViewController.swift new file mode 100644 index 0000000000..4b2b4665c7 --- /dev/null +++ b/Tests/LottieMetalTest/Sources/ViewController.swift @@ -0,0 +1,205 @@ +import Foundation +import UIKit +import LottieMetal +import LottieCpp +import RLottieBinding +import MetalEngine +import Display +import LottieSwift +import SoftwareLottieRenderer + +@available(iOS 13.0, *) +private final class ReferenceCompareTest { + private let view: UIView + private let imageView = UIImageView() + private let referenceImageView = UIImageView() + + init(view: UIView) { + lottieSwift_getPathNativeBoundingBox = { path in + return getPathNativeBoundingBox(path) + } + + self.view = view + + self.view.backgroundColor = .white + + let topInset: CGFloat = 50.0 + + self.view.addSubview(self.imageView) + self.imageView.layer.magnificationFilter = .nearest + self.imageView.frame = CGRect(origin: CGPoint(x: 10.0, y: topInset), size: CGSize(width: 256.0, height: 256.0)) + self.imageView.backgroundColor = self.view.backgroundColor + self.imageView.transform = CGAffineTransform.init(scaleX: 1.0, y: -1.0) + + self.view.addSubview(self.referenceImageView) + self.referenceImageView.layer.magnificationFilter = .nearest + self.referenceImageView.frame = CGRect(origin: CGPoint(x: 10.0, y: topInset + 256.0 + 1.0), size: CGSize(width: 256.0, height: 256.0)) + self.referenceImageView.backgroundColor = self.view.backgroundColor + self.referenceImageView.transform = CGAffineTransform.init(scaleX: 1.0, y: -1.0) + + let bundlePath = Bundle.main.path(forResource: "TestDataBundle", ofType: "bundle")! + + Task.detached { + let sizeMapping: [String: Int] = [ + "5170488605398795246.json": 512, + "35707580709863506.json": 512, + "35707580709863507.json": 512, + "1258816259754246.json": 512, + "1258816259754248.json": 512, + "35707580709863489.json": 512, + "1258816259754150.json": 512, + "35707580709863494.json": 512, + "5021586753580958116.json": 512, + "35707580709863509.json": 512, + "5282957555314728059.json": 512, + "fireworks.json": 512, + "750766425144033565.json": 512, + "1258816259754276.json": 1024, + "1471004892762996753.json": 1024, + "4985886809322947159.json": 1024, + "35707580709863490.json": 1024, + "4986037051573928320.json": 512, + "1258816259754029.json": 1024, + "4987794066860147124.json": 1024, + "1258816259754212.json": 1024, + "750766425144033464.json": 1024, + "750766425144033567.json": 1024, + "1391391008142393350.json": 1024 + ] + + let defaultSize = 128 + + let baseCachePath = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).path + "/frame-cache" + let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: baseCachePath), withIntermediateDirectories: true, attributes: nil) + print("Frame cache: \(baseCachePath)") + + for (filePath, fileName) in buildAnimationFolderItems(basePath: bundlePath, path: "") { + let _ = await cacheReferenceAnimation(baseCachePath: baseCachePath, width: sizeMapping[fileName] ?? defaultSize, path: filePath, name: fileName) + } + + var continueFromName: String? = "5138957708585599529.json" + + let _ = await processAnimationFolderParallel(basePath: bundlePath, path: "", stopOnFailure: true, process: { path, name, alwaysDraw in + if let continueFromNameValue = continueFromName { + if continueFromNameValue == name { + continueFromName = nil + } else { + return true + } + } + + let size = sizeMapping[name] ?? defaultSize + + let result = await processDrawAnimation(baseCachePath: baseCachePath, path: path, name: name, size: CGSize(width: size, height: size), alwaysDraw: alwaysDraw, updateImage: { image, referenceImage in + DispatchQueue.main.async { + self.imageView.image = image + self.referenceImageView.image = referenceImage + } + }) + return result + }) + } + } +} + +public final class ViewController: UIViewController { + private var link: SharedDisplayLinkDriver.Link? + private var test: AnyObject? + + override public func viewDidLoad() { + super.viewDidLoad() + + let bundlePath = Bundle.main.path(forResource: "TestDataBundle", ofType: "bundle")! + let filePath = bundlePath + "/fireworks.json" + + self.view.layer.addSublayer(MetalEngine.shared.rootLayer) + + if "".isEmpty { + if #available(iOS 13.0, *) { + self.test = ReferenceCompareTest(view: self.view) + } + } else if !"".isEmpty { + let animationData = try! Data(contentsOf: URL(fileURLWithPath: filePath)) + + var startTime = CFAbsoluteTimeGetCurrent() + let animation = LottieAnimation(data: animationData)! + print("Load time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") + + startTime = CFAbsoluteTimeGetCurrent() + let animationContainer = LottieAnimationContainer(animation: animation) + animationContainer.update(0) + print("Build time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") + + let lottieLayer = LottieContentLayer(animation: animationContainer) + lottieLayer.frame = CGRect(origin: CGPoint(x: 10.0, y: 50.0), size: CGSize(width: 256.0, height: 256.0)) + self.view.layer.addSublayer(lottieLayer) + lottieLayer.setNeedsUpdate() + + self.link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { _ in + lottieLayer.frameIndex = (lottieLayer.frameIndex + 1) % animation.frameCount + lottieLayer.setNeedsUpdate() + }) + } else if "".isEmpty { + Thread { + let animationData = try! Data(contentsOf: URL(fileURLWithPath: filePath)) + + var startTime = CFAbsoluteTimeGetCurrent() + let animation = LottieAnimation(data: animationData)! + print("Load time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") + + startTime = CFAbsoluteTimeGetCurrent() + let animationContainer = LottieAnimationContainer(animation: animation) + animationContainer.update(0) + print("Build time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") + + startTime = CFAbsoluteTimeGetCurrent() + var numUpdates: Int = 0 + var frameIndex = 0 + while true { + animationContainer.update(frameIndex) + let _ = animationContainer.getCurrentRenderTree(for: CGSize(width: 512.0, height: 512.0)) + frameIndex = (frameIndex + 1) % animationContainer.animation.frameCount + numUpdates += 1 + let timestamp = CFAbsoluteTimeGetCurrent() + let deltaTime = timestamp - startTime + if deltaTime > 2.0 { + let updatesPerSecond = Double(numUpdates) / deltaTime + startTime = timestamp + numUpdates = 0 + print("updatesPerSecond: \(updatesPerSecond)") + } + } + }.start() + } else { + Thread { + var startTime = CFAbsoluteTimeGetCurrent() + let animationInstance = LottieInstance(data: try! Data(contentsOf: URL(fileURLWithPath: filePath)), fitzModifier: .none, colorReplacements: nil, cacheKey: "")! + print("Load time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") + + let frameSize = 8 + let frameBuffer = malloc(frameSize * 4 * frameSize)! + defer { + free(frameBuffer) + } + + startTime = CFAbsoluteTimeGetCurrent() + var numUpdates: Int = 0 + var frameIndex = 0 + while true { + animationInstance.renderFrame(with: Int32(frameIndex), into: frameBuffer, width: Int32(frameSize), height: Int32(frameSize), bytesPerRow: Int32(frameSize * 4)) + + frameIndex = (frameIndex + 1) % Int(animationInstance.frameCount) + numUpdates += 1 + let timestamp = CFAbsoluteTimeGetCurrent() + let deltaTime = timestamp - startTime + if deltaTime > 2.0 { + let updatesPerSecond = Double(numUpdates) / deltaTime + startTime = timestamp + numUpdates = 0 + print("updatesPerSecond: \(updatesPerSecond)") + } + } + }.start() + } + } +} diff --git a/submodules/Reachability/LegacyReachability/Sources/LegacyReachability.m b/submodules/Reachability/LegacyReachability/Sources/LegacyReachability.m index d28d36f6c5..0a8f3a7662 100644 --- a/submodules/Reachability/LegacyReachability/Sources/LegacyReachability.m +++ b/submodules/Reachability/LegacyReachability/Sources/LegacyReachability.m @@ -19,6 +19,7 @@ #import #import #import +#import #pragma mark IPv6 Support //Reachability fully support IPv6. For full details, see ReadMe.md. @@ -117,7 +118,7 @@ static void PrintReachabilityFlags(SCNetworkReachabilityFlags flags, const char* @end -static int32_t nextKey = 1; +static _Atomic int32_t nextKey = 1; static ReachabilityAtomic *contexts() { static ReachabilityAtomic *instance = nil; static dispatch_once_t onceToken; @@ -135,7 +136,7 @@ static void withContext(int32_t key, void (^f)(LegacyReachability *)) { } static int32_t addContext(LegacyReachability *context) { - int32_t key = OSAtomicIncrement32(&nextKey); + int32_t key = atomic_fetch_add_explicit(&nextKey, 1, memory_order_relaxed); [contexts() modify:^id(NSMutableDictionary *dict) { NSMutableDictionary *updatedDict = [[NSMutableDictionary alloc] initWithDictionary:dict]; updatedDict[@(key)] = context; diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/CGPath.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/CGPath.h new file mode 100644 index 0000000000..d7e445e635 --- /dev/null +++ b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/CGPath.h @@ -0,0 +1,80 @@ +#ifndef CGPath_hpp +#define CGPath_hpp + +#ifdef __cplusplus + +#include + +#include +#include + +namespace lottie { + +struct CGPathItem { + enum class Type { + MoveTo, + LineTo, + CurveTo, + Close + }; + + Type type; + Vector2D points[3] = { Vector2D(0.0, 0.0), Vector2D(0.0, 0.0), Vector2D(0.0, 0.0) }; + + explicit CGPathItem(Type type_) : + type(type_) { + } + + bool operator==(const CGPathItem &rhs) const { + if (type != rhs.type) { + return false; + } + if (points[0] != rhs.points[0]) { + return false; + } + if (points[1] != rhs.points[1]) { + return false; + } + if (points[2] != rhs.points[2]) { + return false; + } + + return true; + } + + bool operator!=(const CGPathItem &rhs) const { + return !(*this == rhs); + } +}; + +class CGPath { +public: + static std::shared_ptr makePath(); + + virtual ~CGPath() = default; + + virtual CGRect boundingBox() const = 0; + + virtual bool empty() const = 0; + + virtual std::shared_ptr copyUsingTransform(CATransform3D const &transform) const = 0; + + virtual void addLineTo(Vector2D const &point) = 0; + virtual void addCurveTo(Vector2D const &point, Vector2D const &control1, Vector2D const &control2) = 0; + virtual void moveTo(Vector2D const &point) = 0; + virtual void closeSubpath() = 0; + virtual void addRect(CGRect const &rect) = 0; + virtual void addPath(std::shared_ptr const &path) = 0; + + virtual void enumerate(std::function) = 0; + + virtual bool isEqual(CGPath *other) const = 0; +}; + +Vector2D transformVector(Vector2D const &v, CATransform3D const &m); + +} + +#endif + +#endif /* CGPath_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/CGPathCocoa.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/CGPathCocoa.h new file mode 100644 index 0000000000..bf80eea07b --- /dev/null +++ b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/CGPathCocoa.h @@ -0,0 +1,47 @@ +#ifndef CGPathCocoa_h +#define CGPathCocoa_h + +#ifdef __cplusplus + +#include + +#include +#include + +CGRect calculatePathBoundingBox(CGPathRef path); + +namespace lottie { + +class CGPathCocoaImpl: public CGPath { +public: + CGPathCocoaImpl(); + explicit CGPathCocoaImpl(CGMutablePathRef path); + virtual ~CGPathCocoaImpl(); + + virtual CGRect boundingBox() const override; + + virtual bool empty() const override; + + virtual std::shared_ptr copyUsingTransform(CATransform3D const &transform) const override; + + virtual void addLineTo(Vector2D const &point) override; + virtual void addCurveTo(Vector2D const &point, Vector2D const &control1, Vector2D const &control2) override; + virtual void moveTo(Vector2D const &point) override; + virtual void closeSubpath() override; + virtual void addRect(CGRect const &rect) override; + virtual void addPath(std::shared_ptr const &path) override; + virtual CGPathRef nativePath() const; + virtual bool isEqual(CGPath *other) const override; + virtual void enumerate(std::function) override; + + static void withNativePath(std::shared_ptr const &path, std::function f); + +private: + ::CGMutablePathRef _path = nil; +}; + +} + +#endif + +#endif /* CGPathCocoa_h */ diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieAnimationContainer.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieAnimationContainer.h index 5944bb0488..f2bd278fd6 100644 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieAnimationContainer.h +++ b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieAnimationContainer.h @@ -16,7 +16,7 @@ extern "C" { - (instancetype _Nonnull)initWithAnimation:(LottieAnimation * _Nonnull)animation; - (void)update:(NSInteger)frame; -- (LottieRenderNode * _Nonnull)getCurrentRenderTreeForSize:(CGSize)size; +- (LottieRenderNode * _Nullable)getCurrentRenderTreeForSize:(CGSize)size; @end diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieCpp.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieCpp.h index f877f88faa..93d87a0569 100644 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieCpp.h +++ b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieCpp.h @@ -3,10 +3,12 @@ #import -#ifdef __cplusplus +#import +#import +#import +#import +#import +#import +#import - - -#endif - -#endif /* DctHuffman_h */ +#endif /* LottieCpp_h */ diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/Vectors.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/Vectors.h new file mode 100644 index 0000000000..9c94dde74e --- /dev/null +++ b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/Vectors.h @@ -0,0 +1,379 @@ +#ifndef Vectors_hpp +#define Vectors_hpp + +#ifdef __cplusplus + +#include +#include + +#include + +namespace lottie { + +struct Vector1D { + enum class InternalRepresentationType { + SingleNumber, + Array + }; + + explicit Vector1D(double value_) : + value(value_) { + } + + explicit Vector1D(lottiejson11::Json const &json) noexcept(false); + lottiejson11::Json toJson() const; + + double value; + + double distanceTo(Vector1D const &to) const { + return abs(to.value - value); + } +}; + +double interpolate(double value, double to, double amount); + +Vector1D interpolate( + Vector1D const &from, + Vector1D const &to, + double amount +); + +struct Vector2D { + static Vector2D Zero() { + return Vector2D(0.0, 0.0); + } + + Vector2D() : + x(0.0), + y(0.0) { + } + + explicit Vector2D(double x_, double y_) : + x(x_), + y(y_) { + } + + explicit Vector2D(lottiejson11::Json const &json) noexcept(false); + lottiejson11::Json toJson() const; + + double x; + double y; + + Vector2D operator+(Vector2D const &rhs) const { + return Vector2D(x + rhs.x, y + rhs.y); + } + + Vector2D operator-(Vector2D const &rhs) const { + return Vector2D(x - rhs.x, y - rhs.y); + } + + Vector2D operator*(double scalar) const { + return Vector2D(x * scalar, y * scalar); + } + + bool operator==(Vector2D const &rhs) const { + return x == rhs.x && y == rhs.y; + } + + bool operator!=(Vector2D const &rhs) const { + return !(*this == rhs); + } + + bool isZero() const { + return x == 0.0 && y == 0.0; + } + + double distanceTo(Vector2D const &to) const { + auto deltaX = to.x - x; + auto deltaY = to.y - y; + return sqrt(deltaX * deltaX + deltaY * deltaY); + } + + bool colinear(Vector2D const &a, Vector2D const &b) const { + double area = x * (a.y - b.y) + a.x * (b.y - y) + b.x * (y - a.y); + double accuracy = 0.05; + if (area < accuracy && area > -accuracy) { + return true; + } + return false; + } + + Vector2D pointOnPath(Vector2D const &to, Vector2D const &outTangent, Vector2D const &inTangent, double amount) const; + + Vector2D interpolate(Vector2D const &to, double amount) const; + + Vector2D interpolate( + Vector2D const &to, + Vector2D const &outTangent, + Vector2D const &inTangent, + double amount, + int maxIterations = 3, + int samples = 20, + double accuracy = 1.0 + ) const; +}; + +Vector2D interpolate( + Vector2D const &from, + Vector2D const &to, + double amount +); + +struct Vector3D { + explicit Vector3D(double x_, double y_, double z_) : + x(x_), + y(y_), + z(z_) { + } + + explicit Vector3D(lottiejson11::Json const &json) noexcept(false); + lottiejson11::Json toJson() const; + + double x = 0.0; + double y = 0.0; + double z = 0.0; +}; + +Vector3D interpolate( + Vector3D const &from, + Vector3D const &to, + double amount +); + +inline double degreesToRadians(double value) { + return value * M_PI / 180.0; +} + +inline double radiansToDegrees(double value) { + return value * 180.0 / M_PI; +} + +struct CATransform3D { + double m11, m12, m13, m14; + double m21, m22, m23, m24; + double m31, m32, m33, m34; + double m41, m42, m43, m44; + + CATransform3D( + double m11_, double m12_, double m13_, double m14_, + double m21_, double m22_, double m23_, double m24_, + double m31_, double m32_, double m33_, double m34_, + double m41_, double m42_, double m43_, double m44_ + ) : + m11(m11_), m12(m12_), m13(m13_), m14(m14_), + m21(m21_), m22(m22_), m23(m23_), m24(m24_), + m31(m31_), m32(m32_), m33(m33_), m34(m34_), + m41(m41_), m42(m42_), m43(m43_), m44(m44_) { + } + + bool operator==(CATransform3D const &rhs) const { + return m11 == rhs.m11 && m12 == rhs.m12 && m13 == rhs.m13 && m14 == rhs.m14 && + m21 == rhs.m21 && m22 == rhs.m22 && m23 == rhs.m23 && m24 == rhs.m24 && + m31 == rhs.m31 && m32 == rhs.m32 && m33 == rhs.m33 && m34 == rhs.m34 && + m41 == rhs.m41 && m42 == rhs.m42 && m43 == rhs.m43 && m44 == rhs.m44; + } + + bool operator!=(CATransform3D const &rhs) const { + return !(*this == rhs); + } + + inline bool isIdentity() const { + return m11 == 1.0 && m12 == 0.0 && m13 == 0.0 && m14 == 0.0 && + m21 == 0.0 && m22 == 1.0 && m23 == 0.0 && m24 == 0.0 && + m31 == 0.0 && m32 == 0.0 && m33 == 1.0 && m34 == 0.0 && + m41 == 0.0 && m42 == 0.0 && m43 == 0.0 && m44 == 1.0; + } + + static CATransform3D makeTranslation(double tx, double ty, double tz) { + return CATransform3D( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + tx, ty, tz, 1 + ); + } + + static CATransform3D makeScale(double sx, double sy, double sz) { + return CATransform3D( + sx, 0, 0, 0, + 0, sy, 0, 0, + 0, 0, sz, 0, + 0, 0, 0, 1 + ); + } + + static CATransform3D makeRotation(double radians, double x, double y, double z); + + static CATransform3D makeSkew(double skew, double skewAxis) { + double mCos = cos(degreesToRadians(skewAxis)); + double mSin = sin(degreesToRadians(skewAxis)); + double aTan = tan(degreesToRadians(skew)); + + CATransform3D transform1( + mCos, + mSin, + 0.0, + 0.0, + -mSin, + mCos, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0 + ); + + CATransform3D transform2( + 1.0, + 0.0, + 0.0, + 0.0, + aTan, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0 + ); + + CATransform3D transform3( + mCos, + -mSin, + 0.0, + 0.0, + mSin, + mCos, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0 + ); + + return transform3 * transform2 * transform1; + } + + static CATransform3D makeTransform( + Vector2D const &anchor, + Vector2D const &position, + Vector2D const &scale, + double rotation, + std::optional skew, + std::optional skewAxis + ) { + CATransform3D result = CATransform3D::identity(); + if (skew.has_value() && skewAxis.has_value()) { + result = CATransform3D::identity().translated(position).rotated(rotation).skewed(-skew.value(), skewAxis.value()).scaled(Vector2D(scale.x * 0.01, scale.y * 0.01)).translated(Vector2D(-anchor.x, -anchor.y)); + } else { + result = CATransform3D::identity().translated(position).rotated(rotation).scaled(Vector2D(scale.x * 0.01, scale.y * 0.01)).translated(Vector2D(-anchor.x, -anchor.y)); + } + + return result; + } + + CATransform3D rotated(double degrees) const; + + CATransform3D translated(Vector2D const &translation) const; + + CATransform3D scaled(Vector2D const &scale) const; + + CATransform3D skewed(double skew, double skewAxis) const { + return CATransform3D::makeSkew(skew, skewAxis) * (*this); + } + + static CATransform3D const &identity() { + return _identity; + } + + CATransform3D operator*(CATransform3D const &b) const; + + bool isInvertible() const; + + CATransform3D inverted() const; + +private: + static CATransform3D _identity; +}; + +struct CGRect { + explicit CGRect(double x_, double y_, double width_, double height_) : + x(x_), y(y_), width(width_), height(height_) { + } + + double x = 0.0; + double y = 0.0; + double width = 0.0; + double height = 0.0; + + static CGRect veryLarge() { + return CGRect( + -100000000.0, + -100000000.0, + 200000000.0, + 200000000.0 + ); + } + + bool operator==(CGRect const &rhs) const { + return x == rhs.x && y == rhs.y && width == rhs.width && height == rhs.height; + } + + bool operator!=(CGRect const &rhs) const { + return !(*this == rhs); + } + + bool empty() const { + return width <= 0.0 || height <= 0.0; + } + + CGRect insetBy(double dx, double dy) const { + CGRect result = *this; + + result.x += dx; + result.y += dy; + result.width -= dx * 2.0; + result.height -= dy * 2.0; + + return result; + } + + bool intersects(CGRect const &other) const; + bool contains(CGRect const &other) const; + + CGRect intersection(CGRect const &other) const; + CGRect unionWith(CGRect const &other) const; + + CGRect applyingTransform(CATransform3D const &transform) const; +}; + +inline bool isInRangeOrEqual(double value, double from, double to) { + return from <= value && value <= to; +} + +inline bool isInRange(double value, double from, double to) { + return from < value && value < to; +} + +double cubicBezierInterpolate(double value, Vector2D const &P0, Vector2D const &P1, Vector2D const &P2, Vector2D const &P3); + +} + +#endif + +#endif /* Vectors_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/VectorsCocoa.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/VectorsCocoa.h new file mode 100644 index 0000000000..d706e3643b --- /dev/null +++ b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/VectorsCocoa.h @@ -0,0 +1,17 @@ +#ifndef VectorsCocoa_h +#define VectorsCocoa_h + +#ifdef __cplusplus + +#import + +namespace lottie { + +::CATransform3D nativeTransform(CATransform3D const &value); +CATransform3D fromNativeTransform(::CATransform3D const &value); + +} + +#endif + +#endif /* VectorsCocoa_h */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/lottiejson11/lottiejson11.hpp b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/lottiejson11.hpp similarity index 100% rename from submodules/TelegramUI/Components/LottieCpp/Sources/lottiejson11/lottiejson11.hpp rename to submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/lottiejson11.hpp diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp index df28991e4e..afe0d7eed6 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp @@ -1,7 +1,7 @@ #ifndef CompositionLayer_hpp #define CompositionLayer_hpp -#include "Lottie/Public/Primitives/Vectors.hpp" +#include #include "Lottie/Public/Primitives/CALayer.hpp" #include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.hpp" #include "Lottie/Private/Model/Layers/LayerModel.hpp" @@ -24,12 +24,8 @@ public: void setup(); std::shared_ptr _inputMatte; - //let wrapperLayer = CALayer() virtual void frameUpdated(double frame) override; - /*virtual bool implementsDraw() const override; - virtual void draw(std::shared_ptr const &context) override;*/ - //virtual std::shared_ptr renderableItem() override; virtual bool isInvertedMatte() const override { return true; diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.hpp index 8b9d709245..ff6dc769fa 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.hpp @@ -2,6 +2,7 @@ #define ImageCompositionLayer_hpp #include "Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp" +#include "Lottie/Private/Model/Assets/ImageAsset.hpp" #include "Lottie/Private/Model/Layers/ImageLayerModel.hpp" namespace lottie { @@ -15,12 +16,12 @@ public: contentsLayer()->setMasksToBounds(true); } - std::shared_ptr image() { + std::shared_ptr image() { return _image; } - void setImage(std::shared_ptr image) { + void setImage(std::shared_ptr image) { _image = image; - contentsLayer()->setContents(image); + //contentsLayer()->setContents(image); } std::string const &imageReferenceID() { @@ -34,7 +35,7 @@ public: private: std::string _imageReferenceID; - std::shared_ptr _image; + std::shared_ptr _image; }; } diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp index 0caadc12ac..38043c601e 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp @@ -1307,22 +1307,6 @@ std::shared_ptr ShapeCompositionLayer::renderTreeNode() { std::vector> renderTreeValue; renderTreeValue.push_back(_contentTree->itemTree->renderTree()); - //printf("Name: %s\n", keypathName().c_str()); - /*if (!maskNode && keypathName().find("Shape Layer 3") != -1) { - return std::make_shared( - bounds(), - _contentsLayer->position(), - _contentsLayer->transform(), - _contentsLayer->opacity(), - _contentsLayer->masksToBounds(), - _contentsLayer->isHidden(), - nullptr, - renderTreeValue, - nullptr, - false - ); - }*/ - std::vector> subnodes; subnodes.push_back(std::make_shared( _contentsLayer->bounds(), diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/MainThreadAnimationLayer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/MainThreadAnimationLayer.hpp index dd360588a5..29260f7507 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/MainThreadAnimationLayer.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/MainThreadAnimationLayer.hpp @@ -19,7 +19,7 @@ class BlankImageProvider: public AnimationImageProvider { public: virtual ~BlankImageProvider() = default; - std::shared_ptr imageForAsset(ImageAsset const &asset) { + std::shared_ptr imageForAsset(ImageAsset const &asset) { return nullptr; } }; diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp index 970a51ae24..ee400acf20 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp @@ -1,7 +1,7 @@ #ifndef NodeOutput_hpp #define NodeOutput_hpp -#include "Lottie/Public/Primitives/CGPath.hpp" +#include #include diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Animation.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Animation.hpp index 8a3829e646..289ef8cc09 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Animation.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Animation.hpp @@ -10,7 +10,7 @@ #include "Lottie/Private/Model/Assets/AssetLibrary.hpp" #include "Lottie/Private/Model/Objects/FitzModifier.hpp" -#include "lottiejson11/lottiejson11.hpp" +#include #include "Lottie/Private/Parsing/JsonParsing.hpp" #include "Lottie/Private/Model/Layers/LayerModelSerialization.hpp" diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/ImageAsset.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/ImageAsset.hpp index f4210ea065..992f8534df 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/ImageAsset.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/ImageAsset.hpp @@ -6,6 +6,12 @@ namespace lottie { +class Image { +public: + Image() { + } +}; + class ImageAsset: public Asset { public: ImageAsset( diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModelSerialization.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModelSerialization.hpp index 7b1b8cfe71..cc2f9ee012 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModelSerialization.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModelSerialization.hpp @@ -1,7 +1,7 @@ #ifndef LayerModelSerialization_hpp #define LayerModelSerialization_hpp -#include "lottiejson11/lottiejson11.hpp" +#include #include "Lottie/Private/Parsing/JsonParsing.hpp" #include "Lottie/Private/Model/Layers/LayerModel.hpp" diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/PreCompLayerModel.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/PreCompLayerModel.hpp index 7934474c78..44ca30ea0f 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/PreCompLayerModel.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/PreCompLayerModel.hpp @@ -3,7 +3,7 @@ #include "Lottie/Private/Model/Layers/LayerModel.hpp" #include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" -#include "Lottie/Public/Primitives/Vectors.hpp" +#include #include "Lottie/Private/Parsing/JsonParsing.hpp" #include diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Ellipse.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Ellipse.hpp index 17bd82d716..e1aff804dd 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Ellipse.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Ellipse.hpp @@ -3,7 +3,7 @@ #include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" #include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" -#include "Lottie/Public/Primitives/Vectors.hpp" +#include #include "Lottie/Private/Parsing/JsonParsing.hpp" namespace lottie { diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Fill.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Fill.hpp index 194a08dcbb..78c9432f44 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Fill.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Fill.hpp @@ -4,7 +4,7 @@ #include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" #include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" #include "Lottie/Public/Primitives/Color.hpp" -#include "Lottie/Public/Primitives/Vectors.hpp" +#include #include "Lottie/Private/Parsing/JsonParsing.hpp" namespace lottie { diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Trim.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Trim.hpp index 3e0365548b..e5c59daecd 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Trim.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Trim.hpp @@ -3,7 +3,7 @@ #include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" #include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" -#include "Lottie/Public/Primitives/Vectors.hpp" +#include #include "Lottie/Private/Parsing/JsonParsing.hpp" namespace lottie { diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextAnimator.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextAnimator.hpp index 5004e74189..838cf8aab3 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextAnimator.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextAnimator.hpp @@ -1,7 +1,7 @@ #ifndef TextAnimator_hpp #define TextAnimator_hpp -#include "Lottie/Public/Primitives/Vectors.hpp" +#include #include "Lottie/Public/Primitives/Color.hpp" #include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" #include "Lottie/Private/Parsing/JsonParsing.hpp" diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextDocument.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextDocument.hpp index 72df5474b1..5c7ef247dc 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextDocument.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextDocument.hpp @@ -1,7 +1,7 @@ #ifndef TextDocument_hpp #define TextDocument_hpp -#include "Lottie/Public/Primitives/Vectors.hpp" +#include #include "Lottie/Public/Primitives/Color.hpp" #include "Lottie/Private/Parsing/JsonParsing.hpp" diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Parsing/JsonParsing.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Parsing/JsonParsing.hpp index fc57ae101e..a0bd7951b3 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Parsing/JsonParsing.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Parsing/JsonParsing.hpp @@ -1,7 +1,7 @@ #ifndef JsonParsing_hpp #define JsonParsing_hpp -#include "lottiejson11/lottiejson11.hpp" +#include #include #include diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/BezierPath.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/BezierPath.hpp index 66277737d8..5b1d404c53 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/BezierPath.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/BezierPath.hpp @@ -4,7 +4,7 @@ #include "Lottie/Private/Utility/Primitives/CurveVertex.hpp" #include "Lottie/Private/Utility/Primitives/PathElement.hpp" #include "Lottie/Private/Parsing/JsonParsing.hpp" -#include "Lottie/Public/Primitives/CGPath.hpp" +#include #include @@ -63,9 +63,9 @@ public: } /// Create first point - Vector2D firstPoint(vertexContainer[0]); - Vector2D firstInPoint(inPointsContainer[0]); - Vector2D firstOutPoint(outPointsContainer[0]); + Vector2D firstPoint = Vector2D(vertexContainer[0]); + Vector2D firstInPoint = Vector2D(inPointsContainer[0]); + Vector2D firstOutPoint = Vector2D(outPointsContainer[0]); CurveVertex firstVertex = CurveVertex::relative( firstPoint, firstInPoint, @@ -75,9 +75,9 @@ public: elements.push_back(previousElement); for (size_t i = 1; i < vertexContainer.size(); i++) { - Vector2D point(vertexContainer[i]); - Vector2D inPoint(inPointsContainer[i]); - Vector2D outPoint(outPointsContainer[i]); + Vector2D point = Vector2D(vertexContainer[i]); + Vector2D inPoint = Vector2D(inPointsContainer[i]); + Vector2D outPoint = Vector2D(outPointsContainer[i]); CurveVertex vertex = CurveVertex::relative( point, inPoint, diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CurveVertex.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CurveVertex.hpp index ef67e0765c..93897f0288 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CurveVertex.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CurveVertex.hpp @@ -1,8 +1,8 @@ #ifndef CurveVertex_hpp #define CurveVertex_hpp -#include "Lottie/Public/Primitives/Vectors.hpp" -#include "Lottie/Public/Primitives/CGPath.hpp" +#include +#include #include diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/ImageProvider/AnimationImageProvider.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/ImageProvider/AnimationImageProvider.hpp index 3c55ffd944..7efe74f8c8 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/ImageProvider/AnimationImageProvider.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/ImageProvider/AnimationImageProvider.hpp @@ -8,7 +8,7 @@ namespace lottie { class AnimationImageProvider { public: - virtual std::shared_ptr imageForAsset(ImageAsset const &imageAsset) = 0; + virtual std::shared_ptr imageForAsset(ImageAsset const &imageAsset) = 0; }; } diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Keyframe.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Keyframe.hpp index 1cf21a8667..3d5797db3f 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Keyframe.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Keyframe.hpp @@ -2,7 +2,7 @@ #define Keyframe_hpp #include "Lottie/Public/Primitives/AnimationTime.hpp" -#include "Lottie/Public/Primitives/Vectors.hpp" +#include #include "Lottie/Public/Keyframes/Interpolatable.hpp" #include "Lottie/Public/Keyframes/ValueInterpolators.hpp" diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/ValueInterpolators.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/ValueInterpolators.hpp index b2c063dccc..0e89a6a27a 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/ValueInterpolators.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/ValueInterpolators.hpp @@ -1,7 +1,7 @@ #ifndef ValueInterpolators_hpp #define ValueInterpolators_hpp -#include "Lottie/Public/Primitives/Vectors.hpp" +#include #include "Lottie/Public/Primitives/Color.hpp" #include "Lottie/Private/Utility/Primitives/BezierPath.hpp" #include "Lottie/Private/Model/Text/TextDocument.hpp" diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/AnyValue.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/AnyValue.hpp index 516b2e7d10..72cdaf674a 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/AnyValue.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/AnyValue.hpp @@ -1,7 +1,7 @@ #ifndef AnyValue_hpp #define AnyValue_hpp -#include "Lottie/Public/Primitives/Vectors.hpp" +#include #include "Lottie/Public/Primitives/Color.hpp" #include "Lottie/Private/Utility/Primitives/BezierPath.hpp" #include "Lottie/Private/Model/Text/TextDocument.hpp" diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CALayer.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CALayer.cpp new file mode 100644 index 0000000000..add81ca973 --- /dev/null +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CALayer.cpp @@ -0,0 +1,37 @@ +#include "CALayer.hpp" + +namespace lottie { + +std::shared_ptr CAShapeLayer::renderableItem() { + if (!_path) { + return nullptr; + } + + std::optional fill; + if (_fillColor) { + fill = ShapeRenderableItem::Fill( + _fillColor.value(), + _fillRule + ); + } + + std::optional stroke; + if (_strokeColor) { + stroke = ShapeRenderableItem::Stroke( + _strokeColor.value(), + _lineWidth, + _lineJoin, + _lineCap, + _lineDashPhase, + _dashPattern + ); + } + + return std::make_shared( + _path, + fill, + stroke + ); +} + +} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CALayer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CALayer.hpp index eb1b3837c9..b3928779c6 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CALayer.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CALayer.hpp @@ -2,8 +2,8 @@ #define CALayer_hpp #include "Lottie/Public/Primitives/Color.hpp" -#include "Lottie/Public/Primitives/Vectors.hpp" -#include "Lottie/Public/Primitives/CGPath.hpp" +#include +#include #include "Lottie/Private/Model/ShapeItems/Fill.hpp" #include "Lottie/Private/Model/Layers/LayerModel.hpp" #include "Lottie/Public/Primitives/DrawingAttributes.hpp" @@ -15,68 +15,6 @@ namespace lottie { -enum class CGBlendMode { - Normal, - DestinationIn, - DestinationOut -}; - -class CGImage { -public: - virtual ~CGImage() = default; -}; - -class CGGradient { -public: - CGGradient(std::vector const &colors, std::vector const &locations) : - _colors(colors), - _locations(locations) { - assert(_colors.size() == _locations.size()); - } - - std::vector const &colors() const { - return _colors; - } - - std::vector const &locations() const { - return _locations; - } - -private: - std::vector _colors; - std::vector _locations; -}; - -class CGContext { -public: - virtual ~CGContext() = default; - - virtual int width() const = 0; - virtual int height() const = 0; - - virtual std::shared_ptr makeLayer(int width, int height) = 0; - - virtual void saveState() = 0; - virtual void restoreState() = 0; - - virtual void fillPath(std::shared_ptr const &path, FillRule fillRule, Color const &color) = 0; - virtual void linearGradientFillPath(std::shared_ptr const &path, FillRule fillRule, CGGradient const &gradient, Vector2D const &start, Vector2D const &end) = 0; - virtual void radialGradientFillPath(std::shared_ptr const &path, FillRule fillRule, CGGradient const &gradient, Vector2D const &startCenter, double startRadius, Vector2D const &endCenter, double endRadius) = 0; - - virtual void strokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, Color const &color) = 0; - virtual void linearGradientStrokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, CGGradient const &gradient, Vector2D const &start, Vector2D const &end) = 0; - virtual void radialGradientStrokePath(std::shared_ptr const &path, double lineWidth, LineJoin lineJoin, LineCap lineCap, double dashPhase, std::vector const &dashPattern, CGGradient const &gradient, Vector2D const &startCenter, double startRadius, Vector2D const &endCenter, double endRadius) = 0; - - virtual void fill(CGRect const &rect, Color const &fillColor) = 0; - virtual void setBlendMode(CGBlendMode blendMode) = 0; - - virtual void setAlpha(double alpha) = 0; - - virtual void concatenate(CATransform3D const &transform) = 0; - - virtual void draw(std::shared_ptr const &other, CGRect const &rect) = 0; -}; - class RenderableItem { public: enum class Type { @@ -544,9 +482,6 @@ public: return false; } - virtual void draw(std::shared_ptr const &context) { - } - virtual std::shared_ptr renderableItem() { return nullptr; } @@ -615,13 +550,6 @@ public: _compositingFilter = compositingFilter; } - std::shared_ptr const &contents() const { - return _contents; - } - void setContents(std::shared_ptr contents) { - _contents = contents; - } - protected: template std::shared_ptr shared_from_base() { @@ -651,7 +579,6 @@ private: std::shared_ptr _mask; bool _masksToBounds = false; std::optional _compositingFilter; - std::shared_ptr _contents; }; class CAShapeLayer: public CALayer { @@ -739,12 +666,6 @@ public: } } - /*virtual bool implementsDraw() const override { - return true; - } - - virtual void draw(std::shared_ptr const &context) override;*/ - std::shared_ptr renderableItem() override; private: diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CGPath.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CGPath.cpp index 7b352cf13b..9b174bed67 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CGPath.cpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CGPath.cpp @@ -1,4 +1,4 @@ -#include "CGPath.hpp" +#include #include diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CGPath.mm b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CGPath.mm index a7798daa9a..e80342007f 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CGPath.mm +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CGPath.mm @@ -1,6 +1,6 @@ -#include "CGPath.hpp" -#include "Lottie/Public/Primitives/CGPathCocoa.h" +#include +#include #import namespace { diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/RenderTree.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/RenderTree.hpp index 1ab374e226..3f190522a0 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/RenderTree.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/RenderTree.hpp @@ -3,7 +3,7 @@ #include -#include "Lottie/Public/Primitives/Vectors.hpp" +#include #include "Lottie/Public/Primitives/CALayer.hpp" namespace lottie { diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Vectors.mm b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Vectors.mm index e37b668c02..4b18159388 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Vectors.mm +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Vectors.mm @@ -1,7 +1,7 @@ -#include "Vectors.hpp" - -#include "VectorsCocoa.h" +#include +#include +#include "Lottie/Private/Parsing/JsonParsing.hpp" #include "Lottie/Public/Keyframes/Interpolatable.hpp" #include @@ -12,6 +12,134 @@ namespace lottie { +Vector1D::Vector1D(lottiejson11::Json const &json) noexcept(false) { + if (json.is_number()) { + value = json.number_value(); + } else if (json.is_array()) { + if (json.array_items().empty()) { + throw LottieParsingException(); + } + if (!json.array_items()[0].is_number()) { + throw LottieParsingException(); + } + value = json.array_items()[0].number_value(); + } else { + throw LottieParsingException(); + } +} + +lottiejson11::Json Vector1D::toJson() const { + return lottiejson11::Json(value); +} + +Vector2D::Vector2D(lottiejson11::Json const &json) noexcept(false) { + x = 0.0; + y = 0.0; + + if (json.is_array()) { + int index = 0; + + if (json.array_items().size() > index) { + if (!json.array_items()[index].is_number()) { + throw LottieParsingException(); + } + x = json.array_items()[index].number_value(); + index++; + } + + if (json.array_items().size() > index) { + if (!json.array_items()[index].is_number()) { + throw LottieParsingException(); + } + y = json.array_items()[index].number_value(); + index++; + } + } else if (json.is_object()) { + auto xAny = getAny(json.object_items(), "x"); + if (xAny.is_number()) { + x = xAny.number_value(); + } else if (xAny.is_array()) { + if (xAny.array_items().empty()) { + throw LottieParsingException(); + } + if (!xAny.array_items()[0].is_number()) { + throw LottieParsingException(); + } + x = xAny.array_items()[0].number_value(); + } + + auto yAny = getAny(json.object_items(), "y"); + if (yAny.is_number()) { + y = yAny.number_value(); + } else if (yAny.is_array()) { + if (yAny.array_items().empty()) { + throw LottieParsingException(); + } + if (!yAny.array_items()[0].is_number()) { + throw LottieParsingException(); + } + y = yAny.array_items()[0].number_value(); + } + } else { + throw LottieParsingException(); + } +} + +lottiejson11::Json Vector2D::toJson() const { + lottiejson11::Json::object result; + + result.insert(std::make_pair("x", x)); + result.insert(std::make_pair("y", y)); + + return lottiejson11::Json(result); +} + +Vector3D::Vector3D(lottiejson11::Json const &json) noexcept(false) { + if (!json.is_array()) { + throw LottieParsingException(); + } + + int index = 0; + + x = 0.0; + y = 0.0; + z = 0.0; + + if (json.array_items().size() > index) { + if (!json.array_items()[index].is_number()) { + throw LottieParsingException(); + } + x = json.array_items()[index].number_value(); + index++; + } + + if (json.array_items().size() > index) { + if (!json.array_items()[index].is_number()) { + throw LottieParsingException(); + } + y = json.array_items()[index].number_value(); + index++; + } + + if (json.array_items().size() > index) { + if (!json.array_items()[index].is_number()) { + throw LottieParsingException(); + } + z = json.array_items()[index].number_value(); + index++; + } +} + +lottiejson11::Json Vector3D::toJson() const { + lottiejson11::Json::array result; + + result.push_back(lottiejson11::Json(x)); + result.push_back(lottiejson11::Json(y)); + result.push_back(lottiejson11::Json(z)); + + return lottiejson11::Json(result); +} + CATransform3D CATransform3D::_identity = CATransform3D( 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainer.mm b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainer.mm index db650d76ff..b2fe181e1e 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainer.mm +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainer.mm @@ -97,214 +97,6 @@ struct RenderNodeDesc { } }; -/*static std::optional processRenderTree(std::shared_ptr const &node, Vector2D const &globalSize, CATransform3D const &parentTransform, bool isInvertedMask, BezierPathsBoundingBoxContext &bezierPathsBoundingBoxContext) { - if (node->isHidden() || node->alpha() == 0.0f) { - return std::nullopt; - } - - if (node->masksToBounds()) { - if (node->bounds().empty()) { - return std::nullopt; - } - } - - auto currentTransform = parentTransform; - - Vector2D localTranslation(node->position().x + -node->bounds().x, node->position().y + -node->bounds().y); - CATransform3D localTransform = node->transform(); - if (localTransform.isIdentity()) { - currentTransform.m41 += localTranslation.x; - currentTransform.m42 += localTranslation.y; - - localTransform.m41 += localTranslation.x; - localTransform.m42 += localTranslation.y; - } else { - localTransform.m41 += localTranslation.x; - localTransform.m42 += localTranslation.y; - - currentTransform = localTransform * currentTransform; - } - - if (!currentTransform.isInvertible()) { - return std::nullopt; - } - - std::optional effectiveLocalBounds; - - double alpha = node->alpha(); - - if (node->content()) { - RenderTreeNodeContent *shapeContent = node->content().get(); - - CGRect shapeBounds = bezierPathsBoundingBoxParallel(bezierPathsBoundingBoxContext, shapeContent->paths); - - if (shapeContent->stroke) { - shapeBounds = shapeBounds.insetBy(-shapeContent->stroke->lineWidth / 2.0, -shapeContent->stroke->lineWidth / 2.0); - effectiveLocalBounds = shapeBounds; - - switch (shapeContent->stroke->shading->type()) { - case RenderTreeNodeContent::ShadingType::Solid: { - RenderTreeNodeContent::SolidShading *solidShading = (RenderTreeNodeContent::SolidShading *)shapeContent->stroke->shading.get(); - - alpha *= solidShading->opacity; - - break; - } - case RenderTreeNodeContent::ShadingType::Gradient: { - - break; - } - default: - break; - } - } else if (shapeContent->fill) { - effectiveLocalBounds = shapeBounds; - - switch (shapeContent->fill->shading->type()) { - case RenderTreeNodeContent::ShadingType::Solid: { - RenderTreeNodeContent::SolidShading *solidShading = (RenderTreeNodeContent::SolidShading *)shapeContent->fill->shading.get(); - - alpha *= solidShading->opacity; - - break; - } - case RenderTreeNodeContent::ShadingType::Gradient: { - RenderTreeNodeContent::GradientShading *gradientShading = (RenderTreeNodeContent::GradientShading *)shapeContent->fill->shading.get(); - - alpha *= gradientShading->opacity; - - break; - } - default: - break; - } - } - } - - bool isInvertedMatte = isInvertedMask; - if (isInvertedMatte) { - effectiveLocalBounds = node->bounds(); - } - - if (effectiveLocalBounds && effectiveLocalBounds->empty()) { - effectiveLocalBounds = std::nullopt; - } - - std::optional effectiveLocalRect; - if (effectiveLocalBounds.has_value()) { - effectiveLocalRect = effectiveLocalBounds; - } - - std::vector> subnodes; - std::optional subnodesGlobalRect; - bool masksToBounds = node->masksToBounds(); - - int drawContentDescendants = 0; - - for (const auto &item : node->subnodes()) { - if (const auto subnode = processRenderTree(item, globalSize, currentTransform, false, bezierPathsBoundingBoxContext)) { - drawContentDescendants += subnode->drawContentDescendants; - - if (subnode->renderContent) { - drawContentDescendants += 1; - } - - if (!subnode->localRect.empty()) { - if (effectiveLocalRect.has_value()) { - effectiveLocalRect = effectiveLocalRect->unionWith(subnode->localRect); - } else { - effectiveLocalRect = subnode->localRect; - } - } - - if (subnodesGlobalRect) { - subnodesGlobalRect = subnodesGlobalRect->unionWith(subnode->globalRect); - } else { - subnodesGlobalRect = subnode->globalRect; - } - } - } - - if (masksToBounds && effectiveLocalRect.has_value()) { - if (node->bounds().contains(effectiveLocalRect.value())) { - masksToBounds = false; - } - } - - std::optional fuzzyGlobalRect; - - if (effectiveLocalBounds) { - CGRect effectiveGlobalBounds = effectiveLocalBounds->applyingTransform(currentTransform); - if (fuzzyGlobalRect) { - fuzzyGlobalRect = fuzzyGlobalRect->unionWith(effectiveGlobalBounds); - } else { - fuzzyGlobalRect = effectiveGlobalBounds; - } - } - - if (subnodesGlobalRect) { - if (fuzzyGlobalRect) { - fuzzyGlobalRect = fuzzyGlobalRect->unionWith(subnodesGlobalRect.value()); - } else { - fuzzyGlobalRect = subnodesGlobalRect; - } - } - - if (!fuzzyGlobalRect) { - return std::nullopt; - } - - CGRect globalRect( - std::floor(fuzzyGlobalRect->x), - std::floor(fuzzyGlobalRect->y), - std::ceil(fuzzyGlobalRect->width + fuzzyGlobalRect->x - floor(fuzzyGlobalRect->x)), - std::ceil(fuzzyGlobalRect->height + fuzzyGlobalRect->y - floor(fuzzyGlobalRect->y)) - ); - - if (!CGRect(0.0, 0.0, globalSize.x, globalSize.y).intersects(globalRect)) { - return std::nullopt; - } - - if (masksToBounds && effectiveLocalBounds) { - CGRect effectiveGlobalBounds = effectiveLocalBounds->applyingTransform(currentTransform); - if (effectiveGlobalBounds.contains(CGRect(0.0, 0.0, globalSize.x, globalSize.y))) { - masksToBounds = false; - } - } - - std::optional maskNode; - if (node->mask()) { - if (const auto maskNodeValue = processRenderTree(node->mask(), globalSize, currentTransform, node->invertMask(), bezierPathsBoundingBoxContext)) { - if (!maskNodeValue->globalRect.intersects(globalRect)) { - return std::nullopt; - } - maskNode = maskNodeValue; - } else { - return std::nullopt; - } - } - - CGRect localRect = effectiveLocalRect.value_or(CGRect(0.0, 0.0, 0.0, 0.0)).applyingTransform(localTransform); - - return RenderNodeDesc( - RenderNodeDesc::LayerParams( - node->bounds(), - node->position(), - node->transform(), - alpha, - masksToBounds, - node->isHidden() - ), - globalRect, - localRect, - currentTransform, - effectiveLocalBounds.has_value(), - node->content() != nullptr, - drawContentDescendants, - isInvertedMatte - ); -}*/ - static std::shared_ptr convertRenderTree(std::shared_ptr const &node, Vector2D const &globalSize, CATransform3D const &parentTransform, bool isInvertedMask, BezierPathsBoundingBoxContext &bezierPathsBoundingBoxContext) { if (node->isHidden() || node->alpha() == 0.0f) { return nullptr; @@ -508,6 +300,209 @@ static std::shared_ptr convertRenderTree(std::shared_ptr const &node, Vector2D const &globalSize, CATransform3D const &parentTransform, bool isInvertedMask, BezierPathsBoundingBoxContext &bezierPathsBoundingBoxContext) { + if (node->isHidden() || node->alpha() == 0.0f) { + return nullptr; + } + + if (node->masksToBounds()) { + if (node->bounds().empty()) { + return nullptr; + } + } + + auto currentTransform = parentTransform; + + Vector2D localTranslation(node->position().x - node->bounds().x, node->position().y - node->bounds().y); + CATransform3D localTransform = node->transform(); + localTransform = localTransform.translated(localTranslation); + + currentTransform = localTransform * currentTransform; + + if (!currentTransform.isInvertible()) { + return nullptr; + } + + std::optional effectiveLocalBounds; + + double alpha = node->alpha(); + + if (node->content()) { + RenderTreeNodeContent *shapeContent = node->content().get(); + + CGRect shapeBounds = bezierPathsBoundingBoxParallel(bezierPathsBoundingBoxContext, shapeContent->paths); + + if (shapeContent->stroke) { + shapeBounds = shapeBounds.insetBy(-shapeContent->stroke->lineWidth / 2.0, -shapeContent->stroke->lineWidth / 2.0); + effectiveLocalBounds = shapeBounds; + + switch (shapeContent->stroke->shading->type()) { + case RenderTreeNodeContent::ShadingType::Solid: { + RenderTreeNodeContent::SolidShading *solidShading = (RenderTreeNodeContent::SolidShading *)shapeContent->stroke->shading.get(); + + alpha *= solidShading->opacity; + + break; + } + case RenderTreeNodeContent::ShadingType::Gradient: { + + break; + } + default: + break; + } + } else if (shapeContent->fill) { + effectiveLocalBounds = shapeBounds; + + switch (shapeContent->fill->shading->type()) { + case RenderTreeNodeContent::ShadingType::Solid: { + RenderTreeNodeContent::SolidShading *solidShading = (RenderTreeNodeContent::SolidShading *)shapeContent->fill->shading.get(); + + alpha *= solidShading->opacity; + + break; + } + case RenderTreeNodeContent::ShadingType::Gradient: { + RenderTreeNodeContent::GradientShading *gradientShading = (RenderTreeNodeContent::GradientShading *)shapeContent->fill->shading.get(); + + alpha *= gradientShading->opacity; + + break; + } + default: + break; + } + } + } + + bool isInvertedMatte = isInvertedMask; + if (isInvertedMatte) { + effectiveLocalBounds = node->bounds(); + } + + if (effectiveLocalBounds && effectiveLocalBounds->empty()) { + effectiveLocalBounds = std::nullopt; + } + + std::optional effectiveLocalRect; + if (effectiveLocalBounds.has_value()) { + effectiveLocalRect = effectiveLocalBounds; + } + + std::vector> subnodes; + std::optional subnodesGlobalRect; + bool masksToBounds = node->masksToBounds(); + + int drawContentDescendants = 0; + + for (const auto &item : node->subnodes()) { + if (const auto subnode = convertRenderTree(item, globalSize, currentTransform, false, bezierPathsBoundingBoxContext)) { + subnodes.push_back(subnode); + + drawContentDescendants += subnode->drawContentDescendants; + + if (subnode->renderContent) { + drawContentDescendants += 1; + } + + if (!subnode->localRect.empty()) { + if (effectiveLocalRect.has_value()) { + effectiveLocalRect = effectiveLocalRect->unionWith(subnode->localRect); + } else { + effectiveLocalRect = subnode->localRect; + } + } + + if (subnodesGlobalRect) { + subnodesGlobalRect = subnodesGlobalRect->unionWith(subnode->globalRect); + } else { + subnodesGlobalRect = subnode->globalRect; + } + } + } + + if (masksToBounds && effectiveLocalRect.has_value()) { + if (node->bounds().contains(effectiveLocalRect.value())) { + masksToBounds = false; + } + } + + std::optional fuzzyGlobalRect; + + if (effectiveLocalBounds) { + CGRect effectiveGlobalBounds = effectiveLocalBounds->applyingTransform(currentTransform); + if (fuzzyGlobalRect) { + fuzzyGlobalRect = fuzzyGlobalRect->unionWith(effectiveGlobalBounds); + } else { + fuzzyGlobalRect = effectiveGlobalBounds; + } + } + + if (subnodesGlobalRect) { + if (fuzzyGlobalRect) { + fuzzyGlobalRect = fuzzyGlobalRect->unionWith(subnodesGlobalRect.value()); + } else { + fuzzyGlobalRect = subnodesGlobalRect; + } + } + + if (!fuzzyGlobalRect) { + return nullptr; + } + + CGRect globalRect( + std::floor(fuzzyGlobalRect->x), + std::floor(fuzzyGlobalRect->y), + std::ceil(fuzzyGlobalRect->width + fuzzyGlobalRect->x - floor(fuzzyGlobalRect->x)), + std::ceil(fuzzyGlobalRect->height + fuzzyGlobalRect->y - floor(fuzzyGlobalRect->y)) + ); + + if (!CGRect(0.0, 0.0, globalSize.x, globalSize.y).intersects(globalRect)) { + return nullptr; + } + + if (masksToBounds && effectiveLocalBounds) { + CGRect effectiveGlobalBounds = effectiveLocalBounds->applyingTransform(currentTransform); + if (effectiveGlobalBounds.contains(CGRect(0.0, 0.0, globalSize.x, globalSize.y))) { + masksToBounds = false; + } + } + + std::shared_ptr maskNode; + if (node->mask()) { + if (const auto maskNodeValue = convertRenderTree(node->mask(), globalSize, currentTransform, node->invertMask(), bezierPathsBoundingBoxContext)) { + if (!maskNodeValue->globalRect.intersects(globalRect)) { + return nullptr; + } + maskNode = maskNodeValue; + } else { + return nullptr; + } + } + + CGRect localRect = effectiveLocalRect.value_or(CGRect(0.0, 0.0, 0.0, 0.0)).applyingTransform(localTransform); + + return std::make_shared( + OutputRenderNode::LayerParams( + node->bounds(), + node->position(), + node->transform(), + alpha, + masksToBounds, + node->isHidden() + ), + globalRect, + localRect, + currentTransform, + effectiveLocalBounds.has_value(), + node->content(), + drawContentDescendants, + isInvertedMatte, + subnodes, + maskNode + ); +}*/ + } @interface LottieAnimationContainer () { @@ -541,12 +536,16 @@ static std::shared_ptr convertRenderTree(std::shared_ptrsetCurrentFrame(frame); } -- (LottieRenderNode * _Nonnull)getCurrentRenderTreeForSize:(CGSize)size { +- (LottieRenderNode * _Nullable)getCurrentRenderTreeForSize:(CGSize)size { auto renderNode = _layer->renderTreeNode(); if (!renderNode) { return nil; } + if (size.width < 0.0) { + return nil; + } + auto node = convertRenderTree(renderNode, lottie::Vector2D((int)size.width, (int)size.height), lottie::CATransform3D::identity().scaled(lottie::Vector2D(size.width / (double)_animation.size.width, size.height / (double)_animation.size.height)), false, *_bezierPathsBoundingBoxContext.get()); if (node) { diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieRenderTree.mm b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieRenderTree.mm index 01890e9290..b053b3b19e 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieRenderTree.mm +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieRenderTree.mm @@ -1,11 +1,11 @@ #include "LottieRenderTree.h" #include "LottieRenderTreeInternal.h" -#include "Lottie/Public/Primitives/CGPath.hpp" -#include "Lottie/Public/Primitives/CGPathCocoa.h" +#include +#include #include "Lottie/Public/Primitives/Color.hpp" #include "Lottie/Public/Primitives/CALayer.hpp" -#include "Lottie/Public/Primitives/VectorsCocoa.h" +#include #include "RenderNode.hpp" diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/lottiejson11/lottiejson11.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/lottiejson11/lottiejson11.cpp index c826a925d0..f5a69b64e8 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/lottiejson11/lottiejson11.cpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/lottiejson11/lottiejson11.cpp @@ -19,7 +19,7 @@ * THE SOFTWARE. */ -#include "lottiejson11.hpp" +#include #include #include #include diff --git a/submodules/TelegramUI/Components/LottieMetal/Sources/LottieMetalAnimatedStickerNode.swift b/submodules/TelegramUI/Components/LottieMetal/Sources/LottieMetalAnimatedStickerNode.swift index 2fada71d62..ab5250460c 100644 --- a/submodules/TelegramUI/Components/LottieMetal/Sources/LottieMetalAnimatedStickerNode.swift +++ b/submodules/TelegramUI/Components/LottieMetal/Sources/LottieMetalAnimatedStickerNode.swift @@ -138,9 +138,11 @@ private final class AnimationCacheState { for i in 0 ..< animationContainer.animation.frameCount { animationContainer.update(i) let frameRangeStart = buffer.length - serializeNode(buffer: buffer, node: animationContainer.getCurrentRenderTree(for: CGSize(width: 512.0, height: 512.0))) - let frameRangeEnd = buffer.length - frameMapping.frameRanges[i] = frameRangeStart ..< frameRangeEnd + if let node = animationContainer.getCurrentRenderTree(for: CGSize(width: 512.0, height: 512.0)) { + serializeNode(buffer: buffer, node: node) + let frameRangeEnd = buffer.length + frameMapping.frameRanges[i] = frameRangeStart ..< frameRangeEnd + } } let previousLength = buffer.length @@ -189,7 +191,7 @@ private final class AnimationCacheState { } } -final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubject { +public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubject { enum Content { case serialized(frameMapping: SerializedFrameMapping, data: Data) case animation(LottieAnimationContainer) @@ -239,9 +241,9 @@ final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubject { } private var content: Content? - var frameIndex: Int = 0 + public var frameIndex: Int = 0 - var internalData: MetalEngineSubjectInternalData? + public var internalData: MetalEngineSubjectInternalData? private let msaaSampleCount = 4 private var renderBufferHeap: MTLHeap? @@ -294,11 +296,19 @@ final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubject { self.isOpaque = false } - override init(layer: Any) { + public init(animation: LottieAnimationContainer) { + self.content = .animation(animation) + + super.init() + + self.isOpaque = false + } + + override public init(layer: Any) { super.init(layer: layer) } - required init?(coder: NSCoder) { + required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -368,7 +378,7 @@ final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubject { frameState.add(stroke: strokeState) } - func update(context: MetalEngineSubjectContext) { + public func update(context: MetalEngineSubjectContext) { if self.bounds.isEmpty { return } diff --git a/submodules/sqlcipher/BUILD b/submodules/sqlcipher/BUILD index cce9827360..0df7412fc1 100644 --- a/submodules/sqlcipher/BUILD +++ b/submodules/sqlcipher/BUILD @@ -28,6 +28,7 @@ objc_library( "-DSQLITE_OMIT_DEPRECATED", "-DNDEBUG=1", "-DSQLITE_MAX_MMAP_SIZE=0", + "-Wno-all", ], sdk_frameworks = [ "Foundation",