├── .gitignore ├── LICENSE ├── README.md ├── docs ├── .buildinfo ├── .nojekyll ├── _images │ ├── keyframeReductionExample.gif │ └── keyframeReductionUI.png ├── _sources │ ├── index.rst.txt │ ├── keyframeReduction.classes.fit.rst.txt │ ├── keyframeReduction.classes.keyframe.rst.txt │ ├── keyframeReduction.classes.keyframeReduction.rst.txt │ ├── keyframeReduction.classes.rst.txt │ ├── keyframeReduction.classes.vector.rst.txt │ ├── keyframeReduction.install.rst.txt │ ├── keyframeReduction.rst.txt │ ├── keyframeReduction.ui.rst.txt │ ├── keyframeReduction.utils.rst.txt │ └── modules.rst.txt ├── _static │ ├── ajax-loader.gif │ ├── basic.css │ ├── classic.css │ ├── comment-bright.png │ ├── comment-close.png │ ├── comment.png │ ├── doctools.js │ ├── down-pressed.png │ ├── down.png │ ├── file.png │ ├── jquery-3.1.0.js │ ├── jquery.js │ ├── minus.png │ ├── plus.png │ ├── pygments.css │ ├── searchtools.js │ ├── sidebar.js │ ├── underscore-1.3.1.js │ ├── underscore.js │ ├── up-pressed.png │ ├── up.png │ └── websupport.js ├── genindex.html ├── index.html ├── keyframeReduction.classes.fit.html ├── keyframeReduction.classes.html ├── keyframeReduction.classes.keyframe.html ├── keyframeReduction.classes.keyframeReduction.html ├── keyframeReduction.classes.vector.html ├── keyframeReduction.html ├── keyframeReduction.install.html ├── keyframeReduction.ui.html ├── keyframeReduction.utils.html ├── modules.html ├── objects.inv ├── py-modindex.html ├── search.html └── searchindex.js ├── icons └── KR_icon.png ├── keyframeReduction.mel ├── keyframeReduction.mod └── scripts ├── keyframeReduction ├── __init__.py ├── classes │ ├── __init__.py │ ├── fit.py │ ├── keyframe.py │ ├── keyframeReduction.py │ └── vector.py ├── install.py ├── ui.py └── utils.py └── userSetup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.py[cod] 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Robert Joosten 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # maya-keyframe-reduction 2 | Keyframe Reduction for Maya using least-squares method. 3 | 4 |

5 | 6 | ## Installation 7 | * Extract the content of the .rar file anywhere on disk. 8 | * Drag the keyframeReduction.mel file in Maya to permanently install the script. 9 | 10 | ## Usage 11 | A button on the MiscTools shelf will be created that will allow easy access to 12 | the ui, this way the user doesn't need to worry about any of the code. If user 13 | wishes to not use the shelf button the following commands can be used. 14 | 15 | The ui responds to the current selection where it finds all of the suitable 16 | animation curves for reduction. You will be able to filter the animation 17 | curves based on the plug it is connected to. This will make it easier to 18 | target exactly the curves you want to reduce. 19 | 20 | After an animation curve is reduced the reduction percentage will be printed 21 | to the console. This can give you an idea if you would like to increase or 22 | decrease the error rate to get the desired results. 23 | 24 | ### UI 25 |

