├── .gitignore ├── LICENSE ├── README.md ├── docs ├── _config.yml ├── gallery │ ├── epicycle.md │ ├── index.md │ ├── morphing-grids.md │ ├── mvt.md │ ├── pendulum.md │ ├── tanline.md │ └── torus.md ├── guides │ ├── animation-in-depth.md │ ├── basic-guide.md │ ├── figures-and-gadgets.md │ ├── index.md │ ├── latex.md │ ├── old │ │ ├── animation-in-depth.md │ │ ├── basic-guide.md │ │ ├── figures-and-gadgets.md │ │ ├── projects-old.md │ │ └── skits.md │ ├── projects.md │ └── skits.md └── index.md ├── examples ├── ball.png ├── example1.py ├── example2.py ├── example3.py ├── example4.py ├── oo.png └── scene.py ├── gallery ├── code │ ├── epicycle.py │ ├── mvt.py │ ├── pendulum.py │ ├── resources │ │ ├── df.png │ │ ├── mvt.png │ │ └── secslope.png │ ├── sample.py │ ├── tanline.py │ └── torus.py ├── epicycle.gif ├── epicycle.mp4 ├── mvt.mp4 ├── pendulum.gif ├── pendulum.mp4 ├── sample.mp4 ├── tanline.mp4 └── torus.mp4 ├── logo ├── logo-white.png └── logo.png └── morpholib ├── __init__.py ├── actions.py ├── anim.py ├── base.py ├── bezier.py ├── calculus.py ├── color.py ├── combo.py ├── figure.py ├── gadgets.py ├── giffer.py ├── graph.py ├── graphics.py ├── grid.py ├── latex.py ├── matrix.py ├── sample.py ├── shapes.py ├── text.py ├── tools ├── __init__.py ├── base.py ├── basics.py ├── color.py ├── dev.py ├── ktimer.py ├── latex2svg.py └── subimporter.py ├── transitions.py └── video.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | #.idea/ 153 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kenneth Small 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 | 2 | 3 | **A general-purpose programmatic animation tool** 4 | 5 | ## Features 6 | - Animate basic figures like Points, Paths, Polygons, Splines, Images, and Text 7 | - Helper functions to build more complex composite figures like grids 8 | - Tools for quickly creating custom figures that animate in precisely specified ways 9 | - Support for rendering and animating LaTeX. 10 | + Requires a LaTeX distribution to be preinstalled, as well as [dvisvgm](https://dvisvgm.de/) 11 | - Multiple tweening options and the ability to define custom tweens 12 | - Apply custom transformations to figures to create complex patterns 13 | - Support for multiple layers each with its own independent dynamic camera 14 | - Ability to use layers as masks for other layers 15 | - Color gradients, both as fills and as color gradients along paths 16 | - Some primitive 3D animation capability 17 | - Preview animations along with the ability to locate positions on screen with a click 18 | - Export animations as MP4, GIF, and PNG sequences at arbitrary framerates and resolutions 19 | - (**Note:** [FFmpeg](https://ffmpeg.org/) required to create MP4s, and [Gifsicle](https://www.lcdf.org/gifsicle/) required to make small size GIFs) 20 | 21 | ## Gallery and Documentation 22 | 23 | A gallery of animations made with Morpho [is available here](https://morpho-matters.github.io/morpholib/gallery/). For more, you can also take a look at the YouTube channels [Morphocular](https://www.youtube.com/channel/UCu7Zwf4X_OQ-TEnou0zdyRA) and [Serpentine Integral](https://www.youtube.com/channel/UCo-H6EyTbD-7inMwW70QdtA), which use Morpho to create most of the animations. 24 | 25 | Documentation is currently limited, but there are [a few guides](https://morpho-matters.github.io/morpholib/guides/) you can look thru which will help you learn how to use Morpho and get you started making your own animations. Questions are welcome on the [Discussions page](https://github.com/morpho-matters/morpholib/discussions). 26 | 27 | ## Installation 28 | 29 | Morpho is a library for Python and works on Python 3.8 or higher and requires [Pycairo](https://www.cairographics.org/pycairo/) to run. For Windows users, Morpho and all its basic dependencies, including Pycairo, should be installable via a simple pip command: 30 | 31 | ```sh 32 | pip3 install morpholib 33 | ``` 34 | 35 | Installation on other platforms has not been well-tested, unfortunately, and does not appear to be as straightforward. For now, I think the best method is to first see if you can install Pycairo separately (for instructions on how to do so, [see this](https://pycairo.readthedocs.io/en/latest/getting_started.html)), test that the Pycairo installation is working, and then attempt to install Morpho via the above pip command. 36 | 37 | ### Softer requirements 38 | 39 | If you want to export animations as MP4s or small-sized GIFs, you will need to install [FFmpeg](https://ffmpeg.org/) for MP4 and/or [Gifsicle](https://www.lcdf.org/gifsicle/) for GIF. But if that doesn't matter to you (or if you just want to try out Morpho), you can still preview animations and export them as PNG sequences and large-sized GIFs just using the base installation of Morpho. 40 | 41 | Please note that FFmpeg and Gifsicle will need to be added to your PATH environment variable for Morpho to be able to access them by default. 42 | 43 | For Morpho to be able to parse LaTeX code, you must have a LaTeX distribution installed along with [dvisvgm](https://dvisvgm.de/). But if you're okay with not using Morpho's LaTeX features, this requirement is optional. 44 | 45 | SciPy is an optional, but recommended, dependency that is necessary currently for only a few features (`flowStreamer` and `FlowField`). SciPy can be installed via pip with the command `pip install scipy` 46 | 47 | ### Testing the installation 48 | 49 | To see if it installed correctly, try running the following Python code: 50 | 51 | ```python 52 | import morpholib as morpho 53 | morpho.importAll() 54 | morpho.sample.play() 55 | ``` 56 | 57 | If you see an animation of a morphing grid appear on your screen, congratulations! Morpho should be installed and working properly. 58 | 59 | ## License 60 | 61 | This project is licensed under the terms of the MIT license. 62 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/gallery/epicycle.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Epicycle 4 | --- 5 | 6 | 9 | 10 | ```python 11 | import morpholib as morpho 12 | morpho.importAll() 13 | 14 | from morpholib.tools.basics import * 15 | 16 | import math, cmath 17 | 18 | 19 | def epicycle(): 20 | mainlayer = morpho.Layer() 21 | mation = morpho.Animation(mainlayer) 22 | 23 | r0 = 2 24 | r1 = 1.5 25 | v0 = r0*1j # *cmath.exp(30*deg*1j) 26 | w0 = tau/2 27 | v1 = r1*1j # *cmath.exp(120*deg*1j) 28 | w1 = 5/3*w0 29 | class Epicycle(morpho.Skit): 30 | def makeFrame(self): 31 | t = self.t 32 | 33 | arrow0 = morpho.grid.Arrow(0, v0) 34 | arrow0.color = [0,1,0] 35 | arrow0.rotation = w0*t 36 | arrow0.commitTransforms() 37 | 38 | circ0 = morpho.shapes.Ellipse( 39 | pos=arrow0.tail, xradius=r0, yradius=r0, 40 | strokeWeight=1.5, color=arrow0.color, 41 | alphaFill=0, alpha=0.5 42 | ) 43 | 44 | arrow1 = morpho.grid.Arrow(0, v1) 45 | arrow1.color = [0,1,1] 46 | arrow1.rotation = w1*t 47 | arrow1.origin = arrow0.head 48 | arrow1.commitTransforms() 49 | 50 | circ1 = morpho.shapes.Ellipse( 51 | pos=arrow1.tail, xradius=r1, yradius=r1, 52 | strokeWeight=1.5, color=arrow1.color, 53 | alphaFill=0, alpha=0.5 54 | ) 55 | 56 | path = morpho.grid.line(0, t, steps=60*t) 57 | path = path.fimage(lambda t: v0*cmath.exp(w0*t*1j) + v1*cmath.exp(w1*t*1j)) 58 | 59 | return morpho.Frame([circ0, circ1, path, arrow0, arrow1]) 60 | 61 | ecycle = mainlayer.Actor(Epicycle()) 62 | ecycle.newendkey(6*30).t = 6 63 | 64 | mation.play() 65 | 66 | epicycle() 67 | ``` -------------------------------------------------------------------------------- /docs/gallery/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Gallery 4 | --- 5 | 6 | # Gallery 7 | 8 | You can find links to various short animations below. Click on them to see the animation play and also the code used to create it. They cover a reasonably wide breadth of Morpho's functionality. 9 | 10 | - [Morphing grids](https://morpho-matters.github.io/morpholib/gallery/morphing-grids) 11 | - [Sliding tangent line](https://morpho-matters.github.io/morpholib/gallery/tanline) 12 | - [Pendulum](https://morpho-matters.github.io/morpholib/gallery/pendulum) 13 | - [Epicycle](https://morpho-matters.github.io/morpholib/gallery/epicycle) 14 | - [Torus construction](https://morpho-matters.github.io/morpholib/gallery/torus) 15 | - [Mean Value Theorem](https://morpho-matters.github.io/morpholib/gallery/mvt) 16 | 17 | For more animations, take a look at the [Morphocular](https://www.youtube.com/channel/UCu7Zwf4X_OQ-TEnou0zdyRA) and [Serpentine Integral](https://www.youtube.com/channel/UCo-H6EyTbD-7inMwW70QdtA) YouTube channels, which use Morpho to create most of the animations. -------------------------------------------------------------------------------- /docs/gallery/morphing-grids.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Morphing Grids 4 | --- 5 | 6 | 9 | 10 | ```python 11 | import morpholib as morpho 12 | morpho.importAll() 13 | 14 | 15 | def main(): 16 | mainlayer = morpho.Layer() 17 | mation = morpho.Animation(mainlayer) 18 | 19 | grid0 = morpho.grid.mathgrid( 20 | tweenMethod=morpho.grid.Path.tweenSpiral, 21 | transition=morpho.transition.quadease 22 | ) 23 | 24 | grid = mainlayer.Actor(grid0) 25 | 26 | grid.newendkey(60, grid0.fimage(lambda s: s**2/10)) 27 | mation.wait(30) 28 | 29 | grid.newendkey(60, grid0.fimage(lambda s: s**3/64)) 30 | mation.wait(30) 31 | 32 | grid.newendkey(60, grid0.fimage(lambda s: s**4/8**3)) 33 | mation.wait(30) 34 | 35 | grid.newendkey(60, grid0.fimage(lambda s: s**5/8**4)) 36 | mation.wait(30) 37 | 38 | grid.newendkey(60, grid0.copy()) 39 | 40 | mation.play() 41 | 42 | main() 43 | ``` -------------------------------------------------------------------------------- /docs/gallery/mvt.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Mean Value Theorem 4 | --- 5 | 6 | 9 | 10 | ```python 11 | import morpholib as morpho 12 | mo = morpho 13 | morpho.importAll() 14 | 15 | import math, cmath, random 16 | import numpy as np 17 | 18 | from morpholib.tools.basics import * 19 | from morpholib.video import standardAnimation, ratioXY, std_view 20 | from morpholib.tools.color import colormap 21 | 22 | morpho.transition.default = morpho.transition.quadease 23 | 24 | uniform = morpho.transitions.uniform 25 | quadease = morpho.transitions.quadease 26 | sineease = sinease = morpho.transition.sineease 27 | 28 | exportDir = "./" 29 | 30 | # Returns a film with some standard axes. 31 | def makeAxes(): 32 | axes = morpho.graph.Axes([-100,100, -100,100], 33 | xwidth=10, ywidth=10, 34 | xalpha=1, yalpha=1 35 | ) 36 | # axes.zdepth = 1000 37 | axes = morpho.Actor(axes) 38 | return axes 39 | 40 | def mvt(): 41 | axes = makeAxes() 42 | 43 | mation = morpho.video.standardAnimation(axes) 44 | mainlayer = mation.layers[0] 45 | mainlayer.camera.first().centerAt(14+7j) 46 | 47 | mation.endDelay() 48 | 49 | a = 6 50 | b = 25 51 | tickHeight = 1 52 | 53 | ticka = morpho.grid.Arrow( 54 | a-tickHeight/2*1j, a+tickHeight/2*1j, 55 | width=5, headSize=0, color=[0,0,0] 56 | ) 57 | ticka = morpho.Actor(ticka) 58 | ticka.newkey(15) 59 | ticka.first().head = ticka.first().tail 60 | mainlayer.merge(ticka) 61 | 62 | tickb = morpho.grid.Arrow( 63 | b-tickHeight/2*1j, b+tickHeight/2*1j, 64 | width=5, headSize=0, color=[0,0,0] 65 | ) 66 | tickb = morpho.Actor(tickb) 67 | tickb.newkey(15) 68 | tickb.first().head = tickb.first().tail 69 | mainlayer.append(tickb) 70 | 71 | # Labels 72 | textHeight = 1.25*tickHeight 73 | time = mainlayer.lastID() 74 | labela = morpho.text.Text("a", pos=a, color=[0,0,0], size=64, anchor_x=0, italic=True, alpha=0) 75 | labela = morpho.Actor(labela) 76 | labela.newkey(15) 77 | labela.last().alpha = 1 78 | labela.last().pos -= textHeight*1j 79 | mainlayer.merge(labela, atFrame=time) 80 | 81 | labelb = labela.copy() 82 | for fig in labelb.keys(): 83 | fig.text = "b" 84 | fig.pos += b-a 85 | mainlayer.merge(labelb) 86 | 87 | mation.endDelay(15) 88 | 89 | c = 18 90 | w = 12.5 91 | h = 12 92 | f = lambda x: h*math.sqrt(1-(x-c)**2/w**2) 93 | graph = mo.graph.realgraph(f,a,b, steps=100, width=7, color=[0,0,1]) 94 | 95 | graph = morpho.Actor(graph) 96 | graph.newkey(30) 97 | graph.first().end = 0 98 | mainlayer.append(graph) 99 | 100 | # y ticks 101 | tickfa = morpho.grid.Arrow( 102 | 1j*f(a)-tickHeight/2, 1j*f(a)+tickHeight/2, 103 | width=5, headSize=0, color=[0,0,0] 104 | ) 105 | tickfa = morpho.Actor(tickfa) 106 | tickfa.newkey(15) 107 | tickfa.first().head = tickfa.first().tail 108 | mainlayer.append(tickfa) 109 | 110 | time = mation.lastID() 111 | linea = mo.grid.Arrow( 112 | f(a)*1j, a+f(a)*1j, color=[0,0,0], width=5, 113 | headSize=0 114 | ) 115 | linea.dash = [10] 116 | linea.zdepth = -1 117 | linea = mo.Actor(linea) 118 | linea.newendkey(20) 119 | linea.first().head = linea.first().tail 120 | mainlayer.merge(linea, atFrame=time) 121 | 122 | tickfb = morpho.grid.Arrow( 123 | 1j*f(b)-tickHeight/2, 1j*f(b)+tickHeight/2, 124 | width=5, headSize=0, color=[0,0,0] 125 | ) 126 | tickfb = morpho.Actor(tickfb) 127 | tickfb.newkey(15) 128 | tickfb.first().head = tickfb.first().tail 129 | mainlayer.merge(tickfb, atFrame=time) 130 | 131 | lineb = mo.grid.Arrow( 132 | f(b)*1j, b+f(b)*1j, color=[0,0,0], width=5, 133 | headSize=0 134 | ) 135 | lineb.dash = [10] 136 | lineb.zdepth = -1 137 | lineb = mo.Actor(lineb) 138 | lineb.newendkey(20) 139 | lineb.first().head = lineb.first().tail 140 | mainlayer.append(lineb) 141 | 142 | time = mainlayer.lastID() 143 | # textHeight *= 1.5 144 | labelfa = morpho.text.Text("f(a)", pos=f(a)*1j, color=[0,0,0], size=64, anchor_x=0, italic=True, alpha=0) 145 | labelfa = morpho.Actor(labelfa) 146 | labelfa.newkey(15) 147 | labelfa.last().alpha = 1 148 | labelfa.last().pos -= textHeight*1.5 149 | mainlayer.merge(labelfa, atFrame=time) 150 | 151 | labelfb = morpho.text.Text("f(b)", pos=f(b)*1j, color=[0,0,0], size=64, anchor_x=0, italic=True, alpha=0) 152 | labelfb = morpho.Actor(labelfb) 153 | labelfb.newkey(15) 154 | labelfb.last().alpha = 1 155 | labelfb.last().pos -= textHeight*1.5 156 | mainlayer.merge(labelfb, atFrame=time) 157 | 158 | mation.endDelay() 159 | 160 | time = mation.lastID() 161 | pta = mo.grid.Point(a+f(a)*1j, strokeWeight=3, size=20, alpha=0) 162 | pta = mo.Actor(pta) 163 | pta.newkey(15).alpha = 1 164 | mainlayer.merge(pta, atFrame=time) 165 | 166 | ptb = pta.copy() 167 | for fig in ptb.keys(): 168 | fig.pos = b+f(b)*1j 169 | mainlayer.merge(ptb) 170 | 171 | xmin = -4 172 | xmax = 32 173 | m = (f(b)-f(a))/(b-a) 174 | L = lambda x: m*(x-a) + f(a) 175 | secline = mo.graph.realgraph(L,xmin,xmax, steps=1, width=7, 176 | color=mo.color.colormap["green"] 177 | ) 178 | secline.zdepth = -1 179 | secline.end = 0 180 | secline = mo.Actor(secline) 181 | secline.newkey(30) 182 | secline.last().end = 1 183 | mainlayer.append(secline) 184 | 185 | mation.endDelay(15) 186 | 187 | # Secant formula 188 | secslope = mo.graphics.Image("./resources/secslope.png") 189 | secslope.pos = 16.5 + 7j 190 | secslope.align = [0,0] 191 | secslope.height = 0 192 | secslope.zdepth = 10 193 | # secslope.alpha = 0 194 | 195 | secslope = mo.Actor(secslope) 196 | secslope.newendkey(20) 197 | secslope.last().pos = a+5 + 2j 198 | secslope.last().height = 2 199 | # secslope.last().alpha = 1 200 | mainlayer.append(secslope) 201 | 202 | mation.endDelay() 203 | 204 | dx = 0.00001 205 | deriv = lambda f: (lambda x: (f(x+dx) - f(x-dx))/(2*dx)) 206 | df = deriv(f) 207 | dfcImg = morpho.graphics.Image("./resources/df.png") 208 | @morpho.SkitParameters({ 209 | "f":f, "df":df, "x":a+dx, "radius":5, "ptalpha":1, "alpha":1, 210 | "start":0, "end":1 211 | }) 212 | class Tanline(morpho.Skit): 213 | def makeFrame(self): 214 | x = self.x 215 | radius = self.radius 216 | ptalpha = self.ptalpha 217 | alpha = self.alpha 218 | f = self.f 219 | df = self.df 220 | 221 | m = df(x) 222 | y = f(x) 223 | T = lambda t: m*(t-x)+y 224 | xrad = radius/math.sqrt(1+m**2) 225 | tanline = mo.graph.realgraph(T, x-xrad, x+xrad, steps=1, 226 | color=[0,0,0], width=7, alpha=alpha 227 | ) 228 | tanline.start = self.start 229 | tanline.end = self.end 230 | 231 | pt = mo.grid.Point(x+y*1j, strokeWeight=3, 232 | fill=mo.color.colormap["violet"], alpha=alpha*ptalpha, size=20 233 | ) 234 | 235 | dfc = mo.graphics.Image(dfcImg) 236 | dfc.pos = pt.pos + 4j 237 | dfc.height = 1 238 | dfc.alpha = ptalpha*alpha 239 | 240 | arrow = mo.grid.Arrow(dfc.pos-0.75j, pt.pos+0.75j, 241 | width=5, color=mo.color.colormap["violet"], 242 | alpha=ptalpha*alpha 243 | ) 244 | 245 | vert = mo.grid.Arrow(x, pt.pos, 246 | color=[0,0,0], alpha=ptalpha*alpha, width=5, headSize=0 247 | ) 248 | vert.dash = [10] 249 | vert.zdepth = -1 250 | 251 | tickc = morpho.grid.Arrow( 252 | x-tickHeight/2*1j, x+tickHeight/2*1j, 253 | width=5, headSize=0, alpha=ptalpha*alpha, color=[0,0,0] 254 | ) 255 | 256 | labelc = morpho.text.Text( 257 | "c", pos=x-1j*textHeight, color=mo.color.colormap["violet"], size=64, 258 | anchor_x=0, italic=True, alpha=ptalpha*alpha 259 | ) 260 | 261 | return mo.Frame([tanline, pt, arrow, vert, tickc, labelc, dfc]) 262 | 263 | tanline = Tanline() 264 | tanline.zdepth = 1 265 | tanline.x = a+2 266 | tanline.end = 0 267 | tanline.ptalpha = 0 268 | 269 | tanline = morpho.Actor(tanline) 270 | mainlayer.append(tanline) 271 | 272 | tanline.newendkey(20) 273 | tanline.last().end = 1 274 | tanline.last().ptalpha = 1 275 | 276 | mation.endDelay() 277 | 278 | tanline.newendkey(60).x = b-2 279 | tanline.newendkey(60).x = a+4 280 | tanline.newendkey(45).x = 13.757 # First MVT point 281 | 282 | mation.endDelay() 283 | 284 | # Assert parallel 285 | tline = tanline.last().makeFrame().figures[0] 286 | tline.zdepth = -0.5 287 | tline = mo.Actor(tline) 288 | mainlayer.append(tline) 289 | 290 | tline.newendkey(20) 291 | for n in range(2): 292 | z = tline.last().seq[n] 293 | tline.last().seq[n] = z.real + 1j*(L(z.real)+0.25) 294 | 295 | mation.endDelay() 296 | 297 | tline.newendkey(20, tline.first().copy()) 298 | tline.last().visible = False 299 | 300 | mation.endDelay() 301 | 302 | # Construct equation 303 | time = mation.lastID() 304 | dfc = tanline.last().makeFrame().figures[-1] 305 | dfc.zdepth = 10 306 | dfc = mo.Actor(dfc) 307 | dfc.newendkey(30) 308 | dfc.last().pos = secslope.last().pos + secslope.last().width 309 | mainlayer.merge(dfc, atFrame=time) 310 | 311 | eq = mo.text.Text( 312 | "=", pos=16.4+1.7j, color=[0,0,0], 313 | size=84, anchor_x=0, alpha=0 314 | ) 315 | eq.zdepth = 10 316 | eq = mo.Actor(eq) 317 | eq.newendkey(30).alpha = 1 318 | mainlayer.merge(eq, atFrame=time) 319 | 320 | mation.endDelay() 321 | 322 | # Enbox formula 323 | time = mation.lastID() 324 | box = [6.5,20.5, 0.5,3.5] 325 | boxer = mo.gadgets.enbox(box, width=5) 326 | for key in boxer.keys(): 327 | key.zdepth = 10 328 | mainlayer.append(boxer) 329 | 330 | whitebox = mo.grid.rect(box) 331 | whitebox.width = 0 332 | whitebox.fill = [1,1,1] 333 | whitebox.alpha = 0 334 | whitebox.zdepth = 9 335 | whitebox = mo.Actor(whitebox) 336 | whitebox.newendkey(30).alpha = 1 337 | mainlayer.merge(whitebox, atFrame=time) 338 | 339 | mation.endDelay() 340 | 341 | boxer.newendkey(20) 342 | boxer.last().alpha = 0 343 | boxer.last().visible = False 344 | 345 | whitebox.newendkey(20) 346 | whitebox.last().alpha = 0 347 | whitebox.last().visible = False 348 | 349 | # Morph graph 350 | time = mation.lastID() 351 | f2 = lambda x: 4*math.sin(-x/2.5)+8 352 | graph2 = mo.graph.realgraph(f2,a,b, steps=100, width=7, color=[0,0,1]) 353 | graph.newkey(time) 354 | graph.newendkey(30, graph2) 355 | 356 | # Morph secline 357 | m2 = (f2(b) - f2(a))/(b-a) 358 | L2 = lambda x: m2*(x-a) + f2(a) 359 | secline2 = mo.graph.realgraph(L2,xmin,xmax, steps=1, width=7, 360 | color=mo.color.colormap["green"] 361 | ) 362 | secline2.zdepth = -1 363 | secline.newkey(time) 364 | secline.newendkey(30, secline2) 365 | 366 | pta.newkey(time) 367 | pta.newendkey(30).pos = a + 1j*f2(a) 368 | 369 | ptb.newkey(time) 370 | ptb.newendkey(30).pos = b + 1j*f2(b) 371 | 372 | linea.newkey(time) 373 | linea.newendkey(30) 374 | linea.last().tail = f2(a)*1j 375 | linea.last().head = a + f2(a)*1j 376 | 377 | lineb.newkey(time) 378 | lineb.newendkey(30) 379 | lineb.last().tail = f2(b)*1j 380 | lineb.last().head = b + f2(b)*1j 381 | 382 | tickfa.newkey(time) 383 | tickfa.newendkey(30) 384 | tickfa.last().tail += (f2(a)-f(a))*1j 385 | tickfa.last().head += (f2(a)-f(a))*1j 386 | 387 | tickfb.newkey(time) 388 | tickfb.newendkey(30) 389 | tickfb.last().tail += (f2(b)-f(b))*1j 390 | tickfb.last().head += (f2(b)-f(b))*1j 391 | 392 | labelfa.newkey(time) 393 | labelfa.newendkey(30) 394 | labelfa.last().pos += (f2(a)-f(a))*1j 395 | 396 | labelfb.newkey(time) 397 | labelfb.newendkey(30) 398 | labelfb.last().pos += (f2(b)-f(b))*1j 399 | 400 | tanline.newkey(time) 401 | tanline.newendkey(30) 402 | tanline.last().f = f2 403 | tanline.last().df = deriv(f2) 404 | tanline.last().radius = 3.5 405 | 406 | mation.endDelay() 407 | 408 | # Find both new MVT points 409 | mvt1 = 11.378 410 | mvt2 = 20.038 411 | 412 | tanline.newendkey(45).x = mvt2 413 | 414 | mation.endDelay(15) 415 | 416 | tan2 = tanline.last().makeFrame().figures[0] 417 | tan2.zdepth = 10 418 | mainlayer.append(tan2) 419 | pt2 = tanline.last().makeFrame().figures[1] 420 | pt2.zdepth = 10 421 | mainlayer.append(pt2) 422 | 423 | tanline.newendkey(60).x = mvt1 424 | 425 | tan1 = tanline.last().makeFrame().figures[0] 426 | tan1.zdepth = 10 427 | mainlayer.append(tan1) 428 | pt1 = tanline.last().makeFrame().figures[1] 429 | pt1.zdepth = 10 430 | mainlayer.append(pt1) 431 | 432 | mation.endDelay(15) 433 | 434 | tanline.last().transition = mo.transition.uniform 435 | tanline.newendkey(30).alpha = 0 436 | 437 | mation.endDelay(30*3) 438 | 439 | mation.finitizeDelays(60) 440 | 441 | mation.locatorLayer = 0 442 | mation.play() 443 | 444 | mvt() 445 | ``` -------------------------------------------------------------------------------- /docs/gallery/pendulum.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Pendulum 4 | --- 5 | 6 | 9 | 10 | ```python 11 | import morpholib as morpho 12 | morpho.importAll() 13 | 14 | from morpholib.tools.basics import * 15 | 16 | import math, cmath 17 | 18 | 19 | def pendulum(): 20 | mainlayer = morpho.Layer() 21 | mation = morpho.Animation(mainlayer) 22 | 23 | thetamax = pi/6 # Hard code thetamax 24 | length = 3 # Hard code pendulum string length 25 | class Pendulum(morpho.Skit): 26 | def makeFrame(self): 27 | t = self.t 28 | 29 | theta = thetamax*math.sin(t) 30 | 31 | # Create pendulum string 32 | string = morpho.grid.Path([0, -length*1j]) 33 | string.rotation = theta 34 | # Commit the rotation so that the string's 35 | # final node can be used to position the ball. 36 | string.commitTransforms() 37 | 38 | # Create the ball hanging on the string. 39 | # Its position is equal to the position of the 40 | # final node of the string path 41 | ball = morpho.grid.Point() 42 | ball.pos = string.seq[-1] 43 | ball.strokeWeight = string.width 44 | ball.color = [1,1,1] # Ball border is white 45 | ball.size = 40 # Make it 40 pixels wide 46 | 47 | # Create neutral vertical dashed line 48 | neutral = morpho.grid.Path([0, -length*1j]) 49 | neutral.dash = [10,10] 50 | 51 | # Create connecting arc 52 | arc = morpho.shapes.EllipticalArc( 53 | pos=0, xradius=1, yradius=1, 54 | theta0=-pi/2, theta1=-pi/2+theta, 55 | ) 56 | 57 | # Create theta label 58 | thetaLabel = morpho.text.Text("\u03b8", # Unicode for theta 59 | pos=1.5*cmath.exp(1j*mean([arc.theta0, arc.theta1])), 60 | size=min(36, 36*abs(theta/0.36)), italic=True 61 | ) 62 | 63 | thetanum = morpho.text.formatNumber(theta*180/pi, decimal=0) 64 | tracker = morpho.text.Text( 65 | "\u03b8 = "+thetanum+"\u00b0", 66 | pos=1j, size=56 67 | ) 68 | 69 | return morpho.Frame([neutral, arc, thetaLabel, string, ball, tracker]) 70 | 71 | pend = mainlayer.Actor(Pendulum()) 72 | 73 | # Set internal time parameter t to be 6pi after 5 seconds 74 | # (150 frames) have passed in the animation's clock. 75 | pend.newendkey(150).t = 6*pi 76 | 77 | mation.play() 78 | 79 | pendulum() 80 | ``` -------------------------------------------------------------------------------- /docs/gallery/tanline.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Sliding Tangent Line 4 | --- 5 | 6 | 9 | 10 | ```python 11 | import morpholib as morpho 12 | morpho.importAll() 13 | 14 | from morpholib.tools.basics import * 15 | 16 | import math, cmath 17 | 18 | 19 | def tanline(): 20 | mainlayer = morpho.Layer() 21 | mation = morpho.Animation(mainlayer) 22 | 23 | f = lambda x: 0.2*(x**3 - 12*x) 24 | path = mainlayer.Actor(morpho.graph.realgraph(f, -4, 4)) 25 | 26 | # Define a numerical derivative function 27 | dx = 0.000001 # A small change in x 28 | df = lambda x: (f(x+dx)-f(x-dx))/(2*dx) 29 | 30 | @morpho.SkitParameters(t=-4, length=4, alpha=1) 31 | class TangentLine(morpho.Skit): 32 | def makeFrame(self): 33 | # t will represent the input to the function f 34 | t = self.t 35 | length = self.length 36 | alpha = self.alpha 37 | 38 | # Initialize tangent line to be a horizontal 39 | # line segment of length 4 centered at the 40 | # origin 41 | line = morpho.grid.Path([-length/2, length/2]) 42 | line.color = [1,0,0] # Red color 43 | line.alpha = alpha 44 | 45 | # Compute derivative 46 | slope = df(t) 47 | # Convert into an angle and set it as the rotation 48 | # of the line segment 49 | angle = math.atan(slope) 50 | line.rotation = angle 51 | 52 | # Position the tangent line at the tangent point 53 | x = t 54 | y = f(t) 55 | line.origin = x + 1j*y 56 | 57 | # Create derivative tracker 58 | slopenum = morpho.text.formatNumber(slope, decimal=3, rightDigits=3) 59 | dlabel = morpho.text.Text("Slope = "+slopenum, 60 | pos=line.origin, anchor_y=-1, 61 | size=36, color=[1,1,0], alpha=alpha 62 | ) 63 | dlabel.rotation = angle 64 | 65 | return morpho.Frame([line, dlabel]) 66 | 67 | # Initialize the tangent line Skit 68 | tanline = mainlayer.Actor(TangentLine(t=-4, length=0)) 69 | tanline.first().transition = morpho.transitions.quadease 70 | 71 | # Set t to +4 over the course of 5 seconds (150 frames) 72 | tanline.newendkey(150).set(t=4, length=4) 73 | 74 | # Finally, fade the tangent line to invisibility 75 | tanline.newendkey(30).alpha = 0 76 | 77 | mation.play() 78 | 79 | tanline() 80 | ``` -------------------------------------------------------------------------------- /docs/gallery/torus.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Torus Construction 4 | --- 5 | 6 | 9 | 10 | ```python 11 | import morpholib as morpho 12 | mo = morpho 13 | morpho.importAll() 14 | 15 | from morpholib.tools.basics import * 16 | 17 | import math, cmath 18 | 19 | morpho.transitions.default = morpho.transitions.quadease 20 | 21 | goodblue = tuple((mo.array([0,0.5,1])).tolist()) 22 | 23 | 24 | def torus(): 25 | mation = morpho.video.setupSpaceAlt() 26 | mation.windowShape = (600,600) 27 | mation.fullscreen = False 28 | 29 | mainlayer = mation.layers[0] 30 | xmin,xmax, ymin,ymax = mainlayer.camera.first().view 31 | for keyfig in mainlayer.camera.keys(): 32 | keyfig.view = [ymin,ymax,ymin,ymax] 33 | keyfig.moveBy(-1j) 34 | 35 | meshlayer = morpho.SpaceLayer(view=mainlayer.camera.copy()) 36 | mation.merge(meshlayer) 37 | 38 | mesh = morpho.grid.quadgrid( 39 | view=[0,tau, 0,tau], 40 | dx=tau/16, dy=tau/24, 41 | width=1.5, color=[0,0,0], 42 | fill=goodblue, fill2=(mo.array(goodblue)/2).tolist() 43 | ) 44 | mesh.shading = True 45 | 46 | R = 2 47 | r = 0.75 48 | def tubify(v): 49 | theta, phi, dummy = v 50 | 51 | x = r*np.cos(theta+pi/2) 52 | y = phi-3 53 | z = r*np.cos(theta) + r 54 | 55 | return morpho.array([x,y,z]) 56 | 57 | def torify(v): 58 | theta, phi, dummy = v 59 | 60 | x = (R+r*np.cos(theta-pi/2))*np.cos(phi) 61 | y = -(R+r*np.cos(theta-pi/2))*np.sin(phi) 62 | z = r*np.cos(theta) + r 63 | 64 | return morpho.array([x,y,z]) 65 | 66 | torus = meshlayer.Actor(morpho.grid.quadgrid( 67 | view=[-3,3, -3,3], 68 | dx=6/16, dy=6/24, 69 | width=1.5, color=[0,0,0], 70 | fill=goodblue, fill2=(mo.array(goodblue)/2).tolist() 71 | ), atFrame=0) 72 | torus.first().shading = True 73 | torus.newendkey(45) 74 | torus.newendkey(60, mesh.fimage(tubify)) 75 | torus.newendkey(45) 76 | torus.newendkey(60, mesh.fimage(torify)) 77 | meshlayer.merge(torus) 78 | 79 | # Change up camera 80 | mainlayer.camera.movekey(mainlayer.camera.lastID(), torus.lastID() + 90) 81 | meshlayer.camera = mainlayer.camera.copy() 82 | 83 | mation.newFrameRate(10) 84 | mation.play() 85 | 86 | torus() 87 | ``` -------------------------------------------------------------------------------- /docs/guides/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Morpho Guides 4 | --- 5 | 6 | # Morpho Guides 7 | 8 | Below you can find a collection of guides to help you get familiar with how Morpho works and how to use it. 9 | 10 | ### Fundamentals 11 | I recommend starting with these and following them pretty much in this order: 12 | 1. [Basics](https://morpho-matters.github.io/morpholib/guides/basic-guide) 13 | 2. [Useful Figures and Gadgets](https://morpho-matters.github.io/morpholib/guides/figures-and-gadgets) 14 | 3. [Animation In-depth](https://morpho-matters.github.io/morpholib/guides/animation-in-depth) (possibly skippable if you're in a hurry) 15 | 4. [Skits](https://morpho-matters.github.io/morpholib/guides/skits) 16 | 17 | ### Advanced 18 | - [Making Longer-Form Videos](https://morpho-matters.github.io/morpholib/guides/projects) 19 | 20 | ### Other guides 21 | - [Incorporating LaTeX](https://morpho-matters.github.io/morpholib/guides/latex) 22 | 23 | Example code is provided in each guide, which I would encourage you to code up manually, as I think that will help you learn it better than merely copying and pasting the code. However, finished versions of the examples can be found [on this page](https://github.com/morpho-matters/morpholib/tree/master/examples) if you want to just quickly try them out or debug your own code. 24 | 25 | Also note that these guides are not at all comprehensive, and I plan to add more guides in the future. But also keep in mind that they are merely *guides* and do not count as full-scale documentation of all aspects of Morpho and its capabilities. They are meant to hopefully quickly get you able to create your own animations. 26 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Morpho Documentation 4 | --- 5 | 6 | ## Guides 7 | 8 | For a quick-ish way to get yourself familiar with how Morpho works and how to create animations, [take a look at the guides](https://morpho-matters.github.io/morpholib/guides/). 9 | 10 | ## Gallery 11 | 12 | A gallery of animations (along with code) demonstrating some of Morpho's capabilities can be found [on this page](https://morpho-matters.github.io/morpholib/gallery/). 13 | 14 | You can also take a look at the [Morphocular](https://www.youtube.com/channel/UCu7Zwf4X_OQ-TEnou0zdyRA) and [Serpentine Integral](https://www.youtube.com/channel/UCo-H6EyTbD-7inMwW70QdtA) YouTube channels, which use Morpho to create most of the animations. 15 | 16 | ## Other Resources 17 | 18 | Unfortunately, documentation for Morpho is currently pretty limited. However, you can use Python's ``help()`` function to retrieve some documentation for most classes, functions, and methods in Morpho: e.g. ``help(morpho.grid.Path)`` -------------------------------------------------------------------------------- /examples/ball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morpho-matters/morpholib/f7b029e85b941744cf89ca1729105cf428e2d7cf/examples/ball.png -------------------------------------------------------------------------------- /examples/example1.py: -------------------------------------------------------------------------------- 1 | import morpholib as morpho 2 | morpho.importAll() 3 | 4 | from morpholib.tools.basics import * 5 | from morpholib.video import * 6 | 7 | import math, cmath 8 | 9 | # morpho.transitions.default = morpho.transitions.quadease 10 | 11 | def pointEx(): 12 | mainlayer = morpho.Layer() 13 | mation = morpho.Animation(mainlayer) 14 | 15 | mypoint = mainlayer.Actor(morpho.grid.Point().set( 16 | pos=3+4*1j, # Position as a complex number 17 | size=50, # Diameter in pixels 18 | fill=[0,1,0], # Color in RGB, where 0 is min and 1 is max 19 | color=[1,1,1], # Border color 20 | strokeWeight=5 # Border thickness in pixels 21 | )) 22 | 23 | mation.play() 24 | 25 | def pathEx(): 26 | mainlayer = morpho.Layer() 27 | mation = morpho.Animation(mainlayer) 28 | 29 | mypath = mainlayer.Actor(morpho.grid.Path([3, 3*1j, -3, -3*1j]).close().set( 30 | color=[0,0,1], # Make the path blue 31 | width=5 # Make the path 5 pixels thick 32 | )) 33 | 34 | mation.play() 35 | 36 | def lineEx(): 37 | mainlayer = morpho.Layer() 38 | mation = morpho.Animation(mainlayer) 39 | 40 | # Make a linear path connecting -2-3j to 1+4j 41 | # containing 100 segments 42 | myline = mainlayer.Actor(morpho.grid.line(-2-3*1j, 1+4*1j, steps=100)) 43 | 44 | mation.play() 45 | 46 | def ellipseEx(): 47 | mainlayer = morpho.Layer() 48 | mation = morpho.Animation(mainlayer) 49 | 50 | # Make an elliptical path centered at 1+1j with 51 | # x-radius 3 and y-radius 1 52 | myoval = mainlayer.Actor(morpho.grid.ellipse(1+1j, 3, 1).edge()) 53 | 54 | mation.play() 55 | 56 | def gridEx(): 57 | mainlayer = morpho.Layer() 58 | mation = morpho.Animation(mainlayer) 59 | 60 | mygrid = mainlayer.Actor(morpho.grid.mathgrid( 61 | view=[-5,5, -4,4], # read this as [xmin, xmax, ymin, ymax] 62 | dx=1, dy=1 # Distance between major x and y tick marks 63 | )) 64 | 65 | mation.play() 66 | 67 | def polyEx(): 68 | mainlayer = morpho.Layer() 69 | mation = morpho.Animation(mainlayer) 70 | 71 | mypoly = mainlayer.Actor(morpho.grid.Polygon([3, 3*1j, -3, -3*1j]).set( 72 | width=5, # Border is 5 pixels thick 73 | color=[1,1,0], # Border color is yellow 74 | fill=[1,0,0] # Fill color is red 75 | )) 76 | 77 | mation.play() 78 | 79 | def pointActor(): 80 | mainlayer = morpho.Layer() 81 | mation = morpho.Animation(mainlayer) 82 | 83 | mypoint = mainlayer.Actor(morpho.grid.Point().set( 84 | pos=3+4*1j, # Position as a complex number 85 | size=50, # Diameter in pixels 86 | fill=[0,1,0], # Fill color in RGB, where 0 is min and 1 is max 87 | color=[1,1,1], # Border color 88 | alpha=0.5, # Value from 0 to 1 where 0 = invisible, 1 = opaque 89 | strokeWeight=5 # Border thickness in pixels 90 | )) 91 | mypoint.newendkey(30).set(pos=0) # Move to origin 92 | mypoint.newendkey(30).set(size=20, fill=[1,0,0], alpha=1) # Get smaller, change color, make opaque 93 | mypoint.newendkey(30) # Do nothing, just wait a second 94 | mypoint.newendkey(20).set(pos=-3) # Move to (-3,0) in 20 frames (2/3 sec) 95 | mypoint.newendkey(30, morpho.grid.Point()) # Turn into a default Point figure 96 | 97 | mation.play() 98 | 99 | 100 | def intermediateKeys(): 101 | mainlayer = morpho.Layer() 102 | mation = morpho.Animation(mainlayer) 103 | 104 | mypoint = mainlayer.Actor(morpho.grid.Point().set( 105 | pos=0, # 0 is default, but it's nice to be explicit 106 | size=20, 107 | fill=[1,0,0] 108 | )) 109 | mypoint.newendkey(60).set(pos=3+4*1j, size=50, fill=[0,1,0]) 110 | mypoint.newendkey(-30).set(pos=3) 111 | 112 | mation.play() 113 | 114 | def pathMorph(): 115 | mainlayer = morpho.Layer() 116 | mation = morpho.Animation(mainlayer) 117 | 118 | grid = mainlayer.Actor(morpho.grid.mathgrid( 119 | view=[-5,5, -5,5], # read this as [xmin, xmax, ymin, ymax] 120 | dx=1, dy=1 # Distance between major x and y tick marks 121 | )) 122 | 123 | fgrid = grid.last().fimage(lambda z: z**2/10) 124 | grid.newendkey(60, fgrid) 125 | 126 | mation.play() 127 | 128 | def layerEx(): 129 | mainlayer = morpho.Layer() 130 | mation = morpho.Animation(mainlayer) 131 | 132 | # Create Point actor 133 | mypoint = mainlayer.Actor(morpho.grid.Point().set( 134 | pos=3+4*1j, # Position as a complex number 135 | size=50, # Diameter in pixels 136 | fill=[0,1,0], # Fill color in RGB, where 0 is min and 1 is max 137 | color=[1,1,1], # Border color 138 | strokeWeight=5, # Border thickness in pixels 139 | transition=morpho.transitions.quadease, # Quadease transition 140 | zdepth=-10 # Initial zdepth is now -10 141 | )) 142 | 143 | # Create grid actor 144 | grid = mainlayer.Actor(morpho.grid.mathgrid( 145 | view=[-5,5, -5,5], # read this as [xmin, xmax, ymin, ymax] 146 | dx=1, dy=1 # Distance between major x and y tick marks 147 | )) 148 | 149 | # Define mypoint's keyfigures 150 | mypoint.newendkey(60).set( 151 | pos=0, # Move the point to the origin 152 | size=25, # Cut the size in half 153 | fill=[1,0,0], # Change fill color to red 154 | zdepth=10 # Second keyfigure zdepth is now +10 155 | ) 156 | mypoint.newendkey(60).set( 157 | pos=-3+3*1j, # Move point to (-3+3i) 158 | size=75, # Inflate size of point 159 | alpha=0 # Fade point to invisibility 160 | ) 161 | mypoint.newendkey(-30).set(pos=-3) # New key 30 frames before last key 162 | mypoint.newendkey(60, mypoint.first().copy()) 163 | 164 | # Define grid's keyfigures 165 | fgrid = grid.last().fimage(lambda z: z**2/10) 166 | grid.newendkey(60, fgrid) 167 | 168 | mation.play() 169 | 170 | 171 | # pointEx() 172 | # pathEx() 173 | # lineEx() 174 | # ellipseEx() 175 | # gridEx() 176 | # polyEx() 177 | # pointActor() 178 | # intermediateKeys() 179 | # pathMorph() 180 | # layerEx() 181 | -------------------------------------------------------------------------------- /examples/example2.py: -------------------------------------------------------------------------------- 1 | import morpholib as morpho 2 | morpho.importAll() 3 | 4 | from morpholib.tools.basics import * 5 | from morpholib.video import * 6 | 7 | import math, cmath 8 | 9 | # morpho.transitions.default = morpho.transitions.quadease 10 | 11 | 12 | def textEx(): 13 | mainlayer = morpho.Layer() 14 | mation = morpho.Animation(mainlayer) 15 | 16 | mytext = mainlayer.Actor(morpho.text.Text("Hello World").set( 17 | size=84, 18 | color=[1,0,0] 19 | )) 20 | 21 | mation.play() 22 | 23 | def textMorph(): 24 | mainlayer = morpho.Layer() 25 | mation = morpho.Animation(mainlayer) 26 | 27 | message = mainlayer.Actor(morpho.text.MultiText("Hello World!")) 28 | # Over the course of a second, morph the text to say "Bye!" 29 | message.newendkey(30).set(text="Bye!") 30 | 31 | mation.play() 32 | 33 | def imageMorph(): 34 | mainlayer = morpho.Layer() 35 | mation = morpho.Animation(mainlayer) 36 | 37 | mypic = mainlayer.Actor(morpho.graphics.MultiImage("./ball.png").set( 38 | width=3 39 | )) 40 | # Rescale height while leaving width unchanged 41 | mypic.newendkey(30).newSource("./oo.png").scaleByWidth() 42 | 43 | mation.play() 44 | 45 | def arrowTest(): 46 | mainlayer = morpho.Layer() 47 | mation = morpho.Animation(mainlayer) 48 | 49 | pt = mainlayer.Actor(morpho.grid.Point(0)) 50 | 51 | label = mainlayer.Actor(morpho.text.Text("Watch me carefully!").set( 52 | pos=pt.first().pos-3j, 53 | size=48, 54 | color=[1,0,0] 55 | )) 56 | 57 | arrow = mainlayer.Actor(morpho.grid.Arrow().set( 58 | headSize=0, # Override default headSize of 25 59 | width=5, 60 | color=[1,1,1] # Color it white 61 | )) 62 | arrow.first().tail = arrow.first().head = label.first().pos + 0.5j 63 | 64 | arrow.newendkey(30).set( 65 | head=pt.first().pos-0.5j, 66 | headSize=25 67 | ) 68 | arrow.last().head = pt.first().pos - 0.5j 69 | arrow.last().headSize = 25 70 | 71 | mation.play() 72 | 73 | 74 | def rectTest(): 75 | mainlayer = morpho.Layer() 76 | mation = morpho.Animation(mainlayer) 77 | 78 | myrect = mainlayer.Actor(morpho.grid.rect([-3,3, -1,2]).set( 79 | width=5, 80 | color=[1,0,0], 81 | fill=[1,1,0] 82 | )) 83 | 84 | mation.play() 85 | 86 | def ellipseTest(): 87 | mainlayer = morpho.Layer() 88 | mation = morpho.Animation(mainlayer) 89 | 90 | # Ellipse centered at (2,1) with semi-width 3, and semi-height 1. 91 | myoval = mainlayer.Actor(morpho.grid.ellipse(2+1j, 3, 1)) 92 | 93 | mation.play() 94 | 95 | def arcTest(): 96 | mainlayer = morpho.Layer() 97 | mation = morpho.Animation(mainlayer) 98 | 99 | # Connect the point -2-1j to the point 3+2j with 100 | # an arc of angle pi/2 radians traveling counter- 101 | # clockwise from the first to the second point. 102 | myarc = mainlayer.Actor(morpho.grid.arc(-2-1j, 3+2j, pi/2)) 103 | 104 | mation.play() 105 | 106 | def gridTest(): 107 | mainlayer = morpho.Layer() 108 | mation = morpho.Animation(mainlayer) 109 | 110 | # Make a grid with thick, green horizontal lines 111 | # and 4 minor grid lines between every two major 112 | # lines. Also disable background grid and axes. 113 | mygrid = mainlayer.Actor(morpho.grid.mathgrid( 114 | view=[-3,3, -3,3], 115 | hcolor=[0,1,0], hwidth=5, 116 | hmidlines=4, vmidlines=4, 117 | BGgrid=False, axes=False 118 | )) 119 | 120 | mation.play() 121 | 122 | def graphTest(): 123 | mainlayer = morpho.Layer() 124 | mation = morpho.Animation(mainlayer) 125 | 126 | f = lambda x: x**2 127 | fgraph = mainlayer.Actor(morpho.graph.realgraph(f, -2, 2)) 128 | 129 | mation.play() 130 | 131 | def graphTest2(): 132 | mainlayer = morpho.Layer() 133 | mation = morpho.Animation(mainlayer) 134 | 135 | # This looks awful 136 | f1 = lambda x: 4*(1+math.sin(5*x))/2 137 | fgraph1 = mainlayer.Actor(morpho.graph.realgraph(f1, -4, 4)) 138 | 139 | # This looks way better 140 | f2 = lambda x: 4*(-1+math.sin(5*x))/2 141 | fgraph2 = mainlayer.Actor(morpho.graph.realgraph(f2, -4, 4, steps=200)) 142 | 143 | mation.play() 144 | 145 | def graphTest3(): 146 | mainlayer = morpho.Layer() 147 | mation = morpho.Animation(mainlayer) 148 | 149 | f = lambda x: x**2 150 | # Make graph thick, red, and semi-transparent 151 | fgraph = mainlayer.Actor(morpho.graph.realgraph(f, -2, 2).set( 152 | width=10, 153 | color=[1,0,0], 154 | alpha=0.5 155 | )) 156 | 157 | mation.play() 158 | 159 | def ellipseRotation(): 160 | mainlayer = morpho.Layer() 161 | mation = morpho.Animation(mainlayer) 162 | 163 | myoval = mainlayer.Actor(morpho.grid.ellipse(2+1j, 3, 1).set( 164 | rotation=2*pi/3 165 | )) 166 | 167 | mation.play() 168 | 169 | def ellipseOrigin(): 170 | mainlayer = morpho.Layer() 171 | mation = morpho.Animation(mainlayer) 172 | 173 | myoval = mainlayer.Actor(morpho.grid.ellipse(0, 3, 1).set( 174 | origin=2+1j, 175 | rotation=2*pi/3 176 | )) 177 | 178 | mation.play() 179 | 180 | def parallelogram(): 181 | mainlayer = morpho.Layer() 182 | mation = morpho.Animation(mainlayer) 183 | 184 | # Initialize the shape to be the unit square 185 | # and apply the linear transformation corresponding to the matrix 186 | # [[ 1 1] 187 | # [0.5 2]] 188 | shape = mainlayer.Actor(morpho.grid.rect([0,1,0,1]).set( 189 | transform=np.array([[1,1],[0.5,2]]) 190 | )) 191 | shape.transform = np.array([[1,1],[0.5,2]]) 192 | 193 | mation.play() 194 | 195 | def commitExample(): 196 | mainlayer = morpho.Layer() 197 | mation = morpho.Animation(mainlayer) 198 | 199 | # Ellipse centered at (0,0) with semi-width 3, 200 | # and semi-height 1. 201 | myoval = mainlayer.Actor(morpho.grid.ellipse(0, 3, 1).set( 202 | origin=2+1j, 203 | rotation=2*pi/3 204 | )) 205 | print(myoval.first().origin, myoval.first().rotation) 206 | myoval.first().commitTransforms() 207 | print(myoval.first().origin, myoval.first().rotation) 208 | 209 | mation.play() 210 | 211 | def rotationComparison(): 212 | mainlayer = morpho.Layer() 213 | mation = morpho.Animation(mainlayer) 214 | 215 | myoval = mainlayer.Actor(morpho.grid.ellipse(0, 3, 1)) 216 | 217 | # Set rotation to pi radians after 1 second passes 218 | myoval.newendkey(30).set(rotation=pi) 219 | myoval.last().commitTransforms() 220 | 221 | mation.play() 222 | 223 | def shearedBall(): 224 | mainlayer = morpho.Layer() 225 | mation = morpho.Animation(mainlayer) 226 | 227 | ball = mainlayer.Actor(morpho.graphics.Image("./ball.png").set( 228 | width=2, 229 | transform=np.array([[1,1],[0,1]]) # Shear the ball 230 | )) 231 | 232 | label = mainlayer.Actor(morpho.text.Text("sheared ball", pos=3j).set( 233 | transform=ball.first().transform # Shear the label 234 | )) 235 | 236 | mation.play() 237 | 238 | def ellipticalArcExample(): 239 | mainlayer = morpho.Layer() 240 | mation = morpho.Animation(mainlayer) 241 | 242 | # Initialize the arc centered at the point 1-2j, 243 | # with the semi-width and semi-height of the 244 | # containing arc being 2 and 3 respectively, 245 | # and having the portion of the elliptical curve 246 | # shown being the angle range from pi/2 to 7*pi/2. 247 | earc = mainlayer.Actor(morpho.shapes.EllipticalArc( 248 | pos=1-2j, xradius=2, yradius=3, 249 | theta0=pi/2, theta1=7*pi/6 250 | )) 251 | 252 | mation.play() 253 | 254 | def pieExample(): 255 | mainlayer = morpho.Layer() 256 | mation = morpho.Animation(mainlayer) 257 | 258 | pie = mainlayer.Actor(morpho.shapes.Pie( 259 | pos=0, xradius=4, yradius=2, innerFactor=0.2, 260 | theta0=pi/2, theta1=11*pi/6, 261 | strokeWeight=5, color=[1,1,1], fill=[0,0.8,0.6] 262 | )) 263 | 264 | mation.play() 265 | 266 | def pie2poly(): 267 | mainlayer = morpho.Layer() 268 | mation = morpho.Animation(mainlayer) 269 | 270 | pie = morpho.shapes.Pie( 271 | pos=0, xradius=4, yradius=2, innerFactor=0.2, 272 | theta0=pi/2, theta1=11*pi/6, 273 | strokeWeight=5, color=[1,1,1], fill=[0.8,0.3,0] 274 | ) 275 | # By default, dTheta=5*deg, so this is twice as coarse 276 | poly = mainlayer.Actor(pie.toPolygon(dTheta=10*deg)) 277 | 278 | mation.play() 279 | 280 | def splineTest(): 281 | mainlayer = morpho.Layer() 282 | mation = morpho.Animation(mainlayer) 283 | 284 | spline = morpho.shapes.Spline() 285 | spline.newNode(-1-1j) 286 | 287 | # Set the last node's outhandle control point to be 288 | # located 1 unit above the latest node's position. 289 | spline.outhandle(-1, -1) # These two actually 290 | spline.outhandleRel(-1, 1j) # do the same thing 291 | 292 | spline.newNode(3+1j) 293 | spline.inhandleRel(-1, -1+1j) 294 | 295 | spline.newNode(-4j) 296 | spline.inhandleRel(-1, 1) 297 | 298 | # Make the handle control points visible by drawing 299 | # tangent line segments 300 | spline.showTangents = True 301 | 302 | # Turn into an actor so it can be viewed 303 | spline = mainlayer.Actor(spline) 304 | 305 | mation.play() 306 | 307 | def crossoutTest(): 308 | mainlayer = morpho.Layer() 309 | mation = morpho.Animation(mainlayer) 310 | 311 | # Some text that's just begging to be crossed out 312 | mistake = mainlayer.Actor(morpho.text.Text("2 + 2 = 5")) 313 | 314 | # Generate an actor that does a crossout within 315 | # the specified box 316 | cross = mainlayer.Actor(morpho.gadgets.crossout(mistake.first().box(), 317 | pad=0.5, time=60, width=6, color=[1,1,0], 318 | transition=morpho.transitions.quadease 319 | )) 320 | 321 | mation.play() 322 | 323 | def enboxTest(): 324 | mainlayer = morpho.Layer() 325 | mation = morpho.Animation(mainlayer) 326 | 327 | # Some sample text to enbox 328 | greeting = mainlayer.Actor(morpho.text.Text("Hello World!")) 329 | 330 | boxer = mainlayer.Actor(morpho.gadgets.enbox(greeting.first().box(), 331 | pad=0.5, time=20, width=4, color=[0,1,0], 332 | corner="NE", # Start drawing from northeast corner 333 | CCW=False, # Draw it in a clockwise direction 334 | transition=morpho.transitions.quadease 335 | )) 336 | 337 | mation.play() 338 | 339 | def encircleTest(): 340 | mainlayer = morpho.Layer() 341 | mation = morpho.Animation(mainlayer) 342 | 343 | # Something worth encircling 344 | message = mainlayer.Actor(morpho.text.Text("Success!", color=[0.5,0.5,1])) 345 | 346 | encirc = mainlayer.Actor(morpho.gadgets.encircle(message.first().box(), 347 | pad=0.5, time=45, width=8, color=[0,1,0], 348 | phase=-pi/2, CCW=False, 349 | transition=morpho.transitions.quadease 350 | )) 351 | 352 | mation.play() 353 | 354 | def imageBoxTest(): 355 | mainlayer = morpho.Layer() 356 | mation = morpho.Animation(mainlayer) 357 | 358 | ball = mainlayer.Actor(morpho.graphics.Image("./ball.png")) 359 | # Draw bounding box with 0.25 units of padding on all sides 360 | boxer = morpho.gadgets.enbox(ball.first().box(pad=0.25)) 361 | 362 | mation.play() 363 | 364 | def textBoxTest(): 365 | mainlayer = morpho.Layer() 366 | mation = morpho.Animation(mainlayer) 367 | 368 | mytext = mainlayer.Actor(morpho.text.Text("Hello World!")) 369 | 370 | # The default view and window dimensions of an 371 | # animation are [-5,5]x[-5j,5j] and 600x600 pixels. 372 | # Supply both of these to the box() method. 373 | boxer = mainlayer.Actor(morpho.gadgets.enbox(mytext.first().box(), pad=0.25)) 374 | 375 | mation.play() 376 | 377 | 378 | # textEx() 379 | # textMorph() 380 | # imageMorph() 381 | # arrowTest() 382 | # rectTest() 383 | # ellipseTest() 384 | # arcTest() 385 | # gridTest() 386 | # graphTest() 387 | # graphTest2() 388 | # graphTest3() 389 | # ellipseRotation() 390 | # ellipseOrigin() 391 | # parallelogram() 392 | # commitExample() 393 | # rotationComparison() 394 | # shearedBall() 395 | # ellipticalArcExample() 396 | # pieExample() 397 | # pie2poly() 398 | # splineTest() 399 | # crossoutTest() 400 | # enboxTest() 401 | # encircleTest() 402 | # textBoxTest() 403 | -------------------------------------------------------------------------------- /examples/example3.py: -------------------------------------------------------------------------------- 1 | import morpholib as morpho 2 | morpho.importAll() 3 | 4 | from morpholib.tools.basics import * 5 | from morpholib.video import * 6 | 7 | import math, cmath 8 | 9 | # morpho.transitions.default = morpho.transitions.quadease 10 | 11 | def spiralPoint(): 12 | mainlayer = morpho.Layer() 13 | mation = morpho.Animation(mainlayer) 14 | 15 | # Place a default grid in the background just to 16 | # make the motion clearer 17 | gridBG = mainlayer.Actor(morpho.grid.basicgrid(axes=True)) 18 | 19 | # Setup point at the coordinates (3,2) and 20 | # set the tween method to be the Point class's 21 | # spiral tween method. 22 | mypoint = mainlayer.Actor(morpho.grid.Point(3+2j).set( 23 | tweenMethod=morpho.grid.Point.tweenSpiral 24 | )) 25 | 26 | # Change the position to (-1,0) over the course of 1 second (30 frames). 27 | mypoint.newendkey(30).pos = -1 28 | 29 | mation.play() 30 | 31 | def spiralPath(): 32 | mainlayer = morpho.Layer() 33 | mation = morpho.Animation(mainlayer) 34 | 35 | # Setup a standard grid, but with the spiral 36 | # tween method for a path. 37 | mygrid = mainlayer.Actor(morpho.grid.mathgrid( 38 | tweenMethod=morpho.grid.Path.tweenSpiral 39 | )) 40 | 41 | # Have it tween into a morphed version of itself 42 | fgrid = mygrid.last().fimage(lambda z: z**2/10) 43 | mygrid.newendkey(60, fgrid) 44 | 45 | mation.play() 46 | 47 | def pivotPoint(): 48 | mainlayer = morpho.Layer() 49 | mation = morpho.Animation(mainlayer) 50 | 51 | # Place a default grid in the background just to 52 | # make the motion clearer 53 | gridBG = mainlayer.Actor(morpho.grid.basicgrid(axes=True)) 54 | 55 | # Setup point at the coordinates (3,2) and 56 | # set the tween method to be the Point class's 57 | # pivot tween method with an angle of pi radians. 58 | mypoint = mainlayer.Actor(morpho.grid.Point(3+2j).set( 59 | tweenMethod=morpho.grid.Point.tweenPivot(pi) 60 | )) 61 | 62 | # Change the position to (-1,0) over the course of 1 second (30 frames). 63 | mypoint.newendkey(30).pos = -1 64 | 65 | mation.play() 66 | 67 | def spiralThenLinear(): 68 | mainlayer = morpho.Layer() 69 | mation = morpho.Animation(mainlayer) 70 | 71 | # Place a default grid in the background just to 72 | # make the motion clearer 73 | gridBG = mainlayer.Actor(morpho.grid.basicgrid(axes=True)) 74 | 75 | # Setup point at the coordinates (3,2) and 76 | # set the tween method to be the Point class's spiral tween method. 77 | # Also set the transition to be quadease. 78 | mypoint = mainlayer.Actor(morpho.grid.Point(3+2j).set( 79 | tweenMethod=morpho.grid.Point.tweenSpiral, 80 | transition=morpho.transitions.quadease 81 | )) 82 | 83 | # Change the position to (-1,0) over the course of 1 second (30 frames) 84 | # and also reassign the tween method at this point to be linear tween. 85 | # Also set the transition from this point to be uniform 86 | mypoint.newendkey(30).set( 87 | pos=-1, 88 | tweenMethod=mypoint.figureType.tweenLinear, 89 | transition=morpho.transitions.uniform 90 | ) 91 | 92 | # Create a new keyfigure returning the point to its 93 | # starting location. The tween method is governed by 94 | # the previous keyfigure's tween method: tweenLinear 95 | mypoint.newendkey(30, mypoint.first().copy()) 96 | 97 | mation.play() 98 | 99 | def invisibility(): 100 | mainlayer = morpho.Layer() 101 | mation = morpho.Animation(mainlayer) 102 | 103 | # Initialize point at the origin, but make it big. 104 | mypoint = mainlayer.Actor(morpho.grid.Point(0).set(size=50)) 105 | 106 | # Move point off the left side of the screen 107 | mypoint.newendkey(30).set( 108 | pos=-6, 109 | visible=False 110 | ) 111 | 112 | # While invisible, move to being off the 113 | # right side of the screen 114 | mypoint.newendkey(15).set( 115 | pos=6, 116 | visible=True 117 | ) 118 | 119 | # After being made visible again, move 120 | # back to the origin. 121 | mypoint.newendkey(30).pos = 0 122 | 123 | mation.play() 124 | 125 | def locatorTest(): 126 | mainlayer = morpho.Layer() 127 | mation = morpho.Animation(mainlayer) 128 | 129 | # Setup a default grid 130 | mygrid = mainlayer.Actor(morpho.grid.mathgrid()) 131 | 132 | # Morph it into a distorted version 133 | mygrid.newendkey(30, mygrid.first().fimage(lambda z: z**2/10)) 134 | 135 | # Set the first layer (the layer #0) as the locator layer 136 | mation.locatorLayer = 0 137 | # Set the initial frame of the animation to be its final frame 138 | mation.start = mation.lastID() 139 | mation.play() 140 | 141 | def locatorAlt(): 142 | mainlayer = morpho.Layer() 143 | mation = morpho.Animation(mainlayer) 144 | 145 | # Setup a default grid 146 | mygrid = mainlayer.Actor(morpho.grid.mathgrid()) 147 | 148 | # Morph it into a distorted version 149 | mygrid.newendkey(30, mygrid.first().fimage(lambda z: z**2/10)) 150 | 151 | # Set `mainlayer` as the locator layer 152 | mation.locatorLayer = mainlayer 153 | # Set the initial frame of the animation to be its final frame 154 | mation.start = mation.lastID() 155 | mation.play() 156 | 157 | 158 | # spiralPoint() 159 | # spiralPath() 160 | # pivotPoint() 161 | # spiralThenLinear() 162 | # invisibility() 163 | # locatorTest() 164 | # locatorAlt() 165 | -------------------------------------------------------------------------------- /examples/example4.py: -------------------------------------------------------------------------------- 1 | import morpholib as morpho 2 | morpho.importAll() 3 | 4 | from morpholib.tools.basics import * 5 | from morpholib.video import * 6 | 7 | import math, cmath 8 | 9 | # morpho.transitions.default = morpho.transitions.quadease 10 | 11 | def tracker(): 12 | mainlayer = morpho.Layer() 13 | mation = morpho.Animation(mainlayer) 14 | 15 | class Tracker(morpho.Skit): 16 | def makeFrame(self): 17 | # The t value is stored as a tweenable attribute 18 | # of the tracker itself. Let's extract it just 19 | # to simplify the later syntax. 20 | t = self.t 21 | 22 | # Turn t into a string formatted so 23 | # it's rounded to the third decimal place 24 | # and always displays three digits to the right 25 | # of the decimal place, appending zeros if necessary. 26 | number = morpho.text.formatNumber(t, decimal=3, rightDigits=3) 27 | 28 | # The label's text is the stringified version of 29 | # the "number" object, which does the job of 30 | # rounding and appending trailing zeros for us. 31 | label = morpho.text.Text(number) 32 | 33 | return label 34 | 35 | # Construct an instance of our new Tracker Skit. 36 | # By default, t is initialized to t = 0. 37 | mytracker = mainlayer.Actor(Tracker()) 38 | 39 | # Have its t value progress to the number 1 over the course 40 | # of 2 seconds (60 frames) 41 | mytracker.newendkey(60).t = 1 42 | 43 | mation.play() 44 | 45 | def follower(): 46 | mainlayer = morpho.Layer() 47 | mation = morpho.Animation(mainlayer) 48 | 49 | # Create a curved path that begins at x = -4 and ends at x = +4 50 | path = mainlayer.Actor(morpho.graph.realgraph( 51 | lambda x: 0.2*(x**3 - 12*x), -4, 4)) 52 | 53 | class Follower(morpho.Skit): 54 | def makeFrame(self): 55 | t = self.t 56 | 57 | # Create a generic Point figure 58 | point = morpho.grid.Point() 59 | # Set the position of the point to be the path's 60 | # position at parameter t. 61 | point.pos = path.first().positionAt(t) 62 | 63 | # Format the coordinates 64 | # and handle rounding and trailing zeros. 65 | x,y = point.pos.real, point.pos.imag 66 | xnum = morpho.text.formatNumber(x, decimal=2, rightDigits=2) 67 | ynum = morpho.text.formatNumber(y, decimal=2, rightDigits=2) 68 | 69 | # Create coordinate label 70 | label = morpho.text.Text( 71 | "("+xnum+", "+ynum+")", 72 | pos=point.pos, anchor_y=-1, 73 | size=36, color=[0,1,0] 74 | ) 75 | # Anchor is +1 when t = 1, but -1 when t = 0 76 | label.anchor_x = morpho.lerp(-1, 1, t) 77 | 78 | return morpho.Frame([point, label]) 79 | 80 | # Set the follower to begin at the END of the path, 81 | # just to change things up a little. 82 | myfollower = mainlayer.Actor(Follower(t=1)) 83 | 84 | # Set its t value to be 0 85 | # after 2 seconds (60 frames) have passed. 86 | myfollower.newendkey(60).t = 0 87 | 88 | mation.play() 89 | 90 | def tanline(): 91 | mainlayer = morpho.Layer() 92 | mation = morpho.Animation(mainlayer) 93 | 94 | f = lambda x: 0.2*(x**3 - 12*x) 95 | path = mainlayer.Actor(morpho.graph.realgraph(f, -4, 4)) 96 | 97 | # Define a numerical derivative function 98 | dx = 0.000001 # A small change in x 99 | df = lambda x: (f(x+dx)-f(x-dx))/(2*dx) 100 | 101 | @morpho.SkitParameters(t=-4, length=4, alpha=1) 102 | class TangentLine(morpho.Skit): 103 | def makeFrame(self): 104 | # t will represent the input to the function f 105 | t = self.t 106 | length = self.length 107 | alpha = self.alpha 108 | 109 | # Initialize tangent line to be a horizontal 110 | # line segment of length 4 centered at the 111 | # origin 112 | line = morpho.grid.Path([-length/2, length/2]) 113 | line.color = [1,0,0] # Red color 114 | line.alpha = alpha 115 | 116 | # Compute derivative 117 | slope = df(t) 118 | # Convert into an angle and set it as the rotation 119 | # of the line segment 120 | angle = math.atan(slope) 121 | line.rotation = angle 122 | 123 | # Position the tangent line at the tangent point 124 | x = t 125 | y = f(t) 126 | line.origin = x + 1j*y 127 | 128 | # Create derivative tracker 129 | slopenum = morpho.text.formatNumber(slope, decimal=3, rightDigits=3) 130 | dlabel = morpho.text.Text("Slope = "+slopenum, 131 | pos=line.origin, anchor_y=-1, 132 | size=36, color=[1,1,0], alpha=alpha 133 | ) 134 | dlabel.rotation = angle 135 | 136 | return morpho.Frame([line, dlabel]) 137 | 138 | # Initialize the tangent line Skit 139 | tanline = mainlayer.Actor(TangentLine(t=-4, length=0)) 140 | tanline.first().transition = morpho.transitions.quadease 141 | 142 | # Set t to +4 over the course of 5 seconds (150 frames) 143 | tanline.newendkey(150).set(t=4, length=4) 144 | 145 | # Finally, fade the tangent line to invisibility 146 | tanline.newendkey(30).alpha = 0 147 | 148 | mation.play() 149 | 150 | def imageFollower(): 151 | mainlayer = morpho.Layer() 152 | mation = morpho.Animation(mainlayer) 153 | 154 | # Create a curved path that begins at x = -4 and ends at x = +4 155 | path = mainlayer.Actor(morpho.graph.realgraph( 156 | lambda x: 0.2*(x**3 - 12*x), -4, 4)) 157 | 158 | ballimage = morpho.graphics.Image("./ball.png") 159 | class Follower(morpho.Skit): 160 | def makeFrame(self): 161 | t = self.t 162 | 163 | # Create an Image figure from "ball.png" 164 | ball = morpho.graphics.Image(ballimage) 165 | ball.height = 0.75 166 | # Set the position of the image to be the path's 167 | # position at parameter t. 168 | ball.pos = path.first().positionAt(t) 169 | 170 | return ball 171 | 172 | # Set the follower to begin at the END of the path, 173 | # just to change things up a little. 174 | myfollower = mainlayer.Actor(Follower(t=1)) 175 | 176 | # Set its t value to be 0 177 | # after 2 seconds (60 frames) have passed. 178 | myfollower.newendkey(60).t = 0 179 | 180 | mation.play() 181 | 182 | def pendulum(): 183 | mainlayer = morpho.Layer() 184 | mation = morpho.Animation(mainlayer) 185 | 186 | thetamax = pi/6 # Hard code thetamax 187 | length = 3 # Hard code pendulum string length 188 | class Pendulum(morpho.Skit): 189 | def makeFrame(self): 190 | t = self.t 191 | 192 | theta = thetamax*math.sin(t) 193 | 194 | # Create pendulum string 195 | string = morpho.grid.Path([0, -length*1j]) 196 | string.rotation = theta 197 | # Commit the rotation so that the string's 198 | # final node can be used to position the ball. 199 | string.commitTransforms() 200 | 201 | # Create the ball hanging on the string. 202 | # Its position is equal to the position of the 203 | # final node of the string path 204 | ball = morpho.grid.Point() 205 | ball.pos = string.seq[-1] 206 | ball.strokeWeight = string.width 207 | ball.color = [1,1,1] # Ball border is white 208 | ball.size = 40 # Make it 40 pixels wide 209 | 210 | # Create neutral vertical dashed line 211 | neutral = morpho.grid.Path([0, -length*1j]) 212 | neutral.dash = [10,10] 213 | 214 | # Create connecting arc 215 | arc = morpho.shapes.EllipticalArc( 216 | pos=0, xradius=1, yradius=1, 217 | theta0=-pi/2, theta1=-pi/2+theta, 218 | ) 219 | 220 | # Create theta label 221 | thetaLabel = morpho.text.Text("\u03b8", # Unicode for theta 222 | pos=1.5*cmath.exp(1j*mean([arc.theta0, arc.theta1])), 223 | size=min(36, 36*abs(theta/0.36)), italic=True 224 | ) 225 | 226 | thetanum = morpho.text.formatNumber(theta*180/pi, decimal=0) 227 | tracker = morpho.text.Text( 228 | "\u03b8 = "+thetanum+"\u00b0", 229 | pos=1j, size=56 230 | ) 231 | 232 | return morpho.Frame([neutral, arc, thetaLabel, string, ball, tracker]) 233 | 234 | pend = mainlayer.Actor(Pendulum()) 235 | 236 | # Set internal time parameter t to be 6pi after 5 seconds 237 | # (150 frames) have passed in the animation's clock. 238 | pend.newendkey(150).t = 6*pi 239 | 240 | mation.play() 241 | 242 | # tracker() 243 | # follower() 244 | # tanline() 245 | # imageFollower() 246 | # pendulum() 247 | -------------------------------------------------------------------------------- /examples/oo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morpho-matters/morpholib/f7b029e85b941744cf89ca1729105cf428e2d7cf/examples/oo.png -------------------------------------------------------------------------------- /examples/scene.py: -------------------------------------------------------------------------------- 1 | import morpholib as morpho 2 | morpho.importAll() 3 | mo = morpho # Allows the shorthand "mo" to be optionally used instead of "morpho" 4 | 5 | # Import particular transition functions into the main namespace 6 | from morpholib.transitions import uniform, quadease, drop, toss, sineease, step 7 | # Import useful functions and constants into the main namespace 8 | from morpholib.tools.basics import * 9 | 10 | # Import various other libraries 11 | import math, cmath, random 12 | import numpy as np 13 | 14 | # Set default transition to quadease 15 | morpho.transition.default = quadease 16 | # Set default font to be the LaTeX font 17 | # Note you may have to install this font separately onto your system 18 | morpho.text.defaultFont = "CMU serif" 19 | 20 | # Particular colors are named here. 21 | # Feel free to customize to your heart's content. 22 | violet = tuple(mo.color.parseHexColor("800080")) 23 | orange = tuple(mo.color.parseHexColor("ff6300")) 24 | lighttan = tuple(mo.color.parseHexColor("f4f1c1")) 25 | 26 | 27 | def main(): 28 | # Define layers here 29 | mainlayer = morpho.Layer(view=mo.video.view169()) 30 | mation = morpho.Animation([mainlayer]) 31 | # Display settings 32 | mation.windowShape = (1920, 1080) 33 | mation.fullscreen = True 34 | mation.background = lighttan 35 | 36 | mainlayer.camera.first().zoomIn(2) 37 | 38 | # Define background grid 39 | grid = mainlayer.Actor(mo.grid.mathgrid( 40 | view=[-9,9, -5,5], 41 | steps=1, 42 | hcolor=[0,0.6,0], vcolor=[0,0,1], 43 | axesColor=[0,0,0], 44 | axisWidth=7 45 | )) 46 | 47 | # Define curve to initially be a line 48 | curve = mainlayer.Actor(mo.graph.realgraph(lambda x: 2*x + 1, -3, 3)) 49 | curve.first().set(width=5, color=[1,0,0], end=0) 50 | curve.newendkey(30).end = 1 # Draw curve over 1 second (30 frames) 51 | 52 | # Create "Linear" label. 53 | # MultiText is used so that we can morph the text later 54 | label = mainlayer.Actor(mo.text.MultiText("Linear", 55 | pos=1+0.5j, size=64, color=[1,0,0], alpha=0 56 | )) 57 | label.newendkey(20).alpha = 1 58 | 59 | mation.waitUntil(3*30) 60 | print("Morph line to parabola:", mation.seconds()) 61 | 62 | curve.newendkey() 63 | label.newendkey() 64 | 65 | quadratic = mo.graph.realgraph(lambda x: x**2, -3, 3) 66 | quadratic.set(width=5, color=violet) 67 | curve.newendkey(30, quadratic) 68 | 69 | label.newendkey(30).set(text="Quadratic", pos=2.5+0.5j, color=violet) 70 | 71 | mation.waitUntil(6.25*30) 72 | print("Fade everything out:", mation.seconds()) 73 | 74 | # Create initial keyfigures 75 | curve.newendkey() 76 | label.newendkey() 77 | 78 | # Fade curve 79 | curve.newendkey(30).alpha = 0 80 | 81 | # Simultaneously fade the label 82 | label.newendkey(30).alpha = 0 83 | 84 | 85 | print("Animation length:", mation.seconds()) 86 | mation.wait(10*30) 87 | 88 | mation.finitizeDelays(30) 89 | 90 | # mation.start = mation.lastID() 91 | mation.locatorLayer = mainlayer 92 | mation.clickRound = 2 93 | mation.clickCopy = True 94 | # mation.newFrameRate(10) 95 | mation.play() 96 | 97 | # mation.newFrameRate(60) 98 | # mation.export("./animation.mp4", scale=1) 99 | 100 | def streamlined(): 101 | # Define layers here 102 | mainlayer = morpho.Layer(view=mo.video.view169()) 103 | mation = morpho.Animation([mainlayer]) 104 | # Display settings 105 | mation.windowShape = (1920, 1080) 106 | mation.fullscreen = True 107 | mation.background = lighttan 108 | 109 | mainlayer.camera.first().zoomIn(2) 110 | 111 | # Define background grid 112 | grid = mainlayer.Actor(mo.grid.mathgrid( 113 | view=[-9,9, -5,5], 114 | steps=1, 115 | hcolor=[0,0.6,0], vcolor=[0,0,1], 116 | axesColor=[0,0,0], 117 | axisWidth=7 118 | )) 119 | 120 | # Define curve to initially be a line 121 | curve = mainlayer.Actor(mo.graph.realgraph(lambda x: 2*x + 1, -3, 3)) 122 | curve.first().set(width=5, color=[1,0,0]) # No longer need to say end=0 123 | curve.growIn(duration=30) 124 | 125 | # Create "Linear" label. 126 | # MultiText is used so that we can morph the text later 127 | label = mainlayer.Actor(mo.text.MultiText("Linear", 128 | pos=1+0.5j, size=64, color=[1,0,0] # No longer need to say alpha=0 129 | )) 130 | label.fadeIn(duration=20, jump=1j) 131 | 132 | mation.waitUntil(3*30) 133 | print("Morph line to parabola:", mation.seconds()) 134 | 135 | curve.newendkey() 136 | label.newendkey() 137 | 138 | quadratic = mo.graph.realgraph(lambda x: x**2, -3, 3) 139 | quadratic.set(width=5, color=violet) 140 | curve.newendkey(30, quadratic) 141 | 142 | label.newendkey(30).set(text="Quadratic", pos=2.5+0.5j, color=violet) 143 | 144 | mation.waitUntil(6.25*30) 145 | print("Fade everything out:", mation.seconds()) 146 | 147 | # Create initial keyfigures 148 | curve.newendkey() 149 | label.newendkey() 150 | 151 | # Fade curve and label in a staggered fashion with jumping 152 | mo.action.fadeOut([curve, label], duration=30, stagger=15, jump=2j) 153 | 154 | 155 | print("Animation length:", mation.seconds()) 156 | mation.wait(10*30) 157 | 158 | mation.finitizeDelays(30) 159 | 160 | # mation.start = mation.lastID() 161 | mation.locatorLayer = mainlayer 162 | mation.clickRound = 2 163 | mation.clickCopy = True 164 | # mation.newFrameRate(10) 165 | mation.play() 166 | 167 | # mation.newFrameRate(60) 168 | # mation.export("./animation.mp4", scale=1) 169 | 170 | main() 171 | # streamlined() 172 | -------------------------------------------------------------------------------- /gallery/code/epicycle.py: -------------------------------------------------------------------------------- 1 | import morpholib as morpho 2 | morpho.importAll() 3 | 4 | from morpholib.tools.basics import * 5 | 6 | import math, cmath 7 | 8 | 9 | def epicycle(): 10 | mainlayer = morpho.Layer() 11 | mation = morpho.Animation(mainlayer) 12 | 13 | r0 = 2 14 | r1 = 1.5 15 | v0 = r0*1j # *cmath.exp(30*deg*1j) 16 | w0 = tau/2 17 | v1 = r1*1j # *cmath.exp(120*deg*1j) 18 | w1 = 5/3*w0 19 | class Epicycle(morpho.Skit): 20 | def makeFrame(self): 21 | t = self.t 22 | 23 | arrow0 = morpho.grid.Arrow(0, v0) 24 | arrow0.color = [0,1,0] 25 | arrow0.rotation = w0*t 26 | arrow0.commitTransforms() 27 | 28 | circ0 = morpho.shapes.Ellipse( 29 | pos=arrow0.tail, xradius=r0, yradius=r0, 30 | strokeWeight=1.5, color=arrow0.color, 31 | alphaFill=0, alpha=0.5 32 | ) 33 | 34 | arrow1 = morpho.grid.Arrow(0, v1) 35 | arrow1.color = [0,1,1] 36 | arrow1.rotation = w1*t 37 | arrow1.origin = arrow0.head 38 | arrow1.commitTransforms() 39 | 40 | circ1 = morpho.shapes.Ellipse( 41 | pos=arrow1.tail, xradius=r1, yradius=r1, 42 | strokeWeight=1.5, color=arrow1.color, 43 | alphaFill=0, alpha=0.5 44 | ) 45 | 46 | path = morpho.grid.line(0, t, steps=60*t) 47 | path = path.fimage(lambda t: v0*cmath.exp(w0*t*1j) + v1*cmath.exp(w1*t*1j)) 48 | 49 | return morpho.Frame([circ0, circ1, path, arrow0, arrow1]) 50 | 51 | ecycle = mainlayer.Actor(Epicycle()) 52 | ecycle.newendkey(6*30).t = 6 53 | 54 | mation.play() 55 | 56 | epicycle() 57 | -------------------------------------------------------------------------------- /gallery/code/mvt.py: -------------------------------------------------------------------------------- 1 | import morpholib as morpho 2 | mo = morpho 3 | morpho.importAll() 4 | 5 | import math, cmath, random 6 | import numpy as np 7 | 8 | from morpholib.tools.basics import * 9 | from morpholib.video import standardAnimation, ratioXY, std_view 10 | from morpholib.tools.color import colormap 11 | 12 | morpho.transition.default = morpho.transition.quadease 13 | 14 | uniform = morpho.transitions.uniform 15 | quadease = morpho.transitions.quadease 16 | sineease = sinease = morpho.transition.sineease 17 | 18 | exportDir = "./" 19 | 20 | # Returns a film with some standard axes. 21 | def makeAxes(): 22 | axes = morpho.graph.Axes([-100,100, -100,100], 23 | xwidth=10, ywidth=10, 24 | xalpha=1, yalpha=1 25 | ) 26 | # axes.zdepth = 1000 27 | axes = morpho.Actor(axes) 28 | return axes 29 | 30 | def mvt(): 31 | axes = makeAxes() 32 | 33 | mation = morpho.video.standardAnimation(axes) 34 | mainlayer = mation.layers[0] 35 | mainlayer.camera.first().centerAt(14+7j) 36 | 37 | mation.endDelay() 38 | 39 | a = 6 40 | b = 25 41 | tickHeight = 1 42 | 43 | ticka = morpho.grid.Arrow( 44 | a-tickHeight/2*1j, a+tickHeight/2*1j, 45 | width=5, headSize=0, color=[0,0,0] 46 | ) 47 | ticka = morpho.Actor(ticka) 48 | ticka.newkey(15) 49 | ticka.first().head = ticka.first().tail 50 | mainlayer.merge(ticka) 51 | 52 | tickb = morpho.grid.Arrow( 53 | b-tickHeight/2*1j, b+tickHeight/2*1j, 54 | width=5, headSize=0, color=[0,0,0] 55 | ) 56 | tickb = morpho.Actor(tickb) 57 | tickb.newkey(15) 58 | tickb.first().head = tickb.first().tail 59 | mainlayer.append(tickb) 60 | 61 | # Labels 62 | textHeight = 1.25*tickHeight 63 | time = mainlayer.lastID() 64 | labela = morpho.text.Text("a", pos=a, color=[0,0,0], size=64, anchor_x=0, italic=True, alpha=0) 65 | labela = morpho.Actor(labela) 66 | labela.newkey(15) 67 | labela.last().alpha = 1 68 | labela.last().pos -= textHeight*1j 69 | mainlayer.merge(labela, atFrame=time) 70 | 71 | labelb = labela.copy() 72 | for fig in labelb.keys(): 73 | fig.text = "b" 74 | fig.pos += b-a 75 | mainlayer.merge(labelb) 76 | 77 | mation.endDelay(15) 78 | 79 | c = 18 80 | w = 12.5 81 | h = 12 82 | f = lambda x: h*math.sqrt(1-(x-c)**2/w**2) 83 | graph = mo.graph.realgraph(f,a,b, steps=100, width=7, color=[0,0,1]) 84 | 85 | graph = morpho.Actor(graph) 86 | graph.newkey(30) 87 | graph.first().end = 0 88 | mainlayer.append(graph) 89 | 90 | # y ticks 91 | tickfa = morpho.grid.Arrow( 92 | 1j*f(a)-tickHeight/2, 1j*f(a)+tickHeight/2, 93 | width=5, headSize=0, color=[0,0,0] 94 | ) 95 | tickfa = morpho.Actor(tickfa) 96 | tickfa.newkey(15) 97 | tickfa.first().head = tickfa.first().tail 98 | mainlayer.append(tickfa) 99 | 100 | time = mation.lastID() 101 | linea = mo.grid.Arrow( 102 | f(a)*1j, a+f(a)*1j, color=[0,0,0], width=5, 103 | headSize=0 104 | ) 105 | linea.dash = [10] 106 | linea.zdepth = -1 107 | linea = mo.Actor(linea) 108 | linea.newendkey(20) 109 | linea.first().head = linea.first().tail 110 | mainlayer.merge(linea, atFrame=time) 111 | 112 | tickfb = morpho.grid.Arrow( 113 | 1j*f(b)-tickHeight/2, 1j*f(b)+tickHeight/2, 114 | width=5, headSize=0, color=[0,0,0] 115 | ) 116 | tickfb = morpho.Actor(tickfb) 117 | tickfb.newkey(15) 118 | tickfb.first().head = tickfb.first().tail 119 | mainlayer.merge(tickfb, atFrame=time) 120 | 121 | lineb = mo.grid.Arrow( 122 | f(b)*1j, b+f(b)*1j, color=[0,0,0], width=5, 123 | headSize=0 124 | ) 125 | lineb.dash = [10] 126 | lineb.zdepth = -1 127 | lineb = mo.Actor(lineb) 128 | lineb.newendkey(20) 129 | lineb.first().head = lineb.first().tail 130 | mainlayer.append(lineb) 131 | 132 | time = mainlayer.lastID() 133 | # textHeight *= 1.5 134 | labelfa = morpho.text.Text("f(a)", pos=f(a)*1j, color=[0,0,0], size=64, anchor_x=0, italic=True, alpha=0) 135 | labelfa = morpho.Actor(labelfa) 136 | labelfa.newkey(15) 137 | labelfa.last().alpha = 1 138 | labelfa.last().pos -= textHeight*1.5 139 | mainlayer.merge(labelfa, atFrame=time) 140 | 141 | labelfb = morpho.text.Text("f(b)", pos=f(b)*1j, color=[0,0,0], size=64, anchor_x=0, italic=True, alpha=0) 142 | labelfb = morpho.Actor(labelfb) 143 | labelfb.newkey(15) 144 | labelfb.last().alpha = 1 145 | labelfb.last().pos -= textHeight*1.5 146 | mainlayer.merge(labelfb, atFrame=time) 147 | 148 | mation.endDelay() 149 | 150 | time = mation.lastID() 151 | pta = mo.grid.Point(a+f(a)*1j, strokeWeight=3, size=20, alpha=0) 152 | pta = mo.Actor(pta) 153 | pta.newkey(15).alpha = 1 154 | mainlayer.merge(pta, atFrame=time) 155 | 156 | ptb = pta.copy() 157 | for fig in ptb.keys(): 158 | fig.pos = b+f(b)*1j 159 | mainlayer.merge(ptb) 160 | 161 | xmin = -4 162 | xmax = 32 163 | m = (f(b)-f(a))/(b-a) 164 | L = lambda x: m*(x-a) + f(a) 165 | secline = mo.graph.realgraph(L,xmin,xmax, steps=1, width=7, 166 | color=mo.color.colormap["green"] 167 | ) 168 | secline.zdepth = -1 169 | secline.end = 0 170 | secline = mo.Actor(secline) 171 | secline.newkey(30) 172 | secline.last().end = 1 173 | mainlayer.append(secline) 174 | 175 | mation.endDelay(15) 176 | 177 | # Secant formula 178 | secslope = mo.graphics.Image("./resources/secslope.png") 179 | secslope.pos = 16.5 + 7j 180 | secslope.align = [0,0] 181 | secslope.height = 0 182 | secslope.zdepth = 10 183 | # secslope.alpha = 0 184 | 185 | secslope = mo.Actor(secslope) 186 | secslope.newendkey(20) 187 | secslope.last().pos = a+5 + 2j 188 | secslope.last().height = 2 189 | # secslope.last().alpha = 1 190 | mainlayer.append(secslope) 191 | 192 | mation.endDelay() 193 | 194 | dx = 0.00001 195 | deriv = lambda f: (lambda x: (f(x+dx) - f(x-dx))/(2*dx)) 196 | df = deriv(f) 197 | dfcImg = morpho.graphics.Image("./resources/df.png") 198 | @morpho.SkitParameters({ 199 | "f":f, "df":df, "x":a+dx, "radius":5, "ptalpha":1, "alpha":1, 200 | "start":0, "end":1 201 | }) 202 | class Tanline(morpho.Skit): 203 | def makeFrame(self): 204 | x = self.x 205 | radius = self.radius 206 | ptalpha = self.ptalpha 207 | alpha = self.alpha 208 | f = self.f 209 | df = self.df 210 | 211 | m = df(x) 212 | y = f(x) 213 | T = lambda t: m*(t-x)+y 214 | xrad = radius/math.sqrt(1+m**2) 215 | tanline = mo.graph.realgraph(T, x-xrad, x+xrad, steps=1, 216 | color=[0,0,0], width=7, alpha=alpha 217 | ) 218 | tanline.start = self.start 219 | tanline.end = self.end 220 | 221 | pt = mo.grid.Point(x+y*1j, strokeWeight=3, 222 | fill=mo.color.colormap["violet"], alpha=alpha*ptalpha, size=20 223 | ) 224 | 225 | dfc = mo.graphics.Image(dfcImg) 226 | dfc.pos = pt.pos + 4j 227 | dfc.height = 1 228 | dfc.alpha = ptalpha*alpha 229 | 230 | arrow = mo.grid.Arrow(dfc.pos-0.75j, pt.pos+0.75j, 231 | width=5, color=mo.color.colormap["violet"], 232 | alpha=ptalpha*alpha 233 | ) 234 | 235 | vert = mo.grid.Arrow(x, pt.pos, 236 | color=[0,0,0], alpha=ptalpha*alpha, width=5, headSize=0 237 | ) 238 | vert.dash = [10] 239 | vert.zdepth = -1 240 | 241 | tickc = morpho.grid.Arrow( 242 | x-tickHeight/2*1j, x+tickHeight/2*1j, 243 | width=5, headSize=0, alpha=ptalpha*alpha, color=[0,0,0] 244 | ) 245 | 246 | labelc = morpho.text.Text( 247 | "c", pos=x-1j*textHeight, color=mo.color.colormap["violet"], size=64, 248 | anchor_x=0, italic=True, alpha=ptalpha*alpha 249 | ) 250 | 251 | return mo.Frame([tanline, pt, arrow, vert, tickc, labelc, dfc]) 252 | 253 | tanline = Tanline() 254 | tanline.zdepth = 1 255 | tanline.x = a+2 256 | tanline.end = 0 257 | tanline.ptalpha = 0 258 | 259 | tanline = morpho.Actor(tanline) 260 | mainlayer.append(tanline) 261 | 262 | tanline.newendkey(20) 263 | tanline.last().end = 1 264 | tanline.last().ptalpha = 1 265 | 266 | mation.endDelay() 267 | 268 | tanline.newendkey(60).x = b-2 269 | tanline.newendkey(60).x = a+4 270 | tanline.newendkey(45).x = 13.757 # First MVT point 271 | 272 | mation.endDelay() 273 | 274 | # Assert parallel 275 | tline = tanline.last().makeFrame().figures[0] 276 | tline.zdepth = -0.5 277 | tline = mo.Actor(tline) 278 | mainlayer.append(tline) 279 | 280 | tline.newendkey(20) 281 | for n in range(2): 282 | z = tline.last().seq[n] 283 | tline.last().seq[n] = z.real + 1j*(L(z.real)+0.25) 284 | 285 | mation.endDelay() 286 | 287 | tline.newendkey(20, tline.first().copy()) 288 | tline.last().visible = False 289 | 290 | mation.endDelay() 291 | 292 | # Construct equation 293 | time = mation.lastID() 294 | dfc = tanline.last().makeFrame().figures[-1] 295 | dfc.zdepth = 10 296 | dfc = mo.Actor(dfc) 297 | dfc.newendkey(30) 298 | dfc.last().pos = secslope.last().pos + secslope.last().width 299 | mainlayer.merge(dfc, atFrame=time) 300 | 301 | eq = mo.text.Text( 302 | "=", pos=16.4+1.7j, color=[0,0,0], 303 | size=84, anchor_x=0, alpha=0 304 | ) 305 | eq.zdepth = 10 306 | eq = mo.Actor(eq) 307 | eq.newendkey(30).alpha = 1 308 | mainlayer.merge(eq, atFrame=time) 309 | 310 | mation.endDelay() 311 | 312 | # Enbox formula 313 | time = mation.lastID() 314 | box = [6.5,20.5, 0.5,3.5] 315 | boxer = mo.gadgets.enbox(box, width=5) 316 | for key in boxer.keys(): 317 | key.zdepth = 10 318 | mainlayer.append(boxer) 319 | 320 | whitebox = mo.grid.rect(box) 321 | whitebox.width = 0 322 | whitebox.fill = [1,1,1] 323 | whitebox.alpha = 0 324 | whitebox.zdepth = 9 325 | whitebox = mo.Actor(whitebox) 326 | whitebox.newendkey(30).alpha = 1 327 | mainlayer.merge(whitebox, atFrame=time) 328 | 329 | mation.endDelay() 330 | 331 | boxer.newendkey(20) 332 | boxer.last().alpha = 0 333 | boxer.last().visible = False 334 | 335 | whitebox.newendkey(20) 336 | whitebox.last().alpha = 0 337 | whitebox.last().visible = False 338 | 339 | # Morph graph 340 | time = mation.lastID() 341 | f2 = lambda x: 4*math.sin(-x/2.5)+8 342 | graph2 = mo.graph.realgraph(f2,a,b, steps=100, width=7, color=[0,0,1]) 343 | graph.newkey(time) 344 | graph.newendkey(30, graph2) 345 | 346 | # Morph secline 347 | m2 = (f2(b) - f2(a))/(b-a) 348 | L2 = lambda x: m2*(x-a) + f2(a) 349 | secline2 = mo.graph.realgraph(L2,xmin,xmax, steps=1, width=7, 350 | color=mo.color.colormap["green"] 351 | ) 352 | secline2.zdepth = -1 353 | secline.newkey(time) 354 | secline.newendkey(30, secline2) 355 | 356 | pta.newkey(time) 357 | pta.newendkey(30).pos = a + 1j*f2(a) 358 | 359 | ptb.newkey(time) 360 | ptb.newendkey(30).pos = b + 1j*f2(b) 361 | 362 | linea.newkey(time) 363 | linea.newendkey(30) 364 | linea.last().tail = f2(a)*1j 365 | linea.last().head = a + f2(a)*1j 366 | 367 | lineb.newkey(time) 368 | lineb.newendkey(30) 369 | lineb.last().tail = f2(b)*1j 370 | lineb.last().head = b + f2(b)*1j 371 | 372 | tickfa.newkey(time) 373 | tickfa.newendkey(30) 374 | tickfa.last().tail += (f2(a)-f(a))*1j 375 | tickfa.last().head += (f2(a)-f(a))*1j 376 | 377 | tickfb.newkey(time) 378 | tickfb.newendkey(30) 379 | tickfb.last().tail += (f2(b)-f(b))*1j 380 | tickfb.last().head += (f2(b)-f(b))*1j 381 | 382 | labelfa.newkey(time) 383 | labelfa.newendkey(30) 384 | labelfa.last().pos += (f2(a)-f(a))*1j 385 | 386 | labelfb.newkey(time) 387 | labelfb.newendkey(30) 388 | labelfb.last().pos += (f2(b)-f(b))*1j 389 | 390 | tanline.newkey(time) 391 | tanline.newendkey(30) 392 | tanline.last().f = f2 393 | tanline.last().df = deriv(f2) 394 | tanline.last().radius = 3.5 395 | 396 | mation.endDelay() 397 | 398 | # Find both new MVT points 399 | mvt1 = 11.378 400 | mvt2 = 20.038 401 | 402 | tanline.newendkey(45).x = mvt2 403 | 404 | mation.endDelay(15) 405 | 406 | tan2 = tanline.last().makeFrame().figures[0] 407 | tan2.zdepth = 10 408 | mainlayer.append(tan2) 409 | pt2 = tanline.last().makeFrame().figures[1] 410 | pt2.zdepth = 10 411 | mainlayer.append(pt2) 412 | 413 | tanline.newendkey(60).x = mvt1 414 | 415 | tan1 = tanline.last().makeFrame().figures[0] 416 | tan1.zdepth = 10 417 | mainlayer.append(tan1) 418 | pt1 = tanline.last().makeFrame().figures[1] 419 | pt1.zdepth = 10 420 | mainlayer.append(pt1) 421 | 422 | mation.endDelay(15) 423 | 424 | tanline.last().transition = mo.transition.uniform 425 | tanline.newendkey(30).alpha = 0 426 | 427 | mation.endDelay(30*3) 428 | 429 | mation.finitizeDelays(60) 430 | 431 | mation.locatorLayer = 0 432 | mation.play() 433 | 434 | mvt() -------------------------------------------------------------------------------- /gallery/code/pendulum.py: -------------------------------------------------------------------------------- 1 | import morpholib as morpho 2 | morpho.importAll() 3 | 4 | from morpholib.tools.basics import * 5 | 6 | import math, cmath 7 | 8 | 9 | def pendulum(): 10 | mainlayer = morpho.Layer() 11 | mation = morpho.Animation(mainlayer) 12 | 13 | thetamax = pi/6 # Hard code thetamax 14 | length = 3 # Hard code pendulum string length 15 | class Pendulum(morpho.Skit): 16 | def makeFrame(self): 17 | t = self.t 18 | 19 | theta = thetamax*math.sin(t) 20 | 21 | # Create pendulum string 22 | string = morpho.grid.Path([0, -length*1j]) 23 | string.rotation = theta 24 | # Commit the rotation so that the string's 25 | # final node can be used to position the ball. 26 | string.commitTransforms() 27 | 28 | # Create the ball hanging on the string. 29 | # Its position is equal to the position of the 30 | # final node of the string path 31 | ball = morpho.grid.Point() 32 | ball.pos = string.seq[-1] 33 | ball.strokeWeight = string.width 34 | ball.color = [1,1,1] # Ball border is white 35 | ball.size = 40 # Make it 40 pixels wide 36 | 37 | # Create neutral vertical dashed line 38 | neutral = morpho.grid.Path([0, -length*1j]) 39 | neutral.dash = [10,10] 40 | 41 | # Create connecting arc 42 | arc = morpho.shapes.EllipticalArc( 43 | pos=0, xradius=1, yradius=1, 44 | theta0=-pi/2, theta1=-pi/2+theta, 45 | ) 46 | 47 | # Create theta label 48 | thetaLabel = morpho.text.Text("\u03b8", # Unicode for theta 49 | pos=1.5*cmath.exp(1j*mean([arc.theta0, arc.theta1])), 50 | size=min(36, 36*abs(theta/0.36)), italic=True 51 | ) 52 | 53 | thetanum = morpho.text.formatNumber(theta*180/pi, decimal=0) 54 | tracker = morpho.text.Text( 55 | "\u03b8 = "+thetanum+"\u00b0", 56 | pos=1j, size=56 57 | ) 58 | 59 | return morpho.Frame([neutral, arc, thetaLabel, string, ball, tracker]) 60 | 61 | pend = mainlayer.Actor(Pendulum()) 62 | 63 | # Set internal time parameter t to be 6pi after 5 seconds 64 | # (150 frames) have passed in the animation's clock. 65 | pend.newendkey(150).t = 6*pi 66 | 67 | mation.play() 68 | 69 | pendulum() 70 | -------------------------------------------------------------------------------- /gallery/code/resources/df.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morpho-matters/morpholib/f7b029e85b941744cf89ca1729105cf428e2d7cf/gallery/code/resources/df.png -------------------------------------------------------------------------------- /gallery/code/resources/mvt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morpho-matters/morpholib/f7b029e85b941744cf89ca1729105cf428e2d7cf/gallery/code/resources/mvt.png -------------------------------------------------------------------------------- /gallery/code/resources/secslope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morpho-matters/morpholib/f7b029e85b941744cf89ca1729105cf428e2d7cf/gallery/code/resources/secslope.png -------------------------------------------------------------------------------- /gallery/code/sample.py: -------------------------------------------------------------------------------- 1 | import morpholib as morpho 2 | morpho.importAll() 3 | 4 | 5 | def main(): 6 | mainlayer = morpho.Layer() 7 | mation = morpho.Animation(mainlayer) 8 | 9 | grid0 = morpho.grid.mathgrid( 10 | tweenMethod=morpho.grid.Path.tweenSpiral, 11 | transition=morpho.transition.quadease 12 | ) 13 | 14 | grid = mainlayer.Actor(grid0) 15 | 16 | grid.newendkey(60, grid0.fimage(lambda s: s**2/10)) 17 | mation.wait(30) 18 | 19 | grid.newendkey(60, grid0.fimage(lambda s: s**3/64)) 20 | mation.wait(30) 21 | 22 | grid.newendkey(60, grid0.fimage(lambda s: s**4/8**3)) 23 | mation.wait(30) 24 | 25 | grid.newendkey(60, grid0.fimage(lambda s: s**5/8**4)) 26 | mation.wait(30) 27 | 28 | grid.newendkey(60, grid0.copy()) 29 | 30 | mation.play() 31 | 32 | main() 33 | -------------------------------------------------------------------------------- /gallery/code/tanline.py: -------------------------------------------------------------------------------- 1 | import morpholib as morpho 2 | morpho.importAll() 3 | 4 | from morpholib.tools.basics import * 5 | 6 | import math, cmath 7 | 8 | 9 | def tanline(): 10 | mainlayer = morpho.Layer() 11 | mation = morpho.Animation(mainlayer) 12 | 13 | f = lambda x: 0.2*(x**3 - 12*x) 14 | path = mainlayer.Actor(morpho.graph.realgraph(f, -4, 4)) 15 | 16 | # Define a numerical derivative function 17 | dx = 0.000001 # A small change in x 18 | df = lambda x: (f(x+dx)-f(x-dx))/(2*dx) 19 | 20 | @morpho.SkitParameters(t=-4, length=4, alpha=1) 21 | class TangentLine(morpho.Skit): 22 | def makeFrame(self): 23 | # t will represent the input to the function f 24 | t = self.t 25 | length = self.length 26 | alpha = self.alpha 27 | 28 | # Initialize tangent line to be a horizontal 29 | # line segment of length 4 centered at the 30 | # origin 31 | line = morpho.grid.Path([-length/2, length/2]) 32 | line.color = [1,0,0] # Red color 33 | line.alpha = alpha 34 | 35 | # Compute derivative 36 | slope = df(t) 37 | # Convert into an angle and set it as the rotation 38 | # of the line segment 39 | angle = math.atan(slope) 40 | line.rotation = angle 41 | 42 | # Position the tangent line at the tangent point 43 | x = t 44 | y = f(t) 45 | line.origin = x + 1j*y 46 | 47 | # Create derivative tracker 48 | slopenum = morpho.text.formatNumber(slope, decimal=3, rightDigits=3) 49 | dlabel = morpho.text.Text("Slope = "+slopenum, 50 | pos=line.origin, anchor_y=-1, 51 | size=36, color=[1,1,0], alpha=alpha 52 | ) 53 | dlabel.rotation = angle 54 | 55 | return morpho.Frame([line, dlabel]) 56 | 57 | # Initialize the tangent line Skit 58 | tanline = mainlayer.Actor(TangentLine(t=-4, length=0)) 59 | tanline.first().transition = morpho.transitions.quadease 60 | 61 | # Set t to +4 over the course of 5 seconds (150 frames) 62 | tanline.newendkey(150).set(t=4, length=4) 63 | 64 | # Finally, fade the tangent line to invisibility 65 | tanline.newendkey(30).alpha = 0 66 | 67 | mation.play() 68 | 69 | tanline() 70 | -------------------------------------------------------------------------------- /gallery/code/torus.py: -------------------------------------------------------------------------------- 1 | import morpholib as morpho 2 | mo = morpho 3 | morpho.importAll() 4 | 5 | from morpholib.tools.basics import * 6 | 7 | import math, cmath 8 | 9 | morpho.transitions.default = morpho.transitions.quadease 10 | 11 | goodblue = tuple((mo.array([0,0.5,1])).tolist()) 12 | 13 | 14 | def torus(): 15 | mation = morpho.video.setupSpaceAlt() 16 | mation.windowShape = (600,600) 17 | mation.fullscreen = False 18 | 19 | mainlayer = mation.layers[0] 20 | xmin,xmax, ymin,ymax = mainlayer.camera.first().view 21 | for keyfig in mainlayer.camera.keys(): 22 | keyfig.view = [ymin,ymax,ymin,ymax] 23 | keyfig.moveBy(-1j) 24 | 25 | meshlayer = morpho.SpaceLayer(view=mainlayer.camera.copy()) 26 | mation.merge(meshlayer) 27 | 28 | mesh = morpho.grid.quadgrid( 29 | view=[0,tau, 0,tau], 30 | dx=tau/16, dy=tau/24, 31 | width=1.5, color=[0,0,0], 32 | fill=goodblue, fill2=(mo.array(goodblue)/2).tolist() 33 | ) 34 | mesh.shading = True 35 | 36 | R = 2 37 | r = 0.75 38 | def tubify(v): 39 | theta, phi, dummy = v 40 | 41 | x = r*np.cos(theta+pi/2) 42 | y = phi-3 43 | z = r*np.cos(theta) + r 44 | 45 | return morpho.array([x,y,z]) 46 | 47 | def torify(v): 48 | theta, phi, dummy = v 49 | 50 | x = (R+r*np.cos(theta-pi/2))*np.cos(phi) 51 | y = -(R+r*np.cos(theta-pi/2))*np.sin(phi) 52 | z = r*np.cos(theta) + r 53 | 54 | return morpho.array([x,y,z]) 55 | 56 | torus = meshlayer.Actor(morpho.grid.quadgrid( 57 | view=[-3,3, -3,3], 58 | dx=6/16, dy=6/24, 59 | width=1.5, color=[0,0,0], 60 | fill=goodblue, fill2=(mo.array(goodblue)/2).tolist() 61 | ), atFrame=0) 62 | torus.first().shading = True 63 | torus.newendkey(45) 64 | torus.newendkey(60, mesh.fimage(tubify)) 65 | torus.newendkey(45) 66 | torus.newendkey(60, mesh.fimage(torify)) 67 | meshlayer.merge(torus) 68 | 69 | # Change up camera 70 | mainlayer.camera.movekey(mainlayer.camera.lastID(), torus.lastID() + 90) 71 | meshlayer.camera = mainlayer.camera.copy() 72 | 73 | mation.newFrameRate(10) 74 | mation.play() 75 | 76 | torus() 77 | -------------------------------------------------------------------------------- /gallery/epicycle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morpho-matters/morpholib/f7b029e85b941744cf89ca1729105cf428e2d7cf/gallery/epicycle.gif -------------------------------------------------------------------------------- /gallery/epicycle.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morpho-matters/morpholib/f7b029e85b941744cf89ca1729105cf428e2d7cf/gallery/epicycle.mp4 -------------------------------------------------------------------------------- /gallery/mvt.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morpho-matters/morpholib/f7b029e85b941744cf89ca1729105cf428e2d7cf/gallery/mvt.mp4 -------------------------------------------------------------------------------- /gallery/pendulum.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morpho-matters/morpholib/f7b029e85b941744cf89ca1729105cf428e2d7cf/gallery/pendulum.gif -------------------------------------------------------------------------------- /gallery/pendulum.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morpho-matters/morpholib/f7b029e85b941744cf89ca1729105cf428e2d7cf/gallery/pendulum.mp4 -------------------------------------------------------------------------------- /gallery/sample.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morpho-matters/morpholib/f7b029e85b941744cf89ca1729105cf428e2d7cf/gallery/sample.mp4 -------------------------------------------------------------------------------- /gallery/tanline.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morpho-matters/morpholib/f7b029e85b941744cf89ca1729105cf428e2d7cf/gallery/tanline.mp4 -------------------------------------------------------------------------------- /gallery/torus.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morpho-matters/morpholib/f7b029e85b941744cf89ca1729105cf428e2d7cf/gallery/torus.mp4 -------------------------------------------------------------------------------- /logo/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morpho-matters/morpholib/f7b029e85b941744cf89ca1729105cf428e2d7cf/logo/logo-white.png -------------------------------------------------------------------------------- /logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morpho-matters/morpholib/f7b029e85b941744cf89ca1729105cf428e2d7cf/logo/logo.png -------------------------------------------------------------------------------- /morpholib/__init__.py: -------------------------------------------------------------------------------- 1 | from morpholib.base import * 2 | from morpholib.figure import * 3 | from morpholib.anim import Layer, Animation, Frame, SpaceFrame, \ 4 | Skit, SpaceSkit, SkitParameters, SpaceLayer, MultiFigure, SpaceMultiFigure, \ 5 | Camera, SpaceCamera 6 | import morpholib.color as color 7 | from morpholib.matrix import array 8 | from morpholib.actions import action, subaction 9 | 10 | from morpholib.tools.subimporter import import_submodules 11 | 12 | def importAll(): 13 | import_submodules(morpholib) 14 | -------------------------------------------------------------------------------- /morpholib/actions.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This module contains some helper functions that automate some common 3 | animation actions. 4 | ''' 5 | 6 | import morpholib as morpho 7 | from morpholib.tools.basics import aslist 8 | import math 9 | 10 | autoJumpNames = {"pos", "origin", "_pos", "_origin"} 11 | 12 | # Applies the jump amount `dz` to all supported tweenables 13 | # in the given figure's state 14 | def _applyJump(figure, dz): 15 | # Add dz in place to all supported tweenables 16 | for tweenable in figure._state.values(): 17 | if (tweenable.name in autoJumpNames and "nojump" not in tweenable.tags) \ 18 | or ("jump" in tweenable.tags): 19 | 20 | tweenable.value = tweenable.value + dz 21 | 22 | # This clause works, but doesn't handle alpha. 23 | # I've decided not to use it for now. 24 | # elif "figures" in tweenable.tags: 25 | # figlist = tweenable.value 26 | # for fig in figlist: 27 | # _applyJump(fig, dz) 28 | 29 | # Convenience function fades out a collection of actors and then 30 | # sets the visibility attribute of the final keyfigure of each actor 31 | # to False. Useful for dismissing actors from a scene. 32 | # 33 | # NOTE: Only works for figures that possess an "alpha" tweenable, so 34 | # this function may not work for custom figures like Skits unless you 35 | # implement an alpha tweenable yourself. 36 | # 37 | # PARAMETERS 38 | # actors = List of actors (or a single actor) 39 | # duration = Duration (in frames) of fade out for each actor. 40 | # Default: 30 41 | # atFrame = Frame index at which to begin fade out. 42 | # Default: the latest keyindex among all the given actors. 43 | # stagger = Number of frames to wait between starting fade outs of 44 | # adjacent actors in the list. Leads to a "staggering" effect. 45 | # Example: stagger=5 means the second actor will start fading 46 | # 5 frames after the first actor STARTS fading. Likewise, 47 | # the third actor will begin fading 5 frames after the 48 | # second actor starts fading, and so on. 49 | # `stagger` can also be a sequence of numbers for variable 50 | # stagger values. If the sequence exhausts early, the sequence 51 | # will repeat. 52 | # Default: 0 (meaning all actors begin fading at the same time) 53 | # KEYWORD ONLY 54 | # jump = Displacement each actor should "jump" during the fade out. 55 | # Example: jump=2j causes each actor to jump up by 2 units; 56 | # `jump` can also be a list to specify different jump vectors 57 | # for each actor in the `actors` list. If the list is too short, 58 | # it will loop back to the start. 59 | # Default: () empty tuple (meaning no jumps) 60 | def fadeOut(actors, duration=30, atFrame=None, stagger=0, *, jump=()): 61 | # if not isinstance(actors, list) and not isinstance(actors, tuple): 62 | # actors = [actors] 63 | # Turn into a list if necessary 64 | if isinstance(actors, morpho.Actor): 65 | actors = [actors] 66 | 67 | stagger = aslist(stagger) 68 | 69 | if atFrame is None: 70 | atFrame = max(actor.lastID() for actor in actors) 71 | 72 | # # If jump isn't a subscriptable type, turn it into a singleton list 73 | # if not hasattr(jump, "__getitem__"): 74 | 75 | # If jump isn't a list or tuple, turn it into one. 76 | if not isinstance(jump, list) and not isinstance(jump, tuple): 77 | jump = [jump] 78 | 79 | offset = 0 80 | for n in range(len(actors)): 81 | actor = actors[n] 82 | actor.newkey(atFrame+offset) 83 | offset += stagger[n % len(stagger)] 84 | keyfig = actor.newendkey(duration) 85 | keyfig.alpha = 0 86 | keyfig.visible = False 87 | if len(jump) > 0: 88 | dz = jump[n%len(jump)] 89 | _applyJump(keyfig, dz) 90 | 91 | # # Add dz in place to all supported tweenables 92 | # for tweenable in keyfig._state.values(): 93 | # if (tweenable.name in autoJumpNames and "nojump" not in tweenable.tags) \ 94 | # or ("jump" in tweenable.tags): 95 | 96 | # tweenable.value += dz 97 | 98 | # # Add dz in place to either the "pos" or "origin" 99 | # # attributes (if they exist). 100 | # if hasattr(keyfig, "pos"): 101 | # keyfig.pos += dz 102 | # elif hasattr(keyfig, "origin"): 103 | # keyfig.origin += dz 104 | # else: 105 | # raise TypeError(f"{type(keyfig)} figure cannot be jumped.") 106 | 107 | # Similar to fadeOut(), but fades in actors from invisibility. 108 | # See fadeOut() for more info. 109 | # 110 | # UNIQUE PARAMETERS 111 | # alpha = Final alpha value (keyword only). 112 | # Like `jump`, it can also be a list of alphas to be applied 113 | # to the actors list. 114 | # 115 | # NOTE: Only works for figures that possess an "alpha" tweenable, so 116 | # this function may not work for custom figures like Skits unless you 117 | # implement an alpha tweenable yourself. 118 | # 119 | # Also note that this function will force alpha=0 and visible=False 120 | # for the current final keyfigure in each actor before applying 121 | # the effect. 122 | def fadeIn(actors, duration=30, atFrame=None, stagger=0, *, 123 | jump=(), alpha=(1,)): 124 | 125 | # if not isinstance(actors, list) and not isinstance(actors, tuple): 126 | # actors = [actors] 127 | # Turn into a list if necessary 128 | 129 | if isinstance(actors, morpho.Actor): 130 | actors = [actors] 131 | 132 | if atFrame is None: 133 | atFrame = max(actor.lastID() for actor in actors) 134 | 135 | stagger = aslist(stagger) 136 | 137 | # # If jump isn't a subscriptable type, turn it into a singleton list 138 | # if not hasattr(jump, "__getitem__"): 139 | 140 | # If jump isn't a list or tuple, turn it into one. 141 | if not isinstance(jump, list) and not isinstance(jump, tuple): 142 | jump = [jump] 143 | 144 | # if not hasattr(alpha, "__getitem__"): 145 | if not isinstance(alpha, list) and not isinstance(alpha, tuple): 146 | alpha = [alpha] 147 | 148 | offset = 0 149 | for n in range(len(actors)): 150 | actor = actors[n] 151 | actor.last().set(alpha=0, visible=False) 152 | keyfigInit = actor.newkey(atFrame+offset) 153 | offset += stagger[n % len(stagger)] 154 | keyfigInit.visible = True 155 | keyfig = actor.newendkey(duration) 156 | keyfig.alpha = alpha[n%len(alpha)] 157 | if len(jump) > 0: 158 | dz = jump[n%len(jump)] 159 | _applyJump(keyfigInit, -dz) 160 | 161 | # # Subtract dz in place from all supported tweenables 162 | # for tweenable in keyfigInit._state.values(): 163 | # if (tweenable.name in autoJumpNames and "nojump" not in tweenable.tags) \ 164 | # or ("jump" in tweenable.tags): 165 | 166 | # tweenable.value -= dz 167 | 168 | # # Subtract dz in place from either the "pos" or "origin" 169 | # # attributes (if they exist). 170 | # if hasattr(keyfig, "pos"): 171 | # keyfigInit.pos -= dz 172 | # elif hasattr(keyfig, "origin"): 173 | # keyfigInit.origin -= dz 174 | # else: 175 | # raise TypeError(f"{type(keyfigInit)} figure cannot be jumped.") 176 | 177 | # Convenience function tweens an actor back to its first keyfigure 178 | # and then sets the visibility attribute of the final keyfigure of 179 | # each actor set to False. 180 | # 181 | # Intended to be used when the the first keyfigure of an actor 182 | # is invisible or offscreen. It's a nice way to make an actor 183 | # gracefully leave a scene the same way it entered. 184 | # 185 | # PARAMETERS 186 | # actors = List of actors (or a single actor) 187 | # duration = Duration (in frames) of rollback animation for each actor. 188 | # Default: 30 189 | # atFrame = Frame index at which to begin rollback animation. 190 | # Default: the latest keyindex among all the given actors. 191 | # stagger = Number of frames to wait between starting rollbacks of 192 | # adjacent actors in the list. Leads to a "staggering" effect. 193 | # Example: stagger=5 means the second actor will start rolling 194 | # back 5 frames after the first actor STARTS fading. Likewise, 195 | # the third actor will begin rolling back 5 frames after the 196 | # second actor starts rolling back, and so on. 197 | # `stagger` can also be a sequence of numbers for variable 198 | # stagger values. If the sequence exhausts early, the sequence 199 | # will repeat. 200 | # Default: 0 (all actors begin rolling back at the same time) 201 | def rollback(actors, duration=30, atFrame=None, stagger=0): 202 | 203 | stagger = aslist(stagger) 204 | 205 | # Turn into a list if necessary 206 | if isinstance(actors, morpho.Actor): 207 | actors = [actors] 208 | if atFrame is None: 209 | atFrame = max(actor.lastID() for actor in actors) 210 | 211 | offset = 0 212 | for n in range(len(actors)): 213 | actor = actors[n] 214 | actor.newkey(atFrame+offset) 215 | offset += stagger[n % len(stagger)] 216 | actor.newendkey(duration, actor.first().copy()) 217 | actor.last().visible = False 218 | 219 | # NOT IMPLEMENTED! 220 | # Transforms one figure into another using a fade effect. 221 | # Requires the underlying figures to have "pos" and "alpha" 222 | # attributes to work. 223 | # Returns an animation in which fig move toward pig while 224 | # fading into pig. 225 | def transform(fig, pig, time=30): 226 | raise NotImplementedError 227 | 228 | # Wiggles the actor by rotating it about its origin point 229 | # a set number of times by a certain angle. 230 | # Note that this is only possible if the actor's figure type 231 | # supports the `rotation` transformation attribute. 232 | # 233 | # INPUTS 234 | # duration = Total duration in frames for action. Default: 30 frames 235 | # atFrame = Initial frame to use. Default: None (latest keyframe) 236 | # KEYWORD-ONLY INPUTS 237 | # rotation = Rotation angle in radians. Can be negative to start with a 238 | # clockwise rotation. Default: pi/6 (30 degs) 239 | # times = Number of times to rotate by a full swing. Default: 1 240 | def wiggle(actor, duration=30, atFrame=None, *, 241 | rotation=math.pi/6, times=1): 242 | 243 | if atFrame is None: 244 | atFrame = actor.lastID() 245 | 246 | path0 = actor.last() 247 | final = path0.copy() 248 | 249 | tstep = duration / (2*times + 2) 250 | actor.newkey(atFrame) 251 | # Not using newendkey() because intermediate rounding 252 | # may throw off the time coordinates. 253 | actor.newkey(atFrame + tstep).rotation = rotation 254 | for n in range(1, times+1): 255 | actor.newkey(atFrame + (2*n + 1)*tstep).rotation *= -1 256 | actor.newkey(atFrame+duration, final) 257 | 258 | 259 | # Multi-action summoner 260 | # Mainly for internal use. Used to automatically implement 261 | # morpho.actions.action that automatically implements multi-action 262 | # versions of registered actor actions. 263 | # See "morpho.actions.action" for more info. 264 | class MultiActionSummoner(object): 265 | @staticmethod 266 | def getActionFromName(actor, actionName): 267 | return getattr(actor, actionName) 268 | 269 | # Prepares a given action for use with a particular actor. 270 | @staticmethod 271 | def actionDecorator(action, actor): 272 | def decoratedAction(*args, **kwargs): 273 | return action(actor, *args, **kwargs) 274 | return decoratedAction 275 | 276 | def makeMultiAction(self, action): 277 | def multiaction(actors, *args, stagger=0, **kwargs): 278 | if isinstance(actors, morpho.Actor): 279 | actors = [actors] 280 | elif isinstance(actors, morpho.Layer): 281 | actors = actors.actors 282 | 283 | stagger = aslist(stagger) 284 | 285 | now = max(actor.lastID() for actor in actors) 286 | for n,actor in enumerate(actors): 287 | if isinstance(action, str): 288 | try: 289 | action_n = self.getActionFromName(actor, action) 290 | except AttributeError: 291 | raise AttributeError(f"'{actor.figureType.__name__}' does not implement action '{action}'") 292 | else: 293 | action_n = self.actionDecorator(action, actor) 294 | 295 | if now not in actor.timeline: 296 | actor.newkey(now) 297 | action_n(*args, **kwargs) 298 | if stagger != [0]: 299 | # Go thru the actors and shift them by the stagger 300 | # amount starting at the present. Also preserve the 301 | # keyfigure at the original present time. 302 | offset = 0 303 | for n,actor in enumerate(actors[1:], start=1): 304 | # Post-action present-state of the actor 305 | present = actor.time(now).copy() 306 | offset += stagger[n % len(stagger)] 307 | actor.shiftAfter(now-1, offset) 308 | actor.newkey(now, present, seamless=False) 309 | 310 | return multiaction 311 | 312 | def __getattr__(self, actionName): 313 | return self.makeMultiAction(actionName) 314 | 315 | def __call__(self, action, *args, **kwargs): 316 | multiaction = self.makeMultiAction(action) 317 | return multiaction(*args, **kwargs) 318 | 319 | class MultiSubactionSummoner(MultiActionSummoner): 320 | @staticmethod 321 | def getActionFromName(actor, actionName): 322 | return getattr(actor.subaction, actionName) 323 | 324 | @staticmethod 325 | def actionDecorator(action, actor): 326 | return actor.subaction.makeSubaction(action) 327 | 328 | 329 | # Used to automatically implement multi-actions 330 | # for custom actions similar to how fadeIn/Out() and rollback() work. 331 | # For example, if you have implemented an action called "myaction" 332 | # for a certain figure type, and you have a list of actors of that 333 | # figure type, you can apply `myaction` to the whole list, with optional 334 | # `stagger`, using the syntax 335 | # morpho.action.myaction(myactorlist, *args, [atFrame=etc, stagger=0], **kwargs) 336 | # Please note that a user-specified `atFrame` value must be specified 337 | # by keyword in an auto-generated multi-action. Same with `stagger`. 338 | action = MultiActionSummoner() 339 | 340 | # Used to automatically implement multi-subactions used to 341 | # apply a subaction to multiple Frame-like actors. 342 | # See also: `action` 343 | # 344 | # Usage example: 345 | # subaction.fadeIn(actorlist, 20, substagger=3, stagger=10) 346 | subaction = MultiSubactionSummoner() 347 | -------------------------------------------------------------------------------- /morpholib/bezier.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Various helper functions for dealing with cubic Bezier curves. 3 | ''' 4 | 5 | # import math, cmath 6 | import numpy as np 7 | 8 | from morpholib.base import lerp0, bezierInterp 9 | from morpholib.tools import polyroots 10 | 11 | # Splits the cubic Bezier curve described by p0...p3 at the 12 | # parameter value t (in interval [0,1]), and returns the control 13 | # points describing the first sub-Bezier curve: the part of the 14 | # curve for lower parameter values than t. 15 | def bezierFirstSlice(p0, p1, p2, p3, t): 16 | p01 = lerp0(p0, p1, t) 17 | p12 = lerp0(p1, p2, t) 18 | p012 = lerp0(p01, p12, t) 19 | p0123 = bezierInterp(p0, p1, p2, p3, t) 20 | 21 | return (p0, p01, p012, p0123) 22 | 23 | # Splits the cubic Bezier curve described by p0...p3 at the 24 | # parameter value t (in interval [0,1]), and returns the control 25 | # points describing the last sub-Bezier curve: the part of the 26 | # curve for higher parameter values than t. 27 | def bezierLastSlice(p0, p1, p2, p3, t): 28 | return bezierFirstSlice(p3, p2, p1, p0, 1-t)[::-1] 29 | bezierSecondSlice = bezierLastSlice 30 | 31 | # Splits the cubic Bezier curve described by p0...p3 at the 32 | # parameter value t (in interval [0,1]), and returns two 4-tuples 33 | # of control points describing the two sub-Bezier curves on either 34 | # side. 35 | def splitBezier(p0, p1, p2, p3, t): 36 | slice1 = bezierFirstSlice(p0, p1, p2, p3, t) 37 | slice2 = bezierLastSlice(p0, p1, p2, p3, t) 38 | return slice1, slice2 39 | 40 | # Inverts the bezier function to find the parameter t value 41 | # for a given 2d point z (as a complex number) that is 42 | # assumed to be on the bezier curve. 43 | # 44 | # Returns all solutions found as a list. 45 | # Usually the list will just contain a single solution, 46 | # but it could contain two (self-intersecting curve) 47 | # or none (given point not on the curve). 48 | def invertBezier2d(p0, p1, p2, p3, z, *, tol=1e-9): 49 | if np.allclose([p0, p1, p2, p3], p0): 50 | raise ValueError("Cannot invert a constant Bezier curve!") 51 | 52 | # Calculate coefficients of the polynomial equation 53 | # bezier(t) - z = 0 54 | coeffs = np.array([ 55 | p0 - z, 56 | 3*(p1 - p0), 57 | 3*(p2 - 2*p1 + p0), 58 | p3 - 3*p2 + 3*p1 - p0 59 | ]) 60 | 61 | coeffs_x = coeffs.real 62 | coeffs_y = coeffs.imag 63 | 64 | verticalCurve = np.allclose(coeffs_x, 0) 65 | horizontalCurve = np.allclose(coeffs_y, 0) 66 | # If one of the coeff lists is zero 67 | # (meaning it's a horizontal or vertical curve), 68 | # rotate all points by 45 degrees and try again. 69 | if horizontalCurve or verticalCurve: 70 | p0 = (1+1j)*p0 71 | p1 = (1+1j)*p1 72 | p2 = (1+1j)*p2 73 | p3 = (1+1j)*p3 74 | z = (1+1j)*z 75 | return invertBezier2d(p0, p1, p2, p3, z) 76 | 77 | 78 | # Solve for the roots 79 | txlist = polyroots(coeffs.real).tolist() 80 | tylist = polyroots(coeffs.imag).tolist() 81 | 82 | # Filter out any common real roots 83 | solns = [] 84 | for tx in txlist: 85 | # Skip the tx solutions that are obviously non-real 86 | # or are out of the range [0,1] 87 | if abs(tx.imag) > tol or not(0 <= tx.real <= 1): 88 | continue 89 | for ty in tylist: 90 | if abs(tx - ty) < tol: 91 | solns.append(tx) 92 | 93 | return solns 94 | 95 | # NOT IMPLEMENTED YET. 96 | # Converts a list of Catmull-Rom spline control points to their 97 | # equivalent Bezier spline control points. 98 | def CatmullRomToBezier(pts): 99 | raise NotImplementedError 100 | bezierPts = list(pts) 101 | for n in range(0, len(pts)-1, 3): 102 | p0, p1, p2, p3 = pts[n: n+4] 103 | bezierPts[n: n+4] = (p1, p1 + (p2-p0)/6, p2 - (p3-p1)/6, p2) 104 | return bezierPts 105 | 106 | # Converts a quadratic Bezier curve into the equivalent cubic 107 | # Bezier curve. 108 | # 109 | # INPUTS: 110 | # q0,q1,q2 = Quadratic Bezier curve control points 111 | # 112 | # RETURNS: 113 | # p0,p1,p2,p3 = Equivalent cubic Bezier curve control points 114 | def quad2cubic(q0, q1, q2): 115 | p0 = q0 116 | p1 = q0 + (2/3)*(q1 - q0) 117 | p2 = q2 + (2/3)*(q1 - q2) 118 | p3 = q2 119 | return (p0, p1, p2, p3) 120 | 121 | # NOT IMPLEMENTED BECAUSE OF UNRESOLVED BUG 122 | # Returns the tight bounding box of a cubic Bezier curve. 123 | def cubicbox(p0, p1, p2, p3): 124 | raise NotImplementedError 125 | 126 | import svgelements as se 127 | xmin, ymin, xmax, ymax = np.array(se.CubicBezier(p0, p1, p2, p3).bbox()).tolist() 128 | return [xmin, xmax, ymin, ymax] 129 | -------------------------------------------------------------------------------- /morpholib/gadgets.py: -------------------------------------------------------------------------------- 1 | import morpholib as morpho 2 | import morpholib.anim 3 | import morpholib.grid 4 | from morpholib.tools.basics import * 5 | from morpholib.tools.dev import handleBoxTypecasting 6 | 7 | import math, cmath 8 | 9 | # DEPRECATED! Use crossout() or crossoutPath() instead. 10 | # Returns a layer which when animated makes a colored X cross 11 | # out the box you specify. 12 | # box = a 4-item list/tuple in the same fashion as the view box 13 | # time = duration for the animation 14 | # width = thickness of the lines 15 | # color = color of the lines 16 | def crossout_old(box, time=30, width=3, color=(1,0,0), view=None): 17 | x_min, x_max, y_min, y_max = box 18 | pathSE = morpho.grid.Path([x_min + y_max*1j]*2) 19 | pathSE.width = width 20 | pathSE.color = list(color) 21 | pathSE.transition = morpho.transitions.slow_fast_slow 22 | pathSW = morpho.grid.Path([x_max + y_max*1j]*2) 23 | pathSW.width = width 24 | pathSW.color = list(color) 25 | pathSW.transition = morpho.transitions.slow_fast_slow 26 | 27 | pathSE = morpho.Actor(pathSE) 28 | pathSW = morpho.Actor(pathSW) 29 | 30 | pathSE.newkey(time//2).seq[1] = x_max + y_min*1j 31 | pathSE.key(-1).delay = oo 32 | 33 | 34 | pathSW.movekey(0, time//2) 35 | pathSW.newkey(time).seq[1] = x_min + y_min*1j 36 | pathSW.key(-1).delay = oo 37 | 38 | if view is None: view = tuple(box) 39 | layer = morpho.anim.Layer([pathSE, pathSW], view=view) 40 | return layer 41 | 42 | # Returns a path actor which when animated makes a colored X cross 43 | # out the box you specify. 44 | # 45 | # INPUTS 46 | # box = a 4-item list/tuple [xmin,xmax,ymin,ymax] 47 | # time/duration = duration for the animation 48 | # width = thickness of the lines 49 | # color = color of the lines 50 | # transition = Transition function assigned to path. 51 | # Default: morpho.transition.default 52 | # KEYWORD-ONLY INPUTS 53 | # pad = Padding to apply to given box. Default: 0 54 | # alignOrigin = Alignment of path origin point. 55 | # Default: (0,0) (centered) 56 | # Can also be None to use absolute coordinates. 57 | # Additional keywords are set as attributes of the path. 58 | @handleBoxTypecasting 59 | def crossoutPath(box, time=30, width=3, color=(1,0,0), 60 | transition=None, *, duration=None, pad=0, alignOrigin=(0,0), 61 | **kwargs): 62 | 63 | # "duration" is a dominant alias for the "time" parameter 64 | if duration is not None: 65 | time = duration 66 | 67 | box = padbox(box, pad) 68 | 69 | x_min, x_max, y_min, y_max = box 70 | 71 | path = morpho.grid.Path([x_min+y_max*1j, x_max+y_min*1j, x_max+y_max*1j, x_min+y_min*1j]) 72 | path.deadends.add(1) 73 | path.width = width 74 | path.color = list(color) 75 | path.end = 0 76 | path.transition = transition if transition is not None else morpho.transition.default 77 | # path.origin = mean(box[:2]) + 1j*mean(box[2:]) 78 | if alignOrigin is not None: 79 | path.alignOrigin(alignOrigin) 80 | path.set(**kwargs) 81 | 82 | 83 | path = morpho.Actor(path) 84 | path.newkey(time).end = 1 85 | 86 | return path 87 | 88 | crossout = crossoutPath 89 | 90 | # Returns a path actor that circles a given box region. 91 | # 92 | # INPUTS 93 | # box = 4-item list/tuple [xmin,xmax,ymin,ymax] 94 | # time/duration = Duration of animation (in frames). Default: 30 95 | # width = Border thickness (in pixels). Default: 3 96 | # color = Border color (RGB list). Default: [1,0,0] (red) 97 | # phase = Starting angle in radians measured CCW from east. Default: pi/2 98 | # CCW = Boolean specifying draw direction being counter-clockwise or not. 99 | # Default: True 100 | # steps = Number of line segments in path. Default: 75 101 | # transition = Transition function assigned to path. 102 | # Default: morpho.transition.default 103 | # KEYWORD-ONLY INPUTS 104 | # pad = Padding to apply to given box. Default: 0 105 | # alignOrigin = Alignment of path origin point. 106 | # Default: (0,0) (centered) 107 | # Can also be None to use absolute coordinates. 108 | # Additional keywords are set as attributes of the path. 109 | @handleBoxTypecasting 110 | def encircle(box, time=30, width=3, color=(1,0,0), phase=tau/4, 111 | CCW=True, steps=75, transition=None, 112 | *, duration=None, pad=0, alignOrigin=(0,0), **kwargs): 113 | 114 | # "duration" is a dominant alias for the "time" parameter 115 | if duration is not None: 116 | time = duration 117 | 118 | box = padbox(box, pad) 119 | 120 | orbit = 2*int(CCW) - 1 121 | a = (box[1] - box[0])/2 122 | b = (box[3] - box[2])/2 123 | seq = [a*math.cos(orbit*n/steps*tau + phase) + 1j*b*math.sin(orbit*n/steps*tau + phase) for n in range(steps)] 124 | seq.append(seq[0]) 125 | 126 | path = morpho.grid.Path(seq) 127 | path.width = width 128 | path.color = list(color) 129 | path.end = 0 130 | path.origin = mean(box[:2]) + 1j*mean(box[2:]) 131 | if transition is None: 132 | path.transition = morpho.transition.default 133 | else: 134 | path.transition = transition 135 | if alignOrigin is None: 136 | path.commitTransforms() 137 | else: 138 | path.alignOrigin(alignOrigin) 139 | path.set(**kwargs) # Set any other attributes 140 | 141 | path = morpho.Actor(path) 142 | path.newkey(time) 143 | path.last().end = 1 144 | 145 | return path 146 | 147 | # Draws an enboxing animation. Good for highlighting important things on screen. 148 | # 149 | # INPUTS 150 | # box = 4-item list/tuple [xmin,xmax,ymin,ymax] 151 | # time/duration = Duration of animation (in frames). Default: 30 152 | # width = Border thickness (in pixels). Default: 3 153 | # color = Border color (RGB list). Default: [1,0,0] (red) 154 | # corner = Which corner should the animation start at? 155 | # Values are given as diagonal compass directions: 156 | # "NW", "SW", "SE", "NE". Default: "NW" 157 | # CCW = Boolean specifying draw direction being counter-clockwise or not. 158 | # Default: True 159 | # transition = Transition function assigned to path. 160 | # Default: morpho.transition.default 161 | # KEYWORD-ONLY INPUTS 162 | # pad = Padding to apply to given box. Default: 0 163 | # alignOrigin = Alignment of path origin point. 164 | # Default: (0,0) (centered) 165 | # Can also be None to use absolute coordinates. 166 | # Additional keywords are set as attributes of the path. 167 | @handleBoxTypecasting 168 | def enboxPath(box, time=30, width=3, color=(1,0,0), corner="NW", CCW=True, 169 | transition=None, *, duration=None, pad=0, alignOrigin=(0,0), 170 | _debox=False, _pause=0, 171 | **kwargs): 172 | 173 | # "duration" is a dominant alias for the "time" parameter 174 | if duration is not None: 175 | time = duration 176 | 177 | corner = corner.upper() 178 | dirs = ["NW", "SW", "SE", "NE"] 179 | if corner not in dirs: 180 | raise ValueError('corner must be "NW", "SW", "SE", or "NE".') 181 | 182 | box = padbox(box, pad) 183 | 184 | left = box[0] 185 | right = box[1] 186 | bottom = box[2] 187 | top = box[3] 188 | 189 | corners = [left+1j*top, left+1j*bottom, right+1j*bottom, right+1j*top] 190 | 191 | # Reverse order if done clockwise 192 | if not CCW: 193 | corners = corners[::-1] 194 | dirs = dirs[::-1] 195 | 196 | # Find the index of the starting corner 197 | i = dirs.index(corner) 198 | 199 | # Order the corners according to the starting corner and direction 200 | corners = corners[i:] + corners[:i] 201 | 202 | 203 | path = morpho.grid.Path(corners) 204 | path.close() 205 | path.width = width 206 | path.color = list(color) 207 | constTrans = path.constantSpeedTransition() 208 | if transition is None: 209 | transition = morpho.transition.default 210 | 211 | if _debox: 212 | # Calculate in and out times 213 | t1 = round(time/2) 214 | t2 = time - t1 215 | time = t1 216 | 217 | # Split the transition between the in and out times 218 | tran1, tran2 = morpho.transitions.split(transition, 0.5) 219 | path.transition = lambda t: constTrans(tran1(t)) 220 | else: 221 | path.transition = lambda t: constTrans(transition(t)) 222 | path.end = 0 223 | if alignOrigin is not None: 224 | path.alignOrigin(alignOrigin) 225 | path.set(**kwargs) # Set any other attributes 226 | 227 | path = morpho.Actor(path) 228 | path.newkey(time) 229 | path.last().end = 1 230 | 231 | if _debox: 232 | if _pause > 0: 233 | path.newendkey(_pause) 234 | path.last().transition = lambda t: constTrans(tran2(t)) 235 | path.newendkey(t2).start = 1 236 | path.last().visible = False 237 | 238 | path.last().transition = transition 239 | 240 | return path 241 | 242 | enbox = enboxPath # Synonym for emboxPath() 243 | 244 | # Same as enbox(), but it deboxes immediately afterward. 245 | # Useful for briefly highlighting something with a box. 246 | # An additional keyword-only input `pause` can be specified 247 | # to provide a delay between enboxing and deboxing. 248 | def enboxFlourish(*args, pause=0, **kwargs): 249 | return enbox(*args, _debox=True, _pause=pause, **kwargs) 250 | enboxHighlight = enboxFlourish 251 | 252 | # Scales all the nodes of a path by the given factor about the given 253 | # centerpoint. If the center is unspecified, defaults to the center 254 | # of mass of all the path's nodes. 255 | def expandPath(path, factor, center=None): 256 | if center is None: 257 | center = sum(path.seq)/len(path.seq) 258 | 259 | return path.fimage(lambda s: factor*(s-center)+center) 260 | 261 | # DEPRECATED! Use enbox() or enboxPath() instead. 262 | def enbox_old(box, time=30, width=3, color=(1,0,0), corner="NW", CCW=True, 263 | view=None, transition=morpho.transitions.uniform): 264 | 265 | raise NotImplementedError 266 | 267 | corner = corner.upper() 268 | dirs = ["NW", "SW", "SE", "NE"] 269 | if corner not in dirs: 270 | raise ValueError('corner must be "NW", "SW", "SE", or "NE".') 271 | 272 | if view is None: 273 | view = tuple(box) 274 | 275 | left = box[0] 276 | right = box[1] 277 | bottom = box[2] 278 | top = box[3] 279 | 280 | corners = [left+1j*top, left+1j*bottom, right+1j*bottom, right+1j*top] 281 | 282 | # Reverse order if done clockwise 283 | if not CCW: 284 | corners = corners[::-1] 285 | dirs = dirs[::-1] 286 | 287 | # Find the index of the starting corner 288 | i = dirs.index(corner) 289 | 290 | # Order the corners according to the starting corner and direction 291 | corners = corners[i:] + corners[:i] 292 | 293 | # Get drawing! 294 | layer = morpho.Layer(view=view) 295 | LEN = 2*(box[1]-box[0] + box[3]-box[2]) 296 | len_sofar = 0 297 | for i in range(4): 298 | z0 = corners[i] 299 | z1 = corners[(i+1) % 4] 300 | 301 | t0 = round((len_sofar)/LEN * time) 302 | len_sofar += abs(z1-z0) 303 | t1 = round((len_sofar)/LEN * time) 304 | 305 | path0 = morpho.grid.Path([z0, z0]) 306 | path0.width = width 307 | path0.color = list(color) 308 | path0.transition = transition 309 | 310 | path = morpho.Actor(morpho.grid.Path) 311 | path.newkey(t0, path0) 312 | path.newkey(t1) 313 | path.key(-1).seq = [z0, z1] 314 | path.key(-1).delay = oo 315 | 316 | layer.actors.append(path) 317 | 318 | return layer 319 | 320 | -------------------------------------------------------------------------------- /morpholib/giffer.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This module contains functions to facilitate exporting animations 3 | as animated GIFs. 4 | ''' 5 | 6 | import imageio 7 | import os, platform 8 | import subprocess as sp 9 | 10 | # Code tells sp.call() not to make a console window (for Windows) 11 | CREATE_NO_WINDOW = 0x08000000 12 | 13 | # # Get location of gifsicle 14 | # pwd = os.path.dirname(os.path.abspath(__file__)) + os.sep # + "gifsicle" 15 | # pwd += os.sep 16 | 17 | pwd = os.curdir + os.sep 18 | 19 | # Compiles the set of image files into an animated gif. 20 | # INPUTS: 21 | # filenames = List of filenames (or file paths) 22 | # directory = Directory of the filenames (defaults to "./") 23 | # saveas = filename of the exported gif 24 | # duration = Duration of each frame in seconds. 25 | # Can be a single number to apply to all frames, 26 | # or a list denoting the duration of each filename. 27 | # Defaults to 0.1 seconds. 28 | # Maximum: 655 seconds. If any duration exceeds this value, 29 | # it will be lowered to the maximum. 30 | def makegif(filenames="*", directory=pwd, saveas=pwd+"movie.gif", \ 31 | duration=0.1): 32 | # Lower overflows 33 | if type(duration) is int or type(duration) is float: 34 | duration = min(duration, 655) 35 | else: 36 | for i in range(len(duration)): 37 | duration[i] = min(duration[i], 655) 38 | 39 | if filenames == "*": filenames = os.listdir(directory) 40 | initializegif(saveas) 41 | with imageio.get_writer(saveas, mode='I', duration=duration) as writer: 42 | for filename in filenames: 43 | image = imageio.imread(directory + os.sep + filename) 44 | writer.append_data(image) 45 | 46 | # Optimizes a gif file in place using gifsicle. 47 | # Requires the gifsicle executable to be in the current directory. 48 | def optimizegif(filename): 49 | if not os.path.isfile(filename): 50 | raise FileNotFoundError 51 | if platform.system() == "Windows": 52 | cmd = ["gifsicle", "-b", "-O3", "--careful", filename] 53 | sp.call(cmd, creationflags=CREATE_NO_WINDOW) 54 | # sp.call(cmd) 55 | else: 56 | cmd = ["gifsicle", "-b", "-O3", "--careful", filename] 57 | sp.call(cmd) 58 | 59 | # Attempts to initialize an empty GIF file. 60 | # If file already exists, does nothing. 61 | # Although this is not technically necessary for GIF creation, 62 | # it allows a way to test a filepath's validity. 63 | def initializegif(filename): 64 | # if filename exists, then nothing needs to be done. 65 | if os.path.isfile(filename): 66 | return 67 | # Try to make the file. 68 | with open(filename, "a") as file: 69 | pass 70 | -------------------------------------------------------------------------------- /morpholib/graph.py: -------------------------------------------------------------------------------- 1 | 2 | import morpholib as morpho 3 | import morpholib.anim 4 | from morpholib.tools.basics import * 5 | import morpholib.transitions 6 | 7 | import pyglet as pg 8 | pyglet = pg 9 | from cmath import exp 10 | 11 | import numpy as np 12 | 13 | # Returns a Frame figure that defines axes over the given view. 14 | # 15 | # ARGUMENTS 16 | # xwidth = x-axis thickness (in pixels). Default: 5 17 | # ywidth = y-axis thickness (in pixels). Default: 5 18 | # xcolor = x-axis color (RGB list). Default: [0,0,0] (black) 19 | # ycolor = y-axis color (RGB list). Default: [0,0,0] (black) 20 | # xalpha = x-axis opacity. Default: 1 (opaque) 21 | # yalpha = y-axis opacity. Default: 1 (opaque) 22 | def Axes(view, xwidth=5, ywidth=5, 23 | xcolor=(0,0,0), ycolor=(0,0,0), xalpha=1, yalpha=1): 24 | xAxis = morpho.grid.Path(view[:2]) 25 | xAxis.static = True 26 | xAxis.width = xwidth 27 | xAxis.color = list(xcolor) 28 | xAxis.alpha = xalpha 29 | 30 | yAxis = morpho.grid.Path([view[2]*1j, view[3]*1j]) 31 | yAxis.static = True 32 | yAxis.width = ywidth 33 | yAxis.color = list(ycolor) 34 | yAxis.alpha = yalpha 35 | 36 | return morpho.anim.Frame([xAxis, yAxis]) 37 | 38 | # Returns a path which is the graph of a real function. 39 | # 40 | # ARGUMENTS 41 | # f = Real to real python function (e.g. lambda x: x**2) 42 | # a,b = Domain interval [a,b] 43 | # steps = Number of line segments in path. Default: 50 44 | # width = Path thickness (in pixels). Default: 3 45 | # color = Path color (RGB list). Default: [1,1,1] (white) 46 | # alpha = Path opacity. Default: 1 (opaque) 47 | def realgraph(f, a, b, steps=50, width=3, color=(1,1,1), alpha=1): 48 | line = morpho.grid.line(a, b, steps) 49 | line.width = width 50 | line.color = list(color) 51 | line.alpha = alpha 52 | 53 | transform = lambda s: s.real + f(s.real)*1j 54 | graph = line.fimage(transform) 55 | 56 | return graph 57 | 58 | 59 | 60 | def revolution(): 61 | pass 62 | 63 | 64 | # Given a point and a velocity field, returns as a Path figure 65 | # the path the point travels "flowing" along the velocity field. 66 | # 67 | # INPUTS 68 | # p0 = Initial point (complex number). 69 | # vfield = Velocity field. A function of the form vfield(t,z) 70 | # where t is time and z is position (as a complex number) 71 | # For a static velocity field, the function should not 72 | # actually depend on t, though the `t` argument must still 73 | # be present in vfield()'s function signature. 74 | # tstart = Initial time value. Default: 0 75 | # tend = Final time value. Default: 1 76 | # 77 | # KEYWORD ONLY INPUTS 78 | # rtol = Relative error tolerance of IVP solution (solve_ivp). 79 | # Default: 1e-5 80 | # atol = Absolute error tolerance of IVP solution (solve_ivp). 81 | # Default: 1e-6 82 | # steps = Number of steps to use in the solution. The outputted 83 | # Path will have steps+1 nodes. 84 | # Any additional keyword arguments are set as attributes of the 85 | # returned path. 86 | def flowStreamer(p0, vfield, tstart=0, tend=1, *, 87 | rtol=1e-5, atol=1e-6, steps=50, 88 | _3dmode=False, **kwargs): 89 | 90 | try: 91 | from scipy.integrate import solve_ivp 92 | except ModuleNotFoundError: 93 | raise ModuleNotFoundError("scipy library required to use this function. Install via `pip3 install scipy`.") 94 | 95 | dtype = float if _3dmode else complex 96 | sol = solve_ivp( 97 | vfield, [tstart, tend], np.array(p0, dtype=dtype).reshape(-1), 98 | t_eval=np.linspace(tstart, tend, steps+1), 99 | rtol=rtol, atol=atol 100 | ) 101 | 102 | PathType = morpho.grid.SpacePath if _3dmode else morpho.grid.Path 103 | path = PathType(sol.y.T.squeeze().tolist()) 104 | path.set(**kwargs) 105 | return path 106 | 107 | def flowStreamer3d(*args, **kwargs): 108 | return flowStreamer(*args, _3dmode=True, **kwargs) 109 | 110 | 111 | # Mainly for internal use. 112 | # Variant of Frame class used to make the FlowField gadget 113 | # feel a bit like a regular Actor object. 114 | class _FlowFrame(morpho.Frame): 115 | # Advances all the streamers by the given cycle amount. 116 | # For example, myflow.advance(1) advances all the streamers 117 | # by one cycle. 118 | def advance(self, dt): 119 | for fig in self.figures: 120 | fig.start += dt 121 | fig.end += dt 122 | return self 123 | 124 | # Generates a Gadget defining a field of flow streamers. 125 | # 126 | # INPUTS 127 | # points = List of initial points for each streamer 128 | # 129 | # KEYWORD ONLY INPUTS 130 | # stagger = Cycle offset between consecutive streamers. 131 | # For example, stagger=0.25 means each consecutive streamer 132 | # will be a quarter cycle offset from the previous one. 133 | # Default: 0 (no stagger) 134 | # offset = Cycle position the first streamer should start in. 135 | # Essentially, this advances all the initial streamers 136 | # by the given amount. Default: 0 (no offset) 137 | # Can also be a list of N values which will generate N-many 138 | # copies of each streamer offset by each amount in the list. 139 | # sectorSize = Portion of the cycle that will be visible at any given 140 | # moment. Default: 0.5 (display half a cycle) 141 | # transition = Transition function to use for each streamer. 142 | # Default: Uniform transition 143 | # Any additional inputs will be passed to flowStreamer() for the 144 | # construction of each individual streamer. 145 | class FlowField(morpho.Layer): 146 | def __init__(self, *args, **kwargs): 147 | super().__init__() 148 | 149 | self.actors = self.generateStreamers(*args, **kwargs) 150 | 151 | @property 152 | def streamers(self): 153 | return self.actors 154 | 155 | @streamers.setter 156 | def streamers(self, value): 157 | self.actors = value 158 | 159 | @staticmethod 160 | def generateStreamers(points, *args, stagger=0, offset=0, sectorSize=0.5, 161 | transition=morpho.transitions.uniform, _3dmode=False, **kwargs): 162 | 163 | if isinstance(offset, np.ndarray): 164 | offset = offset.tolist() 165 | elif not isinstance(offset, (list, tuple)): 166 | offset = [offset] 167 | 168 | makeStreamer = flowStreamer3d if _3dmode else flowStreamer 169 | streamers = [] 170 | 171 | for shift in offset: 172 | for n,z in enumerate(points): 173 | streamer = makeStreamer(z, *args, **kwargs) 174 | streamer.start = n*stagger + shift 175 | streamer.end = streamer.start + sectorSize 176 | streamer.transition = transition 177 | 178 | streamer = morpho.Actor(streamer) 179 | streamers.append(streamer) 180 | return streamers 181 | 182 | # Returns a Frame-like object consisting of all the final 183 | # keyfigures for each streamer. 184 | # Note that these may not all correspond to exactly the same 185 | # time coordinate! 186 | def last(self): 187 | return _FlowFrame([streamer.last() for streamer in self.streamers]) 188 | 189 | # Returns a Frame-like object consisting of all the initial 190 | # keyfigures for each streamer. 191 | # Note that these may not all correspond to exactly the same 192 | # time coordinate! 193 | def first(self): 194 | return _FlowFrame([streamer.first() for streamer in self.streamers]) 195 | 196 | def lastID(self): 197 | return max(streamer.lastID() for streamer in self.streamers) 198 | 199 | def firstID(self): 200 | return min(streamer.firstID() for streamer in self.streamers) 201 | 202 | def keyID(self, n): 203 | indices = set() 204 | for streamer in self.streamers: 205 | indices.update(streamer.keyIDs) 206 | indices = sorted(indices) 207 | return indices[n] 208 | 209 | # Calls newkey() on all component streamers and returns 210 | # a selection of all the newly created keyfigures so 211 | # attributes can be modified en masse by calling .set() 212 | # myflow.newkey(time).set(width=5, color=(1,0,0), ...) 213 | def newkey(self, f): 214 | frame = _FlowFrame() 215 | for streamer in self.streamers: 216 | keyfig = streamer.newkey(f) 217 | frame.figures.append(keyfig) 218 | return frame 219 | 220 | # Calls newendkey() on all component streamers and returns 221 | # a selection of all the newly created keyfigures so 222 | # attributes can be modified en masse by calling .set() 223 | # myflow.newendkey(time).set(width=5, color=(1,0,0), ...) 224 | def newendkey(self, df=None, *, glob=False): 225 | frame = _FlowFrame() 226 | if df is None: 227 | df = 0 228 | glob = True 229 | if glob: 230 | f = self.glastID() + df 231 | else: 232 | f = max(streamer.lastID() for streamer in self.streamers) + df 233 | 234 | for streamer in self.streamers: 235 | keyfig = streamer.newkey(f) 236 | frame.figures.append(keyfig) 237 | return frame 238 | 239 | ### GADGET ACTIONS ### 240 | 241 | # Retroactively makes the FlowField fade in from invisibility. 242 | # The main difference with regular fadeIn() for Actors is 243 | # this method should only be used RETROACTIVELY after the 244 | # FlowField has some keyframes. 245 | def prefadeIn(self, duration=30, *, jump=0): 246 | initial = self.newkey(self.keyID(-2)) 247 | self.newkey(self.keyID(-2)+duration).all.set(visible=True) 248 | 249 | initial.all.set(alpha=0, visible=True) 250 | if jump != 0: 251 | for keyfig in self.first().figures: 252 | # Avoiding using `-=` so it works with mutable np.arrays 253 | keyfig.origin = keyfig.origin - jump 254 | 255 | # Retroactively makes the FlowField fade out to invisibility. 256 | # The main difference with regular fadeOut() for Actors is 257 | # this method should only be used RETROACTIVELY after the 258 | # FlowField has some keyframes. 259 | def prefadeOut(self, duration=30, *, jump=0): 260 | self.newkey(self.lastID()-duration) 261 | 262 | self.last().all.set(alpha=0, visible=False) 263 | if jump != 0: 264 | for keyfig in self.last().figures: 265 | # Avoiding using `+=` so it works with mutable np.arrays 266 | keyfig.origin = keyfig.origin + jump 267 | 268 | # 3D version of FlowField class. See FlowField for more info. 269 | class FlowField3D(FlowField): 270 | @staticmethod 271 | def generateStreamers(*args, **kwargs): 272 | return FlowField.generateStreamers(*args, _3dmode=True, **kwargs) 273 | 274 | FlowField3d = FlowField3D 275 | -------------------------------------------------------------------------------- /morpholib/latex.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Contains code to facilitate parsing and rendering LaTeX. 3 | Note that to use the functions in this module, you will 4 | need to have LaTeX installed on your system. 5 | ''' 6 | 7 | import os, io, hashlib 8 | 9 | import morpholib as morpho 10 | import morpholib.tools.latex2svg as latex2svg 11 | 12 | # Import itself so that global names can be accessed 13 | # from a local scope that uses the same names. 14 | import morpholib.latex 15 | 16 | template = latex2svg.default_template 17 | preamble = latex2svg.default_preamble 18 | params = latex2svg.default_params.copy() 19 | 20 | # Directory in which to cache LaTeX code converted to SVG 21 | # By default, it's None, meaning caching is disabled. 22 | cacheDir = None 23 | 24 | # Number of hex digits to use as part of the hash 25 | cacheHashLength = 32 26 | 27 | # Takes a string as input and returns a string 28 | # which is the input string's SHA-256 hash expressed 29 | # in hexadecimal notation. 30 | # To convert to a standard integer, run the following: 31 | # 32 | # int(sha256(strng), 16) 33 | # 34 | # This function currently only accepts UTF-8 strings 35 | # for input. Someday (probably when need arises) I'll 36 | # generalize it for other kinds of input. 37 | def sha256(strng): 38 | sha = hashlib.sha256() 39 | sha.update(bytes(strng, "utf-8")) 40 | return sha.hexdigest() 41 | 42 | # Hashes an ordered list of strings into a single hash 43 | # using SHA-256. The return value is a string representing 44 | # the hash in hexadecimal notation. 45 | def hashlist(iterable): 46 | return sha256("".join(sha256(item) for item in iterable)) 47 | 48 | # Mainly for internal use. 49 | # Takes LaTeX code and surrounds it with $$ if it 50 | # doesn't already. These are needed for the LaTeX 51 | # parser to work. 52 | def _sanitizeTex(tex): 53 | tex = tex.strip() 54 | if not(tex.startswith("$$") and tex.endswith("$$")): 55 | tex = r"$$" + tex + r"$$" 56 | return tex 57 | 58 | # Returns a filename for the given TeX code that can be 59 | # used to cache the SVG the TeX code was converted into. 60 | # The hash is generated from the given TeX code itself, 61 | # along with the current template and preamble. 62 | def hashTex(tex): 63 | texhash = hashlist([template, preamble, tex])[:cacheHashLength] 64 | return f"tex-{texhash}.svg" 65 | 66 | # Returns boolean on whether the given TeX code is cached 67 | # in the current cache directory. 68 | def iscached(tex): 69 | filename = hashTex(tex) 70 | return filename in os.listdir(cacheDir) 71 | 72 | # Parses a string containing LaTeX code and returns a 73 | # MultiSpline figure representing it. 74 | # 75 | # By default, the MultiSpline is positioned at 0, but this 76 | # can be changed by passing in a complex number to the 77 | # optional keyword argument `pos`. 78 | # 79 | # Optionally a `preamble` keyword argument can be specified. 80 | # This is mainly to change which packages are imported when 81 | # the LaTeX is parsed. If unspecified, the preamble will be 82 | # taken from morpho.latex.preamble. 83 | # 84 | # If keyword argument `useCache` is set to False, the 85 | # TeX cache will be skipped if one was defined. 86 | # 87 | # Any other args/kwargs will be passed into the MultiSpline 88 | # fromsvg() constructor (e.g. boxWidth) 89 | def parse(tex, *args, 90 | preamble=None, pos=0, useCache=True, 91 | **kwargs): 92 | 93 | tex = _sanitizeTex(tex) 94 | 95 | # Check if the SVG for this TeX code is cached 96 | if useCache and cacheDir is not None and iscached(tex): 97 | filepath = cacheDir + os.sep + hashTex(tex) 98 | spline = morpho.shapes.MultiSpline.fromsvg(filepath, *args, **kwargs) 99 | else: # Generate TeX Spline in house 100 | if preamble is None: 101 | # Referencing the global scope `preamble` variable via 102 | # the module itself is required here since the local 103 | # variable and global variable have the same name. 104 | preamble = morpho.latex.preamble 105 | params = morpho.latex.params.copy() 106 | params["preamble"] = preamble 107 | 108 | out = latex2svg.latex2svg(tex, params) 109 | svgcode = out["svg"] 110 | 111 | # If caching is enabled, save the output svg code 112 | # as a file in the specified cache directory. 113 | if useCache and cacheDir is not None: 114 | filepath = cacheDir + os.sep + hashTex(tex) 115 | with open(filepath, "w") as file: 116 | file.write(svgcode) 117 | 118 | with io.StringIO() as stream: 119 | stream.write(svgcode) 120 | stream.seek(0) 121 | spline = morpho.shapes.MultiSpline.fromsvg(stream, *args, **kwargs) 122 | spline.origin = pos 123 | return spline 124 | 125 | # Identical to parse(), except the return value is a MultiSpline3D 126 | # figure. See parse() for more info. 127 | # 128 | # Also has an additional optional keyword argument `orient` 129 | # where an initial orientation can be set. Note that if a value is 130 | # supplied to the `orient` parameter in this function, the returned 131 | # MultiSpline3D figure will have `orientable=True`. 132 | # You can also pass in the keyword `orientable=True` to create 133 | # an orientable figure in the default orientation. 134 | def parse3d(*args, orient=None, orientable=False, **kwargs): 135 | mspline = parse(*args, **kwargs) 136 | # Extract and reset `origin` attribute because for 3D LaTeX, 137 | # `pos` should map to the `pos` attribute, not `origin`. 138 | pos = mspline.origin 139 | mspline.origin = 0 140 | mspline3d = morpho.shapes.MultiSpline3D(mspline) 141 | mspline3d.pos = pos 142 | 143 | if orient is not None: 144 | mspline3d.orientable = True 145 | mspline3d.orient = orient 146 | elif orientable: 147 | mspline3d.orientable = True 148 | 149 | return mspline3d 150 | 151 | # Returns a function that checks whether a given Spline figure 152 | # matches a given LaTeX glyph. Mainly for use in .select[] and .sub[] 153 | # as a choice function: 154 | # mytex.select[morpho.latex.matches(r"\pi")].set(...) 155 | # 156 | # Note this method only works if the given TeX code produces only a single 157 | # spline when parsed. It won't work on TeX that codes for multiple glyphs 158 | # (e.g. `x^2`) or certain glyphs that render as two splines (e.g. `\implies`) 159 | # 160 | # Optionally a list (or other non-string iterable) of TeX strings 161 | # can be inputted, in which case it will try to match on ANY of the 162 | # contained expressions. 163 | # mytex.select[morpho.latex.matches([r"\pi", r"x"])].set(...) 164 | def matches(*tex): 165 | # If given a single list as input, extract it. 166 | if len(tex) == 1 and isinstance(tex[0], (list, tuple)): 167 | tex = tex[0] 168 | 169 | targets = [] 170 | for expr in tex: 171 | target = morpho.latex.parse(expr, pos=0, boxHeight=1) if isinstance(expr, str) else expr 172 | if len(target.figures) != 1: 173 | raise TypeError("Given LaTeX code results in more than one Spline.") 174 | target = target.figures[0] 175 | targets.append(target) 176 | 177 | return lambda glyph: any(glyph.matchesShape(target) for target in targets) 178 | -------------------------------------------------------------------------------- /morpholib/matrix.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This module contains various matrix tools. 3 | It implements a 2x2 matrix class so that linear 4 | transformations can also be animated. 5 | Also contains helper functions for creating rotation/orientation 6 | matrices. 7 | ''' 8 | 9 | 10 | import math 11 | import numpy as np 12 | 13 | tol = 1.0e-9 14 | tau = 2*math.pi 15 | 16 | # # Special 2x2 matrix class for Morpho which can handle 17 | # # multiplication on a complex number by treating it as 18 | # # a 2D column vector. 19 | # class _Mat_old(np.matrix): 20 | 21 | # def __mul__(self, other): 22 | # if type(other) is _Mat_old: 23 | # # Use the superclass to compute the matrix product 24 | # return np.matrix.__mul__(self, other) 25 | # else: 26 | # Z = np.matrix([[other.real], [other.imag]]) 27 | # prod = np.matrix.__mul__(self, Z) 28 | # return float(prod[0,0]) + float(prod[1,0])*1j 29 | 30 | # # Convenience function: returns inverse of the mat 31 | # @property 32 | # def inv(self): 33 | # return self**(-1) 34 | 35 | # Special 2x2 matrix class for Morpho which can handle 36 | # multiplication on a complex number by treating it as 37 | # a 2D column vector. 38 | class Mat(object): 39 | def __init__(self, x1=0, x2=0, y1=0, y2=0): 40 | if isinstance(x1, list) or isinstance(x1, tuple) or \ 41 | isinstance(x1, np.ndarray): 42 | 43 | array = np.array(x1, dtype=float) 44 | else: 45 | array = np.array([[x1,x2],[y1,y2]], dtype=float) 46 | self.array = array 47 | 48 | def __mul__(self, other): 49 | # If given a scalar, treat it as a 2D vector and do 50 | # matrix multiplication 51 | if isinstance(other, complex) or isinstance(other, float) or \ 52 | isinstance(other, int) or isinstance(other, np.number) or \ 53 | (isinstance(other, np.ndarray) and other.size == 1): 54 | 55 | # Convert to 2D vector, compute matrix product, and 56 | # convert back to complex 57 | Z = np.array([other.real, other.imag]).squeeze() 58 | prod = (self.array @ Z).tolist() 59 | return prod[0] + 1j*prod[1] 60 | 61 | # If given another matrix, do matrix multiplication directly 62 | # on the underlying arrays. 63 | elif isinstance(other, type(self)): 64 | return type(self)(self.array @ other.array) 65 | 66 | # Otherwise, use regular multiplication 67 | else: 68 | return self.array * other 69 | 70 | def __rmul__(self, other): 71 | return self.array * other 72 | 73 | @property 74 | def T(self): 75 | return type(self)(self.array.T) 76 | 77 | @property 78 | def inv(self): 79 | return type(self)(np.linalg.inv(self.array)) 80 | 81 | def __repr__(self): 82 | return repr(self.array) 83 | 84 | def __str__(self): 85 | return str(self.array) 86 | 87 | 88 | 89 | 90 | # # Converts a numpy matrix into a Morpho _Mat 91 | # # (Maybe unnecessary! numpy.matrix handles this somehow!) 92 | # def matrix2Mat(matrix): 93 | # return Mat(matrix[0,0], matrix[0,1], matrix[1,0], matrix[1,1]) 94 | 95 | # # This is the ACTUAL constructor for the 2x2 matrix class _Mat_old. 96 | # # Overriding the inherited __init__ from np.matrix turned out 97 | # # to be more complicated than I thought, so this was a workaround. 98 | # def Mat_old(x1=0, x2=0, y1=0, y2=0): 99 | # return _Mat_old([[x1, x2], [y1, y2]], dtype=float) 100 | 101 | 102 | # Extracts the upper-left 2x2 submatrix of the given numpy matrix 103 | # and returns it as a morpho Mat. 104 | # Geometrically, this converts a 3D matrix transformation into the 105 | # equivalent 2x2 transformation resulting from treating (x,y) as 106 | # (x,y,0), performing the 3D transformation to it, and then 107 | # flattening it back onto the xy-plane. 108 | def flatten(npMat): 109 | return Mat(npMat[0,0], npMat[0,1], npMat[1,0], npMat[1,1]) 110 | 111 | # Returns numpy matrix that encodes a 3D rotation of 112 | # theta radians about the vector u according to the right hand rule. 113 | # If theta is unspecified, the magnitude of the u-vector is used. 114 | def rotation(u, theta=None): 115 | # Type check the unit vector 116 | if type(u) not in (list, tuple, np.ndarray, np.matrix): 117 | raise TypeError("u must be list, tuple, numpy array or numpy matrix.") 118 | 119 | # Convert to np.array and append trailing zeros if needed 120 | new_u = np.zeros(3) 121 | new_u[:len(u)] = u 122 | u = new_u 123 | 124 | # Infer theta from magnitude of u if unspecified 125 | if theta is None: 126 | theta = np.linalg.norm(u) 127 | 128 | # Throw error if u is the zero vector AND theta is non-zero (mod tau) 129 | # If u = 0 AND theta = 0 (mod tau), then interpret this as the 130 | # identity rotation and return the identity matrix. 131 | if np.allclose(u, 0): 132 | if abs(theta % tau) < tol: 133 | return np.identity(3) 134 | else: 135 | raise ValueError("u must be a non-zero 3D vector") 136 | 137 | # Convert to unit vector 138 | u = u / np.linalg.norm(u) 139 | x,y,z = u 140 | 141 | # Compute useful constants 142 | c = math.cos(theta) 143 | c_c = 1 - c # Complement of c 144 | s = math.sin(theta) 145 | 146 | # Return rotation matrix as numpy array 147 | return np.array([ 148 | [c+x*x*c_c, x*y*c_c-z*s, x*z*c_c+y*s], 149 | [y*x*c_c+z*s, c+y*y*c_c, y*z*c_c-x*s], 150 | [z*x*c_c-y*s, z*y*c_c+x*s, c+z*z*c_c] 151 | ], dtype=float) 152 | 153 | 154 | # Returns pair (u, theta) characterizing the rotation matrix mat. 155 | # u is a unit vector and theta is the rotation angle according to the 156 | # right hand rule. 157 | # Note: This function assumes mat is a rotation matrix. It may behave 158 | # unpredictably if given a non-rotation matrix. 159 | def rotationVector(mat): 160 | R = np.array(mat, dtype=float) 161 | A = (R-R.T)/2 162 | rho = np.array([A[2,1], A[0,2], A[1,0]], dtype=float) 163 | s = np.linalg.norm(rho) 164 | c = (R.trace() - 1)/2 165 | if abs(s) < tol: 166 | if abs(c-1) < tol: 167 | return (np.array([1,0,0], dtype=float), 0) 168 | else: 169 | mat = R + np.identity(3) 170 | for j in range(3): 171 | v = mat[:,j] 172 | if not np.allclose(v, 0): 173 | break 174 | return (v/np.linalg.norm(v), math.pi) 175 | 176 | return (rho/s, math.atan2(s,c)) 177 | 178 | # Returns the wedge product as a skew-symmetric matrix. 179 | # That is, returns outer(u,v) - outer(v,u) 180 | def wedge(u, v): 181 | outprod = np.outer(u,v) 182 | return outprod - outprod.T 183 | 184 | # Returns the tilt product of u and v. 185 | # That is, returns outer(v,u) - outer(u,v) 186 | # or equivalently: wedge(v,u) 187 | def tilt(u, v): 188 | return wedge(v,u) 189 | 190 | # Given two N-dimensional vectors u,v and an angle theta, 191 | # returns the rotation matrix that rotates theta radians in the 192 | # direction from u toward v. 193 | # 194 | # Optionally the keyword `orthonormal=True` may be passed in to 195 | # tell the function to assume u and v are orthogonal unit vectors, 196 | # thus bypassing an unneccessary initial computation. 197 | def rotationNd(u, v, theta, *, orthonormal=False): 198 | G = tilt(u,v) 199 | if not orthonormal: 200 | mag = np.sqrt((u@u)*(v@v) - (u @ v)**2) 201 | G = G / mag 202 | 203 | # Apply General Euler's Formula 204 | return np.eye(*G.shape) + G*np.sin(theta) + (G@G)*(1-np.cos(theta)) 205 | 206 | # Returns the 2D rotation matrix associated with the 207 | # input angle. 208 | def rotation2d(angle): 209 | c = math.cos(angle) 210 | s = math.sin(angle) 211 | 212 | return np.array([[c, -s], [s, c]], dtype=float) 213 | 214 | # Tweens two 3D rotation matrices (orient matrices). 215 | def orientTween(A, B, t, start=0, end=1): 216 | # If A and B are basically equal, just return a copy of A. 217 | if np.allclose(A, B): 218 | return A.copy() 219 | 220 | return orientTween1(A, B, t, start, end) 221 | 222 | 223 | # Functionally equivalent to orientTween(), but doesn't check 224 | # if A and B are close before applying the tween. This function 225 | # can slightly speed up the code if you know in advance that 226 | # A and B are meaningfully different from each other. 227 | def orientTween1(A, B, t, start=0, end=1): 228 | # Delta matrix is the rotation needed to turn self.orient into 229 | # other.orient 230 | # delta = np.linalg.inv(self.orient) @ other.orient 231 | delta = B @ A.T # Transpose is inverse for rotation mats 232 | 233 | # Compute rotation vector for delta 234 | u, theta = rotationVector(delta) 235 | 236 | # Normalize t if start and end are different from default. 237 | if not(start == 0 and end == 1): 238 | t = (t - start)/(end - start) 239 | return rotation(u, theta*t) @ A 240 | 241 | 242 | # Returns the 2D scaling matrix associated with the input 243 | # scale factor. Optionally, two scale factors can be 244 | # inputted, (horizontal scale, vertical scale) 245 | def scale2d(x, y=None): 246 | if y is None: 247 | y = x 248 | return np.array([[x, 0], [0, y]], dtype=float) 249 | 250 | 251 | # Given a vector-like thing (tuple, list, complex number, np.array), 252 | # converts a copy of it into a np.array with dtype=float. 253 | # If given an np.array that is already dtype=float, does nothing 254 | # and returns the original np.array uncopied. 255 | # Note: if given a complex number, returns a 3-vector with z = 0. 256 | def array(v): 257 | if isinstance(v, np.ndarray): 258 | if v.dtype is np.dtype(float): 259 | return v 260 | else: 261 | return np.array(v, dtype=float) 262 | elif isinstance(v, list) or isinstance(v, tuple): 263 | return np.array(v, dtype=float) 264 | elif type(v) in (int, float, complex): 265 | return np.array([v.real, v.imag, 0], dtype=float) 266 | else: 267 | raise TypeError("Unable to convert given vector-like object to np.array!") 268 | 269 | # Given a vector-like thing in Morpho like a tuple, list, 270 | # or complex number, converts it into a numpy 3-vector 271 | # (i.e. np.array with 3 items). 272 | # Interprets complex numbers as 2d vectors [x,y] 273 | def vector3d(v): 274 | if type(v) in (complex, float, int): 275 | v = np.array([v.real, v.imag, 0], dtype=float) 276 | # if isinstance(v, list) or isinstance(v, tuple): 277 | else: 278 | # v = np.array(v, dtype=float) 279 | # Convert to np.array and append trailing zeros if needed 280 | new_v = np.zeros(3) 281 | new_v[:len(v)] = v 282 | v = new_v 283 | 284 | return v 285 | 286 | # An extension of numpy.interp() that can handle fp as a list 287 | # of vectors instead of a list of scalars. However, I believe 288 | # scipy.interp1() fulfills this role, so this function will 289 | # probably be obsolete if/when scipy is ever added as a 290 | # dependency to Morpho. 291 | # 292 | # Note: the individual vectors in fp should not have a very 293 | # large number of components. This function is designed for 294 | # relatively low-dimension fp vectors. 295 | def interpVectors(x, xp, fp, left=None, right=None): 296 | try: 297 | ndim = len(fp[0]) 298 | isvector = True 299 | except TypeError: 300 | isvector = False 301 | 302 | if isvector: 303 | fmat = np.array(fp) if not isinstance(fp, np.ndarray) else fp 304 | interpMat = np.zeros((len(x), ndim)) 305 | for j in range(ndim): 306 | interpMat[:,j] = np.interp(x, xp, fmat[:,j], left=left, right=right) 307 | return interpMat 308 | else: 309 | return np.interp(x, xp, fp, left=left, right=right) 310 | 311 | def positionArray(domain, res): 312 | xmin, xmax, ymin, ymax = domain 313 | xres, yres = res 314 | if xres < 2 or yres < 2: 315 | raise ValueError("Resolution values must be > 1") 316 | dx = (xmax-xmin)/(xres-1) # if xres > 1 else (xmax-xmin) 317 | dy = (ymax-ymin)/(yres-1) # if yres > 1 else (ymax-ymin) 318 | 319 | # Note: I think you could also have implemented this by 320 | # adding a column linspace to a row linspace. Numpy broadcasting 321 | # would (I think) result in this creating a cartesian addition 322 | # of the two. Something to consider if you ever want to change 323 | # this implementation. 324 | array = np.mgrid[xmin:xmax+dx/2:dx, ymin:ymax+dy/2:dy] 325 | zarray = array[0] + 1j*array[1] 326 | return zarray 327 | 328 | # Opposite of array(). Takes an iterable and converts it to a 329 | # standard python list of python floats. Useful for turning 330 | # numpy arrays back into native python types. 331 | # NOTE: This function has been mostly obsoleted since np.arrays 332 | # have a tolist() method that basically does this. 333 | def floatlist(v): 334 | return [float(item) for item in v] 335 | 336 | # Like floatlist(), but turns all items into complex type. 337 | # NOTE: This function has been mostly obsoleted since np.arrays 338 | # have a tolist() method that basically does this. 339 | def complexlist(v): 340 | return [complex(item) for item in v] 341 | 342 | # Like floatlist(), but turns all items into ints via int(). 343 | # NOTE: This function has been mostly obsoleted since np.arrays 344 | # have a tolist() method that basically does this. 345 | def intlist(v): 346 | return [int(item) for item in v] 347 | 348 | # Like intlist(), but rounds instead of floors. 349 | # Optionally specify ndigits (same optional arg that round() takes) 350 | # NOTE: This function has been mostly obsoleted since np.arrays 351 | # have a tolist() method that basically does this. 352 | def roundlist(v, ndigits=None): 353 | return [int(round(item, ndigits)) for item in v] 354 | 355 | ### VARIOUS OTHER CONSTANTS AND FUNCTIONS ### 356 | 357 | mat = MAT = Mat # Any case works. 358 | det = lambda M: np.linalg.det(M) # For mats only 359 | 360 | # Returns a measure of how "thin" the matrix transformation is. 361 | # What it actually does is takes the unit square, applies M to it, 362 | # and then computes the height of the parallelogram the unit square 363 | # turns in to, where the base of the parallelogram is taken to be 364 | # its longest edge. 365 | # This function is only designed to work for 2x2 matrices. 366 | def thinHeight2x2(M): 367 | det = np.linalg.det(M) 368 | if det == 0: 369 | return 0 370 | else: 371 | return abs(det/max(np.linalg.norm(M[:,0]), np.linalg.norm(M[:,1]))) 372 | 373 | # Returns a relative measure of how "thin" the matrix transformation is. 374 | # A number in the range [0,1]. 375 | # It applies the matrix M to the unit square, and then returns the ratio 376 | # of the resulting parallelogram's height to its width, where "width" 377 | # is taken to mean the parallelogram's longest side length. 378 | def thinness2x2(M): 379 | det = np.linalg.det(M) 380 | if det == 0: 381 | return 0 382 | else: 383 | return abs(det/max(np.linalg.norm(M[:,0]), np.linalg.norm(M[:,1]))**2) 384 | -------------------------------------------------------------------------------- /morpholib/sample.py: -------------------------------------------------------------------------------- 1 | import morpholib as morpho 2 | import morpholib.grid, morpholib.transitions 3 | 4 | 5 | def play(): 6 | grid0 = morpho.grid.mathgrid( 7 | tweenMethod=morpho.grid.Path.tweenSpiral, 8 | transition=morpho.transition.quadease 9 | ) 10 | 11 | grid = morpho.Actor(grid0) 12 | mation = morpho.Animation(grid) 13 | 14 | grid.newendkey(60, grid0.fimage(lambda s: s**2/10)) 15 | mation.endDelay(30) 16 | 17 | grid.newendkey(60, grid0.fimage(lambda s: s**3/64)) 18 | mation.endDelay(30) 19 | 20 | grid.newendkey(60, grid0.fimage(lambda s: s**4/8**3)) 21 | mation.endDelay(30) 22 | 23 | grid.newendkey(60, grid0.fimage(lambda s: s**5/8**4)) 24 | mation.endDelay(30) 25 | 26 | grid.newendkey(60, grid0.copy()) 27 | 28 | 29 | mation.play() 30 | -------------------------------------------------------------------------------- /morpholib/tools/__init__.py: -------------------------------------------------------------------------------- 1 | from morpholib.tools.base import * 2 | from morpholib.tools.basics import * -------------------------------------------------------------------------------- /morpholib/tools/base.py: -------------------------------------------------------------------------------- 1 | pass -------------------------------------------------------------------------------- /morpholib/tools/basics.py: -------------------------------------------------------------------------------- 1 | import morpholib as morpho 2 | 3 | import math, cmath 4 | import numpy as np 5 | from bisect import bisect_right, bisect_left 6 | from collections.abc import Iterable 7 | 8 | ### CONSTANTS ### 9 | pi = math.pi 10 | tau = 2*pi 11 | oo = inf = float("inf") 12 | nan = float("nan") 13 | # Basic unit vectors for 3D animations 14 | ihat = np.array([1,0,0], dtype=float); ihat.flags.writeable = False 15 | jhat = np.array([0,1,0], dtype=float); jhat.flags.writeable = False 16 | khat = np.array([0,0,1], dtype=float); khat.flags.writeable = False 17 | # Basic matrices 18 | I2 = np.eye(2); I2.flags.writeable = False 19 | I3 = np.eye(3); I3.flags.writeable = False 20 | 21 | ### DECORATORS ### 22 | 23 | # Decorator for functions that operate on a box. 24 | # Allows such a function to accept a Figure type input 25 | # whereby it will attempt to infer a box by calling the 26 | # figure's `box()` method, assuming it exists. 27 | # If given an Actor object, it will use the latest keyfigure. 28 | def handleBoxTypecasting(func): 29 | def wrapper(box, *args, **kwargs): 30 | box = inferBox(box) 31 | return func(box, *args, **kwargs) 32 | return wrapper 33 | 34 | ### FUNCTIONS ### 35 | 36 | isbadnum_old = lambda x: math.isnan(abs(x)*0) 37 | 38 | # Detect infinite or nan (real or complex) 39 | def isbadnum(x): 40 | return cmath.isnan(x) or cmath.isinf(x) 41 | 42 | # isbadnum() but for np.arrays. Returns True if and only if 43 | # nan or inf is found ANYWHERE in the array. 44 | # Actually, this function should also work on scalars (both numpy 45 | # and python builtins), so it can totally replace isbadnum(). 46 | # However, it might run slower than isbadnum() on python scalars, 47 | # so I'm keeping the original isbadnum() around anyway. 48 | def isbadarray(x): 49 | return (np.any(np.isnan(x)) or np.any(np.isinf(x))).tolist() 50 | 51 | # isequal(a,b) does a == b, but works even if a and/or b is 52 | # a numpy array. 53 | def isequal(a, b, /): 54 | if isinstance(a, np.ndarray) and isinstance(b, np.ndarray): 55 | return np.array_equal(a,b) 56 | elif isinstance(a, np.ndarray) != isinstance(b, np.ndarray): 57 | return False 58 | else: 59 | try: 60 | return (a == b) 61 | except ValueError: 62 | return np.array_equal(a,b) 63 | 64 | # Conversion factors between degrees and radians. 65 | deg = tau/360 66 | rad = 1/deg 67 | 68 | # Real cotangent function 69 | PI_OVER_2 = pi/2 70 | def cot(theta): 71 | return math.tan(PI_OVER_2 - theta) 72 | 73 | # Signum function. Returns x/abs(x) unless x = 0, in which case, 74 | # return 0 75 | def sgn(x): 76 | if x > 0: 77 | return 1 78 | elif x < 0: 79 | return -1 80 | else: 81 | return 0 82 | 83 | # Constrains x to being in the interval [low, high]. 84 | # If x is above high, constrain() returns high. 85 | # If x is below low, constrain() returns low. 86 | # Equivalent to min(max(x,low), high) 87 | def constrain(x, low, high): 88 | return min(max(x, low), high) 89 | clamp = constrain # Alternate name 90 | 91 | # Computes the mean of a vector-like thing. 92 | def mean(a): 93 | if len(a) == 0: 94 | raise IndexError("Can't take mean of an empty list-like object!") 95 | return sum(a)/len(a) 96 | 97 | # Rounds a float x to an int if the float is sufficiently 98 | # close to an int. By default, it rounds if x is within 99 | # 1e-9 of an int. 100 | def snapround(x, tol=1e-9): 101 | n = round(x) 102 | return n if abs(n-x) < tol else x 103 | 104 | # Like round(), but truncates instead of rounding at the decimal digit you 105 | # specify 106 | def truncate(num, ndigits): 107 | decshift = 10**ndigits 108 | return int(num*decshift)/decshift 109 | 110 | def _rounder(x, roundfunc): 111 | # Convert to np.array if needed 112 | array = np.array(x) if not isinstance(x, np.ndarray) else x 113 | array = np.sign(array)*roundfunc(np.abs(array)) 114 | array = np.array(array, dtype=int) # Convert to int array 115 | if isinstance(x, np.ndarray): 116 | return array 117 | else: 118 | return array.tolist() 119 | 120 | # Rounds the input away from zero. 121 | # Supports lists and np.arrays as input. 122 | # roundOut(1.1) --> 2 123 | # roundOut(-1.1) --> -2 124 | def roundOut(x): 125 | return _rounder(x, np.ceil) 126 | 127 | # Rounds the input toward zero. 128 | # Supports lists and np.arrays as input. 129 | # roundIn(1.9) --> 1 130 | # roundIn(-1.9) --> -1 131 | def roundIn(x): 132 | return _rounder(x, np.floor) 133 | 134 | # If a float is equal to an int, converts it into an int. 135 | def squeezeFloat(number): 136 | try: 137 | integer = int(number) 138 | except OverflowError: 139 | return number 140 | except ValueError: 141 | return number 142 | 143 | if number == integer: 144 | return integer 145 | else: 146 | return number 147 | 148 | flattenFloat = squeezeFloat 149 | 150 | # Used in the spiral tween method. 151 | # Computes the correct amount to shift an angle th1 so that it 152 | # becomes th2 in the shortest possible path 153 | # i.e. a path that does not traverse more than pi radians 154 | # The value returned is called "dth" and should be used in 155 | # expressions like this: th(t) = th1 + t*dth 156 | # However, before using the above expression, make sure th1 and th2 157 | # are modded 2pi. 158 | # (Actually, maybe it's okay if they're not?) 159 | def argShift(th1, th2): 160 | th1 = th1 % tau 161 | th2 = th2 % tau 162 | 163 | dth = th2 - th1 164 | if abs(dth) > pi + 1.189e-12: 165 | dth = dth - math.copysign(tau, dth) 166 | return dth 167 | 168 | # Identical to argShift(), but works on np.arrays 169 | def argShiftArray(th1, th2): 170 | th1 = th1 % tau 171 | th2 = th2 % tau 172 | 173 | dth = th2 - th1 174 | 175 | flagset = abs(dth) > pi + 1.189e-12 176 | subset = dth[flagset] 177 | dth[flagset] = subset - np.copysign(tau, subset) 178 | 179 | return dth 180 | 181 | # Computes the total winding angle of a complex-valued function 182 | # around the origin on a specified interval of its parameter. 183 | # Divide the output of this function by 2pi to obtain the winding 184 | # number. 185 | # 186 | # INPUTS 187 | # f = Complex-valued function. Should be non-zero on the 188 | # closed interval given. 189 | # a = Lowerbound of the input interval 190 | # b = Upperbound of the input interval 191 | # step = The step size to use when computing the angle sum. 192 | # It should be chosen so that the winding angle traveled 193 | # from any f(t) to f(t+step) is strictly less than pi radians. 194 | def windingAngle(f, a, b, step): 195 | if b < a: 196 | return -windingAngle(f, b, a, step) 197 | 198 | length = b-a 199 | if length == 0: 200 | return 0 201 | # if step is None: 202 | # step = length/10 203 | 204 | N = math.ceil(length/step) 205 | step = length/N 206 | angleSum = 0 207 | z0 = f(a) 208 | for n in range(1,N+1): 209 | z = f(a+n*step) 210 | angleSum += cmath.phase(z/z0) 211 | z0 = z 212 | 213 | return angleSum 214 | 215 | # Given two points in the complex plane and the angle (in radians) 216 | # of the circular arc that is supposed to go between them, 217 | # returns the center point of the arc (as a complex number). 218 | # Note that the angle is signed. Positive means the arc 219 | # travels CCW from p to q. 220 | # Also note that the angle cannot be 0 or a multiple of 2pi. 221 | def arcCenter(p, q, angle): 222 | if angle % tau == 0: 223 | raise ValueError("Angle is 0 or a multiple of 2pi. Center point is undefined!") 224 | 225 | m = (p+q)/2 # midpoint 226 | return m + 1j*(m-p)*cot(angle/2) # center point 227 | 228 | # Array version of arcCenter() 229 | def arcCenterArray(p, q, angle): 230 | if np.any(angle % tau == 0): 231 | raise ValueError("Angle is 0 or a multiple of 2pi. Center point is undefined!") 232 | 233 | m = (p+q)/2 # midpoint 234 | halfangle = angle/2 235 | return m + 1j*(m-p)*(np.cos(halfangle)/np.sin(halfangle)) 236 | 237 | # Converts a 2D tuple/list into a complex number 238 | def vect2complex(v): 239 | return complex(*v) 240 | 241 | # Converts a complex number into an ordered pair 242 | # (z.real, z.imag) 243 | def complex2vect(z): 244 | return (z.real, z.imag) 245 | 246 | # Given a sorted list of numbers a and value x, 247 | # returns the highest index i such that a[i] <= x. 248 | # If all the numbers in a are larger than x, it returns -1. 249 | def listfloor(a, x): 250 | return bisect_right(a,x)-1 251 | 252 | # Given a sorted list of numbers a and value x, 253 | # returns the lowest index i such that a[i] >= x. 254 | # If all the numbers in a are less than x, it returns len(a). 255 | def listceil(a, x): 256 | if x in a: 257 | return a.index(x) 258 | else: 259 | return listfloor(a,x) + 1 260 | 261 | # Returns a dictionary mapping indices to items representing a 262 | # selection of items in a list. The `index` parameter can either 263 | # be an index, a slice, a choice function, or a combination of 264 | # these expressed as a tuple/iterable. 265 | # 266 | # One of the main features of this function is it won't return 267 | # duplicates when the indices in a multi-selection overlap. 268 | # And by using dicts, it is hopefully still a very speedy 269 | # function. 270 | def listselect(lst, index, /): 271 | if callable(index): 272 | condition = index 273 | return dict((i,item) for i,item in enumerate(lst) if condition(item)) 274 | # Handle case of multiple index ranges provided 275 | elif isinstance(index, Iterable): 276 | pieces = index 277 | selection = dict() 278 | for piece in pieces: 279 | selection.update(listselect(lst, piece)) 280 | return selection 281 | else: 282 | # Turn index into a slice if it's just a single int 283 | if isinstance(index, int): 284 | if index < 0: 285 | index = index % len(lst) 286 | index = sel[index:index+1] 287 | return dict(zip(range(len(lst))[index], lst[index])) 288 | 289 | # # If x is a float that is really an integer, returns int(x). 290 | # # Otherwise, just returns back x unchanged. 291 | # def flattenFloat(x): 292 | # if type(x) is float and x == int(x): 293 | # x = int(x) 294 | # return x 295 | 296 | # Returns the functional composition of all the functions provided. 297 | # e.g. Given compose(f,g,h), returns f o g o h = f(g(h(*))) 298 | def compose(*funcs): 299 | if len(funcs) == 0: 300 | raise ValueError("No functions to compose!") 301 | def composition(*args, **kwargs): 302 | value = funcs[-1](*args, **kwargs) 303 | for n in range(len(funcs)-2,-1,-1): 304 | value = funcs[n](value) 305 | return value 306 | 307 | return composition 308 | 309 | # Returns the functional composition of f with g: (f o g) 310 | def compose2(f,g): 311 | return lambda *args, **kwargs: f(g(*args, **kwargs)) 312 | 313 | 314 | # Finds the roots of the polynomial defined as a list of 315 | # coefficients in ascending order of degree. 316 | # e.g. [-4, 0, 1] represents the polynomial -4 + x^2 317 | # The answer is returned as a numpy array. 318 | def polyroots(coeffs): 319 | polynom = np.polynomial.Polynomial(coeffs) 320 | return polynom.roots() 321 | 322 | 323 | # Given a standard viewbox [xmin,xmax, ymin,ymax], 324 | # returns the corners of the box as a list of complex numbers. 325 | # Useful for passing into a Path or Polygon figure's seq or vertices. 326 | # By default, it does so starting at the northwest corner and 327 | # going counter-clockwise, but this can be modified by altering 328 | # the "initCorner" and "CCW" parameters. By default: 329 | # initCorner = "NW" 330 | # CCW = True 331 | @handleBoxTypecasting 332 | def boxCorners(box, initCorner="NW", CCW=True): 333 | initCorner = initCorner.upper() 334 | dirs = ["NW", "SW", "SE", "NE"] 335 | if initCorner not in dirs: 336 | raise ValueError('initCorner must be "NW", "SW", "SE", or "NE".') 337 | 338 | left = box[0] 339 | right = box[1] 340 | bottom = box[2] 341 | top = box[3] 342 | 343 | corners = [left+1j*top, left+1j*bottom, right+1j*bottom, right+1j*top] 344 | 345 | # Reverse order if done clockwise 346 | if not CCW: 347 | corners = corners[::-1] 348 | dirs = dirs[::-1] 349 | 350 | # Find the index of the starting corner 351 | i = dirs.index(initCorner) 352 | 353 | # Order the corners according to the starting corner and direction 354 | corners = corners[i:] + corners[:i] 355 | 356 | return corners 357 | 358 | # Computes the box 4-item list [xmin, xmax, ymin, ymax] 359 | # given the positions of two of its opposite corners 360 | # (specified as complex numbers). 361 | def boxFromDiagonal(c1, c2, /): 362 | x1, y1 = c1.real, c1.imag 363 | x2, y2 = c2.real, c2.imag 364 | 365 | xmin = min(x1, x2) 366 | xmax = max(x1, x2) 367 | ymin = min(y1, y2) 368 | ymax = max(y1, y2) 369 | 370 | return [xmin, xmax, ymin, ymax] 371 | 372 | # Pads a bounding box by the given pad amount. 373 | # Usage: padbox(box, pad) -> paddedBox 374 | # where `box` is a 4-tuple defining the bounding box in the format 375 | # [xmin, xmax, ymin, ymax] 376 | # Optionally, a ypad value can be specified, allowing the box to be 377 | # padded differently vertically vs horizontally: 378 | # padbox(box, xpad, ypad) -> paddedBox 379 | @handleBoxTypecasting 380 | def padbox(box, xpad, ypad=None, /): 381 | if ypad is None: 382 | ypad = xpad 383 | 384 | box = list(box) 385 | box[0] -= xpad 386 | box[1] += xpad 387 | box[2] -= ypad 388 | box[3] += ypad 389 | 390 | return box 391 | padBox = padbox # Alias 392 | 393 | # Shifts a bounding box of the form [xmin,xmax,ymin,ymax] 394 | # by the given 2d vector `shift` expressed as a complex number. 395 | # Returns the modified box. 396 | @handleBoxTypecasting 397 | def shiftBox(box, shift): 398 | left, right, bottom, top = box 399 | # Adjust by origin 400 | left += shift.real 401 | right += shift.real 402 | bottom += shift.imag 403 | top += shift.imag 404 | return [left, right, bottom, top] 405 | 406 | # Computes the total bounding box of a list of boxes. 407 | def totalBox(boxes, pad=0): 408 | XMIN, YMIN, XMAX, YMAX = oo, oo, -oo, -oo 409 | for box in boxes: 410 | if isinstance(box, morpho.Actor): 411 | box = box.last().box() 412 | elif isinstance(box, morpho.Figure): 413 | box = box.box() 414 | xmin, xmax, ymin, ymax = box 415 | XMIN = min(XMIN, xmin) 416 | YMIN = min(YMIN, ymin) 417 | XMAX = max(XMAX, xmax) 418 | YMAX = max(YMAX, ymax) 419 | 420 | bigbox = [XMIN, XMAX, YMIN, YMAX] 421 | if isbadarray(bigbox): 422 | raise ValueError("Total box is unbounded or undefined.") 423 | 424 | return padbox(bigbox, pad) 425 | 426 | # Attempts to infer the bounding box of an Actor or Figure. 427 | # Given a figure, calls its box() method and returns the result. 428 | # Given an Actor, calls the box() method of its latest keyfigure 429 | # and returns the result. 430 | # Otherwise returns the original input unchanged. 431 | def inferBox(obj): 432 | if isinstance(obj, morpho.Actor): 433 | obj = obj.last() 434 | if isinstance(obj, morpho.Figure): 435 | if not hasattr(obj, "box"): 436 | raise TypeError(f"`{type(obj).__name__}` type figure does not support box() method.") 437 | box = obj.box() 438 | else: 439 | box = obj 440 | return box 441 | 442 | # Converts minutes with seconds into just seconds. 443 | # minsec(m,s) --> 60*m + s 444 | # 445 | # If given only a single decimal number as input, it treats 446 | # it as (minutes).(seconds) and converts it to seconds. 447 | # Example: minsec(2.05) --> 125 (within rounding error) 448 | def minsec(mins, secs=None, /): 449 | if secs is None: 450 | value = mins 451 | mins = int(value) 452 | secs = (value-mins)/0.6 * 60 453 | return 60*mins + secs 454 | 455 | # Concatenates several sequences into a single list. 456 | # No underlying sequence is modified in the process. 457 | def concat(*seqs): 458 | total = [] 459 | for seq in seqs: 460 | total.extend(seq) 461 | return total 462 | 463 | # Converts an iterable object into a list, but if it's not 464 | # iterable, returns a singleton list containing the object. 465 | def aslist(x, /): 466 | try: 467 | x = list(x) 468 | except TypeError: 469 | x = [x] 470 | return x 471 | 472 | # Allows one to easily define a Python slice object 473 | # using slice syntax. 474 | # Example: sel[1:3] --> slice(1,3) 475 | sel = np.s_ 476 | -------------------------------------------------------------------------------- /morpholib/tools/color.py: -------------------------------------------------------------------------------- 1 | from morpholib.color import * 2 | -------------------------------------------------------------------------------- /morpholib/tools/ktimer.py: -------------------------------------------------------------------------------- 1 | # Provides a class of stopwatch objects. 2 | import time 3 | 4 | ''' 5 | Stopwatch object. Measures the duration between a .tic() call 6 | and a .toc() call in units of seconds.''' 7 | class Timer(object): 8 | # Constructor. Initializes startTime to 9 | # the time of construction. 10 | def __init__(self): 11 | self.startTime = time.perf_counter() 12 | 13 | # Resets the timer. 14 | def tic(self): 15 | self.startTime = time.perf_counter() 16 | 17 | # Returns time elapsed (in seconds) since the last 18 | # .tic() was called. 19 | def toc(self): 20 | return time.perf_counter() - self.startTime 21 | 22 | # Function waits for the condition specified by cond to be True. 23 | # cond is an expression given using lambda. 24 | # e.g. waitfor(lambda: fileExists("someFile.txt")) 25 | # "until" is the maximum amount of time to wait for cond to be True 26 | # Defaults to infinity. 27 | # "period" is how many seconds to wait before checking again if cond is True 28 | # Defaults to 1 second. 29 | # waitfor() returns True if cond becomes True 30 | # waitfor() returns False if until exceeded, and so waitfor() gave up. 31 | # NOTE: cond is evaluated lazily. waitfor() uses the variable names: 32 | # cond, __waitfor_timer, until, time, period 33 | # If you use the same names in your cond, waitfor() may act badly. 34 | # Hopefully, your expression for cond doesn't use any of these names, 35 | # but just to be safe, the correct way to provide input into cond is to 36 | # explicitly declare your variables like so: 37 | # 38 | # lambda x=x, y=y, z=z: your_expression_containing_xyz 39 | # 40 | # That way, when cond is evaluated, x, y, and z will be YOUR x, y, and z. 41 | def waitfor(cond, until=float("inf"), period=1): 42 | if type(cond) is str: 43 | cond = lambda cond=cond: eval(cond) 44 | __waitfor_timer = Timer() 45 | while not cond() and __waitfor_timer.toc() < until: 46 | time.sleep(period) 47 | return cond() 48 | 49 | # Object used for the tic() and toc() functions that can 50 | # be used in the global scope. 51 | masterTimer = Timer() 52 | 53 | # Resets the master timer 54 | def tic(): 55 | masterTimer.tic() 56 | 57 | # Returns seconds elapsed since last tic() was called. 58 | def toc(): 59 | return masterTimer.toc() 60 | -------------------------------------------------------------------------------- /morpholib/tools/latex2svg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """latex2svg 3 | 4 | Read LaTeX code from stdin and render a SVG using LaTeX + dvisvgm. 5 | 6 | Modified by Kenneth Small for use in Morpho. 7 | """ 8 | 9 | """ 10 | The MIT License (MIT) 11 | ===================== 12 | 13 | Copyright © 2017 Tino Wagner 14 | 15 | Permission is hereby granted, free of charge, to any person 16 | obtaining a copy of this software and associated documentation 17 | files (the “Software”), to deal in the Software without 18 | restriction, including without limitation the rights to use, 19 | copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | copies of the Software, and to permit persons to whom the 21 | Software is furnished to do so, subject to the following 22 | conditions: 23 | 24 | The above copyright notice and this permission notice shall be 25 | included in all copies or substantial portions of the Software. 26 | """ 27 | 28 | __version__ = '0.1.0' 29 | __author__ = 'Tino Wagner' 30 | __email__ = 'ich@tinowagner.com' 31 | __license__ = 'MIT' 32 | __copyright__ = '(c) 2017, Tino Wagner' 33 | 34 | import os 35 | import sys 36 | import subprocess 37 | import shlex 38 | import re 39 | from tempfile import TemporaryDirectory 40 | from ctypes.util import find_library 41 | 42 | default_template = r""" 43 | \documentclass[{{ fontsize }}pt,preview]{standalone} 44 | {{ preamble }} 45 | \begin{document} 46 | \begin{preview} 47 | {{ code }} 48 | \end{preview} 49 | \end{document} 50 | """ 51 | 52 | default_preamble = r""" 53 | \usepackage{amsmath} 54 | \usepackage{amsfonts} 55 | \usepackage{amssymb} 56 | \usepackage{xcolor} 57 | """ 58 | 59 | latex_cmd = 'latex -interaction nonstopmode -halt-on-error' 60 | dvisvgm_cmd = 'dvisvgm --no-fonts' 61 | 62 | default_params = { 63 | 'fontsize': 12, # pt 64 | 'template': default_template, 65 | 'preamble': default_preamble, 66 | 'latex_cmd': latex_cmd, 67 | 'dvisvgm_cmd': dvisvgm_cmd, 68 | 'libgs': None, 69 | } 70 | 71 | 72 | if not hasattr(os.environ, 'LIBGS') and not find_library('libgs'): 73 | if sys.platform == 'darwin': 74 | # Fallback to homebrew Ghostscript on macOS 75 | homebrew_libgs = '/usr/local/opt/ghostscript/lib/libgs.dylib' 76 | if os.path.exists(homebrew_libgs): 77 | default_params['libgs'] = homebrew_libgs 78 | if not default_params['libgs']: 79 | # print('Warning: libgs not found') 80 | pass 81 | 82 | 83 | def latex2svg(code, params=default_params, working_directory=None): 84 | """Convert LaTeX to SVG using dvisvgm. 85 | 86 | Parameters 87 | ---------- 88 | code : str 89 | LaTeX code to render. 90 | params : dict 91 | Conversion parameters. 92 | working_directory : str or None 93 | Working directory for external commands and place for temporary files. 94 | 95 | Returns 96 | ------- 97 | dict 98 | Dictionary of SVG output and output information: 99 | 100 | * `svg`: SVG data 101 | * `width`: image width in *em* 102 | * `height`: image height in *em* 103 | * `depth`: baseline position in *em* 104 | """ 105 | if working_directory is None: 106 | with TemporaryDirectory() as tmpdir: 107 | return latex2svg(code, params, working_directory=tmpdir) 108 | 109 | fontsize = params['fontsize'] 110 | document = (params['template'] 111 | .replace('{{ preamble }}', params['preamble']) 112 | .replace('{{ fontsize }}', str(fontsize)) 113 | .replace('{{ code }}', code)) 114 | 115 | with open(os.path.join(working_directory, 'code.tex'), 'w') as f: 116 | f.write(document) 117 | 118 | # Run LaTeX and create DVI file 119 | try: 120 | ret = subprocess.run(shlex.split(params['latex_cmd']+' code.tex'), 121 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, 122 | cwd=working_directory) 123 | ret.check_returncode() 124 | except FileNotFoundError: 125 | raise RuntimeError('latex not found') 126 | 127 | # Add LIBGS to environment if supplied 128 | env = os.environ.copy() 129 | if params['libgs']: 130 | env['LIBGS'] = params['libgs'] 131 | 132 | # Convert DVI to SVG 133 | try: 134 | ret = subprocess.run(shlex.split(params['dvisvgm_cmd']+' code.dvi'), 135 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, 136 | cwd=working_directory, env=env) 137 | ret.check_returncode() 138 | except FileNotFoundError: 139 | raise RuntimeError('dvisvgm not found') 140 | 141 | with open(os.path.join(working_directory, 'code.svg'), 'r') as f: 142 | svg = f.read() 143 | 144 | # Parse dvisvgm output for size and alignment 145 | def get_size(output): 146 | regex = r'\b([0-9.]+)pt x ([0-9.]+)pt' 147 | match = re.search(regex, output) 148 | if match: 149 | return (float(match.group(1)) / fontsize, 150 | float(match.group(2)) / fontsize) 151 | else: 152 | return None, None 153 | 154 | def get_measure(output, name): 155 | regex = r'\b%s=([0-9.]+)pt' % name 156 | match = re.search(regex, output) 157 | if match: 158 | return float(match.group(1)) / fontsize 159 | else: 160 | return None 161 | 162 | output = ret.stderr.decode('utf-8') 163 | width, height = get_size(output) 164 | depth = get_measure(output, 'depth') 165 | return {'svg': svg, 'depth': depth, 'width': width, 'height': height} 166 | 167 | 168 | # def main(): 169 | # """Simple command line interface to latex2svg. 170 | 171 | # - Read from `stdin`. 172 | # - Write SVG to `stdout`. 173 | # - Write metadata as JSON to `stderr`. 174 | # - On error: write error messages to `stdout` and return with error code. 175 | # """ 176 | # import json 177 | # import argparse 178 | # parser = argparse.ArgumentParser(description=""" 179 | # Render LaTeX code from stdin as SVG to stdout. Writes metadata (baseline 180 | # position, width, height in em units) as JSON to stderr. 181 | # """) 182 | # parser.add_argument('--preamble', 183 | # help="LaTeX preamble code to read from file") 184 | # args = parser.parse_args() 185 | # preamble = default_preamble 186 | # if args.preamble is not None: 187 | # with open(args.preamble) as f: 188 | # preamble = f.read() 189 | # latex = sys.stdin.read() 190 | # try: 191 | # params = default_params.copy() 192 | # params['preamble'] = preamble 193 | # out = latex2svg(latex, params) 194 | # sys.stdout.write(out['svg']) 195 | # meta = {key: out[key] for key in out if key != 'svg'} 196 | # sys.stderr.write(json.dumps(meta)) 197 | # except subprocess.CalledProcessError as exc: 198 | # print(exc.output.decode('utf-8')) 199 | # sys.exit(exc.returncode) 200 | 201 | 202 | # if __name__ == '__main__': 203 | # main() 204 | -------------------------------------------------------------------------------- /morpholib/tools/subimporter.py: -------------------------------------------------------------------------------- 1 | ''' 2 | CREDIT: Thanks to Mr. B's answer on StackOverflow 3 | https://stackoverflow.com/a/25562415 4 | ''' 5 | 6 | import importlib 7 | import pkgutil 8 | 9 | 10 | def import_submodules(package, recursive=True): 11 | """ Import all submodules of a module, recursively, including subpackages 12 | 13 | :param package: package (name or actual module) 14 | :type package: str | module 15 | :rtype: dict[str, types.ModuleType] 16 | """ 17 | if isinstance(package, str): 18 | package = importlib.import_module(package) 19 | results = {} 20 | for loader, name, is_pkg in pkgutil.walk_packages(package.__path__): 21 | full_name = package.__name__ + '.' + name 22 | results[full_name] = importlib.import_module(full_name) 23 | if recursive and is_pkg: 24 | results.update(import_submodules(full_name)) 25 | return results 26 | -------------------------------------------------------------------------------- /morpholib/transitions.py: -------------------------------------------------------------------------------- 1 | import morpholib as morpho 2 | import math, sys 3 | 4 | # Module has alternative name "transition" (singular) 5 | self = sys.modules[__name__] 6 | morpho.transition = self 7 | 8 | # # Returns a transition which is the composition of f and g (f o g) 9 | # def compose(f, g): 10 | # return lambda t: f(g(t)) 11 | 12 | # BUILT-IN TRANSITIONS 13 | 14 | # Easing transition based on the arctangent function. 15 | # Eases in and out of keyframes. 16 | # This was the very first easing transition I implemented, 17 | # but I now almost always use quadease instead, as I think 18 | # it looks better and is probably faster to compute. 19 | # Qualitatively, the main difference between atanease and 20 | # quadease is that atanease is slower at the start and end, 21 | # and faster in the middle compared to quadease. 22 | def atanease(t): 23 | return (math.atan(14*t-7) + 1.4289)/2.8578 24 | # atanease = lambda t: (math.atan(14*t-7) + 1.4289)/2.8578 25 | slow_fast_slow = atanease # Alternate name 26 | 27 | # Interpolates like how a ball is dropped. 28 | # Slow at first, fast at end. 29 | def drop(t): 30 | return t**2 31 | # drop = lambda t: t**2 32 | 33 | # Interpolates like how a ball is tossed upward. 34 | # Fast at first, slow at end. 35 | def toss(t): 36 | return 1 - (t-1)**2 37 | # toss = lambda t: 1 - (t-1)**2 38 | 39 | # Quadratic Easing transition. 40 | # Slow at the start and end, and faster in the middle, 41 | # according to a quadratic curve. 42 | # This is the primary transition function I use to make 43 | # interpolations that ease in and out of keyframes. 44 | # It is a piecewise combination of the drop and toss 45 | # transitions. 46 | def quadease(t): 47 | return drop(2*t)/2 if t < 0.5 else (1+toss(2*t-1))/2 48 | droptoss = quadease # Alternate name. Drop then a toss. 49 | # quadease = lambda t: drop(2*t)/2 if t < 0.5 else (1+toss(2*t-1))/2 50 | 51 | 52 | # Glide transition maker. 53 | # Returns a glide transition based on the inflection points 54 | # provided. 55 | # A glide transition is where an animation accelerates in, 56 | # moves at a constant speed for a while, then decelerates out. 57 | # The points at which one behavior switches to the next are 58 | # called inflection points. 59 | # The acceleration and deceleration follow a quadratic 60 | # trajectory. 61 | # 62 | # INPUTS (positional only) 63 | # a = First inflection: a number in the range [0,1]. 64 | # For example: a = 0.25 means acceleration phase will 65 | # last until 25% of the transition is completed. 66 | # b = Second inflection. If unspecified, defaults to 1-a 67 | # to make a symmetric glide. Must satisfy a <= b. 68 | # 69 | # Example usage: 70 | # myfig.transition = morpho.transition.glide(0.2) 71 | # myfig2.transition = morpho.transition.glide(0.1, 0.75) 72 | # 73 | # If a = b = 0, the transition will be identical to toss. 74 | # If a = b = 0.5, the transition will be identical to quadease. 75 | # If a = b = 1, the transition will be identical to drop. 76 | def glide(a, b=None, /): 77 | if b is None: 78 | b = 1-a 79 | if not(0 <= a <= b <= 1): 80 | raise ValueError("Invalid inflection value. Must have 0 <= a <= b <= 1") 81 | 82 | m = 2/(b-a+1) 83 | def glideTransition(t): 84 | if t < a: 85 | return m*t**2/(2*a) 86 | elif t <= b: 87 | return m*(t-a/2) 88 | else: 89 | return 0.5*m*(b-a+1 - (t-1)**2/(1-b)) 90 | return glideTransition 91 | 92 | # Equivalent to glide(), but the second inflection is specified 93 | # in terms of how far away it is from 1. 94 | # e.g. The following are equivalent: 95 | # coast(a, b) 96 | # glide(a, 1-b) 97 | def coast(a, b=None, /): 98 | if b is not None: 99 | b = 1-b 100 | return glide(a,b) 101 | 102 | 103 | halfpi = math.pi/2 104 | # sinetoss = lambda t: math.sin(halfpi*t) 105 | # sinedrop = lambda t: 1 - math.cos(halfpi*t) 106 | # sineease = sinease = lambda t: (1-math.cos(math.pi*t))/2 107 | 108 | # Trig versions of toss/drop and ease. 109 | # The movement is sinusoidal instead of quadratic. 110 | 111 | # Trig version of toss() where the curve is sinusoidal 112 | # instead of quadratic. 113 | def sinetoss(t): 114 | return math.sin(halfpi*t) 115 | 116 | # Trig version of drop() where the curve is sinusoidal 117 | # instead of quadratic. 118 | def sinedrop(t): 119 | return 1 - math.cos(halfpi*t) 120 | 121 | # Trig version of quadease() where the curve is sinusoidal 122 | # instead of quadratic. 123 | def sineease(t): 124 | return (1-math.cos(math.pi*t))/2 125 | sinease = sineease 126 | 127 | # No special transition. Just transition uniformly at a constant 128 | # speed to the next keyframe. 129 | def uniform(t): 130 | return t 131 | # uniform = lambda t: t 132 | 133 | # DEPRECATED! Set `static=True` instead! 134 | # Instant transition. Makes the figure jump instantly from 135 | # initial to final keyfigure where t = 1 maps to the final 136 | # keyfigure, and all 0 <= t < 1 map to the initial keyfigure. 137 | def instant(t): 138 | raise NotImplementedError("instant() transition is deprecated. Use `static=True` or `tweenInstant()` instead.") 139 | return int(t) 140 | step = jump = instant # Alternate name 141 | # instant = lambda t: int(t) 142 | 143 | # Set default transition to uniform. This can be modified anywhere 144 | # in the code by calling 145 | # morpho.transitions.default = newtransition 146 | default = uniform 147 | 148 | # Splits a strictly increasing transition function that maps 149 | # [0,1] onto [0,1] into two transition functions that are 150 | # normalized to work as regular transition functions. 151 | def split(func, t): 152 | if not(0 < t < 1): 153 | raise ValueError(f"t must be strictly between 0 and 1. Got t={t}") 154 | 155 | y = func(t) 156 | 157 | def func1(s): 158 | return func(morpho.lerp0(0,t, s))/y 159 | 160 | def func2(s): 161 | return (func(morpho.lerp0(t,1, s))-y)/(1-y) 162 | 163 | return func1, func2 164 | 165 | # Mainly for internal use by incorporateTransition(). 166 | # Generates the splitter for the modified tween method 167 | # returned by incorporateTransition(). 168 | def _generateTransitionSplitter(transition, tweenmethod): 169 | def newSplitter(t, beg, mid, fin): 170 | trans1, trans2 = split(transition, t) 171 | 172 | if morpho.tweenSplittable(tweenmethod): 173 | # Create temporary copies of the keyfigures 174 | # with their tween methods set to `tweenmethod` 175 | # so that we can split `tweenmethod` into its 176 | # two partial methods independent of whatever 177 | # the tween methods of the original beg, mid, 178 | # and fin are. 179 | beg0 = beg.copy().set(tweenMethod=tweenmethod) 180 | mid0 = mid.copy().set(tweenMethod=tweenmethod) 181 | fin0 = fin.copy().set(tweenMethod=tweenmethod) 182 | tr_t = transition(t) 183 | 184 | # Apply splitter at transition(t) to the temporary 185 | # keyfigures in order to extract the partial tween 186 | # methods of `tweenmethod` which are needed later 187 | # to split the transition-incorporated tween method. 188 | tweenmethod.splitter(tr_t, beg0, mid0, fin0) 189 | # Apply the splitter a second time to the original 190 | # keyfigures in case the splitter needs to actually 191 | # modify them beyond just their tween methods 192 | # (i.e. non-standard splitters). 193 | # NOTE FOR FUTURE: Possibly get rid of this line, as 194 | # I suspect it will very, very rarely be useful, and 195 | # might not be worth the slight code slowdown. 196 | tweenmethod.splitter(tr_t, beg, mid, fin) 197 | basetween1, basetween2 = beg0.tweenMethod, mid0.tweenMethod 198 | else: 199 | # Assume the tween method respects splitting 200 | basetween1 = basetween2 = tweenmethod 201 | 202 | @morpho.TweenMethod(splitter=_generateTransitionSplitter(trans1, basetween1)) 203 | def tween1(self, other, t, *args, **kwargs): 204 | return basetween1(self, other, trans1(t), *args, **kwargs) 205 | 206 | @morpho.TweenMethod(splitter=_generateTransitionSplitter(trans2, basetween2)) 207 | def tween2(self, other, t, *args, **kwargs): 208 | return basetween2(self, other, trans2(t), *args, **kwargs) 209 | 210 | beg.tweenMethod = tween1 211 | mid.tweenMethod = tween2 212 | return newSplitter 213 | 214 | # Incorporates the given transition function directly into the given 215 | # tween method, meaning using this tween method with a uniform 216 | # transition will produce the same effect as the original tween method 217 | # with the given transition function applied separately. 218 | # The modified tween method is returned and is not changed in place. 219 | # 220 | # Note that to work reliably, the given tween method's splitter should 221 | # be a standard splitter, meaning it only modifies the tween methods 222 | # of the beginning and middle keyfigures and has no other effects. 223 | def incorporateTransition(transition, tweenmethod): 224 | @morpho.TweenMethod(splitter=_generateTransitionSplitter(transition, tweenmethod)) 225 | def newTween(self, other, t, *args, **kwargs): 226 | return tweenmethod(self, other, transition(t), *args, **kwargs) 227 | return newTween 228 | -------------------------------------------------------------------------------- /morpholib/video.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Contains some functions useful for making 1080p videos. 3 | ''' 4 | 5 | import morpholib as morpho 6 | from morpholib.tools.basics import * 7 | 8 | import math 9 | 10 | ratio = 1920/1080 11 | ratioXY = ratio 12 | ratioYX = 1/ratio 13 | std_view = (-10*ratio, 10*ratio, -10, 10) 14 | 15 | def view169(): 16 | return list(std_view) 17 | 18 | # Returns a standard video Animation object. 19 | # Meaning an animation with window shape 1920 x 1080, fullscreen. 20 | # A layer list can optionally be supplied. So can background color and opacity. 21 | # Default background and opacity are [1,1,1] (white) and 1 (opaque) 22 | def standardAnimation(layers=None, background=(1,1,1), alpha=1): 23 | # if layers is None: 24 | # layers = morpho.Actor(morpho.Figure) 25 | if type(layers) not in (tuple, list) and isinstance(layers, morpho.Actor): 26 | layers = morpho.anim.Layer(layers, view=std_view) 27 | elif isinstance(layers, morpho.Figure): 28 | layers = morpho.anim.Layer(morpho.Actor(layers), view=std_view) 29 | elif layers is None: 30 | layers = morpho.Layer(view=std_view) 31 | 32 | mation = morpho.Animation(layers) 33 | mation.windowShape = (1920, 1080) 34 | mation.fullscreen = True 35 | mation.resizable = False 36 | # mation.firstIndex = 0 37 | mation.background = background[:3] 38 | mation.alpha = alpha 39 | 40 | # if BGgrid: 41 | # nhorz = math.ceil(std_view[3])-math.floor(std_view[2])+1 42 | # nvert = math.ceil(std_view[1])-math.floor(std_view[0])+1 43 | # hcolor = morpho.tools.color.rgbNormalize(0x23, 0xb5, 0x83) 44 | # vcolor = (0,0,1) 45 | # grid = morpho.grid.standardGrid( 46 | # view=std_view, 47 | # nhorz=nhorz, nvert=nvert, 48 | # hres=0.05, vres=0.05, 49 | # hcolor=hcolor, vcolor=vcolor, alpha=0.5, 50 | # hmidColor=morpho.tools.color.alphaOverlay(mation.background[:3], hcolor, 0.5), 51 | # vmidColor=morpho.tools.color.alphaOverlay(mation.background[:3], vcolor, 0.5), 52 | # BGgrid=False, axesColor=(0,0,0) 53 | # ) 54 | # grid = grid.fimage(lambda s: ((nvert-1)/2)/std_view[1]*s.real + 1j*s.imag) 55 | # grid.static = True 56 | # grid.delay = oo 57 | # grid = morpho.Actor(grid) 58 | 59 | # layer = morpho.Layer(grid, std_view) 60 | # mation.layers.insert(0, layer) 61 | # mation.locaterLayer = 0 62 | 63 | return mation 64 | 65 | 66 | # Essentially same as standardAnimation(), but intended for SpaceLayers. 67 | def standardSpaceAnimation(layers=None, background=(1,1,1), alpha=1): 68 | # if layers is None: 69 | # layers = morpho.Actor(morpho.Figure) 70 | if type(layers) not in (tuple, list) and isinstance(layers, morpho.Actor): 71 | layers = morpho.anim.SpaceLayer(layers, view=std_view) 72 | elif isinstance(layers, morpho.Figure): 73 | layers = morpho.anim.SpaceLayer(morpho.Actor(layers), view=std_view) 74 | elif layers is None: 75 | layers = morpho.SpaceLayer(view=std_view) 76 | 77 | mation = morpho.Animation(layers) 78 | mation.windowShape = (1920, 1080) 79 | mation.fullscreen = True 80 | mation.resizable = False 81 | # mation.firstIndex = 0 82 | mation.background = background[:3] 83 | mation.alpha = alpha 84 | 85 | return mation 86 | 87 | 88 | # Sets up a standard 3D animation viewing the first octant. 89 | # 90 | # ARGUMENTS 91 | # t_rot = Camera rotation duration (in frames). Default: 300 92 | # initOrient = Initial camera orientation before tilting and spinning. 93 | # Default: np.eye(3) 94 | # spin = Angular distance the camera will revolve (in rad). 95 | # Default: -80*pi/180 (80 degs clockwise) 96 | # tilt = Amount camera is tilted down from viewing the xy-plane from above. 97 | # Default: 70*pi/180 (70 degs) 98 | def setupSpace( 99 | t_rot=300, initOrient=np.identity(3), spin=-80*tau/360, 100 | tilt=70*tau/360, *, 101 | xColor=(0, 0.5, 0), yColor=(0,0,0.7), zColor=(0.7,0,0) 102 | ): 103 | 104 | xAxis = morpho.grid.SpaceArrow(0, 7.5) 105 | xAxis.color = xColor 106 | xAxis.width = 7 107 | xAxis.headSize = 20 108 | 109 | yAxis = xAxis.copy() 110 | yAxis.head = 7.5j 111 | yAxis.color = yColor 112 | 113 | zAxis = xAxis.copy() 114 | zAxis.head = [0,0,4.5] 115 | zAxis.color = zColor 116 | 117 | xLabel = morpho.text.SpaceText( 118 | text="x", font="Times New Roman", 119 | pos=xAxis.head + [0, -0.25, 0.25], 120 | size=55, italic=True, 121 | anchor_x=0, anchor_y=0, 122 | color=[0,0,0] 123 | ) 124 | yLabel = morpho.text.SpaceText( 125 | text="y", font="Times New Roman", 126 | pos=yAxis.head + [-0.25, 0, 0.25], 127 | size=55, italic=True, 128 | anchor_x=0, anchor_y=0, 129 | color=[0,0,0] 130 | ) 131 | zLabel = morpho.text.SpaceText( 132 | text="z", font="Times New Roman", 133 | pos=zAxis.head + [0,0,0.25], 134 | size=55, italic=True, 135 | anchor_x=0, anchor_y=0, 136 | color=[0,0,0] 137 | ) 138 | 139 | orient = initOrient 140 | theta0 = -pi/2 - 5*pi/180 141 | orient = morpho.matrix.rotation([0,0,1], theta0) @ orient 142 | orient = morpho.matrix.rotation([1,0,0], -tilt) @ orient 143 | focus = [3.5, 3.5, 2.5] 144 | # focus = 0 145 | layer = morpho.SpaceLayer(orient=orient, focus=focus) 146 | layer.camera.time(0).view = list(std_view) 147 | 148 | # orient = morpho.matrix.rotation([0,0,1], -pi + 5*pi/180) 149 | orient = initOrient 150 | orient = morpho.matrix.rotation([0,0,1], theta0+spin) @ orient 151 | orient = morpho.matrix.rotation([1,0,0], -tilt) @ orient 152 | layer.camera.first().transition = morpho.transition.uniform 153 | layer.camera.first().centerAt(3.5 + 3.5j) 154 | layer.camera.first().zoomIn(2.5) 155 | layer.camera.newkey(t_rot) 156 | layer.camera.last().orient = orient 157 | 158 | # Grid 159 | grid = morpho.grid.wireframe( 160 | view=[0,7, 0,7], 161 | dx=1, dy=1, 162 | hnodes=2, vnodes=2, 163 | hcolor=xColor[:], vcolor=yColor[:], 164 | axes=False, optimize=True 165 | ) 166 | # grid.figures.pop(14) 167 | # grid.figures.pop(21) 168 | # grid.figures = morpho.grid.optimizePathList(grid.figures) 169 | grid = morpho.Frame(grid.figures) 170 | layer.merge(grid) 171 | 172 | # Merge axes and labels 173 | layer.merge(xAxis) 174 | layer.merge(yAxis) 175 | layer.merge(zAxis) 176 | layer.merge(xLabel) 177 | layer.merge(yLabel) 178 | layer.merge(zLabel) 179 | 180 | # mation = standardAnimation(background=(1,1,1,1)) 181 | # mation.layers[0] = layer 182 | 183 | mation = standardSpaceAnimation(layer) 184 | 185 | return mation 186 | 187 | 188 | # Sets up an alternative 3D animation object centered around the z-axis. 189 | # 190 | # ARGUMENTS 191 | # t_rot = Camera rotation duration (in frames). Default: 300 192 | # initOrient = Initial camera orientation before tilting and spinning. 193 | # Default: np.eye(3) 194 | # spin = Angular distance the camera will revolve (in rad). 195 | # Default: -80*pi/180 (80 degs clockwise) 196 | # tilt = Amount camera is tilted down from viewing the xy-plane from above. 197 | # Default: 70*pi/180 (70 degs) 198 | def setupSpaceAlt( 199 | t_rot=300, initOrient=np.identity(3), spin=-80*tau/360, 200 | tilt=70*tau/360, *, 201 | xColor=(0, 0.5, 0), yColor=(0,0,0.7), zColor=(0.7,0,0) 202 | ): 203 | 204 | xAxis = morpho.grid.SpaceArrow(0, 4.5) 205 | xAxis.color = xColor 206 | xAxis.width = 7 207 | xAxis.headSize = 20 208 | 209 | yAxis = xAxis.copy() 210 | yAxis.head = 4.5j 211 | yAxis.color = yColor 212 | 213 | # zAxis = xAxis.copy() 214 | # zAxis.head = [0,0,4.5] 215 | # zAxis.color = [0.7,0,0] 216 | 217 | xLabel = morpho.text.SpaceText( 218 | text="x", font="Times New Roman", 219 | pos=xAxis.head + [0.4,0-0.25,0], 220 | size=55, italic=True, 221 | anchor_x=0, anchor_y=0, 222 | color=[0,0,0] 223 | ) 224 | yLabel = morpho.text.SpaceText( 225 | text="y", font="Times New Roman", 226 | pos=yAxis.head + [-0.25,0.4,0.1], 227 | size=55, italic=True, 228 | anchor_x=0, anchor_y=0, 229 | color=[0,0,0] 230 | ) 231 | # zLabel = morpho.text.SpaceText( 232 | # text="z", pos=zAxis.head + [0,0,0.25], 233 | # size=55, italic=True, 234 | # anchor_x=0, anchor_y=0, 235 | # color=[0,0,0] 236 | # ) 237 | 238 | orient = initOrient 239 | theta0 = -pi/2 - 5*pi/180 240 | orient = morpho.matrix.rotation([0,0,1], theta0) @ orient 241 | orient = morpho.matrix.rotation([1,0,0], -tilt) @ orient 242 | focus = [0, 0, 2] 243 | # focus = 0 244 | layer = morpho.SpaceLayer(orient=orient, focus=focus) 245 | layer.camera.time(0).view = list(std_view) 246 | 247 | orient = initOrient 248 | theta1 = theta0 + spin 249 | orient = morpho.matrix.rotation([0,0,1], theta1) @ orient 250 | orient = morpho.matrix.rotation([1,0,0], -tilt) @ orient 251 | layer.camera.first().transition = morpho.transition.uniform 252 | # layer.camera.first().centerAt(3.5 + 3.5j) 253 | layer.camera.first().zoomIn(2.5) 254 | layer.camera.newkey(t_rot) 255 | layer.camera.last().orient = orient 256 | 257 | # Grid 258 | grid = morpho.grid.wireframe( 259 | view=[-4,4, -4,4], 260 | dx=1, dy=1, 261 | hnodes=2, vnodes=2, 262 | hcolor=xColor[:], vcolor=yColor[:], 263 | axes=False 264 | ) 265 | grid = morpho.Frame(grid.figures) 266 | layer.merge(grid) 267 | 268 | layer.merge(xAxis) 269 | layer.merge(yAxis) 270 | # layer.merge(zAxis) 271 | layer.merge(xLabel) 272 | layer.merge(yLabel) 273 | # layer.merge(zLabel) 274 | 275 | # mation = standardAnimation(background=(1,1,1,1)) 276 | # mation.layers[0] = layer 277 | 278 | mation = standardSpaceAnimation(layer) 279 | 280 | return mation 281 | --------------------------------------------------------------------------------