├── .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 |
--------------------------------------------------------------------------------