26 | 27 | Display the UI with the following code. 28 | ```python 29 | import keyframeReduction.ui 30 | keyframeReduction.ui.show() 31 | ``` 32 | 33 | ### Command Line 34 | Use the KeyframeReduction class on individual animation curves. 35 | ```python 36 | from keyframeReduction import KeyframeReduction 37 | obj = KeyframeReduction(pathToAnimCurve) 38 | obj.reduce(error=0.1) 39 | ``` 40 | 41 | ### Options 42 | * **error**: The maximum amount the reduced curve is allowed to deviate from the sampled curve. 43 | * **step**: The step size to sample the curve, default is set to one. 44 | * **weightedTangents**: Reduce curve using weighted tangents, using weighted tangents will result in less keyframes. 45 | * **tangentSplitAuto**: Automatically split tangents. 46 | * **tangentSplitExisting**: Use existing keyframes that have split tangents. 47 | * **tangentSplitAngleThreshold**: Split tangents based on an angle threshold. 48 | * **tangentSplitAngleThresholdValue**: Split tangent angle value. 49 | 50 | ## Note 51 | The fitting algorithm is ported from Paper.js - The Swiss Army Knife of Vector Graphics Scripting. 52 | http://paperjs.org/ 53 | -------------------------------------------------------------------------------- /docs/.buildinfo: -------------------------------------------------------------------------------- 1 | # Sphinx build info version 1 2 | # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. 3 | config: 2d38131645911f72fc4cb65d3ed7a7bf 4 | tags: 645f666f9bcd5a90fca523b33c5a78b7 5 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertjoosten/maya-keyframe-reduction/3c781b9d9bf6f43a3df56fe2a325ab6a2be19cc0/docs/.nojekyll -------------------------------------------------------------------------------- /docs/_images/keyframeReductionExample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertjoosten/maya-keyframe-reduction/3c781b9d9bf6f43a3df56fe2a325ab6a2be19cc0/docs/_images/keyframeReductionExample.gif -------------------------------------------------------------------------------- /docs/_images/keyframeReductionUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertjoosten/maya-keyframe-reduction/3c781b9d9bf6f43a3df56fe2a325ab6a2be19cc0/docs/_images/keyframeReductionUI.png -------------------------------------------------------------------------------- /docs/_sources/index.rst.txt: -------------------------------------------------------------------------------- 1 | .. keyframeReduction documentation master file, created by 2 | sphinx-quickstart on Wed Jun 26 08:18:30 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to keyframeReduction's documentation! 7 | ============================================= 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | 14 | 15 | Indices and tables 16 | ================== 17 | 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /docs/_sources/keyframeReduction.classes.fit.rst.txt: -------------------------------------------------------------------------------- 1 | keyframeReduction\.classes\.fit module 2 | ====================================== 3 | 4 | .. automodule:: keyframeReduction.classes.fit 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/_sources/keyframeReduction.classes.keyframe.rst.txt: -------------------------------------------------------------------------------- 1 | keyframeReduction\.classes\.keyframe module 2 | =========================================== 3 | 4 | .. automodule:: keyframeReduction.classes.keyframe 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/_sources/keyframeReduction.classes.keyframeReduction.rst.txt: -------------------------------------------------------------------------------- 1 | keyframeReduction\.classes\.keyframeReduction module 2 | ==================================================== 3 | 4 | .. automodule:: keyframeReduction.classes.keyframeReduction 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/_sources/keyframeReduction.classes.rst.txt: -------------------------------------------------------------------------------- 1 | keyframeReduction\.classes package 2 | ================================== 3 | 4 | .. automodule:: keyframeReduction.classes 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | .. toctree:: 13 | 14 | keyframeReduction.classes.fit 15 | keyframeReduction.classes.keyframe 16 | keyframeReduction.classes.keyframeReduction 17 | keyframeReduction.classes.vector 18 | 19 | -------------------------------------------------------------------------------- /docs/_sources/keyframeReduction.classes.vector.rst.txt: -------------------------------------------------------------------------------- 1 | keyframeReduction\.classes\.vector module 2 | ========================================= 3 | 4 | .. automodule:: keyframeReduction.classes.vector 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/_sources/keyframeReduction.install.rst.txt: -------------------------------------------------------------------------------- 1 | keyframeReduction\.install module 2 | ================================= 3 | 4 | .. automodule:: keyframeReduction.install 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/_sources/keyframeReduction.rst.txt: -------------------------------------------------------------------------------- 1 | keyframeReduction package 2 | ========================= 3 | 4 | .. automodule:: keyframeReduction 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | 14 | keyframeReduction.classes 15 | 16 | Submodules 17 | ---------- 18 | 19 | .. toctree:: 20 | 21 | keyframeReduction.install 22 | keyframeReduction.ui 23 | keyframeReduction.utils 24 | 25 | -------------------------------------------------------------------------------- /docs/_sources/keyframeReduction.ui.rst.txt: -------------------------------------------------------------------------------- 1 | keyframeReduction\.ui module 2 | ============================ 3 | 4 | .. automodule:: keyframeReduction.ui 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/_sources/keyframeReduction.utils.rst.txt: -------------------------------------------------------------------------------- 1 | keyframeReduction\.utils module 2 | =============================== 3 | 4 | .. automodule:: keyframeReduction.utils 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/_sources/modules.rst.txt: -------------------------------------------------------------------------------- 1 | keyframeReduction 2 | ================= 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | keyframeReduction 8 | -------------------------------------------------------------------------------- /docs/_static/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertjoosten/maya-keyframe-reduction/3c781b9d9bf6f43a3df56fe2a325ab6a2be19cc0/docs/_static/ajax-loader.gif -------------------------------------------------------------------------------- /docs/_static/basic.css: -------------------------------------------------------------------------------- 1 | /* 2 | * basic.css 3 | * ~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- basic theme. 6 | * 7 | * :copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | /* -- main layout ----------------------------------------------------------- */ 13 | 14 | div.clearer { 15 | clear: both; 16 | } 17 | 18 | /* -- relbar ---------------------------------------------------------------- */ 19 | 20 | div.related { 21 | width: 100%; 22 | font-size: 90%; 23 | } 24 | 25 | div.related h3 { 26 | display: none; 27 | } 28 | 29 | div.related ul { 30 | margin: 0; 31 | padding: 0 0 0 10px; 32 | list-style: none; 33 | } 34 | 35 | div.related li { 36 | display: inline; 37 | } 38 | 39 | div.related li.right { 40 | float: right; 41 | margin-right: 5px; 42 | } 43 | 44 | /* -- sidebar --------------------------------------------------------------- */ 45 | 46 | div.sphinxsidebarwrapper { 47 | padding: 10px 5px 0 10px; 48 | } 49 | 50 | div.sphinxsidebar { 51 | float: left; 52 | width: 230px; 53 | margin-left: -100%; 54 | font-size: 90%; 55 | word-wrap: break-word; 56 | overflow-wrap : break-word; 57 | } 58 | 59 | div.sphinxsidebar ul { 60 | list-style: none; 61 | } 62 | 63 | div.sphinxsidebar ul ul, 64 | div.sphinxsidebar ul.want-points { 65 | margin-left: 20px; 66 | list-style: square; 67 | } 68 | 69 | div.sphinxsidebar ul ul { 70 | margin-top: 0; 71 | margin-bottom: 0; 72 | } 73 | 74 | div.sphinxsidebar form { 75 | margin-top: 10px; 76 | } 77 | 78 | div.sphinxsidebar input { 79 | border: 1px solid #98dbcc; 80 | font-family: sans-serif; 81 | font-size: 1em; 82 | } 83 | 84 | div.sphinxsidebar #searchbox input[type="text"] { 85 | width: 170px; 86 | } 87 | 88 | img { 89 | border: 0; 90 | max-width: 100%; 91 | } 92 | 93 | /* -- search page ----------------------------------------------------------- */ 94 | 95 | ul.search { 96 | margin: 10px 0 0 20px; 97 | padding: 0; 98 | } 99 | 100 | ul.search li { 101 | padding: 5px 0 5px 20px; 102 | background-image: url(file.png); 103 | background-repeat: no-repeat; 104 | background-position: 0 7px; 105 | } 106 | 107 | ul.search li a { 108 | font-weight: bold; 109 | } 110 | 111 | ul.search li div.context { 112 | color: #888; 113 | margin: 2px 0 0 30px; 114 | text-align: left; 115 | } 116 | 117 | ul.keywordmatches li.goodmatch a { 118 | font-weight: bold; 119 | } 120 | 121 | /* -- index page ------------------------------------------------------------ */ 122 | 123 | table.contentstable { 124 | width: 90%; 125 | margin-left: auto; 126 | margin-right: auto; 127 | } 128 | 129 | table.contentstable p.biglink { 130 | line-height: 150%; 131 | } 132 | 133 | a.biglink { 134 | font-size: 1.3em; 135 | } 136 | 137 | span.linkdescr { 138 | font-style: italic; 139 | padding-top: 5px; 140 | font-size: 90%; 141 | } 142 | 143 | /* -- general index --------------------------------------------------------- */ 144 | 145 | table.indextable { 146 | width: 100%; 147 | } 148 | 149 | table.indextable td { 150 | text-align: left; 151 | vertical-align: top; 152 | } 153 | 154 | table.indextable ul { 155 | margin-top: 0; 156 | margin-bottom: 0; 157 | list-style-type: none; 158 | } 159 | 160 | table.indextable > tbody > tr > td > ul { 161 | padding-left: 0em; 162 | } 163 | 164 | table.indextable tr.pcap { 165 | height: 10px; 166 | } 167 | 168 | table.indextable tr.cap { 169 | margin-top: 10px; 170 | background-color: #f2f2f2; 171 | } 172 | 173 | img.toggler { 174 | margin-right: 3px; 175 | margin-top: 3px; 176 | cursor: pointer; 177 | } 178 | 179 | div.modindex-jumpbox { 180 | border-top: 1px solid #ddd; 181 | border-bottom: 1px solid #ddd; 182 | margin: 1em 0 1em 0; 183 | padding: 0.4em; 184 | } 185 | 186 | div.genindex-jumpbox { 187 | border-top: 1px solid #ddd; 188 | border-bottom: 1px solid #ddd; 189 | margin: 1em 0 1em 0; 190 | padding: 0.4em; 191 | } 192 | 193 | /* -- domain module index --------------------------------------------------- */ 194 | 195 | table.modindextable td { 196 | padding: 2px; 197 | border-collapse: collapse; 198 | } 199 | 200 | /* -- general body styles --------------------------------------------------- */ 201 | 202 | div.body p, div.body dd, div.body li, div.body blockquote { 203 | -moz-hyphens: auto; 204 | -ms-hyphens: auto; 205 | -webkit-hyphens: auto; 206 | hyphens: auto; 207 | } 208 | 209 | a.headerlink { 210 | visibility: hidden; 211 | } 212 | 213 | h1:hover > a.headerlink, 214 | h2:hover > a.headerlink, 215 | h3:hover > a.headerlink, 216 | h4:hover > a.headerlink, 217 | h5:hover > a.headerlink, 218 | h6:hover > a.headerlink, 219 | dt:hover > a.headerlink, 220 | caption:hover > a.headerlink, 221 | p.caption:hover > a.headerlink, 222 | div.code-block-caption:hover > a.headerlink { 223 | visibility: visible; 224 | } 225 | 226 | div.body p.caption { 227 | text-align: inherit; 228 | } 229 | 230 | div.body td { 231 | text-align: left; 232 | } 233 | 234 | .first { 235 | margin-top: 0 !important; 236 | } 237 | 238 | p.rubric { 239 | margin-top: 30px; 240 | font-weight: bold; 241 | } 242 | 243 | img.align-left, .figure.align-left, object.align-left { 244 | clear: left; 245 | float: left; 246 | margin-right: 1em; 247 | } 248 | 249 | img.align-right, .figure.align-right, object.align-right { 250 | clear: right; 251 | float: right; 252 | margin-left: 1em; 253 | } 254 | 255 | img.align-center, .figure.align-center, object.align-center { 256 | display: block; 257 | margin-left: auto; 258 | margin-right: auto; 259 | } 260 | 261 | .align-left { 262 | text-align: left; 263 | } 264 | 265 | .align-center { 266 | text-align: center; 267 | } 268 | 269 | .align-right { 270 | text-align: right; 271 | } 272 | 273 | /* -- sidebars -------------------------------------------------------------- */ 274 | 275 | div.sidebar { 276 | margin: 0 0 0.5em 1em; 277 | border: 1px solid #ddb; 278 | padding: 7px 7px 0 7px; 279 | background-color: #ffe; 280 | width: 40%; 281 | float: right; 282 | } 283 | 284 | p.sidebar-title { 285 | font-weight: bold; 286 | } 287 | 288 | /* -- topics ---------------------------------------------------------------- */ 289 | 290 | div.topic { 291 | border: 1px solid #ccc; 292 | padding: 7px 7px 0 7px; 293 | margin: 10px 0 10px 0; 294 | } 295 | 296 | p.topic-title { 297 | font-size: 1.1em; 298 | font-weight: bold; 299 | margin-top: 10px; 300 | } 301 | 302 | /* -- admonitions ----------------------------------------------------------- */ 303 | 304 | div.admonition { 305 | margin-top: 10px; 306 | margin-bottom: 10px; 307 | padding: 7px; 308 | } 309 | 310 | div.admonition dt { 311 | font-weight: bold; 312 | } 313 | 314 | div.admonition dl { 315 | margin-bottom: 0; 316 | } 317 | 318 | p.admonition-title { 319 | margin: 0px 10px 5px 0px; 320 | font-weight: bold; 321 | } 322 | 323 | div.body p.centered { 324 | text-align: center; 325 | margin-top: 25px; 326 | } 327 | 328 | /* -- tables ---------------------------------------------------------------- */ 329 | 330 | table.docutils { 331 | border: 0; 332 | border-collapse: collapse; 333 | } 334 | 335 | table caption span.caption-number { 336 | font-style: italic; 337 | } 338 | 339 | table caption span.caption-text { 340 | } 341 | 342 | table.docutils td, table.docutils th { 343 | padding: 1px 8px 1px 5px; 344 | border-top: 0; 345 | border-left: 0; 346 | border-right: 0; 347 | border-bottom: 1px solid #aaa; 348 | } 349 | 350 | table.footnote td, table.footnote th { 351 | border: 0 !important; 352 | } 353 | 354 | th { 355 | text-align: left; 356 | padding-right: 5px; 357 | } 358 | 359 | table.citation { 360 | border-left: solid 1px gray; 361 | margin-left: 1px; 362 | } 363 | 364 | table.citation td { 365 | border-bottom: none; 366 | } 367 | 368 | /* -- figures --------------------------------------------------------------- */ 369 | 370 | div.figure { 371 | margin: 0.5em; 372 | padding: 0.5em; 373 | } 374 | 375 | div.figure p.caption { 376 | padding: 0.3em; 377 | } 378 | 379 | div.figure p.caption span.caption-number { 380 | font-style: italic; 381 | } 382 | 383 | div.figure p.caption span.caption-text { 384 | } 385 | 386 | /* -- field list styles ----------------------------------------------------- */ 387 | 388 | table.field-list td, table.field-list th { 389 | border: 0 !important; 390 | } 391 | 392 | .field-list ul { 393 | margin: 0; 394 | padding-left: 1em; 395 | } 396 | 397 | .field-list p { 398 | margin: 0; 399 | } 400 | 401 | .field-name { 402 | -moz-hyphens: manual; 403 | -ms-hyphens: manual; 404 | -webkit-hyphens: manual; 405 | hyphens: manual; 406 | } 407 | 408 | /* -- other body styles ----------------------------------------------------- */ 409 | 410 | ol.arabic { 411 | list-style: decimal; 412 | } 413 | 414 | ol.loweralpha { 415 | list-style: lower-alpha; 416 | } 417 | 418 | ol.upperalpha { 419 | list-style: upper-alpha; 420 | } 421 | 422 | ol.lowerroman { 423 | list-style: lower-roman; 424 | } 425 | 426 | ol.upperroman { 427 | list-style: upper-roman; 428 | } 429 | 430 | dl { 431 | margin-bottom: 15px; 432 | } 433 | 434 | dd p { 435 | margin-top: 0px; 436 | } 437 | 438 | dd ul, dd table { 439 | margin-bottom: 10px; 440 | } 441 | 442 | dd { 443 | margin-top: 3px; 444 | margin-bottom: 10px; 445 | margin-left: 30px; 446 | } 447 | 448 | dt:target, .highlighted { 449 | background-color: #fbe54e; 450 | } 451 | 452 | dl.glossary dt { 453 | font-weight: bold; 454 | font-size: 1.1em; 455 | } 456 | 457 | .optional { 458 | font-size: 1.3em; 459 | } 460 | 461 | .sig-paren { 462 | font-size: larger; 463 | } 464 | 465 | .versionmodified { 466 | font-style: italic; 467 | } 468 | 469 | .system-message { 470 | background-color: #fda; 471 | padding: 5px; 472 | border: 3px solid red; 473 | } 474 | 475 | .footnote:target { 476 | background-color: #ffa; 477 | } 478 | 479 | .line-block { 480 | display: block; 481 | margin-top: 1em; 482 | margin-bottom: 1em; 483 | } 484 | 485 | .line-block .line-block { 486 | margin-top: 0; 487 | margin-bottom: 0; 488 | margin-left: 1.5em; 489 | } 490 | 491 | .guilabel, .menuselection { 492 | font-family: sans-serif; 493 | } 494 | 495 | .accelerator { 496 | text-decoration: underline; 497 | } 498 | 499 | .classifier { 500 | font-style: oblique; 501 | } 502 | 503 | abbr, acronym { 504 | border-bottom: dotted 1px; 505 | cursor: help; 506 | } 507 | 508 | /* -- code displays --------------------------------------------------------- */ 509 | 510 | pre { 511 | overflow: auto; 512 | overflow-y: hidden; /* fixes display issues on Chrome browsers */ 513 | } 514 | 515 | span.pre { 516 | -moz-hyphens: none; 517 | -ms-hyphens: none; 518 | -webkit-hyphens: none; 519 | hyphens: none; 520 | } 521 | 522 | td.linenos pre { 523 | padding: 5px 0px; 524 | border: 0; 525 | background-color: transparent; 526 | color: #aaa; 527 | } 528 | 529 | table.highlighttable { 530 | margin-left: 0.5em; 531 | } 532 | 533 | table.highlighttable td { 534 | padding: 0 0.5em 0 0.5em; 535 | } 536 | 537 | div.code-block-caption { 538 | padding: 2px 5px; 539 | font-size: small; 540 | } 541 | 542 | div.code-block-caption code { 543 | background-color: transparent; 544 | } 545 | 546 | div.code-block-caption + div > div.highlight > pre { 547 | margin-top: 0; 548 | } 549 | 550 | div.code-block-caption span.caption-number { 551 | padding: 0.1em 0.3em; 552 | font-style: italic; 553 | } 554 | 555 | div.code-block-caption span.caption-text { 556 | } 557 | 558 | div.literal-block-wrapper { 559 | padding: 1em 1em 0; 560 | } 561 | 562 | div.literal-block-wrapper div.highlight { 563 | margin: 0; 564 | } 565 | 566 | code.descname { 567 | background-color: transparent; 568 | font-weight: bold; 569 | font-size: 1.2em; 570 | } 571 | 572 | code.descclassname { 573 | background-color: transparent; 574 | } 575 | 576 | code.xref, a code { 577 | background-color: transparent; 578 | font-weight: bold; 579 | } 580 | 581 | h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { 582 | background-color: transparent; 583 | } 584 | 585 | .viewcode-link { 586 | float: right; 587 | } 588 | 589 | .viewcode-back { 590 | float: right; 591 | font-family: sans-serif; 592 | } 593 | 594 | div.viewcode-block:target { 595 | margin: -1px -10px; 596 | padding: 0 10px; 597 | } 598 | 599 | /* -- math display ---------------------------------------------------------- */ 600 | 601 | img.math { 602 | vertical-align: middle; 603 | } 604 | 605 | div.body div.math p { 606 | text-align: center; 607 | } 608 | 609 | span.eqno { 610 | float: right; 611 | } 612 | 613 | span.eqno a.headerlink { 614 | position: relative; 615 | left: 0px; 616 | z-index: 1; 617 | } 618 | 619 | div.math:hover a.headerlink { 620 | visibility: visible; 621 | } 622 | 623 | /* -- printout stylesheet --------------------------------------------------- */ 624 | 625 | @media print { 626 | div.document, 627 | div.documentwrapper, 628 | div.bodywrapper { 629 | margin: 0 !important; 630 | width: 100%; 631 | } 632 | 633 | div.sphinxsidebar, 634 | div.related, 635 | div.footer, 636 | #top-link { 637 | display: none; 638 | } 639 | } -------------------------------------------------------------------------------- /docs/_static/classic.css: -------------------------------------------------------------------------------- 1 | /* 2 | * classic.css_t 3 | * ~~~~~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- classic theme. 6 | * 7 | * :copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: sans-serif; 18 | font-size: 100%; 19 | background-color: #11303d; 20 | color: #000; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.document { 26 | background-color: #1c4e63; 27 | } 28 | 29 | div.documentwrapper { 30 | float: left; 31 | width: 100%; 32 | } 33 | 34 | div.bodywrapper { 35 | margin: 0 0 0 230px; 36 | } 37 | 38 | div.body { 39 | background-color: #ffffff; 40 | color: #000000; 41 | padding: 0 20px 30px 20px; 42 | } 43 | 44 | div.footer { 45 | color: #ffffff; 46 | width: 100%; 47 | padding: 9px 0 9px 0; 48 | text-align: center; 49 | font-size: 75%; 50 | } 51 | 52 | div.footer a { 53 | color: #ffffff; 54 | text-decoration: underline; 55 | } 56 | 57 | div.related { 58 | background-color: #133f52; 59 | line-height: 30px; 60 | color: #ffffff; 61 | } 62 | 63 | div.related a { 64 | color: #ffffff; 65 | } 66 | 67 | div.sphinxsidebar { 68 | } 69 | 70 | div.sphinxsidebar h3 { 71 | font-family: 'Trebuchet MS', sans-serif; 72 | color: #ffffff; 73 | font-size: 1.4em; 74 | font-weight: normal; 75 | margin: 0; 76 | padding: 0; 77 | } 78 | 79 | div.sphinxsidebar h3 a { 80 | color: #ffffff; 81 | } 82 | 83 | div.sphinxsidebar h4 { 84 | font-family: 'Trebuchet MS', sans-serif; 85 | color: #ffffff; 86 | font-size: 1.3em; 87 | font-weight: normal; 88 | margin: 5px 0 0 0; 89 | padding: 0; 90 | } 91 | 92 | div.sphinxsidebar p { 93 | color: #ffffff; 94 | } 95 | 96 | div.sphinxsidebar p.topless { 97 | margin: 5px 10px 10px 10px; 98 | } 99 | 100 | div.sphinxsidebar ul { 101 | margin: 10px; 102 | padding: 0; 103 | color: #ffffff; 104 | } 105 | 106 | div.sphinxsidebar a { 107 | color: #98dbcc; 108 | } 109 | 110 | div.sphinxsidebar input { 111 | border: 1px solid #98dbcc; 112 | font-family: sans-serif; 113 | font-size: 1em; 114 | } 115 | 116 | 117 | 118 | /* -- hyperlink styles ------------------------------------------------------ */ 119 | 120 | a { 121 | color: #355f7c; 122 | text-decoration: none; 123 | } 124 | 125 | a:visited { 126 | color: #355f7c; 127 | text-decoration: none; 128 | } 129 | 130 | a:hover { 131 | text-decoration: underline; 132 | } 133 | 134 | 135 | 136 | /* -- body styles ----------------------------------------------------------- */ 137 | 138 | div.body h1, 139 | div.body h2, 140 | div.body h3, 141 | div.body h4, 142 | div.body h5, 143 | div.body h6 { 144 | font-family: 'Trebuchet MS', sans-serif; 145 | background-color: #f2f2f2; 146 | font-weight: normal; 147 | color: #20435c; 148 | border-bottom: 1px solid #ccc; 149 | margin: 20px -20px 10px -20px; 150 | padding: 3px 0 3px 10px; 151 | } 152 | 153 | div.body h1 { margin-top: 0; font-size: 200%; } 154 | div.body h2 { font-size: 160%; } 155 | div.body h3 { font-size: 140%; } 156 | div.body h4 { font-size: 120%; } 157 | div.body h5 { font-size: 110%; } 158 | div.body h6 { font-size: 100%; } 159 | 160 | a.headerlink { 161 | color: #c60f0f; 162 | font-size: 0.8em; 163 | padding: 0 4px 0 4px; 164 | text-decoration: none; 165 | } 166 | 167 | a.headerlink:hover { 168 | background-color: #c60f0f; 169 | color: white; 170 | } 171 | 172 | div.body p, div.body dd, div.body li, div.body blockquote { 173 | text-align: justify; 174 | line-height: 130%; 175 | } 176 | 177 | div.admonition p.admonition-title + p { 178 | display: inline; 179 | } 180 | 181 | div.admonition p { 182 | margin-bottom: 5px; 183 | } 184 | 185 | div.admonition pre { 186 | margin-bottom: 5px; 187 | } 188 | 189 | div.admonition ul, div.admonition ol { 190 | margin-bottom: 5px; 191 | } 192 | 193 | div.note { 194 | background-color: #eee; 195 | border: 1px solid #ccc; 196 | } 197 | 198 | div.seealso { 199 | background-color: #ffc; 200 | border: 1px solid #ff6; 201 | } 202 | 203 | div.topic { 204 | background-color: #eee; 205 | } 206 | 207 | div.warning { 208 | background-color: #ffe4e4; 209 | border: 1px solid #f66; 210 | } 211 | 212 | p.admonition-title { 213 | display: inline; 214 | } 215 | 216 | p.admonition-title:after { 217 | content: ":"; 218 | } 219 | 220 | pre { 221 | padding: 5px; 222 | background-color: #eeffcc; 223 | color: #333333; 224 | line-height: 120%; 225 | border: 1px solid #ac9; 226 | border-left: none; 227 | border-right: none; 228 | } 229 | 230 | code { 231 | background-color: #ecf0f3; 232 | padding: 0 1px 0 1px; 233 | font-size: 0.95em; 234 | } 235 | 236 | th { 237 | background-color: #ede; 238 | } 239 | 240 | .warning code { 241 | background: #efc2c2; 242 | } 243 | 244 | .note code { 245 | background: #d6d6d6; 246 | } 247 | 248 | .viewcode-back { 249 | font-family: sans-serif; 250 | } 251 | 252 | div.viewcode-block:target { 253 | background-color: #f4debf; 254 | border-top: 1px solid #ac9; 255 | border-bottom: 1px solid #ac9; 256 | } 257 | 258 | div.code-block-caption { 259 | color: #efefef; 260 | background-color: #1c4e63; 261 | } -------------------------------------------------------------------------------- /docs/_static/comment-bright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertjoosten/maya-keyframe-reduction/3c781b9d9bf6f43a3df56fe2a325ab6a2be19cc0/docs/_static/comment-bright.png -------------------------------------------------------------------------------- /docs/_static/comment-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertjoosten/maya-keyframe-reduction/3c781b9d9bf6f43a3df56fe2a325ab6a2be19cc0/docs/_static/comment-close.png -------------------------------------------------------------------------------- /docs/_static/comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertjoosten/maya-keyframe-reduction/3c781b9d9bf6f43a3df56fe2a325ab6a2be19cc0/docs/_static/comment.png -------------------------------------------------------------------------------- /docs/_static/doctools.js: -------------------------------------------------------------------------------- 1 | /* 2 | * doctools.js 3 | * ~~~~~~~~~~~ 4 | * 5 | * Sphinx JavaScript utilities for all documentation. 6 | * 7 | * :copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | /** 13 | * select a different prefix for underscore 14 | */ 15 | $u = _.noConflict(); 16 | 17 | /** 18 | * make the code below compatible with browsers without 19 | * an installed firebug like debugger 20 | if (!window.console || !console.firebug) { 21 | var names = ["log", "debug", "info", "warn", "error", "assert", "dir", 22 | "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", 23 | "profile", "profileEnd"]; 24 | window.console = {}; 25 | for (var i = 0; i < names.length; ++i) 26 | window.console[names[i]] = function() {}; 27 | } 28 | */ 29 | 30 | /** 31 | * small helper function to urldecode strings 32 | */ 33 | jQuery.urldecode = function(x) { 34 | return decodeURIComponent(x).replace(/\+/g, ' '); 35 | }; 36 | 37 | /** 38 | * small helper function to urlencode strings 39 | */ 40 | jQuery.urlencode = encodeURIComponent; 41 | 42 | /** 43 | * This function returns the parsed url parameters of the 44 | * current request. Multiple values per key are supported, 45 | * it will always return arrays of strings for the value parts. 46 | */ 47 | jQuery.getQueryParameters = function(s) { 48 | if (typeof s == 'undefined') 49 | s = document.location.search; 50 | var parts = s.substr(s.indexOf('?') + 1).split('&'); 51 | var result = {}; 52 | for (var i = 0; i < parts.length; i++) { 53 | var tmp = parts[i].split('=', 2); 54 | var key = jQuery.urldecode(tmp[0]); 55 | var value = jQuery.urldecode(tmp[1]); 56 | if (key in result) 57 | result[key].push(value); 58 | else 59 | result[key] = [value]; 60 | } 61 | return result; 62 | }; 63 | 64 | /** 65 | * highlight a given string on a jquery object by wrapping it in 66 | * span elements with the given class name. 67 | */ 68 | jQuery.fn.highlightText = function(text, className) { 69 | function highlight(node) { 70 | if (node.nodeType == 3) { 71 | var val = node.nodeValue; 72 | var pos = val.toLowerCase().indexOf(text); 73 | if (pos >= 0 && !jQuery(node.parentNode).hasClass(className)) { 74 | var span = document.createElement("span"); 75 | span.className = className; 76 | span.appendChild(document.createTextNode(val.substr(pos, text.length))); 77 | node.parentNode.insertBefore(span, node.parentNode.insertBefore( 78 | document.createTextNode(val.substr(pos + text.length)), 79 | node.nextSibling)); 80 | node.nodeValue = val.substr(0, pos); 81 | } 82 | } 83 | else if (!jQuery(node).is("button, select, textarea")) { 84 | jQuery.each(node.childNodes, function() { 85 | highlight(this); 86 | }); 87 | } 88 | } 89 | return this.each(function() { 90 | highlight(this); 91 | }); 92 | }; 93 | 94 | /* 95 | * backward compatibility for jQuery.browser 96 | * This will be supported until firefox bug is fixed. 97 | */ 98 | if (!jQuery.browser) { 99 | jQuery.uaMatch = function(ua) { 100 | ua = ua.toLowerCase(); 101 | 102 | var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || 103 | /(webkit)[ \/]([\w.]+)/.exec(ua) || 104 | /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || 105 | /(msie) ([\w.]+)/.exec(ua) || 106 | ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || 107 | []; 108 | 109 | return { 110 | browser: match[ 1 ] || "", 111 | version: match[ 2 ] || "0" 112 | }; 113 | }; 114 | jQuery.browser = {}; 115 | jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; 116 | } 117 | 118 | /** 119 | * Small JavaScript module for the documentation. 120 | */ 121 | var Documentation = { 122 | 123 | init : function() { 124 | this.fixFirefoxAnchorBug(); 125 | this.highlightSearchWords(); 126 | this.initIndexTable(); 127 | 128 | }, 129 | 130 | /** 131 | * i18n support 132 | */ 133 | TRANSLATIONS : {}, 134 | PLURAL_EXPR : function(n) { return n == 1 ? 0 : 1; }, 135 | LOCALE : 'unknown', 136 | 137 | // gettext and ngettext don't access this so that the functions 138 | // can safely bound to a different name (_ = Documentation.gettext) 139 | gettext : function(string) { 140 | var translated = Documentation.TRANSLATIONS[string]; 141 | if (typeof translated == 'undefined') 142 | return string; 143 | return (typeof translated == 'string') ? translated : translated[0]; 144 | }, 145 | 146 | ngettext : function(singular, plural, n) { 147 | var translated = Documentation.TRANSLATIONS[singular]; 148 | if (typeof translated == 'undefined') 149 | return (n == 1) ? singular : plural; 150 | return translated[Documentation.PLURALEXPR(n)]; 151 | }, 152 | 153 | addTranslations : function(catalog) { 154 | for (var key in catalog.messages) 155 | this.TRANSLATIONS[key] = catalog.messages[key]; 156 | this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); 157 | this.LOCALE = catalog.locale; 158 | }, 159 | 160 | /** 161 | * add context elements like header anchor links 162 | */ 163 | addContextElements : function() { 164 | $('div[id] > :header:first').each(function() { 165 | $('\u00B6'). 166 | attr('href', '#' + this.id). 167 | attr('title', _('Permalink to this headline')). 168 | appendTo(this); 169 | }); 170 | $('dt[id]').each(function() { 171 | $('\u00B6'). 172 | attr('href', '#' + this.id). 173 | attr('title', _('Permalink to this definition')). 174 | appendTo(this); 175 | }); 176 | }, 177 | 178 | /** 179 | * workaround a firefox stupidity 180 | * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075 181 | */ 182 | fixFirefoxAnchorBug : function() { 183 | if (document.location.hash) 184 | window.setTimeout(function() { 185 | document.location.href += ''; 186 | }, 10); 187 | }, 188 | 189 | /** 190 | * highlight the search words provided in the url in the text 191 | */ 192 | highlightSearchWords : function() { 193 | var params = $.getQueryParameters(); 194 | var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; 195 | if (terms.length) { 196 | var body = $('div.body'); 197 | if (!body.length) { 198 | body = $('body'); 199 | } 200 | window.setTimeout(function() { 201 | $.each(terms, function() { 202 | body.highlightText(this.toLowerCase(), 'highlighted'); 203 | }); 204 | }, 10); 205 | $('') 207 | .appendTo($('#searchbox')); 208 | } 209 | }, 210 | 211 | /** 212 | * init the domain index toggle buttons 213 | */ 214 | initIndexTable : function() { 215 | var togglers = $('img.toggler').click(function() { 216 | var src = $(this).attr('src'); 217 | var idnum = $(this).attr('id').substr(7); 218 | $('tr.cg-' + idnum).toggle(); 219 | if (src.substr(-9) == 'minus.png') 220 | $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); 221 | else 222 | $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); 223 | }).css('display', ''); 224 | if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { 225 | togglers.click(); 226 | } 227 | }, 228 | 229 | /** 230 | * helper function to hide the search marks again 231 | */ 232 | hideSearchWords : function() { 233 | $('#searchbox .highlight-link').fadeOut(300); 234 | $('span.highlighted').removeClass('highlighted'); 235 | }, 236 | 237 | /** 238 | * make the url absolute 239 | */ 240 | makeURL : function(relativeURL) { 241 | return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; 242 | }, 243 | 244 | /** 245 | * get the current relative url 246 | */ 247 | getCurrentURL : function() { 248 | var path = document.location.pathname; 249 | var parts = path.split(/\//); 250 | $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { 251 | if (this == '..') 252 | parts.pop(); 253 | }); 254 | var url = parts.join('/'); 255 | return path.substring(url.lastIndexOf('/') + 1, path.length - 1); 256 | }, 257 | 258 | initOnKeyListeners: function() { 259 | $(document).keyup(function(event) { 260 | var activeElementType = document.activeElement.tagName; 261 | // don't navigate when in search box or textarea 262 | if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT') { 263 | switch (event.keyCode) { 264 | case 37: // left 265 | var prevHref = $('link[rel="prev"]').prop('href'); 266 | if (prevHref) { 267 | window.location.href = prevHref; 268 | return false; 269 | } 270 | case 39: // right 271 | var nextHref = $('link[rel="next"]').prop('href'); 272 | if (nextHref) { 273 | window.location.href = nextHref; 274 | return false; 275 | } 276 | } 277 | } 278 | }); 279 | } 280 | }; 281 | 282 | // quick alias for translations 283 | _ = Documentation.gettext; 284 | 285 | $(document).ready(function() { 286 | Documentation.init(); 287 | }); -------------------------------------------------------------------------------- /docs/_static/down-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertjoosten/maya-keyframe-reduction/3c781b9d9bf6f43a3df56fe2a325ab6a2be19cc0/docs/_static/down-pressed.png -------------------------------------------------------------------------------- /docs/_static/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertjoosten/maya-keyframe-reduction/3c781b9d9bf6f43a3df56fe2a325ab6a2be19cc0/docs/_static/down.png -------------------------------------------------------------------------------- /docs/_static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertjoosten/maya-keyframe-reduction/3c781b9d9bf6f43a3df56fe2a325ab6a2be19cc0/docs/_static/file.png -------------------------------------------------------------------------------- /docs/_static/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertjoosten/maya-keyframe-reduction/3c781b9d9bf6f43a3df56fe2a325ab6a2be19cc0/docs/_static/minus.png -------------------------------------------------------------------------------- /docs/_static/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertjoosten/maya-keyframe-reduction/3c781b9d9bf6f43a3df56fe2a325ab6a2be19cc0/docs/_static/plus.png -------------------------------------------------------------------------------- /docs/_static/pygments.css: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffffcc } 2 | .highlight { background: #eeffcc; } 3 | .highlight .c { color: #408090; font-style: italic } /* Comment */ 4 | .highlight .err { border: 1px solid #FF0000 } /* Error */ 5 | .highlight .k { color: #007020; font-weight: bold } /* Keyword */ 6 | .highlight .o { color: #666666 } /* Operator */ 7 | .highlight .ch { color: #408090; font-style: italic } /* Comment.Hashbang */ 8 | .highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */ 9 | .highlight .cp { color: #007020 } /* Comment.Preproc */ 10 | .highlight .cpf { color: #408090; font-style: italic } /* Comment.PreprocFile */ 11 | .highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */ 12 | .highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ 13 | .highlight .gd { color: #A00000 } /* Generic.Deleted */ 14 | .highlight .ge { font-style: italic } /* Generic.Emph */ 15 | .highlight .gr { color: #FF0000 } /* Generic.Error */ 16 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 17 | .highlight .gi { color: #00A000 } /* Generic.Inserted */ 18 | .highlight .go { color: #333333 } /* Generic.Output */ 19 | .highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ 20 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 21 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 22 | .highlight .gt { color: #0044DD } /* Generic.Traceback */ 23 | .highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ 24 | .highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ 25 | .highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ 26 | .highlight .kp { color: #007020 } /* Keyword.Pseudo */ 27 | .highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ 28 | .highlight .kt { color: #902000 } /* Keyword.Type */ 29 | .highlight .m { color: #208050 } /* Literal.Number */ 30 | .highlight .s { color: #4070a0 } /* Literal.String */ 31 | .highlight .na { color: #4070a0 } /* Name.Attribute */ 32 | .highlight .nb { color: #007020 } /* Name.Builtin */ 33 | .highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ 34 | .highlight .no { color: #60add5 } /* Name.Constant */ 35 | .highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ 36 | .highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ 37 | .highlight .ne { color: #007020 } /* Name.Exception */ 38 | .highlight .nf { color: #06287e } /* Name.Function */ 39 | .highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ 40 | .highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ 41 | .highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ 42 | .highlight .nv { color: #bb60d5 } /* Name.Variable */ 43 | .highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ 44 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 45 | .highlight .mb { color: #208050 } /* Literal.Number.Bin */ 46 | .highlight .mf { color: #208050 } /* Literal.Number.Float */ 47 | .highlight .mh { color: #208050 } /* Literal.Number.Hex */ 48 | .highlight .mi { color: #208050 } /* Literal.Number.Integer */ 49 | .highlight .mo { color: #208050 } /* Literal.Number.Oct */ 50 | .highlight .sa { color: #4070a0 } /* Literal.String.Affix */ 51 | .highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ 52 | .highlight .sc { color: #4070a0 } /* Literal.String.Char */ 53 | .highlight .dl { color: #4070a0 } /* Literal.String.Delimiter */ 54 | .highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ 55 | .highlight .s2 { color: #4070a0 } /* Literal.String.Double */ 56 | .highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ 57 | .highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ 58 | .highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ 59 | .highlight .sx { color: #c65d09 } /* Literal.String.Other */ 60 | .highlight .sr { color: #235388 } /* Literal.String.Regex */ 61 | .highlight .s1 { color: #4070a0 } /* Literal.String.Single */ 62 | .highlight .ss { color: #517918 } /* Literal.String.Symbol */ 63 | .highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ 64 | .highlight .fm { color: #06287e } /* Name.Function.Magic */ 65 | .highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ 66 | .highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ 67 | .highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ 68 | .highlight .vm { color: #bb60d5 } /* Name.Variable.Magic */ 69 | .highlight .il { color: #208050 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /docs/_static/sidebar.js: -------------------------------------------------------------------------------- 1 | /* 2 | * sidebar.js 3 | * ~~~~~~~~~~ 4 | * 5 | * This script makes the Sphinx sidebar collapsible. 6 | * 7 | * .sphinxsidebar contains .sphinxsidebarwrapper. This script adds 8 | * in .sphixsidebar, after .sphinxsidebarwrapper, the #sidebarbutton 9 | * used to collapse and expand the sidebar. 10 | * 11 | * When the sidebar is collapsed the .sphinxsidebarwrapper is hidden 12 | * and the width of the sidebar and the margin-left of the document 13 | * are decreased. When the sidebar is expanded the opposite happens. 14 | * This script saves a per-browser/per-session cookie used to 15 | * remember the position of the sidebar among the pages. 16 | * Once the browser is closed the cookie is deleted and the position 17 | * reset to the default (expanded). 18 | * 19 | * :copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS. 20 | * :license: BSD, see LICENSE for details. 21 | * 22 | */ 23 | 24 | $(function() { 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | // global elements used by the functions. 34 | // the 'sidebarbutton' element is defined as global after its 35 | // creation, in the add_sidebar_button function 36 | var bodywrapper = $('.bodywrapper'); 37 | var sidebar = $('.sphinxsidebar'); 38 | var sidebarwrapper = $('.sphinxsidebarwrapper'); 39 | 40 | // for some reason, the document has no sidebar; do not run into errors 41 | if (!sidebar.length) return; 42 | 43 | // original margin-left of the bodywrapper and width of the sidebar 44 | // with the sidebar expanded 45 | var bw_margin_expanded = bodywrapper.css('margin-left'); 46 | var ssb_width_expanded = sidebar.width(); 47 | 48 | // margin-left of the bodywrapper and width of the sidebar 49 | // with the sidebar collapsed 50 | var bw_margin_collapsed = '.8em'; 51 | var ssb_width_collapsed = '.8em'; 52 | 53 | // colors used by the current theme 54 | var dark_color = $('.related').css('background-color'); 55 | var light_color = $('.document').css('background-color'); 56 | 57 | function sidebar_is_collapsed() { 58 | return sidebarwrapper.is(':not(:visible)'); 59 | } 60 | 61 | function toggle_sidebar() { 62 | if (sidebar_is_collapsed()) 63 | expand_sidebar(); 64 | else 65 | collapse_sidebar(); 66 | } 67 | 68 | function collapse_sidebar() { 69 | sidebarwrapper.hide(); 70 | sidebar.css('width', ssb_width_collapsed); 71 | bodywrapper.css('margin-left', bw_margin_collapsed); 72 | sidebarbutton.css({ 73 | 'margin-left': '0', 74 | 'height': bodywrapper.height() 75 | }); 76 | sidebarbutton.find('span').text('»'); 77 | sidebarbutton.attr('title', _('Expand sidebar')); 78 | document.cookie = 'sidebar=collapsed'; 79 | } 80 | 81 | function expand_sidebar() { 82 | bodywrapper.css('margin-left', bw_margin_expanded); 83 | sidebar.css('width', ssb_width_expanded); 84 | sidebarwrapper.show(); 85 | sidebarbutton.css({ 86 | 'margin-left': ssb_width_expanded-12, 87 | 'height': bodywrapper.height() 88 | }); 89 | sidebarbutton.find('span').text('«'); 90 | sidebarbutton.attr('title', _('Collapse sidebar')); 91 | document.cookie = 'sidebar=expanded'; 92 | } 93 | 94 | function add_sidebar_button() { 95 | sidebarwrapper.css({ 96 | 'float': 'left', 97 | 'margin-right': '0', 98 | 'width': ssb_width_expanded - 28 99 | }); 100 | // create the button 101 | sidebar.append( 102 | '
«
' 103 | ); 104 | var sidebarbutton = $('#sidebarbutton'); 105 | light_color = sidebarbutton.css('background-color'); 106 | // find the height of the viewport to center the '<<' in the page 107 | var viewport_height; 108 | if (window.innerHeight) 109 | viewport_height = window.innerHeight; 110 | else 111 | viewport_height = $(window).height(); 112 | sidebarbutton.find('span').css({ 113 | 'display': 'block', 114 | 'margin-top': (viewport_height - sidebar.position().top - 20) / 2 115 | }); 116 | 117 | sidebarbutton.click(toggle_sidebar); 118 | sidebarbutton.attr('title', _('Collapse sidebar')); 119 | sidebarbutton.css({ 120 | 'color': '#FFFFFF', 121 | 'border-left': '1px solid ' + dark_color, 122 | 'font-size': '1.2em', 123 | 'cursor': 'pointer', 124 | 'height': bodywrapper.height(), 125 | 'padding-top': '1px', 126 | 'margin-left': ssb_width_expanded - 12 127 | }); 128 | 129 | sidebarbutton.hover( 130 | function () { 131 | $(this).css('background-color', dark_color); 132 | }, 133 | function () { 134 | $(this).css('background-color', light_color); 135 | } 136 | ); 137 | } 138 | 139 | function set_position_from_cookie() { 140 | if (!document.cookie) 141 | return; 142 | var items = document.cookie.split(';'); 143 | for(var k=0; k2;a== 12 | null&&(a=[]);if(y&&a.reduce===y)return e&&(c=b.bind(c,e)),f?a.reduce(c,d):a.reduce(c);j(a,function(a,b,i){f?d=c.call(e,d,a,b,i):(d=a,f=true)});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(z&&a.reduceRight===z)return e&&(c=b.bind(c,e)),f?a.reduceRight(c,d):a.reduceRight(c);var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect= 13 | function(a,c,b){var e;E(a,function(a,g,h){if(c.call(b,a,g,h))return e=a,true});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(A&&a.filter===A)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(B&&a.every===B)return a.every(c,b);j(a,function(a,g,h){if(!(e= 14 | e&&c.call(b,a,g,h)))return n});return e};var E=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(C&&a.some===C)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return n});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;return p&&a.indexOf===p?a.indexOf(c)!=-1:b=E(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck= 15 | function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a))return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a))return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;bd?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a, 17 | c,d){d||(d=b.identity);for(var e=0,f=a.length;e>1;d(a[g])=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1));return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e=0;d--)b=[a[d].apply(this,b)];return b[0]}}; 24 | b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=J||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.defaults=function(a){j(i.call(arguments, 25 | 1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return q(a,b,[])};b.isEmpty=function(a){if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=o||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)}; 26 | b.isArguments=function(a){return l.call(a)=="[object Arguments]"};if(!b.isArguments(arguments))b.isArguments=function(a){return!(!a||!b.has(a,"callee"))};b.isFunction=function(a){return l.call(a)=="[object Function]"};b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"}; 27 | b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,b){return I.call(a,b)};b.noConflict=function(){r._=G;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};b.mixin=function(a){j(b.functions(a), 28 | function(c){K(c,b[c]=a[c])})};var L=0;b.uniqueId=function(a){var b=L++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var t=/.^/,u=function(a){return a.replace(/\\\\/g,"\\").replace(/\\'/g,"'")};b.template=function(a,c){var d=b.templateSettings,d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.escape||t,function(a,b){return"',_.escape("+ 29 | u(b)+"),'"}).replace(d.interpolate||t,function(a,b){return"',"+u(b)+",'"}).replace(d.evaluate||t,function(a,b){return"');"+u(b).replace(/[\r\n\t]/g," ")+";__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');",e=new Function("obj","_",d);return c?e(c,b):function(a){return e.call(this,a,b)}};b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var v=function(a,c){return c?b(a).chain():a},K=function(a,c){m.prototype[a]= 30 | function(){var a=i.call(arguments);H.call(a,this._wrapped);return v(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return v(d,this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return v(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain= 31 | true;return this};m.prototype.value=function(){return this._wrapped}}).call(this); 32 | -------------------------------------------------------------------------------- /docs/_static/up-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertjoosten/maya-keyframe-reduction/3c781b9d9bf6f43a3df56fe2a325ab6a2be19cc0/docs/_static/up-pressed.png -------------------------------------------------------------------------------- /docs/_static/up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertjoosten/maya-keyframe-reduction/3c781b9d9bf6f43a3df56fe2a325ab6a2be19cc0/docs/_static/up.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | keyframeReduction package — keyframeReduction '' documentation 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 40 | 41 |
42 |
43 |
44 |
45 | 46 |
47 |

keyframeReduction package

48 |

Keyframe Reduction for Maya using least-squares method.

49 |
50 | _images/keyframeReductionExample.gif 51 |
52 |
53 |

Installation

54 |
    55 |
  • Extract the content of the .rar file anywhere on disk.
  • 56 |
  • Drag the keyframeReduction.mel file in Maya to permanently install the script.
  • 57 |
58 |
59 |
60 |

Usage

61 |

A button on the MiscTools shelf will be created that will allow easy access to 62 | the ui, this way the user doesn’t need to worry about any of the code. If user 63 | wishes to not use the shelf button the following commands can be used.

64 |

The ui responds to the current selection where it finds all of the suitable 65 | animation curves for reduction. You will be able to filter the animation 66 | curves based on the plug it is connected to. This will make it easier to 67 | target exactly the curves you want to reduce.

68 |

After an animation curve is reduced the reduction percentage will be printed 69 | to the console. This can give you an idea if you would like to increase or 70 | decrease the error rate to get the desired results.

71 |
72 |

UI

73 |
74 | _images/keyframeReductionUI.png 75 |
76 |

Display the UI with the following code.

77 |
import keyframeReduction.ui
 78 | keyframeReduction.ui.show()
 79 | 
80 |
81 |
82 |
83 |

Command Line

84 |

Use the KeyframeReduction class on individual animation curves.

85 |
from keyframeReduction import KeyframeReduction
 86 | obj = KeyframeReduction(pathToAnimCurve)
 87 | obj.reduce(error=0.1)
 88 | 
89 |
90 |
91 |
92 |

Options

93 |
    94 |
  • error: The maximum amount the reduced curve is allowed to deviate from the sampled curve.
  • 95 |
  • step: The step size to sample the curve, default is set to one.
  • 96 |
  • weightedTangents: Reduce curve using weighted tangents, using weighted tangents will result in less keyframes.
  • 97 |
  • tangentSplitAuto: Automatically split tangents.
  • 98 |
  • tangentSplitExisting: Use existing keyframes that have split tangents.
  • 99 |
  • tangentSplitAngleThreshold: Split tangents based on an angle threshold.
  • 100 |
  • tangentSplitAngleThresholdValue: Split tangent angle value.
  • 101 |
102 |
103 |
104 |
105 |

Note

106 |

The fitting algorithm is ported from Paper.js - The Swiss Army Knife of Vector Graphics Scripting. 107 | http://paperjs.org/

108 |
109 | 126 |
127 |

Submodules

128 | 135 |
136 |
137 | 138 | 139 |
140 |
141 |
142 | 180 |
181 |
182 | 194 | 198 | 199 | -------------------------------------------------------------------------------- /docs/keyframeReduction.classes.fit.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | keyframeReduction.classes.fit module — keyframeReduction '' documentation 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 40 | 41 |
42 |
43 |
44 |
45 | 46 |
47 |

keyframeReduction.classes.fit module

48 |
49 |
50 | class keyframeReduction.classes.fit.FitBezier(points, error=2.5, weightedTangents=True)
51 |

Bases: object

52 |

Ported from Paper.js - The Swiss Army Knife of Vector Graphics Scripting. 53 | http://paperjs.org/

54 |
55 |
56 | addCurve(pt1, tan1, tan2, pt2)
57 |
58 | 59 | 60 | 61 | 68 | 69 | 70 |
Parameters:
    62 |
  • pt1 (Vector2D) –
  • 63 |
  • tan1 (Vector2D) –
  • 64 |
  • tan2 (Vector2D) –
  • 65 |
  • pt2 (Vector2D) –
  • 66 |
67 |
71 |
72 | 73 |
74 |
75 | chordLengthParameterize(first, last)
76 |

Assign parameter values to digitized points using relative distances 77 | between points.

78 | 79 | 80 | 81 | 82 | 87 | 88 | 90 | 91 | 93 | 94 | 95 |
Parameters:
    83 |
  • first (int) –
  • 84 |
  • last (int) –
  • 85 |
86 |
Returns:

Chord length parameterization

89 |
Return type:

dict

92 |
96 |
97 | 98 |
99 |
100 | error
101 |
102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 |
Returns:Maximum error
Return type:int/float
111 |
112 | 113 |
114 |
115 | evaluate(degree, curve, t)
116 |

Evaluate a bezier curve at a particular parameter value.

117 | 118 | 119 | 120 | 121 | 127 | 128 | 130 | 131 | 133 | 134 | 135 |
Parameters:
    122 |
  • degree (int) –
  • 123 |
  • curve (list) –
  • 124 |
  • t (float) –
  • 125 |
126 |
Returns:

Point on curve

129 |
Return type:

Vector2D

132 |
136 |
137 | 138 |
139 |
140 | findMaxError(first, last, curve, u)
141 |

Find the maximum squared distance of digitized points to fitted 142 | curve.

143 | 144 | 145 | 146 | 147 | 154 | 155 | 157 | 158 | 160 | 161 | 162 |
Parameters:
    148 |
  • first (int) –
  • 149 |
  • last (int) –
  • 150 |
  • curve (list) –
  • 151 |
  • u (dict) –
  • 152 |
153 |
Returns:

Max distance and max index

156 |
Return type:

tuple

159 |
163 |
164 | 165 |
166 |
167 | findRoot(curve, point, u)
168 |

Use Newton-Raphson iteration to find better root.

169 | 170 | 171 | 172 | 173 | 179 | 180 | 182 | 183 | 185 | 186 | 187 |
Parameters:
    174 |
  • curve (list) –
  • 175 |
  • point (Vector2D) –
  • 176 |
  • u (Vector2D) –
  • 177 |
178 |
Returns:

New root point

181 |
Return type:

Vector2D

184 |
188 |
189 | 190 |
191 |
192 | fit()
193 |

Fit bezier curves to the points based on the provided maximum error 194 | value and the bezier weighted tangents.

195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 |
Returns:Keyframes
Return type:list
205 |
206 | 207 |
208 |
209 | fitCubic(first, last, tan1, tan2)
210 |

But a cubic bezier points between the provided first and last index 211 | and it’s tangents. Based in the weighted tangent settings the 212 | iterations will be adjusted to gain speed. If a curve can be matched 213 | the curve will be added to the keyframes, if not the curve will be 214 | split at the point of max error and the function will be called 215 | again.

216 | 217 | 218 | 219 | 220 | 227 | 228 | 229 |
Parameters:
    221 |
  • first (int) –
  • 222 |
  • last (int) –
  • 223 |
  • tan1 (Vector2D) –
  • 224 |
  • tan2 (Vector2D) –
  • 225 |
226 |
230 |
231 | 232 |
233 |
234 | generateBezier(first, last, uPrime, tan1, tan2)
235 |

Based on the weighted tangent setting either use a least-squares 236 | method to find Bezier controls points for a region or use Wu/Barsky 237 | heuristic.

238 | 239 | 240 | 241 | 242 | 250 | 251 | 252 |
Parameters:
    243 |
  • first (int) –
  • 244 |
  • last (int) –
  • 245 |
  • uPrime (dict) –
  • 246 |
  • tan1 (Vector2D) –
  • 247 |
  • tan2 (Vector2D) –
  • 248 |
249 |
253 |
254 | 255 |
256 |
257 | keyframes
258 |
259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 |
Returns:Keyframes
Return type:list
268 |
269 | 270 |
271 |
272 | points
273 |
274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 |
Returns:Vector2Ds
Return type:list
283 |
284 | 285 |
286 |
287 | reparameterize(first, last, u, curve)
288 |

Given set of points and their parameterization, try to find a better 289 | parameterization.

290 | 291 | 292 | 293 | 294 | 301 | 302 | 303 |
Parameters:
    295 |
  • first (int) –
  • 296 |
  • last (int) –
  • 297 |
  • u (dict) –
  • 298 |
  • curve (list) –
  • 299 |
300 |
304 |
305 | 306 |
307 |
308 | weightedTangents
309 |
310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 |
Returns:Weighted tangents
Return type:bool
319 |
320 | 321 |
322 | 323 |
324 | 325 | 326 |
327 |
328 |
329 | 350 |
351 |
352 | 364 | 368 | 369 | -------------------------------------------------------------------------------- /docs/keyframeReduction.classes.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | keyframeReduction.classes package — keyframeReduction '' documentation 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 40 | 41 |
42 |
43 |
44 |
45 | 46 |
47 |

keyframeReduction.classes package

48 | 59 |
60 | 61 | 62 |
63 |
64 |
65 | 94 |
95 |
96 | 108 | 112 | 113 | -------------------------------------------------------------------------------- /docs/keyframeReduction.classes.keyframe.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | keyframeReduction.classes.keyframe module — keyframeReduction '' documentation 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 40 | 41 |
42 |
43 |
44 |
45 | 46 |
47 |

keyframeReduction.classes.keyframe module

48 |
49 |
50 | class keyframeReduction.classes.keyframe.Keyframe(point, inHandle=None, outHandle=None)
51 |

Bases: object

52 |
53 |
54 | inHandle
55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
Returns:In handle
Return type:Vector2D
65 |
66 | 67 |
68 |
69 | outHandle
70 |
71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
Returns:Out handle
Return type:Vector2D
80 |
81 | 82 |
83 |
84 | point
85 |
86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 |
Returns:Vector2D
Return type:Vector2D
95 |
96 | 97 |
98 | 99 |
100 | 101 | 102 |
103 |
104 |
105 | 126 |
127 |
128 | 140 | 144 | 145 | -------------------------------------------------------------------------------- /docs/keyframeReduction.classes.keyframeReduction.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | keyframeReduction.classes.keyframeReduction module — keyframeReduction '' documentation 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 40 | 41 |
42 |
43 |
44 |
45 | 46 |
47 |

keyframeReduction.classes.keyframeReduction module

48 |
49 |
50 | class keyframeReduction.classes.keyframeReduction.KeyframeReduction(path)
51 |

Bases: object

52 |
53 |
54 | getFrames()
55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
Returns:List of keyframe frames
Return type:list
65 |
66 | 67 |
68 |
69 | getIndices()
70 |
71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
Returns:List of keyframe indices
Return type:list
80 |
81 | 82 |
83 |
84 | getValues(frames)
85 |
86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 |
Parameters:frames (list) – Frames to sample
Returns:List of values
Return type:list
97 |
98 | 99 |
100 |
101 | path
102 |
103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 |
Returns:Animation curve path
Return type:str
112 |
113 | 114 |
115 |
116 | reduce(error=1, step=1, weightedTangents=True, tangentSplitAuto=False, tangentSplitExisting=False, tangentSplitAngleThreshold=False, tangentSplitAngleThresholdValue=15.0)
117 |

Reduce the number of keyframes on the animation curve. Useful when 118 | you are working with baked curves.

119 | 120 | 121 | 122 | 123 | 133 | 134 | 136 | 137 | 139 | 140 | 141 |
Parameters:
    124 |
  • error (int/float) –
  • 125 |
  • step (int/float) –
  • 126 |
  • weightedTangents (bool) –
  • 127 |
  • tangentSplitAuto (bool) –
  • 128 |
  • tangentSplitExisting (bool) –
  • 129 |
  • tangentSplitAngleThreshold (bool) –
  • 130 |
  • tangentSplitAngleThresholdValue (int/float) –
  • 131 |
132 |
Returns:

Reduction rate

135 |
Return type:

float

138 |
142 |
143 | 144 |
145 |
146 | sample(start, end, step)
147 |

Sample the current animation curve based on the start and end frame, 148 | and the provided step size. Vector2Ds and angles will be returned.

149 | 150 | 151 | 152 | 153 | 159 | 160 | 162 | 163 | 165 | 166 | 167 |
Parameters:
    154 |
  • start (int) –
  • 155 |
  • end (int) –
  • 156 |
  • step (int/float) –
  • 157 |
158 |
Returns:

Sample points and angles

161 |
Return type:

list

164 |
168 |
169 | 170 |
171 | 172 |
173 | 174 | 175 |
176 |
177 |
178 | 199 |
200 |
201 | 213 | 217 | 218 | -------------------------------------------------------------------------------- /docs/keyframeReduction.classes.vector.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | keyframeReduction.classes.vector module — keyframeReduction '' documentation 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 40 | 41 |
42 |
43 |
44 |
45 | 46 |
47 |

keyframeReduction.classes.vector module

48 |
49 | 50 | 51 |
52 |
53 |
54 | 75 |
76 |
77 | 89 | 93 | 94 | -------------------------------------------------------------------------------- /docs/keyframeReduction.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | keyframeReduction package — keyframeReduction '' documentation 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 40 | 41 |
42 |
43 |
44 |
45 | 46 |
47 |

keyframeReduction package

48 |

Keyframe Reduction for Maya using least-squares method.

49 |
50 | _images/keyframeReductionExample.gif 51 |
52 |
53 |

Installation

54 |
    55 |
  • Extract the content of the .rar file anywhere on disk.
  • 56 |
  • Drag the keyframeReduction.mel file in Maya to permanently install the script.
  • 57 |
58 |
59 |
60 |

Usage

61 |

A button on the MiscTools shelf will be created that will allow easy access to 62 | the ui, this way the user doesn’t need to worry about any of the code. If user 63 | wishes to not use the shelf button the following commands can be used.

64 |

The ui responds to the current selection where it finds all of the suitable 65 | animation curves for reduction. You will be able to filter the animation 66 | curves based on the plug it is connected to. This will make it easier to 67 | target exactly the curves you want to reduce.

68 |

After an animation curve is reduced the reduction percentage will be printed 69 | to the console. This can give you an idea if you would like to increase or 70 | decrease the error rate to get the desired results.

71 |
72 |

UI

73 |
74 | _images/keyframeReductionUI.png 75 |
76 |

Display the UI with the following code.

77 |
import keyframeReduction.ui
 78 | keyframeReduction.ui.show()
 79 | 
80 |
81 |
82 |
83 |

Command Line

84 |

Use the KeyframeReduction class on individual animation curves.

85 |
from keyframeReduction import KeyframeReduction
 86 | obj = KeyframeReduction(pathToAnimCurve)
 87 | obj.reduce(error=0.1)
 88 | 
89 |
90 |
91 |
92 |

Options

93 |
    94 |
  • error: The maximum amount the reduced curve is allowed to deviate from the sampled curve.
  • 95 |
  • step: The step size to sample the curve, default is set to one.
  • 96 |
  • weightedTangents: Reduce curve using weighted tangents, using weighted tangents will result in less keyframes.
  • 97 |
  • tangentSplitAuto: Automatically split tangents.
  • 98 |
  • tangentSplitExisting: Use existing keyframes that have split tangents.
  • 99 |
  • tangentSplitAngleThreshold: Split tangents based on an angle threshold.
  • 100 |
  • tangentSplitAngleThresholdValue: Split tangent angle value.
  • 101 |
102 |
103 |
104 |
105 |

Note

106 |

The fitting algorithm is ported from Paper.js - The Swiss Army Knife of Vector Graphics Scripting. 107 | http://paperjs.org/

108 |
109 | 126 |
127 |

Submodules

128 | 135 |
136 |
137 | 138 | 139 |
140 |
141 |
142 | 180 |
181 |
182 | 194 | 198 | 199 | -------------------------------------------------------------------------------- /docs/keyframeReduction.install.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | keyframeReduction.install module — keyframeReduction '' documentation 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 40 | 41 |
42 |
43 |
44 |
45 | 46 |
47 |

keyframeReduction.install module

48 |
49 |
50 | keyframeReduction.install.shelf()
51 |

Add a new shelf in Maya with the tools that is provided in the SHELF_TOOL 52 | variable. If the tab exists it will be checked to see if the button is 53 | already added. If this is the case the previous button will be deleted and 54 | a new one will be created in its place.

55 |
56 | 57 |
58 | 59 | 60 |
61 |
62 |
63 | 84 |
85 |
86 | 98 | 102 | 103 | -------------------------------------------------------------------------------- /docs/keyframeReduction.utils.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | keyframeReduction.utils module — keyframeReduction '' documentation 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 40 | 41 |
42 |
43 |
44 |
45 | 46 |
47 |

keyframeReduction.utils module

48 |
49 |
50 | class keyframeReduction.utils.UndoChunkContext
51 |

Bases: object

52 |

The undo context is used to combine a chain of commands into one undo. 53 | Can be used in combination with the “with” statement.

54 |
55 |
with UndoChunkContext():
56 |
# code
57 |
58 |
59 | 60 |
61 |
62 | keyframeReduction.utils.filterAnimationCurves(animationCurves)
63 |

Loop all the animation curves an run the validation function to make sure 64 | the animation curves are suitable for reduction.

65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
Parameters:animationCurves (list) –
Returns:Animation curves
Return type:list
77 |
78 | 79 |
80 |
81 | keyframeReduction.utils.filterAnimationCurvesByPlug(animationCurves)
82 |
83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 |
Parameters:animationCurves (list) –
Returns:Filtered animation curves
Return type:dict
94 |
95 | 96 |
97 |
98 | keyframeReduction.utils.floatRange(start, end, step)
99 |
100 | 101 | 102 | 103 | 109 | 110 | 112 | 113 | 115 | 116 | 117 |
Parameters:
    104 |
  • start (int/float) –
  • 105 |
  • end (int/float) –
  • 106 |
  • step (int/float) –
  • 107 |
108 |
Returns:

Float range

111 |
Return type:

list

114 |
118 |
119 | 120 |
121 |
122 | keyframeReduction.utils.getAllAnimationCurves()
123 |
124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 |
Returns:All suitable animation curves in the current scene
Return type:list
133 |
134 | 135 |
136 |
137 | keyframeReduction.utils.getSelectionAnimationCurves()
138 |
139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 |
Returns:Selection animation curves
Return type:list
148 |
149 | 150 |
151 |
152 | keyframeReduction.utils.validateAnimationCurve(animationCurve)
153 |

Check if the parsed animation curve can be reduces. Set driven keyframes 154 | and referenced animation curves will be ignored.

155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 |
Parameters:animationCurve (str) –
Returns:Validation state of the animation curve
Return type:bool
167 |
168 | 169 |
170 | 171 | 172 |
173 |
174 |
175 | 196 |
197 |
198 | 210 | 214 | 215 | -------------------------------------------------------------------------------- /docs/modules.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | keyframeReduction — keyframeReduction '' documentation 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 40 | 41 |
42 |
43 |
44 |
45 | 46 |
47 |

keyframeReduction

48 |
49 | 75 |
76 |
77 | 78 | 79 |
80 |
81 |
82 | 103 |
104 |
105 | 117 | 121 | 122 | -------------------------------------------------------------------------------- /docs/objects.inv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertjoosten/maya-keyframe-reduction/3c781b9d9bf6f43a3df56fe2a325ab6a2be19cc0/docs/objects.inv -------------------------------------------------------------------------------- /docs/py-modindex.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | Python Module Index — keyframeReduction '' documentation 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 43 | 44 |
45 |
46 |
47 |
48 | 49 | 50 |

Python Module Index

51 | 52 |
53 | k 54 |
55 | 56 | 57 | 58 | 60 | 61 | 63 | 66 | 67 | 68 | 71 | 72 | 73 | 76 | 77 | 78 | 81 | 82 | 83 | 86 | 87 | 88 | 91 | 92 | 93 | 96 | 97 | 98 | 101 | 102 | 103 | 106 |
 
59 | k
64 | keyframeReduction 65 |
    69 | keyframeReduction.classes 70 |
    74 | keyframeReduction.classes.fit 75 |
    79 | keyframeReduction.classes.keyframe 80 |
    84 | keyframeReduction.classes.keyframeReduction 85 |
    89 | keyframeReduction.classes.vector 90 |
    94 | keyframeReduction.install 95 |
    99 | keyframeReduction.ui 100 |
    104 | keyframeReduction.utils 105 |
107 | 108 | 109 |
110 |
111 |
112 | 126 |
127 |
128 | 140 | 144 | 145 | -------------------------------------------------------------------------------- /docs/search.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | Search — keyframeReduction '' documentation 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 48 | 49 |
50 |
51 |
52 |
53 | 54 |

Search

55 |
56 | 57 |

58 | Please activate JavaScript to enable the search 59 | functionality. 60 |

61 |
62 |

63 | From here you can search these documents. Enter your search 64 | words into the box below and click "search". Note that the search 65 | function will automatically search for all of the words. Pages 66 | containing fewer words won't appear in the result list. 67 |

68 |
69 | 70 | 71 | 72 |
73 | 74 |
75 | 76 |
77 | 78 |
79 |
80 |
81 | 85 |
86 |
87 | 99 | 103 | 104 | -------------------------------------------------------------------------------- /docs/searchindex.js: -------------------------------------------------------------------------------- 1 | Search.setIndex({docnames:["index","keyframeReduction","keyframeReduction.classes","keyframeReduction.classes.fit","keyframeReduction.classes.keyframe","keyframeReduction.classes.keyframeReduction","keyframeReduction.classes.vector","keyframeReduction.install","keyframeReduction.ui","keyframeReduction.utils","modules"],envversion:52,filenames:["index.rst","keyframeReduction.rst","keyframeReduction.classes.rst","keyframeReduction.classes.fit.rst","keyframeReduction.classes.keyframe.rst","keyframeReduction.classes.keyframeReduction.rst","keyframeReduction.classes.vector.rst","keyframeReduction.install.rst","keyframeReduction.ui.rst","keyframeReduction.utils.rst","modules.rst"],objects:{"":{keyframeReduction:[1,0,0,"-"]},"keyframeReduction.classes":{fit:[3,0,0,"-"],keyframe:[4,0,0,"-"],keyframeReduction:[5,0,0,"-"],vector:[6,0,0,"-"]},"keyframeReduction.classes.fit":{FitBezier:[3,1,1,""]},"keyframeReduction.classes.fit.FitBezier":{addCurve:[3,2,1,""],chordLengthParameterize:[3,2,1,""],error:[3,3,1,""],evaluate:[3,2,1,""],findMaxError:[3,2,1,""],findRoot:[3,2,1,""],fit:[3,2,1,""],fitCubic:[3,2,1,""],generateBezier:[3,2,1,""],keyframes:[3,3,1,""],points:[3,3,1,""],reparameterize:[3,2,1,""],weightedTangents:[3,3,1,""]},"keyframeReduction.classes.keyframe":{Keyframe:[4,1,1,""]},"keyframeReduction.classes.keyframe.Keyframe":{inHandle:[4,3,1,""],outHandle:[4,3,1,""],point:[4,3,1,""]},"keyframeReduction.classes.keyframeReduction":{KeyframeReduction:[5,1,1,""]},"keyframeReduction.classes.keyframeReduction.KeyframeReduction":{getFrames:[5,2,1,""],getIndices:[5,2,1,""],getValues:[5,2,1,""],path:[5,3,1,""],reduce:[5,2,1,""],sample:[5,2,1,""]},"keyframeReduction.install":{shelf:[7,4,1,""]},"keyframeReduction.ui":{AnimationCurvesFilterWidget:[8,1,1,""],CheckBox:[8,1,1,""],Divider:[8,1,1,""],Header:[8,1,1,""],KeyframeReductionWidget:[8,1,1,""],LabelWidget:[8,1,1,""],ReductionSettingsWidget:[8,1,1,""],getIconPath:[8,4,1,""],mayaWindow:[8,4,1,""],show:[8,4,1,""]},"keyframeReduction.ui.AnimationCurvesFilterWidget":{addPlugCheckbox:[8,2,1,""],clear:[8,2,1,""],getAnimationCurves:[8,2,1,""],plugDefaults:[8,3,1,""],plugFilteredAnimationCurves:[8,3,1,""],plugMaxLength:[8,3,1,""],plugStates:[8,3,1,""],registerCallback:[8,2,1,""],removeCallback:[8,2,1,""],selectAllAnimationCurves:[8,2,1,""],selectionChanged:[8,2,1,""],staticMetaObject:[8,3,1,""],updatePlugState:[8,2,1,""]},"keyframeReduction.ui.CheckBox":{staticMetaObject:[8,3,1,""]},"keyframeReduction.ui.Divider":{staticMetaObject:[8,3,1,""]},"keyframeReduction.ui.Header":{staticMetaObject:[8,3,1,""]},"keyframeReduction.ui.KeyframeReductionWidget":{closeEvent:[8,2,1,""],reduce:[8,2,1,""],staticMetaObject:[8,3,1,""]},"keyframeReduction.ui.LabelWidget":{staticMetaObject:[8,3,1,""]},"keyframeReduction.ui.ReductionSettingsWidget":{getSettings:[8,2,1,""],reduceReleased:[8,3,1,""],staticMetaObject:[8,3,1,""]},"keyframeReduction.utils":{UndoChunkContext:[9,1,1,""],filterAnimationCurves:[9,4,1,""],filterAnimationCurvesByPlug:[9,4,1,""],floatRange:[9,4,1,""],getAllAnimationCurves:[9,4,1,""],getSelectionAnimationCurves:[9,4,1,""],validateAnimationCurve:[9,4,1,""]},keyframeReduction:{classes:[2,0,0,"-"],install:[7,0,0,"-"],ui:[8,0,0,"-"],utils:[9,0,0,"-"]}},objnames:{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","attribute","Python attribute"],"4":["py","function","Python function"]},objtypes:{"0":"py:module","1":"py:class","2":"py:method","3":"py:attribute","4":"py:function"},terms:{"case":[7,8],"class":[1,8,9,10],"default":[1,8],"float":[3,5,9],"function":[3,8,9],"import":1,"int":[3,5,8,9],"new":[3,7],"return":[3,4,5,8,9],"true":[3,5,8],"try":3,But:3,The:[1,3,8,9],Use:[1,3],Useful:5,abl:1,about:1,access:1,add:[7,8],addcurv:3,added:[3,7],addplugcheckbox:8,adjust:3,after:1,again:3,algorithm:1,all:[1,8,9],allow:1,alreadi:7,amount:1,angl:[1,5],ani:1,anim:[1,5,8,9],animationcurv:9,animationcurvesfilterwidget:8,anywher:1,apart:8,appear:8,arg:8,armi:[1,3],assign:3,automat:1,bake:5,barski:3,base:[1,3,4,5,8,9],better:3,between:[3,8],bezier:3,bool:[3,5,8,9],button:[1,7],call:[3,8],callback:8,can:[1,3,8,9],chain:9,chang:8,check:[7,8,9],checkbox:8,chord:3,chordlengthparameter:3,clear:8,closeev:8,code:[1,9],combin:9,command:[9,10],connect:1,consol:1,contain:8,content:1,context:9,control:3,creat:[1,7],cubic:3,current:[1,5,9],curv:[1,3,5,8,9],decreas:1,degre:3,delet:7,desir:1,determin:8,deviat:1,dict:[3,8,9],dictionari:8,digit:3,disk:1,displai:1,distanc:3,divid:8,doesn:1,drag:1,driven:[8,9],each:8,easi:1,easier:1,either:3,end:[5,9],ensur:8,error:[1,3,5],evalu:3,event:8,everytim:8,exactli:1,exist:[1,7],extract:1,fals:5,file:[1,8],filter:[1,8,9],filteranimationcurv:9,filteranimationcurvesbyplug:9,find:[1,3],findmaxerror:3,findroot:3,first:3,fit:[1,2],fitbezi:3,fitcub:3,floatrang:9,follow:1,found:8,frame:5,from:[1,3,8],gain:3,generatebezi:3,get:[1,8],getallanimationcurv:9,getanimationcurv:8,getfram:5,geticonpath:8,getindic:5,getselectionanimationcurv:9,getset:8,getvalu:5,give:1,given:3,graphic:[1,3],handl:4,has:8,have:[1,8],header:8,help:8,heurist:3,http:[1,3],icon:8,idea:1,ignor:9,increas:1,index:[0,3],indic:5,individu:1,inhandl:4,instal:10,item:8,iter:3,its:[7,8],kei:8,keyfram:[1,2,3,5,8,9],keyframereductionwidget:8,knife:[1,3],labelwidget:8,last:[3,8],layout:8,least:[1,3],length:[3,8],less:1,like:[1,8],line:10,list:[3,5,8,9],loop:[8,9],main:8,maintain:8,make:[1,9],match:3,max:[3,8],maximum:[1,3],maya:[1,7,8],mayawindow:8,mel:1,method:[1,3],misctool:1,modul:[0,1,2,10],most:8,name:8,need:1,newton:3,none:[4,8],note:10,number:5,obj:1,object:[3,4,5,8,9],one:[1,7,9],ones:8,onli:8,option:10,order:8,org:[1,3],out:4,outhandl:4,over:8,packag:10,page:0,paper:[1,3],paperj:[1,3],paramet:[3,5,8,9],parameter:3,parent:8,pars:9,part:8,particular:3,path:[5,8],pathtoanimcurv:1,percentag:1,perman:1,persist:8,place:7,plug:[1,8],plugdefault:8,plugfilteredanimationcurv:8,plugmaxlength:8,plugstat:8,point:[3,4,5],port:[1,3],present:8,previou:7,print:1,process:8,provid:[3,5,7,8],pt1:3,pt2:3,pysid:8,qcheckbox:8,qframe:8,qlabel:8,qmainwindow:8,qmetaobject:8,qtcore:8,qtgui:8,qwidget:8,rang:9,raphson:3,rar:1,rate:[1,5],reduc:[1,5,8,9],reducereleas:8,reduct:[1,5,9],reductionsettingswidget:8,referenc:[8,9],reflect:8,region:3,regist:8,registercallback:8,rel:3,remov:8,removecallback:8,reparameter:3,respond:1,result:1,root:3,run:9,sampl:[1,5],scene:[8,9],script:[1,3],scroll:8,search:0,see:[7,8],select:[1,8,9],selectallanimationcurv:8,selectionchang:8,set:[1,3,8,9],shelf:[1,7],shelf_tool:7,show:[1,8],signal:8,size:[1,5],spacer:8,speed:3,split:[1,3],squar:[1,3],start:[5,9],state:[8,9],statement:9,staticmetaobject:8,step:[1,5,9],str:[5,8,9],string:8,submodul:10,subpackag:10,suitabl:[1,8,9],sure:9,swiss:[1,3],tab:7,tan1:3,tan2:3,tangent:[1,3],tangentsplitanglethreshold:[1,5],tangentsplitanglethresholdvalu:[1,5],tangentsplitauto:[1,5],tangentsplitexist:[1,5],target:1,text:8,thei:8,them:8,thi:[1,7,8],those:8,threshold:1,time:8,tool:7,top:8,tupl:3,type:[3,4,5,8,9],undo:9,undochunkcontext:9,updat:8,updateplugst:8,uprim:3,usag:10,use:[1,3],used:[1,8,9],user:1,using:[1,3,8],util:[1,10],valid:9,validateanimationcurv:9,valu:[1,3,5,8],variabl:[7,8],vector2d:[3,4,5],vector:[1,2,3],wai:1,want:1,weight:[1,3],weightedtang:[1,3,5],when:5,where:1,which:8,widget:8,window:8,wish:1,work:5,worri:1,would:1,xbmlangpath:8,you:[1,5]},titles:["Welcome to keyframeReduction\u2019s documentation!","keyframeReduction package","keyframeReduction.classes package","keyframeReduction.classes.fit module","keyframeReduction.classes.keyframe module","keyframeReduction.classes.keyframeReduction module","keyframeReduction.classes.vector module","keyframeReduction.install module","keyframeReduction.ui module","keyframeReduction.utils module","keyframeReduction"],titleterms:{"class":[2,3,4,5,6],command:1,document:0,fit:3,indic:0,instal:[1,7],keyfram:4,keyframereduct:[0,1,2,3,4,5,6,7,8,9,10],line:1,modul:[3,4,5,6,7,8,9],note:1,option:1,packag:[1,2],submodul:[1,2],subpackag:1,tabl:0,usag:1,util:9,vector:6,welcom:0}}) -------------------------------------------------------------------------------- /icons/KR_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertjoosten/maya-keyframe-reduction/3c781b9d9bf6f43a3df56fe2a325ab6a2be19cc0/icons/KR_icon.png -------------------------------------------------------------------------------- /keyframeReduction.mel: -------------------------------------------------------------------------------- 1 | // *************************************************************************** 2 | // INSTALL MODULE 3 | // *************************************************************************** 4 | // 5 | // DESCRIPTION: 6 | // Add the module which is located in the same directory as the .mel file 7 | // into the first available MAYA_MODULE_PATH directory. It doesn't matter 8 | // where on disk the module lives as the script will make sure the path in 9 | // the .mod file links to the correct place. There are some basic sanity 10 | // checks in place to make sure the script doesn't error out. This includes 11 | // Maya version compatibility with the module. Permissions of the directories 12 | // files have to be written to and incorrectly formatted module files. 13 | // 14 | // REQUIRES: 15 | // - Template .mod file ( replace regular file path with ) 16 | // - Maya module ( scripts/icons/plug-ins etc directories ) 17 | // 18 | // USAGE: 19 | // source ; 20 | // 21 | // AUTHORS: 22 | // Robert Joosten - rwm.joosten@gmail.com 23 | // 24 | // LICENSE: 25 | // MIT 26 | // 27 | // VERSIONS: 28 | // 1.0.0 - Aug 23, 2018 - Initial Release. 29 | // 1.0.1 - Sep 17, 2018 - Allow for use in paths that contain a space. 30 | // 1.0.2 - Oct 03, 2018 - Fix path error installing on UNIX. 31 | // 32 | // *************************************************************************** 33 | 34 | 35 | proc string[] getModulePathsFromEnvironment() 36 | { 37 | // get all of the paths in the MAYA_MODULE_PATH variable and split them 38 | // into a string array. 39 | string $paths[]; 40 | string $separator = (`about -nt`) ? ";" : ":"; 41 | tokenize(getenv("MAYA_MODULE_PATH"), $separator, $paths); 42 | 43 | return $paths; 44 | } 45 | 46 | // *************************************************************************** 47 | 48 | 49 | proc string findModuleFileInPath(string $path) 50 | { 51 | // get the first module file found in the directory. 52 | string $files[] = `getFileList -folder $path -filespec "*.mod"`; 53 | return $files[0]; 54 | } 55 | 56 | 57 | proc string findModuleFileInModulePath(string $moduleFile, string $modulePaths[]) 58 | { 59 | // loop module paths 60 | for( $path in $modulePaths ) 61 | { 62 | // find moduleFile in path 63 | string $files[] = `getFileList -folder $path -filespec $moduleFile`; 64 | if ($files[0] != "") 65 | return $path; 66 | } 67 | 68 | // declare empty output 69 | string $empty; 70 | return $empty; 71 | } 72 | 73 | 74 | // *************************************************************************** 75 | 76 | 77 | proc string[] readModuleFile(string $path) 78 | { 79 | // read the module file and return each line as a string part of a string 80 | // array. 81 | string $lines[]; 82 | 83 | $fileId = `fopen $path "r"`; 84 | string $data = `fread $fileId $data`; 85 | tokenize($data, "\n", $lines); 86 | 87 | return $lines; 88 | } 89 | 90 | 91 | // *************************************************************************** 92 | 93 | 94 | proc string getConditionValue(string $line) 95 | { 96 | // get the condition value by splitting the string on the : character and 97 | // returning the second section of the partition. 98 | string $partitions[]; 99 | tokenize($line, ":", $partitions); 100 | 101 | return $partitions[1]; 102 | } 103 | 104 | 105 | // *************************************************************************** 106 | 107 | 108 | proc string[] parseModuleLine(string $line) 109 | { 110 | // declare output variable 111 | string $output[]; 112 | 113 | // declare default output 114 | string $mayaVersion; 115 | string $language; 116 | string $platform; 117 | 118 | // make sure its a module line by checking if the line starts with a + 119 | // or -. 120 | if (startsWith($line, "+") == 0 && startsWith($line, "-") == 0) 121 | return $output; 122 | 123 | // get partitions 124 | string $partitions[]; 125 | tokenize($line, " ", $partitions); 126 | 127 | // get partitions length 128 | $length = size($partitions); 129 | 130 | // loop partitions in reverse 131 | for( $i=1; $i<$length+1; ++$i ) 132 | { 133 | // get reverse index 134 | int $index = $length - $i; 135 | string $part = $partitions[$index]; 136 | 137 | // extract conditions 138 | if (startsWith($part, "MAYAVERSION") == 1) 139 | { 140 | $mayaVersion = getConditionValue($part); 141 | stringArrayRemoveAtIndex($index, $partitions); 142 | } 143 | else if (startsWith($part, "LANGUAGE") == 1) 144 | { 145 | $language = getConditionValue($part); 146 | stringArrayRemoveAtIndex($index, $partitions); 147 | } 148 | else if (startsWith($part, "PLATFORM") == 1) 149 | { 150 | $platform = getConditionValue($part); 151 | stringArrayRemoveAtIndex($index, $partitions); 152 | } 153 | } 154 | 155 | // set output 156 | $output[0] = $partitions[0]; 157 | $output[1] = $partitions[1]; 158 | $output[2] = $partitions[2]; 159 | $output[3] = $partitions[3]; 160 | $output[4] = $mayaVersion; 161 | $output[5] = $language; 162 | $output[6] = $platform; 163 | 164 | return $output; 165 | } 166 | 167 | 168 | // *************************************************************************** 169 | 170 | 171 | proc string[] findModuleMatch(string $lines[]) 172 | { 173 | // get maya data 174 | string $mayaVersion = `about -version`; 175 | string $language = `about -uiLanguage`; 176 | string $platform = `about -operatingSystem`; 177 | 178 | // process module to see if its suitable for the current version of Maya. 179 | for($line in $lines) 180 | { 181 | // get line data 182 | string $lineData[] = parseModuleLine($line); 183 | 184 | // validate line data 185 | if ($lineData[0] == "") 186 | continue; 187 | 188 | // validate maya version 189 | if ($lineData[4] != "" && $lineData[4] != $mayaVersion) 190 | continue; 191 | 192 | // validate language 193 | if ($lineData[5] != "" && $lineData[5] != $language) 194 | continue; 195 | 196 | // validate platform 197 | if ($lineData[6] != "" && $lineData[6] != $platform) 198 | continue; 199 | 200 | // return match 201 | return $lineData; 202 | } 203 | 204 | // declare empty output 205 | string $empty[]; 206 | return $empty; 207 | } 208 | 209 | 210 | // *************************************************************************** 211 | 212 | 213 | proc loadModuleExtended(string $moduleFile, string $path) 214 | { 215 | // get separator 216 | string $separator = "/"; 217 | 218 | // load module 219 | loadModule -load $moduleFile; 220 | 221 | // add scripts path to system path, for some reason after loading the 222 | // module we are still not able to import the scripts even though the 223 | // path is available in the MAYA_SCRIPT_PATH. 224 | string $scriptPath = $path + $separator + "scripts"; 225 | python("import sys; sys.path.append('" + $scriptPath + "')"); 226 | 227 | // see if a userSetup.py is present in the script folder. If this is the 228 | // case execute that file. 229 | string $userSetup = $scriptPath + $separator + "userSetup.py"; 230 | if (`filetest -e $userSetup` == 1) 231 | python("import __main__; execfile('" + $userSetup + "', __main__.__dict__ )"); 232 | } 233 | 234 | 235 | // *************************************************************************** 236 | 237 | 238 | global proc installModule() 239 | { 240 | // get path from mel file 241 | string $melWhatIs = `whatIs "installModule"`; 242 | string $melWhatIsPath = `match ": (.*)" $melWhatIs`; 243 | 244 | int $melSize = `size $melWhatIsPath`; 245 | string $melPath = `substring $melWhatIsPath 3 ($melSize-3)`; 246 | string $path = dirname($melPath); 247 | 248 | // get separator 249 | string $separator = "/"; 250 | 251 | // find installed module 252 | string $modules[] = `moduleInfo -listModules`; 253 | 254 | // get module paths from the environment and choose the first one if this 255 | // variable is not overwritten the .mod file will be written to this path. 256 | string $modulePaths[] = getModulePathsFromEnvironment(); 257 | string $modulePathDefault; 258 | 259 | // find module in current directory 260 | string $moduleFileBase = findModuleFileInPath($path); 261 | 262 | // validate module 263 | if ($moduleFileBase == "") 264 | error("\nNo template module file found in " + $path); 265 | 266 | // find full module path 267 | string $moduleFile = $path + $separator + $moduleFileBase; 268 | 269 | // read module content 270 | string $moduleContent[] = readModuleFile($moduleFile); 271 | 272 | // find module match 273 | string $moduleMatch[] = findModuleMatch($moduleContent); 274 | string $moduleName = $moduleMatch[1]; 275 | string $moduleVersion = $moduleMatch[2]; 276 | 277 | // validate module content 278 | if ($moduleName == "") 279 | error("\nNo valid module information found matching the current version/language/platform of Maya in " + $moduleFile); 280 | 281 | // check if module already exists 282 | if (stringArrayContains($moduleName, $modules) == 1) 283 | { 284 | string $moduleVersionExisting = `moduleInfo -moduleName $moduleName -version`; 285 | string $existingPath = `moduleInfo -moduleName $moduleName -path`; 286 | 287 | if ($moduleVersion == $moduleVersionExisting && $path ==$existingPath) 288 | { 289 | print("The " + $moduleName + " module, version " + $moduleVersion + " at " + $path + " is already installed!"); 290 | return; 291 | } 292 | 293 | // find existing module path 294 | $modulePathDefault = findModuleFileInModulePath($moduleFileBase, $modulePaths); 295 | } 296 | 297 | // get existing module path to the front of the list 298 | if ($modulePathDefault != "") 299 | { 300 | int $index = stringArrayFind($modulePathDefault, 0, $modulePaths); 301 | stringArrayRemoveAtIndex($index, $modulePaths); 302 | stringArrayInsertAtIndex(0, $modulePaths, $modulePathDefault); 303 | } 304 | 305 | // loop module paths 306 | for($modulePath in $modulePaths) 307 | { 308 | // only allow default path to be written if it exists 309 | if ($modulePathDefault != "" && $modulePathDefault != $modulePath) 310 | continue; 311 | 312 | // create dir in case it doesn't exist 313 | sysFile -makeDir $modulePath; 314 | 315 | // construct module path 316 | string $outputPath = $modulePath + $separator + $moduleFileBase; 317 | 318 | // write module file 319 | $fileId = `fopen $outputPath "w"`; 320 | 321 | // if the default path exists error if the file cannot be written 322 | // this means the module has been installed by hand in a directory 323 | // that is not writable without administrator permissions. The error 324 | // will ask the user to copy the script by hand. 325 | if ($fileId == 0 && $modulePathDefault == $modulePath) 326 | error("\nThe " + $moduleName + " module, version " + $moduleVersion + " could not be updated as the current path it is installed in is not writable.\nPlease overwrite the " + $moduleFileBase + " file in " + $modulePath + "."); 327 | else if ($fileId == 0) 328 | continue; 329 | 330 | // write lines 331 | print("Content:\n"); 332 | for($line in $moduleContent){ 333 | string $processedLine = `substitute "" $line $path`; 334 | print(" " + $processedLine + "\n"); 335 | fprint $fileId ($processedLine + "\n"); 336 | } 337 | 338 | // print output 339 | print("Output:\n"); 340 | print(" " + $outputPath + "\n"); 341 | 342 | // close output path 343 | fclose $fileId; 344 | 345 | // load module 346 | loadModuleExtended($outputPath, $path); 347 | return; 348 | } 349 | 350 | // throw error as module has not been installed 351 | error("\nThe " + $moduleName + " module, version " + $moduleVersion + " could not be installed as non of the module paths were writable."); 352 | } 353 | 354 | // process 355 | installModule(); 356 | -------------------------------------------------------------------------------- /keyframeReduction.mod: -------------------------------------------------------------------------------- 1 | + MAYAVERSION:2014 keyframeReduction 0.0.1 2 | + MAYAVERSION:2015 keyframeReduction 0.0.1 3 | + MAYAVERSION:2016 keyframeReduction 0.0.1 4 | + MAYAVERSION:2017 keyframeReduction 0.0.1 5 | + MAYAVERSION:2018 keyframeReduction 0.0.1 6 | + MAYAVERSION:2019 keyframeReduction 0.0.1 7 | + MAYAVERSION:2020 keyframeReduction 0.0.1 -------------------------------------------------------------------------------- /scripts/keyframeReduction/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Keyframe Reduction for Maya using least-squares method. 3 | 4 | .. figure:: /_images/keyframeReductionExample.gif 5 | :align: center 6 | 7 | Installation 8 | ============ 9 | * Extract the content of the .rar file anywhere on disk. 10 | * Drag the keyframeReduction.mel file in Maya to permanently install the script. 11 | 12 | Usage 13 | ===== 14 | A button on the MiscTools shelf will be created that will allow easy access to 15 | the ui, this way the user doesn't need to worry about any of the code. If user 16 | wishes to not use the shelf button the following commands can be used. 17 | 18 | The ui responds to the current selection where it finds all of the suitable 19 | animation curves for reduction. You will be able to filter the animation 20 | curves based on the plug it is connected to. This will make it easier to 21 | target exactly the curves you want to reduce. 22 | 23 | After an animation curve is reduced the reduction percentage will be printed 24 | to the console. This can give you an idea if you would like to increase or 25 | decrease the error rate to get the desired results. 26 | 27 | UI 28 | -- 29 | 30 | .. figure:: /_images/keyframeReductionUI.png 31 | :align: center 32 | 33 | 34 | Display the UI with the following code. 35 | :: 36 | import keyframeReduction.ui 37 | keyframeReduction.ui.show() 38 | 39 | Command Line 40 | ------------ 41 | 42 | Use the KeyframeReduction class on individual animation curves. 43 | :: 44 | from keyframeReduction import KeyframeReduction 45 | obj = KeyframeReduction(pathToAnimCurve) 46 | obj.reduce(error=0.1) 47 | 48 | Options 49 | ------- 50 | 51 | * **error**: The maximum amount the reduced curve is allowed to deviate from the sampled curve. 52 | * **step**: The step size to sample the curve, default is set to one. 53 | * **weightedTangents**: Reduce curve using weighted tangents, using weighted tangents will result in less keyframes. 54 | * **tangentSplitAuto**: Automatically split tangents. 55 | * **tangentSplitExisting**: Use existing keyframes that have split tangents. 56 | * **tangentSplitAngleThreshold**: Split tangents based on an angle threshold. 57 | * **tangentSplitAngleThresholdValue**: Split tangent angle value. 58 | 59 | Note 60 | ==== 61 | The fitting algorithm is ported from Paper.js - The Swiss Army Knife of Vector Graphics Scripting. 62 | http://paperjs.org/ 63 | """ 64 | from .classes.keyframeReduction import KeyframeReduction 65 | 66 | __author__ = "Robert Joosten" 67 | __version__ = "0.0.1" 68 | __email__ = "rwm.joosten@gmail.com" 69 | -------------------------------------------------------------------------------- /scripts/keyframeReduction/classes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertjoosten/maya-keyframe-reduction/3c781b9d9bf6f43a3df56fe2a325ab6a2be19cc0/scripts/keyframeReduction/classes/__init__.py -------------------------------------------------------------------------------- /scripts/keyframeReduction/classes/fit.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from .vector import Vector2D 4 | from .keyframe import Keyframe 5 | from ..utils import EPSILON 6 | 7 | 8 | class FitBezier(object): 9 | """ 10 | Ported from Paper.js - The Swiss Army Knife of Vector Graphics Scripting. 11 | http://paperjs.org/ 12 | """ 13 | def __init__(self, points, error=2.5, weightedTangents=True): 14 | """ 15 | :param list points: 16 | :param int/float error: 17 | :param bool weightedTangents: 18 | """ 19 | # variables 20 | self._keyframes = [] 21 | self._points = points 22 | self._error = error 23 | self._weightedTangents = weightedTangents 24 | 25 | def __repr__(self): 26 | return "< BezierFitter object | points: {} | error: {} | weighted-tangents: {} >".format( 27 | len(self.points), 28 | self.error, 29 | self.weightedTangents 30 | ) 31 | 32 | # ------------------------------------------------------------------------ 33 | 34 | @property 35 | def points(self): 36 | """ 37 | :return: Vector2Ds 38 | :rtype: list 39 | """ 40 | return self._points 41 | 42 | @property 43 | def error(self): 44 | """ 45 | :return: Maximum error 46 | :rtype: int/float 47 | """ 48 | return self._error 49 | 50 | @property 51 | def weightedTangents(self): 52 | """ 53 | :return: Weighted tangents 54 | :rtype: bool 55 | """ 56 | return self._weightedTangents 57 | 58 | # ------------------------------------------------------------------------ 59 | 60 | @property 61 | def keyframes(self): 62 | """ 63 | :return: Keyframes 64 | :rtype: list 65 | """ 66 | return self._keyframes 67 | 68 | @keyframes.setter 69 | def keyframes(self, s): 70 | """ 71 | :param list s: 72 | """ 73 | self._keyframes = s 74 | 75 | # ------------------------------------------------------------------------ 76 | 77 | def fit(self): 78 | """ 79 | Fit bezier curves to the points based on the provided maximum error 80 | value and the bezier weighted tangents. 81 | 82 | :return: Keyframes 83 | :rtype: list 84 | """ 85 | # get length of point 86 | length = len(self.points) 87 | 88 | # validate points 89 | if length == 0: 90 | return 91 | 92 | # add first point as a keyframe 93 | self.keyframes = [] 94 | self.keyframes.append(Keyframe(self.points[0])) 95 | 96 | # return keyframes if there is only 1 point 97 | if length == 1: 98 | return self.keyframes 99 | 100 | # get tangents 101 | tan1 = (self.points[1] - self.points[0]).normal() 102 | tan2 = (self.points[length - 2] - self.points[length - 1]).normal() 103 | 104 | # fit cubic 105 | self.fitCubic(0, length - 1, tan1, tan2) 106 | 107 | return self.keyframes 108 | 109 | def fitCubic(self, first, last, tan1, tan2): 110 | """ 111 | But a cubic bezier points between the provided first and last index 112 | and it's tangents. Based in the weighted tangent settings the 113 | iterations will be adjusted to gain speed. If a curve can be matched 114 | the curve will be added to the keyframes, if not the curve will be 115 | split at the point of max error and the function will be called 116 | again. 117 | 118 | :param int first: 119 | :param int last: 120 | :param Vector2D tan1: 121 | :param Vector2D tan2: 122 | """ 123 | # use heuristic if region only has two points in it 124 | if last - first == 1: 125 | # get points 126 | pt1 = self.points[first] 127 | pt2 = self.points[last] 128 | 129 | # get distance between points 130 | dist = pt1.distanceBetween(pt2) / 3 131 | 132 | # add curve 133 | self.addCurve(pt1, pt1 + tan1 * dist, pt2 + tan2 * dist, pt2) 134 | return 135 | 136 | # parameterize points, and attempt to fit curve 137 | uPrime = self.chordLengthParameterize(first, last) 138 | errorThreshold = max(self.error, self.error * 4) 139 | 140 | # get iterations, when weighted tangents is turned off the bezier 141 | # generator cannot be improved using multiple iterations. 142 | iterations = 4 if self.weightedTangents else 1 143 | 144 | # try 4 iterations 145 | for i in range(iterations): 146 | # generate curve 147 | curve = self.generateBezier(first, last, uPrime, tan1, tan2) 148 | 149 | # find max deviation of points to fitted curve 150 | maxError, maxIndex = self.findMaxError(first, last, curve, uPrime) 151 | 152 | # validate max error and add curve 153 | if maxError < self.error: 154 | self.addCurve(*curve) 155 | return 156 | 157 | # if error not too large, try reparameterization and iteration 158 | if maxError >= errorThreshold: 159 | break 160 | 161 | self.reparameterize(first, last, uPrime, curve) 162 | errorThreshold = maxError 163 | 164 | # fitting failed -- split at max error point and fit recursively 165 | tanCenter = (self.points[maxIndex - 1] - self.points[ 166 | maxIndex + 1]).normal() 167 | 168 | self.fitCubic(first, maxIndex, tan1, tanCenter) 169 | self.fitCubic(maxIndex, last, tanCenter * -1, tan2) 170 | 171 | # ------------------------------------------------------------------------ 172 | 173 | def addCurve(self, pt1, tan1, tan2, pt2): 174 | """ 175 | :param Vector2D pt1: 176 | :param Vector2D tan1: 177 | :param Vector2D tan2: 178 | :param Vector2D pt2: 179 | """ 180 | # update previous keyframe with out handle 181 | prev = self.keyframes[len(self.keyframes) - 1] 182 | prev.outHandle = Vector2D(tan1 - pt1) 183 | 184 | # create new keyframe 185 | keyframe = Keyframe(pt2, Vector2D(tan2 - pt2)) 186 | self.keyframes.append(keyframe) 187 | 188 | # ------------------------------------------------------------------------ 189 | 190 | def generateBezier(self, first, last, uPrime, tan1, tan2): 191 | """ 192 | Based on the weighted tangent setting either use a least-squares 193 | method to find Bezier controls points for a region or use Wu/Barsky 194 | heuristic. 195 | 196 | :param int first: 197 | :param int last: 198 | :param dict uPrime: 199 | :param Vector2D tan1: 200 | :param Vector2D tan2: 201 | """ 202 | # variables 203 | epsilon = EPSILON 204 | pt1 = self.points[first] 205 | pt2 = self.points[last] 206 | 207 | alpha1 = alpha2 = 0 208 | handle1 = handle2 = None 209 | 210 | # use least-squares method to find Bezier control points for region. 211 | # Only if weighted tangents are allowed. If this is not the case we 212 | # will fall back on Wu/Barsky heuristic. 213 | if self.weightedTangents: 214 | # create the C and X matrices 215 | C = [[0, 0], [0, 0]] 216 | X = [0, 0] 217 | 218 | for i in range(last - first + 1): 219 | u = uPrime[i] 220 | t = 1 - u 221 | b = 3 * u * t 222 | b0 = t * t * t 223 | b1 = b * t 224 | b2 = b * u 225 | b3 = u * u * u 226 | a1 = tan1 * b1 227 | a2 = tan2 * b2 228 | tmp = (self.points[first + i] - pt1 * (b0 + b1) - pt2 * ( 229 | b2 + b3)) 230 | C[0][0] += a1 * a1 231 | C[0][1] += a1 * a2 232 | C[1][0] = C[0][1] 233 | C[1][1] += a2 * a2 234 | X[0] += a1 * tmp 235 | X[1] += a2 * tmp 236 | 237 | # compute the determinants of C and X 238 | detC0C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1] 239 | if abs(detC0C1) > epsilon: 240 | # kramer's rule 241 | detC0X = C[0][0] * X[1] - C[1][0] * X[0] 242 | detXC1 = X[0] * C[1][1] - X[1] * C[0][1] 243 | 244 | # derive alpha values 245 | alpha1 = detXC1 / detC0C1 246 | alpha2 = detC0X / detC0C1 247 | else: 248 | # matrix is under-determined, try assuming alpha1 == alpha2 249 | c0 = C[0][0] + C[0][1] 250 | c1 = C[1][0] + C[1][1] 251 | 252 | if abs(c0) > epsilon: 253 | alpha1 = alpha2 = X[0] / c0 254 | elif abs(c1) > epsilon: 255 | alpha1 = alpha2 = X[1] / c1 256 | 257 | # if alpha negative, use the Wu/Barsky heuristic (see text) 258 | # (if alpha is 0, you get coincident control points that lead to 259 | # divide by zero in any subsequent NewtonRaphsonRootFind() call. 260 | segLength = pt2.distanceBetween(pt1) 261 | epsilon *= segLength 262 | if alpha1 < epsilon or alpha2 < epsilon: 263 | # fall back on standard (probably inaccurate) formula, 264 | # and subdivide further if needed. 265 | alpha1 = alpha2 = segLength / 3 266 | else: 267 | # check if the found control points are in the right order when 268 | # projected onto the line through pt1 and pt2. 269 | line = pt2 - pt1 270 | 271 | # control points 1 and 2 are positioned an alpha distance out 272 | # on the tangent vectors, left and right, respectively 273 | handle1 = tan1 * alpha1 274 | handle2 = tan2 * alpha2 275 | 276 | if ((handle1 * line) - (handle2 * line)) > segLength * segLength: 277 | # fall back to the Wu/Barsky heuristic above. 278 | alpha1 = alpha2 = segLength / 3; 279 | handle1 = handle2 = None 280 | 281 | # first and last control points of the Bezier curve are 282 | # positioned exactly at the first and last data points 283 | # Control points 1 and 2 are positioned an alpha distance out 284 | # on the tangent vectors, left and right, respectively 285 | return [ 286 | pt1, 287 | pt1 + (handle1 or (tan1 * alpha1)), 288 | pt2 + (handle2 or (tan2 * alpha2)), 289 | pt2 290 | ] 291 | 292 | # ------------------------------------------------------------------------ 293 | 294 | def reparameterize(self, first, last, u, curve): 295 | """ 296 | Given set of points and their parameterization, try to find a better 297 | parameterization. 298 | 299 | :param int first: 300 | :param int last: 301 | :param dict u: 302 | :param list curve: 303 | """ 304 | for i in range(first, last + 1): 305 | u[i - first] = self.findRoot(curve, self.points[i], u[i - first]) 306 | 307 | def findRoot(self, curve, point, u): 308 | """ 309 | Use Newton-Raphson iteration to find better root. 310 | 311 | :param list curve: 312 | :param Vector2D point: 313 | :param Vector2D u: 314 | :return: New root point 315 | :rtype: Vector2D 316 | """ 317 | # generate control vertices for Q' 318 | curve1 = [(curve[i + 1] - curve[i]) * 3 for i in range(3)] 319 | 320 | # generate control vertices for Q'' 321 | curve2 = [(curve1[i + 1] - curve1[i]) * 2 for i in range(2)] 322 | 323 | # compute Q(u), Q'(u) and Q''(u) 324 | pt = self.evaluate(3, curve, u) 325 | pt1 = self.evaluate(2, curve1, u) 326 | pt2 = self.evaluate(1, curve2, u) 327 | diff = pt - point 328 | df = (pt1 * pt1) + (diff * pt2) 329 | 330 | # compute f(u) / f'(u) 331 | if abs(df) < EPSILON: 332 | return u 333 | 334 | # u = u - f(u) / f'(u) 335 | return u - (diff * pt1) / df 336 | 337 | def evaluate(self, degree, curve, t): 338 | """ 339 | Evaluate a bezier curve at a particular parameter value. 340 | 341 | :param int degree: 342 | :param list curve: 343 | :param float t: 344 | :return: Point on curve 345 | :rtype: Vector2D 346 | """ 347 | # copy array 348 | tmp = curve[:] 349 | 350 | # triangle computation 351 | for i in range(1, degree + 1): 352 | for j in range(degree - i + 1): 353 | tmp[j] = (tmp[j] * (1 - t)) + (tmp[j + 1] * t) 354 | 355 | return tmp[0] 356 | 357 | def chordLengthParameterize(self, first, last): 358 | """ 359 | Assign parameter values to digitized points using relative distances 360 | between points. 361 | 362 | :param int first: 363 | :param int last: 364 | :return: Chord length parameterization 365 | :rtype: dict 366 | """ 367 | u = {0: 0} 368 | 369 | for i in range(first + 1, last + 1): 370 | u[i - first] = u[i - first - 1] + self.points[i].distanceBetween( 371 | self.points[i - 1]) 372 | 373 | m = last - first 374 | for i in range(1, m + 1): 375 | u[i] /= u[m] 376 | 377 | return u 378 | 379 | def findMaxError(self, first, last, curve, u): 380 | """ 381 | Find the maximum squared distance of digitized points to fitted 382 | curve. 383 | 384 | :param int first: 385 | :param int last: 386 | :param list curve: 387 | :param dict u: 388 | :return: Max distance and max index 389 | :rtype: tuple 390 | """ 391 | maxDist = 0 392 | maxIndex = math.floor((last - first + 1) / 2) 393 | 394 | for i in range(first + 1, last): 395 | P = self.evaluate(3, curve, u[i - first]) 396 | dist = (P - self.points[i]).length() 397 | 398 | if dist >= maxDist: 399 | maxDist = dist 400 | maxIndex = i 401 | 402 | return maxDist, maxIndex 403 | -------------------------------------------------------------------------------- /scripts/keyframeReduction/classes/keyframe.py: -------------------------------------------------------------------------------- 1 | class Keyframe(object): 2 | def __init__(self, point, inHandle=None, outHandle=None): 3 | """ 4 | :param Vector2D point: 5 | :param Vector2D inHandle: 6 | :param Vector2D outHandle: 7 | """ 8 | self._point = point 9 | self._inHandle = inHandle 10 | self._outHandle = outHandle 11 | 12 | def __repr__(self): 13 | return "< Keyframe object | point: {} | in-handle: {} | out-handle: {} >".format( 14 | str(self.point), 15 | str(self.inHandle), 16 | str(self.outHandle) 17 | ) 18 | 19 | # ------------------------------------------------------------------------ 20 | 21 | @property 22 | def point(self): 23 | """ 24 | :return: Vector2D 25 | :rtype: Vector2D 26 | """ 27 | return self._point 28 | 29 | @point.setter 30 | def point(self, p): 31 | """ 32 | :param Vector2D p: 33 | """ 34 | self._point = p 35 | 36 | # ------------------------------------------------------------------------ 37 | 38 | @property 39 | def inHandle(self): 40 | """ 41 | :return: In handle 42 | :rtype: Vector2D 43 | """ 44 | return self._inHandle 45 | 46 | @inHandle.setter 47 | def inHandle(self, p): 48 | """ 49 | :param Vector2D p: 50 | """ 51 | self._inHandle = p 52 | 53 | @property 54 | def outHandle(self): 55 | """ 56 | :return: Out handle 57 | :rtype: Vector2D 58 | """ 59 | return self._outHandle 60 | 61 | @outHandle.setter 62 | def outHandle(self, p): 63 | """ 64 | :param Vector2D p: 65 | """ 66 | self._outHandle = p 67 | -------------------------------------------------------------------------------- /scripts/keyframeReduction/classes/keyframeReduction.py: -------------------------------------------------------------------------------- 1 | import math 2 | import time 3 | from maya import cmds 4 | 5 | from .vector import Vector2D 6 | from .fit import FitBezier 7 | from ..utils import floatRange, THRESHOLD 8 | 9 | 10 | class KeyframeReduction(object): 11 | def __init__(self, path): 12 | """ 13 | :param str path: 14 | """ 15 | self._path = path 16 | 17 | def __repr__(self): 18 | return "< KeyframeReduction object | path: {} >".format(self.path) 19 | 20 | # ------------------------------------------------------------------------ 21 | 22 | @property 23 | def path(self): 24 | """ 25 | :return: Animation curve path 26 | :rtype: str 27 | """ 28 | return self._path 29 | 30 | # ------------------------------------------------------------------------ 31 | 32 | def getIndices(self): 33 | """ 34 | :return: List of keyframe indices 35 | :rtype: list 36 | """ 37 | return cmds.keyframe(self.path, query=True, indexValue=True) 38 | 39 | def getFrames(self): 40 | """ 41 | :return: List of keyframe frames 42 | :rtype: list 43 | """ 44 | indices = self.getIndices() 45 | return cmds.keyframe( 46 | self.path, 47 | query=True, 48 | index=(indices[0], indices[-1]) 49 | ) or [] 50 | 51 | def getValues(self, frames): 52 | """ 53 | :param list frames: Frames to sample 54 | :return: List of values 55 | :rtype: list 56 | """ 57 | return [ 58 | cmds.keyframe(self.path, query=True, eval=True, time=(frame,))[0] 59 | for frame in frames 60 | ] 61 | 62 | # ------------------------------------------------------------------------ 63 | 64 | def _findTangentSplitAuto(self, angles): 65 | """ 66 | The automatic tangent split will take the average of all values and 67 | the average of just the minimum and maximum value and remaps that on 68 | a logarithmic scale, this will give a predicted split angle value. 69 | All angles will be processed to see if they fall in or outside that 70 | threshold. 71 | 72 | :param list angles: 73 | :return: Split indices 74 | :rtype: list 75 | """ 76 | # get angles from points 77 | splits = [] 78 | 79 | # get average variables 80 | minAngle = min(angles) or 0.00001 81 | maxAngle = max(angles) 82 | average = (minAngle + maxAngle) * 0.5 83 | mean = sum(angles) / len(angles) * 0.5 84 | 85 | # get value at which to split 86 | threshold = (math.log(average) - math.log(mean)) / (math.log(maxAngle) - math.log(minAngle)) * average 87 | 88 | # if curve is relatively smooth don't split 89 | if mean * 10 > average: 90 | return [] 91 | 92 | # split based on angles 93 | for i, angle in enumerate(angles): 94 | if angle > threshold: 95 | splits.append(i + 1) 96 | 97 | return splits 98 | 99 | def _findTangentSplitExisting(self, frames, start, end, step): 100 | """ 101 | Loop existing frames and see if any keyframes contain tangents that 102 | are not unified. If this is the case the index of the closest sampled 103 | point will be returned. 104 | 105 | :param list frames: 106 | :param int start: 107 | :param int end: 108 | :param int/float step: 109 | :return: Split indices 110 | :rtype: list 111 | """ 112 | splits = [] 113 | inTangentType = "step" 114 | outTangentType = "stepnext" 115 | 116 | inAngles = cmds.keyTangent(self.path, query=True, time=(start, end), inAngle=True) 117 | outAngles = cmds.keyTangent(self.path, query=True, time=(start, end), outAngle=True) 118 | inTangentTypes = cmds.keyTangent(self.path, query=True, time=(start, end), inTangentType=True) 119 | outTangentTypes = cmds.keyTangent(self.path, query=True, time=(start, end), outTangentType=True) 120 | 121 | iterator = zip(frames, inAngles, outAngles, inTangentTypes, outTangentTypes) 122 | for frame, inAngle, outAngle, inType, outType in iterator: 123 | # get closest index 124 | index = int((frame - start) / step) 125 | 126 | # validate split 127 | if abs(inAngle - outAngle) > THRESHOLD: 128 | splits.append(index) 129 | elif inType == inTangentType: 130 | splits.append(index) 131 | elif outType == outTangentType: 132 | splits.append(index) 133 | 134 | return splits 135 | 136 | def _findTangentSplitThreshold(self, angles, threshold): 137 | """ 138 | The threshold tangent split will process all angles and check if that 139 | angle falls in or outside of user provided threshold. 140 | 141 | :param list angles: 142 | :param int/float threshold: 143 | :return: Split indices 144 | :rtype: list 145 | """ 146 | splits = [] 147 | 148 | # split based on angles 149 | for i, angle in enumerate(angles): 150 | if angle > threshold: 151 | splits.append(i + 1) 152 | 153 | return splits 154 | 155 | # ------------------------------------------------------------------------ 156 | 157 | def _splitPoints(self, points, split): 158 | """ 159 | Split provided points list based on the split indices provided. The 160 | lists will have a duplicate end and start points relating to each 161 | other. 162 | 163 | :param list points: 164 | :param list split: 165 | :return: Split points 166 | :rtype: list 167 | """ 168 | # validate split 169 | if not split: 170 | return [points] 171 | 172 | # complete split with adding start and end frames 173 | if split[0] != 0: 174 | split.insert(0, 0) 175 | 176 | if split[-1] != len(points): 177 | split.append(len(points)) 178 | 179 | # make sure split is sorted and doesn't contain any duplicates 180 | split = list(set(split)) 181 | split.sort() 182 | 183 | # split range for looping 184 | splitA = split[:-1] 185 | splitB = split[1:] 186 | 187 | # get lists 188 | return [points[a:b + 1] for a, b in zip(splitA, splitB)] 189 | 190 | # ------------------------------------------------------------------------ 191 | 192 | def _removeKeys(self, frames, start): 193 | """ 194 | Remove all keys appart from the start key and move this start key to 195 | the start frame. This needs to be done to make sure the start frame 196 | is on an even frame. 197 | 198 | :param list frames: 199 | :param int start: 200 | """ 201 | # remove keys 202 | cmds.cutKey(self.path, time=(frames[0] + 0.01, frames[-1]), option="keys") 203 | 204 | # move first key to start position in case start position is not on 205 | # an even frame. 206 | cmds.keyframe(self.path, edit=True, index=(0,), timeChange=start) 207 | 208 | def _addKeys(self, keyframes, weightedTangents): 209 | """ 210 | Loop all the keyframes and create a keyframe and set the correct 211 | tangent information. 212 | 213 | :param list keyframes: 214 | :param bool weightedTangents: 215 | """ 216 | # variables 217 | inAngle = Vector2D(-1, 0) 218 | outAngle = Vector2D(1, 0) 219 | 220 | # loop keyframes 221 | for keyframe in keyframes: 222 | # create keyframe point 223 | cmds.setKeyframe(self.path, time=keyframe.point.x, value=keyframe.point.y) 224 | 225 | # set keyframe tangent variable 226 | arguments = {"edit": True, "absolute": True, "time": (keyframe.point.x,)} 227 | 228 | # set weighted tangents 229 | cmds.keyTangent(self.path, weightedTangents=weightedTangents, **arguments) 230 | 231 | # unlock tangents if either in our out handle is not defined. 232 | if not keyframe.inHandle or not keyframe.outHandle: 233 | cmds.keyTangent(self.path, lock=False, **arguments) 234 | 235 | # add in tangent to arguments 236 | if keyframe.inHandle: 237 | arguments["inAngle"] = math.degrees(inAngle.signedAngle(keyframe.inHandle)) 238 | arguments["inWeight"] = keyframe.inHandle.length() 239 | 240 | # add out tangent to arguments 241 | if keyframe.outHandle: 242 | arguments["outAngle"] = math.degrees(outAngle.signedAngle(keyframe.outHandle)) 243 | arguments["outWeight"] = keyframe.outHandle.length() 244 | 245 | # set keyframe tangent 246 | cmds.keyTangent(self.path, **arguments) 247 | 248 | # ------------------------------------------------------------------------ 249 | 250 | def sample(self, start, end, step): 251 | """ 252 | Sample the current animation curve based on the start and end frame, 253 | and the provided step size. Vector2Ds and angles will be returned. 254 | 255 | :param int start: 256 | :param int end: 257 | :param int/float step: 258 | :return: Sample points and angles 259 | :rtype: list 260 | """ 261 | # get frames and values 262 | frames = floatRange(start, end, step) 263 | values = self.getValues(frames) 264 | 265 | # get points and angles 266 | angles = [] 267 | points = [Vector2D(*coord) for coord in zip(frames, values)] 268 | 269 | for i in range(1, len(points) - 2): 270 | v1 = points[i - 1] - points[i] 271 | v2 = points[i + 1] - points[i] 272 | angles.append(math.degrees(math.pi - v1.angle(v2))) 273 | 274 | return [points, angles] 275 | 276 | # ------------------------------------------------------------------------ 277 | 278 | def reduce( 279 | self, 280 | error=1, 281 | step=1, 282 | weightedTangents=True, 283 | tangentSplitAuto=False, 284 | tangentSplitExisting=False, 285 | tangentSplitAngleThreshold=False, 286 | tangentSplitAngleThresholdValue=15.0, 287 | ): 288 | """ 289 | Reduce the number of keyframes on the animation curve. Useful when 290 | you are working with baked curves. 291 | 292 | :param int/float error: 293 | :param int/float step: 294 | :param bool weightedTangents: 295 | :param bool tangentSplitAuto: 296 | :param bool tangentSplitExisting: 297 | :param bool tangentSplitAngleThreshold: 298 | :param int/float tangentSplitAngleThresholdValue: 299 | :return: Reduction rate 300 | :rtype: float 301 | """ 302 | # get existing frames 303 | t = time.time() 304 | original = self.getFrames() 305 | 306 | # get start and end frames 307 | start = int(math.floor(original[0])) 308 | end = int(math.ceil(original[-1])) + 1 309 | 310 | # get sample frames and values 311 | points, angles = self.sample(start, end, step) 312 | 313 | # get split indices 314 | split = [] 315 | 316 | if tangentSplitAuto: 317 | split.extend(self._findTangentSplitAuto(angles)) 318 | if tangentSplitExisting: 319 | split.extend(self._findTangentSplitExisting(original, start, end, step)) 320 | if tangentSplitAngleThreshold: 321 | split.extend(self._findTangentSplitThreshold(angles, tangentSplitAngleThresholdValue)) 322 | 323 | # get split points 324 | points = self._splitPoints(points, split) 325 | 326 | # fit points and get keyframes 327 | keyframes = [ 328 | keyframe 329 | for p in points 330 | for keyframe in FitBezier(p, error, weightedTangents).fit() 331 | ] 332 | 333 | # only set values if the curve can be optimized. 334 | if len(keyframes) >= len(original): 335 | print "< KeyframeReduction.reduce() " \ 336 | "| path: {0} " \ 337 | "| process-time: {1:,.2f} seconds " \ 338 | "| unable-to-reduce >".format(self.path, time.time() - t) 339 | return 0 340 | 341 | # remove all keys but the first one and add keyframes 342 | self._removeKeys(original, start) 343 | self._addKeys(keyframes, weightedTangents) 344 | 345 | # print reduction rate 346 | rate = 100 - ((len(keyframes) / float(len(original))) * 100) 347 | print "< KeyframeReduction.reduce() " \ 348 | "| path: {0} " \ 349 | "| process-time: {1:,.2f} seconds " \ 350 | "| reduction-rate: {2:,.2f}% >".format( 351 | self.path, 352 | time.time() - t, 353 | rate 354 | ) 355 | 356 | return rate 357 | -------------------------------------------------------------------------------- /scripts/keyframeReduction/classes/vector.py: -------------------------------------------------------------------------------- 1 | from maya import OpenMaya 2 | 3 | 4 | class Vector2D(OpenMaya.MVector): 5 | def __init__(self, *args): 6 | if len(args) == 1: 7 | OpenMaya.MVector.__init__(self, args[0]) 8 | elif len(args) == 2: 9 | OpenMaya.MVector.__init__(self, args[0], args[1], 0) 10 | 11 | def __str__(self): 12 | return "{}, {}".format(self.x, self.y) 13 | 14 | def __repr__(self): 15 | return "< Vector2D object | point: {} >".format(str(self)) 16 | 17 | # ------------------------------------------------------------------------ 18 | 19 | def distanceBetween(self, other): 20 | """ 21 | :return: Distance between two points 22 | :rtype: float 23 | """ 24 | return (self - other).length() 25 | 26 | def signedAngle(self, other): 27 | """ 28 | :return: Signed angle between points 29 | :rtype: float 30 | """ 31 | angle = self.angle(other) 32 | cross = self ^ other 33 | 34 | if cross.z < 0: 35 | return -angle 36 | 37 | return angle 38 | -------------------------------------------------------------------------------- /scripts/keyframeReduction/install.py: -------------------------------------------------------------------------------- 1 | from maya import cmds, mel 2 | 3 | 4 | # ---------------------------------------------------------------------------- 5 | 6 | 7 | ROOT_PACKAGE = __name__.rsplit(".", 1)[0] 8 | 9 | SHELF_NAME = "MiscTools" 10 | SHELF_TOOL = { 11 | "label": "keyframeReduction", 12 | "command": "import {0}.ui; {0}.ui.show()".format(ROOT_PACKAGE), 13 | "annotation": "Reduce animation curve keyframes", 14 | "image1": "KR_icon.png", 15 | "imageOverlayLabel": "reduce", 16 | "sourceType": "python", 17 | "overlayLabelColor": [1, 1, 1], 18 | "overlayLabelBackColor": [0, 0, 0, 0], 19 | } 20 | 21 | 22 | # ---------------------------------------------------------------------------- 23 | 24 | 25 | def shelf(): 26 | """ 27 | Add a new shelf in Maya with the tools that is provided in the SHELF_TOOL 28 | variable. If the tab exists it will be checked to see if the button is 29 | already added. If this is the case the previous button will be deleted and 30 | a new one will be created in its place. 31 | """ 32 | # get top shelf 33 | gShelfTopLevel = mel.eval("$tmpVar=$gShelfTopLevel") 34 | 35 | # get top shelf names 36 | shelves = cmds.tabLayout(gShelfTopLevel, query=1, ca=1) 37 | 38 | # create shelf 39 | if SHELF_NAME not in shelves: 40 | cmds.shelfLayout(SHELF_NAME, parent=gShelfTopLevel) 41 | 42 | # get existing members 43 | names = cmds.shelfLayout(SHELF_NAME, query=True, childArray=True) or [] 44 | labels = [cmds.shelfButton(n, query=True, label=True) for n in names] 45 | 46 | # delete existing button 47 | if SHELF_TOOL.get("label") in labels: 48 | index = labels.index(SHELF_TOOL.get("label")) 49 | cmds.deleteUI(names[index]) 50 | 51 | # add button 52 | cmds.shelfButton(style="iconOnly", parent=SHELF_NAME, **SHELF_TOOL) 53 | -------------------------------------------------------------------------------- /scripts/keyframeReduction/utils.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | from maya import cmds 3 | 4 | 5 | # ---------------------------------------------------------------------------- 6 | 7 | 8 | EPSILON = 12e-11 9 | THRESHOLD = 12e-5 10 | 11 | 12 | # ---------------------------------------------------------------------------- 13 | 14 | 15 | class UndoChunkContext(object): 16 | """ 17 | The undo context is used to combine a chain of commands into one undo. 18 | Can be used in combination with the "with" statement. 19 | 20 | with UndoChunkContext(): 21 | # code 22 | """ 23 | 24 | def __enter__(self): 25 | cmds.undoInfo(openChunk=True) 26 | 27 | def __exit__(self, *exc_info): 28 | cmds.undoInfo(closeChunk=True) 29 | 30 | 31 | # ---------------------------------------------------------------------------- 32 | 33 | 34 | def floatRange(start, end, step): 35 | """ 36 | :param int/float start: 37 | :param int/float end: 38 | :param int/float step: 39 | :return: Float range 40 | :rtype: list 41 | """ 42 | # store values 43 | values = [] 44 | 45 | # convert step to decimal to ensure float precision 46 | step = decimal.Decimal(str(step)) 47 | while start < end: 48 | values.append(float(start)) 49 | start += step 50 | 51 | return values 52 | 53 | 54 | # ---------------------------------------------------------------------------- 55 | 56 | 57 | def validateAnimationCurve(animationCurve): 58 | """ 59 | Check if the parsed animation curve can be reduces. Set driven keyframes 60 | and referenced animation curves will be ignored. 61 | 62 | :param str animationCurve: 63 | :return: Validation state of the animation curve 64 | :rtype: bool 65 | """ 66 | if cmds.listConnections("{}.input".format(animationCurve)): 67 | return False 68 | elif cmds.referenceQuery(animationCurve, isNodeReferenced=True): 69 | return False 70 | 71 | return True 72 | 73 | 74 | # ---------------------------------------------------------------------------- 75 | 76 | 77 | def filterAnimationCurves(animationCurves): 78 | """ 79 | Loop all the animation curves an run the validation function to make sure 80 | the animation curves are suitable for reduction. 81 | 82 | :param list animationCurves: 83 | :return: Animation curves 84 | :rtype: list 85 | """ 86 | return [ 87 | animationCurve 88 | for animationCurve in animationCurves 89 | if validateAnimationCurve(animationCurve) 90 | ] 91 | 92 | 93 | def filterAnimationCurvesByPlug(animationCurves): 94 | """ 95 | :param list animationCurves: 96 | :return: Filtered animation curves 97 | :rtype: dict 98 | """ 99 | data = {} 100 | 101 | for animationCurve in animationCurves: 102 | # get plug 103 | plug = cmds.listConnections( 104 | "{}.output".format(animationCurve), 105 | plugs=True, 106 | source=False, 107 | destination=True, 108 | skipConversionNodes=True, 109 | ) 110 | 111 | if not plug: 112 | continue 113 | 114 | # get attribute 115 | attribute = plug[0].split(".", 1)[-1] 116 | 117 | # add attribute as a key 118 | if attribute not in data.keys(): 119 | data[attribute] = [] 120 | 121 | # append animation curve to attribute list 122 | data[attribute].append(animationCurve) 123 | 124 | return data 125 | 126 | 127 | # ---------------------------------------------------------------------------- 128 | 129 | 130 | def getAllAnimationCurves(): 131 | """ 132 | :return: All suitable animation curves in the current scene 133 | :rtype: list 134 | """ 135 | return filterAnimationCurves(cmds.ls(type="animCurve") or []) 136 | 137 | 138 | def getSelectionAnimationCurves(): 139 | """ 140 | :return: Selection animation curves 141 | :rtype: list 142 | """ 143 | # get selection 144 | animationCurves = set() 145 | selection = cmds.ls(sl=True) or [] 146 | 147 | # loop selection 148 | for sel in selection: 149 | # add selection is an animation curve 150 | if cmds.nodeType(sel).startswith("animCurve"): 151 | animationCurves.add(sel) 152 | continue 153 | 154 | # check if any animation curves are connected to node 155 | for animationCurve in cmds.listConnections( 156 | sel, 157 | type="animCurve", 158 | source=True, 159 | destination=False, 160 | skipConversionNodes=True, 161 | ) or []: 162 | animationCurves.add(animationCurve) 163 | 164 | # convert animation curves to list 165 | animationCurves = list(animationCurves) 166 | return filterAnimationCurves(animationCurves) 167 | -------------------------------------------------------------------------------- /scripts/userSetup.py: -------------------------------------------------------------------------------- 1 | import maya.cmds as cmds 2 | 3 | if not cmds.about(batch=True): 4 | import keyframeReduction.install 5 | cmds.evalDeferred(keyframeReduction.install.shelf) 6 | --------------------------------------------------------------------------------