mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
Merge with main
This commit is contained in:
parent
8b24a433ae
commit
7df4abd334
69 changed files with 745 additions and 557 deletions
2
.github/workflows/python-publish.yml
vendored
2
.github/workflows/python-publish.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install dependencies
|
||||
run: apt-get update && apt-get install -y build-essential python3-dev libcairo2-dev libpango1.0-dev
|
||||
run: sudo apt-get update && sudo apt-get install -y build-essential python3-dev libcairo2-dev libpango1.0-dev
|
||||
|
||||
- name: Set up Python 3.13
|
||||
uses: actions/setup-python@v6
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ authors:
|
|||
-
|
||||
name: "The Manim Community Developers"
|
||||
cff-version: "1.2.0"
|
||||
date-released: 2026-01-17
|
||||
date-released: 2026-02-20
|
||||
license: MIT
|
||||
message: "We acknowledge the importance of good software to support research, and we note that research becomes more valuable when it is communicated effectively. To demonstrate the value of Manim, we ask that you cite Manim in your work."
|
||||
title: Manim – Mathematical Animation Framework
|
||||
url: "https://www.manim.community/"
|
||||
version: "v0.19.2"
|
||||
version: "v0.20.0"
|
||||
...
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ This page contains a list of changes made between releases.
|
|||
:maxdepth: 1
|
||||
|
||||
changelog/experimental
|
||||
changelog/0.20.0-changelog
|
||||
changelog/0.19.2-changelog
|
||||
changelog/0.19.1-changelog
|
||||
changelog/0.19.0-changelog
|
||||
|
|
|
|||
86
docs/source/changelog/0.20.0-changelog.md
Normal file
86
docs/source/changelog/0.20.0-changelog.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
---
|
||||
short-title: v0.20.0
|
||||
description: Changelog for v0.20.0
|
||||
---
|
||||
|
||||
# v0.20.0
|
||||
|
||||
Date
|
||||
: February 20, 2026
|
||||
|
||||
|
||||
## What's Changed
|
||||
### Breaking Changes 🚨
|
||||
* Fix `ImageMobject` 3D rotation/flipping and remove resampling algorithms `lanczos` (`antialias`), `box` and `hamming` by {user}`chopan050` in {pr}`4266`
|
||||
* Fix `YELLOW_C` and add `PURE_CYAN`, `PURE_MAGENTA` and `PURE_YELLOW` by {user}`chopan050` in {pr}`4562`
|
||||
|
||||
### Highlights 🌟
|
||||
* Rewrite MathTex to make it more robust regarding splitting by {user}`henrikmidtiby` in {pr}`4515`
|
||||
|
||||
The MathTex implementation has been updated to make it more robust and fix a number of issues.
|
||||
A beneficial side effect is that named groups in svg files can now be accessed through SVGMobject.
|
||||
|
||||
* Add new Animation Builder `Mobject.always` by {user}`JasonGrace2282` in {pr}`4594`
|
||||
|
||||
This new feature is a convenience wrapper around `add_updater` that allows adding
|
||||
updaters to a mobject in an intuitive and easy-to-read way. Example usage in a scene:
|
||||
```python
|
||||
d = Dot()
|
||||
s = Square()
|
||||
d.always.next_to(s, UP)
|
||||
self.add(s, d)
|
||||
self.play(s.animate.to_edge(LEFT))
|
||||
```
|
||||
|
||||
|
||||
### New Features ✨
|
||||
* Add a `seed` config option + `--seed` CLI option for reproducible randomness in rendered scenes by {user}`arnaud-ma` in {pr}`4532`
|
||||
|
||||
### Enhancements 🚀
|
||||
* Enable `strict=True` for `zip()` where safe by {user}`Oll-iver` in {pr}`4547`
|
||||
|
||||
### Bug Fixes 🐛
|
||||
* using `color` instead of `fill_color` with MathTeX for node labels by {user}`Schefflera-Arboricola` in {pr}`4501`
|
||||
* fix: infinite recursion caused by accessing color of a highlighted Ta… by {user}`BHearron` in {pr}`4435`
|
||||
* Prevent potential `UnboundLocalError` in `PolarPlane` by {user}`RinZ27` in {pr}`4557`
|
||||
* Fixed division by 0 in `turn_animation_into_updater` by {user}`SoldierSacha` in {pr}`4567`
|
||||
* Fix TOCTOU Race Conditions when creating directories by {user}`SoldierSacha` in {pr}`4587`
|
||||
* Resolve more race conditions potentially happening during directory creation by {user}`SoldierSacha` in {pr}`4589`
|
||||
* Fix `c2p`/`coords_to_point` method call with single flat list or 1D array input by {user}`danielalanbates` in {pr}`4596`
|
||||
|
||||
### Documentation 📚
|
||||
* Enable rendered documentation of `RandomColorGenerator` by {user}`arnaud-ma` in {pr}`4533`
|
||||
* Remove pin to Python 3.13 in installation docs by {user}`chopan050` in {pr}`4534`
|
||||
* Fix broken aquabeam OpenGL link using Wayback Machine by {user}`behackl` in {pr}`4545`
|
||||
* Add type annotations and docstrings in `opengl_renderer.py` by {user}`arnaud-ma` in {pr}`4537`
|
||||
* docs: improve `TransformFromCopy` docstring by {user}`GoThrones` in {pr}`4597`
|
||||
|
||||
### Infrastructure & Build 🔨
|
||||
* Install missing dependencies in release pipeline by {user}`behackl` in {pr}`4531`
|
||||
|
||||
### Code Quality & Refactoring 🧹
|
||||
* Rework and consolidate release changelog script, add previously skipped changelog entries by {user}`behackl` in {pr}`4568`
|
||||
* Remove `__future__.annotations` from required imports by {user}`JasonGrace2282` in {pr}`4571`
|
||||
* Cleaned up `mypy.ini` by {user}`henrikmidtiby` in {pr}`4584`
|
||||
* Add `py.typed` to declare manim as having type hints by {user}`Timmmm` in {pr}`4553`
|
||||
* Fix assertion in `ImageMobjectFromCamera.interpolate_color()` by {user}`chopan050` in {pr}`4593`
|
||||
* Reduce dependency on scipy - replace `scipy.special.comb` with `math.comb` by {user}`fmuenkel` in {pr}`4598`
|
||||
|
||||
### Type Hints 📝
|
||||
* Add type annotations to `rotation.py` by {user}`fmuenkel` in {pr}`4535`
|
||||
* Add type annotations to `opengl_compatibility.py` by {user}`fmuenkel` in {pr}`4585`
|
||||
* Add type annotations to `image_mobject.py` by {user}`henrikmidtiby` in {pr}`4458`
|
||||
* Add type annotations to `opengl_image_mobject.py` by {user}`fmuenkel` in {pr}`4536`
|
||||
* Add type annotations to `point_cloud_mobject.py` by {user}`fmuenkel` in {pr}`4586`
|
||||
|
||||
## New Contributors
|
||||
* {user}`arnaud-ma` made their first contribution in {pr}`4533`
|
||||
* {user}`Schefflera-Arboricola` made their first contribution in {pr}`4501`
|
||||
* {user}`BHearron` made their first contribution in {pr}`4435`
|
||||
* {user}`RinZ27` made their first contribution in {pr}`4557`
|
||||
* {user}`SoldierSacha` made their first contribution in {pr}`4567`
|
||||
* {user}`Oll-iver` made their first contribution in {pr}`4547`
|
||||
* {user}`GoThrones` made their first contribution in {pr}`4597`
|
||||
* {user}`danielalanbates` made their first contribution in {pr}`4596`
|
||||
|
||||
**Full Changelog**: [Compare view](https://github.com/ManimCommunity/manim/compare/v0.19.2...v0.20.0)
|
||||
|
|
@ -424,7 +424,7 @@ may be expected. To color only ``x`` yellow, we have to do the following:
|
|||
class CorrectLaTeXSubstringColoring(Scene):
|
||||
def construct(self):
|
||||
equation = MathTex(
|
||||
r"e^x = x^0 + x^1 + \frac{1}{2} x^2 + \frac{1}{6} x^3 + \cdots + \frac{1}{n!} x^n + \cdots",
|
||||
r"e^{x} = x^0 + x^1 + \frac{1}{2} x^2 + \frac{1}{6} x^3 + \cdots + \frac{1}{n!} x^n + \cdots",
|
||||
substrings_to_isolate="x"
|
||||
)
|
||||
equation.set_color_by_tex("x", YELLOW)
|
||||
|
|
@ -434,6 +434,8 @@ By setting ``substrings_to_isolate`` to ``x``, we split up the
|
|||
:class:`~.MathTex` into substrings automatically and isolate the ``x`` components
|
||||
into individual substrings. Only then can :meth:`~.set_color_by_tex` be used
|
||||
to achieve the desired result.
|
||||
If one of the ``substrings_to_isolate`` is in a sub or superscript, it needs
|
||||
to be enclosed by curly brackets.
|
||||
|
||||
Note that Manim also supports a custom syntax that allows splitting
|
||||
a TeX string into substrings easily: simply enclose parts of your formula
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ class AnimationGroup(Animation):
|
|||
sub_alphas[(sub_alphas > 1) | with_zero_run_time] = 1
|
||||
|
||||
for anim_to_update, sub_alpha in zip(
|
||||
to_update["anim"], sub_alphas, strict=False
|
||||
to_update["anim"], sub_alphas, strict=True
|
||||
):
|
||||
anim_to_update.interpolate(sub_alpha)
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ from ..mobject.opengl.opengl_vectorized_mobject import (
|
|||
)
|
||||
from ..typing import Point3D, Point3DLike, Vector3DLike
|
||||
from ..utils.bezier import interpolate, inverse_interpolate
|
||||
from ..utils.color import GREY, YELLOW, ParsableManimColor
|
||||
from ..utils.color import GREY, PURE_YELLOW, ParsableManimColor
|
||||
from ..utils.rate_functions import RateFunction, smooth, there_and_back, wiggle
|
||||
from ..utils.space_ops import normalize
|
||||
|
||||
|
|
@ -92,7 +92,7 @@ class FocusOn(Transform):
|
|||
|
||||
class UsingFocusOn(Scene):
|
||||
def construct(self):
|
||||
dot = Dot(color=YELLOW).shift(DOWN)
|
||||
dot = Dot(color=PURE_YELLOW).shift(DOWN)
|
||||
self.add(Tex("Focusing on the dot below:"), dot)
|
||||
self.play(FocusOn(dot))
|
||||
self.wait()
|
||||
|
|
@ -156,7 +156,7 @@ class Indicate(Transform):
|
|||
self,
|
||||
mobject: Mobject,
|
||||
scale_factor: float = 1.2,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
rate_func: RateFunction = there_and_back,
|
||||
**kwargs: Any,
|
||||
):
|
||||
|
|
@ -201,7 +201,7 @@ class Flash(AnimationGroup):
|
|||
|
||||
class UsingFlash(Scene):
|
||||
def construct(self):
|
||||
dot = Dot(color=YELLOW).shift(DOWN)
|
||||
dot = Dot(color=PURE_YELLOW).shift(DOWN)
|
||||
self.add(Tex("Flash the dot below:"), dot)
|
||||
self.play(Flash(dot))
|
||||
self.wait()
|
||||
|
|
@ -229,7 +229,7 @@ class Flash(AnimationGroup):
|
|||
num_lines: int = 12,
|
||||
flash_radius: float = 0.1,
|
||||
line_stroke_width: int = 3,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
time_width: float = 1,
|
||||
run_time: float = 1.0,
|
||||
**kwargs: Any,
|
||||
|
|
@ -352,7 +352,7 @@ class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup):
|
|||
for stroke_width, time_width in zip(
|
||||
np.linspace(0, max_stroke_width, self.n_segments),
|
||||
np.linspace(max_time_width, 0, self.n_segments),
|
||||
strict=False,
|
||||
strict=True,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
|
@ -625,7 +625,7 @@ class Circumscribe(Succession):
|
|||
fade_out: bool = False,
|
||||
time_width: float = 0.3,
|
||||
buff: float = SMALL_BUFF,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
run_time: float = 1,
|
||||
stroke_width: float = DEFAULT_STROKE_WIDTH,
|
||||
**kwargs: Any,
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ class Transform(Animation):
|
|||
self.starting_mobject,
|
||||
self.target_copy,
|
||||
]
|
||||
return zip(*(mob.get_family() for mob in mobs))
|
||||
return zip(*(mob.get_family() for mob in mobs), strict=True)
|
||||
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
|
|
@ -292,7 +292,7 @@ class ReplacementTransform(Transform):
|
|||
|
||||
|
||||
class TransformFromCopy(Transform):
|
||||
"""Performs a reversed Transform"""
|
||||
"""Preserves a copy of the original VMobject and transforms only it's copy to the target VMobject"""
|
||||
|
||||
def __init__(self, mobject: Mobject, target_mobject: Mobject, **kwargs) -> None:
|
||||
super().__init__(target_mobject, mobject, **kwargs)
|
||||
|
|
@ -730,7 +730,7 @@ class CyclicReplace(Transform):
|
|||
def create_target(self) -> Group:
|
||||
target = self.group.copy()
|
||||
cycled_targets = [target[-1], *target[:-1]]
|
||||
for m1, m2 in zip(cycled_targets, self.group, strict=False):
|
||||
for m1, m2 in zip(cycled_targets, self.group, strict=True):
|
||||
m1.move_to(m2)
|
||||
return target
|
||||
|
||||
|
|
@ -915,5 +915,5 @@ class FadeTransformPieces(FadeTransform):
|
|||
"""Replaces the source submobjects by the target submobjects and sets
|
||||
the opacity to 0.
|
||||
"""
|
||||
for sm0, sm1 in zip(source.get_family(), target.get_family(), strict=False):
|
||||
for sm0, sm1 in zip(source.get_family(), target.get_family(), strict=True):
|
||||
super().ghost_to(sm0, sm1)
|
||||
|
|
|
|||
|
|
@ -308,7 +308,7 @@ Are you sure you want to continue? (y/n)""",
|
|||
if proceed:
|
||||
if not directory_path.is_dir():
|
||||
console.print(f"Creating folder: {directory}.", style="red bold")
|
||||
directory_path.mkdir(parents=True)
|
||||
directory_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
ctx.invoke(write)
|
||||
from_path = Path.cwd() / "manim.cfg"
|
||||
|
|
|
|||
|
|
@ -110,14 +110,10 @@ ULTRAHEAVY = "ULTRAHEAVY"
|
|||
RESAMPLING_ALGORITHMS = {
|
||||
"nearest": Resampling.NEAREST,
|
||||
"none": Resampling.NEAREST,
|
||||
"lanczos": Resampling.LANCZOS,
|
||||
"antialias": Resampling.LANCZOS,
|
||||
"bilinear": Resampling.BILINEAR,
|
||||
"linear": Resampling.BILINEAR,
|
||||
"bicubic": Resampling.BICUBIC,
|
||||
"cubic": Resampling.BICUBIC,
|
||||
"box": Resampling.BOX,
|
||||
"hamming": Resampling.HAMMING,
|
||||
}
|
||||
|
||||
# Geometry: directions
|
||||
|
|
|
|||
|
|
@ -1231,7 +1231,7 @@ class ArcPolygon(VMobject):
|
|||
|
||||
arcs = [
|
||||
ArcBetweenPoints(*pair, **conf)
|
||||
for (pair, conf) in zip(point_pairs, all_arc_configs, strict=False)
|
||||
for (pair, conf) in zip(point_pairs, all_arc_configs, strict=True)
|
||||
]
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ class Polygram(VMobject):
|
|||
# times, then .get_vertex_groups() splits it into N vertex groups.
|
||||
group = []
|
||||
for start, end in zip(
|
||||
self.get_start_anchors(), self.get_end_anchors(), strict=False
|
||||
self.get_start_anchors(), self.get_end_anchors(), strict=True
|
||||
):
|
||||
group.append(start)
|
||||
|
||||
|
|
@ -244,7 +244,7 @@ class Polygram(VMobject):
|
|||
radius_list = radius * ceil(len(vertex_group) / len(radius))
|
||||
|
||||
for current_radius, (v1, v2, v3) in zip(
|
||||
radius_list, adjacent_n_tuples(vertex_group, 3), strict=False
|
||||
radius_list, adjacent_n_tuples(vertex_group, 3), strict=True
|
||||
):
|
||||
vect1 = v2 - v1
|
||||
vect2 = v3 - v2
|
||||
|
|
@ -556,7 +556,7 @@ class Star(Polygon):
|
|||
)
|
||||
|
||||
vertices: list[npt.NDArray] = []
|
||||
for pair in zip(outer_vertices, inner_vertices, strict=False):
|
||||
for pair in zip(outer_vertices, inner_vertices, strict=True):
|
||||
vertices.extend(pair)
|
||||
|
||||
super().__init__(*vertices, **kwargs)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ from manim.mobject.geometry.line import Line
|
|||
from manim.mobject.geometry.polygram import RoundedRectangle
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
from manim.utils.color import BLACK, RED, YELLOW, ParsableManimColor
|
||||
from manim.utils.color import BLACK, PURE_YELLOW, RED, ParsableManimColor
|
||||
|
||||
|
||||
class SurroundingRectangle(RoundedRectangle):
|
||||
|
|
@ -49,7 +49,7 @@ class SurroundingRectangle(RoundedRectangle):
|
|||
def __init__(
|
||||
self,
|
||||
*mobjects: Mobject,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
buff: float | tuple[float, float] = SMALL_BUFF,
|
||||
corner_radius: float = 0.0,
|
||||
**kwargs: Any,
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ from manim.utils.color import (
|
|||
BLUE,
|
||||
BLUE_D,
|
||||
GREEN,
|
||||
PURE_YELLOW,
|
||||
WHITE,
|
||||
YELLOW,
|
||||
ManimColor,
|
||||
ParsableManimColor,
|
||||
color_gradient,
|
||||
|
|
@ -452,7 +452,7 @@ class CoordinateSystem:
|
|||
zip(
|
||||
tick_range,
|
||||
axis.scaling.get_custom_labels(tick_range),
|
||||
strict=False,
|
||||
strict=True,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -1294,7 +1294,7 @@ class CoordinateSystem:
|
|||
|
||||
colors = color_gradient(color, len(x_range_array))
|
||||
|
||||
for x, color in zip(x_range_array, colors, strict=False):
|
||||
for x, color in zip(x_range_array, colors, strict=True):
|
||||
if input_sample_type == "left":
|
||||
sample_input = x
|
||||
elif input_sample_type == "right":
|
||||
|
|
@ -1608,7 +1608,7 @@ class CoordinateSystem:
|
|||
x: float,
|
||||
graph: ParametricFunction,
|
||||
dx: float | None = None,
|
||||
dx_line_color: ParsableManimColor = YELLOW,
|
||||
dx_line_color: ParsableManimColor = PURE_YELLOW,
|
||||
dy_line_color: ParsableManimColor | None = None,
|
||||
dx_label: float | str | None = None,
|
||||
dy_label: float | str | None = None,
|
||||
|
|
@ -1790,7 +1790,7 @@ class CoordinateSystem:
|
|||
triangle_size: float = MED_SMALL_BUFF,
|
||||
triangle_color: ParsableManimColor | None = WHITE,
|
||||
line_func: type[Line] = Line,
|
||||
line_color: ParsableManimColor = YELLOW,
|
||||
line_color: ParsableManimColor = PURE_YELLOW,
|
||||
) -> VGroup:
|
||||
"""Creates a labelled triangle marker with a vertical line from the x-axis
|
||||
to a curve at a given x-value.
|
||||
|
|
@ -2083,6 +2083,10 @@ class Axes(VGroup, CoordinateSystem):
|
|||
|
||||
``ax.coords_to_point( [[x_0, y_0, z_0], [x_1, y_1, z_1]] )``
|
||||
|
||||
A single coordinate can also be passed as a flat list or 1D array:
|
||||
|
||||
``ax.coords_to_point( [x, y, z] )``
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.ndarray
|
||||
|
|
@ -2111,6 +2115,10 @@ class Axes(VGroup, CoordinateSystem):
|
|||
array([[0. , 0.86, 0.86],
|
||||
[0.75, 0.75, 0. ],
|
||||
[0. , 0. , 0. ]])
|
||||
>>> np.around(ax.coords_to_point([1, 0, 0]), 2)
|
||||
array([0.86, 0. , 0. ])
|
||||
>>> np.around(ax.coords_to_point(np.array([1, 0])), 2)
|
||||
array([0.86, 0. , 0. ])
|
||||
|
||||
.. manim:: CoordsToPointExample
|
||||
:save_last_frame:
|
||||
|
|
@ -2153,6 +2161,10 @@ class Axes(VGroup, CoordinateSystem):
|
|||
else:
|
||||
coords = coords.T
|
||||
are_coordinates_transposed = True
|
||||
# If coords is in the format ([x, y, z]) -- a single flat list/array passed as one argument:
|
||||
elif coords.ndim == 2 and coords.shape[0] == 1:
|
||||
# Extract the single list so [x, y, z] is treated like c2p(x, y, z).
|
||||
coords = coords[0]
|
||||
# Otherwise, coords already looked like (x, y, z) or ([x1 x2 ...], [y1 y2 ...], [z1 z2 ...]),
|
||||
# so no further processing is needed.
|
||||
|
||||
|
|
@ -2287,7 +2299,7 @@ class Axes(VGroup, CoordinateSystem):
|
|||
x_values: Iterable[float],
|
||||
y_values: Iterable[float],
|
||||
z_values: Iterable[float] | None = None,
|
||||
line_color: ParsableManimColor = YELLOW,
|
||||
line_color: ParsableManimColor = PURE_YELLOW,
|
||||
add_vertex_dots: bool = True,
|
||||
vertex_dot_radius: float = DEFAULT_DOT_RADIUS,
|
||||
vertex_dot_style: dict[str, Any] | None = None,
|
||||
|
|
@ -2356,7 +2368,7 @@ class Axes(VGroup, CoordinateSystem):
|
|||
|
||||
vertices = [
|
||||
self.coords_to_point(x, y, z)
|
||||
for x, y, z in zip(x_values, y_values, z_values, strict=False)
|
||||
for x, y, z in zip(x_values, y_values, z_values, strict=True)
|
||||
]
|
||||
graph.set_points_as_corners(vertices)
|
||||
line_graph["line_graph"] = graph
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ if TYPE_CHECKING:
|
|||
from manim.typing import Point3D, Point3DLike
|
||||
from manim.utils.color import ParsableManimColor
|
||||
|
||||
from manim.utils.color import YELLOW
|
||||
from manim.utils.color import PURE_YELLOW
|
||||
|
||||
|
||||
class ParametricFunction(VMobject):
|
||||
|
|
@ -156,7 +156,7 @@ class ParametricFunction(VMobject):
|
|||
else:
|
||||
boundary_times = [self.t_min, self.t_max]
|
||||
|
||||
for t1, t2 in zip(boundary_times[0::2], boundary_times[1::2], strict=False):
|
||||
for t1, t2 in zip(boundary_times[0::2], boundary_times[1::2], strict=True):
|
||||
t_range = np.array(
|
||||
[
|
||||
*self.scaling.function(np.arange(t1, t2, self.t_step)),
|
||||
|
|
@ -216,7 +216,7 @@ class FunctionGraph(ParametricFunction):
|
|||
self,
|
||||
function: Callable[[float], Any],
|
||||
x_range: tuple[float, float] | tuple[float, float, float] | None = None,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
if x_range is None:
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ class NumberLine(Line):
|
|||
zip(
|
||||
tick_range,
|
||||
custom_labels,
|
||||
strict=False,
|
||||
strict=True,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ class SampleSpace(Rectangle):
|
|||
|
||||
last_point = self.get_edge_center(-vect)
|
||||
parts = VGroup()
|
||||
for factor, color in zip(p_list_complete, colors_in_gradient, strict=False):
|
||||
for factor, color in zip(p_list_complete, colors_in_gradient, strict=True):
|
||||
part = SampleSpace()
|
||||
part.set_fill(color, 1)
|
||||
part.replace(self, stretch=True)
|
||||
|
|
@ -372,7 +372,7 @@ class BarChart(Axes):
|
|||
labels = VGroup()
|
||||
|
||||
for i, (value, bar_name) in enumerate(
|
||||
zip(val_range, self.bar_names, strict=False)
|
||||
zip(val_range, self.bar_names, strict=True)
|
||||
):
|
||||
# to accommodate negative bars, the label may need to be
|
||||
# below or above the x_axis depending on the value of the bar
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import warnings
|
|||
from collections.abc import Callable, Iterable
|
||||
from functools import partialmethod, reduce
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -28,8 +28,8 @@ from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
|||
from manim.mobject.opengl.opengl_mobject import InvisibleMobject
|
||||
from manim.utils.color import (
|
||||
BLACK,
|
||||
PURE_YELLOW,
|
||||
WHITE,
|
||||
YELLOW_C,
|
||||
ManimColor,
|
||||
ParsableManimColor,
|
||||
color_gradient,
|
||||
|
|
@ -395,6 +395,42 @@ class Mobject:
|
|||
"""
|
||||
return _AnimationBuilder(self)
|
||||
|
||||
@property
|
||||
def always(self) -> Self:
|
||||
"""Call a method on a mobject every frame.
|
||||
|
||||
This is syntactic sugar for ``mob.add_updater(lambda m: m.method(*args, **kwargs), call_updater=True)``.
|
||||
Note that this will call the method immediately. If this behavior is not
|
||||
desired, you should use :meth:`add_updater` directly.
|
||||
|
||||
.. warning::
|
||||
|
||||
Chaining of methods is allowed, but each method will be added
|
||||
as its own updater. If you are chaining methods, make sure they
|
||||
do not interfere with each other or you may get unexpected results.
|
||||
|
||||
.. warning::
|
||||
|
||||
:attr:`always` is not compatible with :meth:`.ValueTracker.get_value`, because
|
||||
the value will be computed once and then never updated again. Use :meth:`add_updater`
|
||||
if you would like to use a :class:`~.ValueTracker` to update the value.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
.. manim:: AlwaysExample
|
||||
|
||||
class AlwaysExample(Scene):
|
||||
def construct(self):
|
||||
sq = Square().to_edge(LEFT)
|
||||
t = Text("Hello World!")
|
||||
t.always.next_to(sq, UP)
|
||||
self.add(sq, t)
|
||||
self.play(sq.animate.to_edge(RIGHT))
|
||||
"""
|
||||
# can't use typing.cast because Self is under TYPE_CHECKING
|
||||
return _UpdaterBuilder(self) # type: ignore[return-value]
|
||||
|
||||
def __deepcopy__(self, clone_from_id) -> Self:
|
||||
cls = self.__class__
|
||||
result = cls.__new__(cls)
|
||||
|
|
@ -407,9 +443,10 @@ class Mobject:
|
|||
def __repr__(self) -> str:
|
||||
return str(self.name)
|
||||
|
||||
def reset_points(self) -> None:
|
||||
def reset_points(self) -> Self:
|
||||
"""Sets :attr:`points` to be an empty array."""
|
||||
self.points = np.zeros((0, self.dim))
|
||||
return self
|
||||
|
||||
def init_colors(self) -> object:
|
||||
"""Initializes the colors.
|
||||
|
|
@ -813,7 +850,7 @@ class Mobject:
|
|||
self.scale_to_fit_depth(value)
|
||||
|
||||
# Can't be staticmethod because of point_cloud_mobject.py
|
||||
def get_array_attrs(self) -> list[Literal["points"]]:
|
||||
def get_array_attrs(self) -> list[str]:
|
||||
return ["points"]
|
||||
|
||||
def apply_over_attr_arrays(self, func: MultiMappingFunction) -> Self:
|
||||
|
|
@ -1980,7 +2017,7 @@ class Mobject:
|
|||
# Color functions
|
||||
|
||||
def set_color(
|
||||
self, color: ParsableManimColor = YELLOW_C, family: bool = True
|
||||
self, color: ParsableManimColor = PURE_YELLOW, family: bool = True
|
||||
) -> Self:
|
||||
"""Condition is function which takes in one arguments, (x, y, z).
|
||||
Here it just recurses to submobjects, but in subclasses this
|
||||
|
|
@ -2031,7 +2068,7 @@ class Mobject:
|
|||
mobs = self.family_members_with_points()
|
||||
new_colors = color_gradient(colors, len(mobs))
|
||||
|
||||
for mob, color in zip(mobs, new_colors, strict=False):
|
||||
for mob, color in zip(mobs, new_colors, strict=True):
|
||||
mob.set_color(color, family=False)
|
||||
return self
|
||||
|
||||
|
|
@ -2324,7 +2361,7 @@ class Mobject:
|
|||
return Group(
|
||||
*(
|
||||
template.copy().pointwise_become_partial(self, a1, a2)
|
||||
for a1, a2 in zip(alphas[:-1], alphas[1:], strict=False)
|
||||
for a1, a2 in zip(alphas[:-1], alphas[1:], strict=True)
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -2535,7 +2572,7 @@ class Mobject:
|
|||
x = VGroup(s1, s2, s3, s4).set_x(0).arrange(buff=1.0)
|
||||
self.add(x)
|
||||
"""
|
||||
for m1, m2 in zip(self.submobjects, self.submobjects[1:], strict=False):
|
||||
for m1, m2 in zip(self.submobjects[:-1], self.submobjects[1:], strict=True):
|
||||
m2.next_to(m1, direction, buff, **kwargs)
|
||||
if center:
|
||||
self.center()
|
||||
|
|
@ -2920,7 +2957,7 @@ class Mobject:
|
|||
if not skip_point_alignment:
|
||||
self.align_points(mobject)
|
||||
# Recurse
|
||||
for m1, m2 in zip(self.submobjects, mobject.submobjects, strict=False):
|
||||
for m1, m2 in zip(self.submobjects, mobject.submobjects, strict=True):
|
||||
m1.align_data(m2)
|
||||
|
||||
def get_point_mobject(self, center=None):
|
||||
|
|
@ -2990,7 +3027,7 @@ class Mobject:
|
|||
repeat_indices = (np.arange(target) * curr) // target
|
||||
split_factors = [sum(repeat_indices == i) for i in range(curr)]
|
||||
new_submobs = []
|
||||
for submob, sf in zip(self.submobjects, split_factors, strict=False):
|
||||
for submob, sf in zip(self.submobjects, split_factors, strict=True):
|
||||
new_submobs.append(submob)
|
||||
new_submobs.extend(submob.copy().fade(1) for _ in range(1, sf))
|
||||
self.submobjects = new_submobs
|
||||
|
|
@ -3202,7 +3239,7 @@ class Mobject:
|
|||
mobject.move_to(self.get_center())
|
||||
|
||||
self.align_data(mobject, skip_point_alignment=True)
|
||||
for sm1, sm2 in zip(self.get_family(), mobject.get_family(), strict=False):
|
||||
for sm1, sm2 in zip(self.get_family(), mobject.get_family(), strict=True):
|
||||
sm1.points = np.array(sm2.points)
|
||||
sm1.interpolate_color(sm1, sm2, 1)
|
||||
return self
|
||||
|
|
@ -3411,6 +3448,24 @@ class _AnimationBuilder:
|
|||
return anim
|
||||
|
||||
|
||||
class _UpdaterBuilder:
|
||||
"""Syntactic sugar for adding updaters to mobjects."""
|
||||
|
||||
def __init__(self, mobject: Mobject):
|
||||
self._mobject = mobject
|
||||
|
||||
def __getattr__(self, name: str, /) -> Callable[..., Self]:
|
||||
# just return a function that will add the updater
|
||||
def add_updater(*method_args, **method_kwargs) -> Self:
|
||||
self._mobject.add_updater(
|
||||
lambda m: getattr(m, name)(*method_args, **method_kwargs),
|
||||
call_updater=True,
|
||||
)
|
||||
return self
|
||||
|
||||
return add_updater
|
||||
|
||||
|
||||
def override_animate(method) -> types.FunctionType:
|
||||
r"""Decorator for overriding method animations.
|
||||
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@ import numpy as np
|
|||
from manim.constants import ORIGIN, RIGHT, UP
|
||||
from manim.mobject.opengl.opengl_point_cloud_mobject import OpenGLPMobject
|
||||
from manim.typing import Point3DLike
|
||||
from manim.utils.color import YELLOW, ParsableManimColor
|
||||
from manim.utils.color import PURE_YELLOW, ParsableManimColor
|
||||
|
||||
|
||||
class DotCloud(OpenGLPMobject):
|
||||
def __init__(
|
||||
self,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
stroke_width: float = 2.0,
|
||||
radius: float = 2.0,
|
||||
density: float = 10,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from abc import ABCMeta
|
||||
from typing import Any
|
||||
|
||||
from manim import config
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
|
|
@ -19,13 +20,15 @@ class ConvertToOpenGL(ABCMeta):
|
|||
on the lowest order inheritance classes such as Mobject and VMobject.
|
||||
"""
|
||||
|
||||
_converted_classes = []
|
||||
_converted_classes: list[type] = []
|
||||
|
||||
def __new__(mcls, name, bases, namespace):
|
||||
def __new__(
|
||||
mcls, name: str, bases: tuple[type, ...], namespace: dict[str, Any]
|
||||
) -> type:
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
# Must check class names to prevent
|
||||
# cyclic importing.
|
||||
base_names_to_opengl = {
|
||||
base_names_to_opengl: dict[str, type] = {
|
||||
"Mobject": OpenGLMobject,
|
||||
"VMobject": OpenGLVMobject,
|
||||
"PMobject": OpenGLPMobject,
|
||||
|
|
@ -40,6 +43,6 @@ class ConvertToOpenGL(ABCMeta):
|
|||
|
||||
return super().__new__(mcls, name, bases, namespace)
|
||||
|
||||
def __init__(cls, name, bases, namespace):
|
||||
def __init__(cls, name: str, bases: tuple[type, ...], namespace: dict[str, Any]):
|
||||
super().__init__(name, bases, namespace)
|
||||
cls._converted_classes.append(cls)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ __all__ = [
|
|||
]
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
|
@ -13,26 +14,29 @@ from PIL.Image import Resampling
|
|||
from manim.mobject.opengl.opengl_surface import OpenGLSurface, OpenGLTexturedSurface
|
||||
from manim.utils.images import get_full_raster_image_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy.typing as npt
|
||||
|
||||
__all__ = ["OpenGLImageMobject"]
|
||||
|
||||
|
||||
class OpenGLImageMobject(OpenGLTexturedSurface):
|
||||
def __init__(
|
||||
self,
|
||||
filename_or_array: str | Path | np.ndarray,
|
||||
width: float = None,
|
||||
height: float = None,
|
||||
filename_or_array: str | Path | npt.NDArray,
|
||||
width: float | None = None,
|
||||
height: float | None = None,
|
||||
image_mode: str = "RGBA",
|
||||
resampling_algorithm: int = Resampling.BICUBIC,
|
||||
resampling_algorithm: Resampling = Resampling.BICUBIC,
|
||||
opacity: float = 1,
|
||||
gloss: float = 0,
|
||||
shadow: float = 0,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.image = filename_or_array
|
||||
self.resampling_algorithm = resampling_algorithm
|
||||
if isinstance(filename_or_array, np.ndarray):
|
||||
self.size = self.image.shape[1::-1]
|
||||
self.size = filename_or_array.shape[1::-1]
|
||||
elif isinstance(filename_or_array, (str, Path)):
|
||||
path = get_full_raster_image_path(filename_or_array)
|
||||
self.size = Image.open(path).size
|
||||
|
|
@ -68,7 +72,7 @@ class OpenGLImageMobject(OpenGLTexturedSurface):
|
|||
self,
|
||||
image_file: str | Path | np.ndarray,
|
||||
image_mode: str,
|
||||
):
|
||||
) -> Image.Image:
|
||||
if isinstance(image_file, (str, Path)):
|
||||
return super().get_image_from_file(image_file, image_mode)
|
||||
else:
|
||||
|
|
@ -76,7 +80,7 @@ class OpenGLImageMobject(OpenGLTexturedSurface):
|
|||
Image.fromarray(image_file.astype("uint8"))
|
||||
.convert(image_mode)
|
||||
.resize(
|
||||
np.array(image_file.shape[:2])
|
||||
image_file.shape[:2]
|
||||
* 200, # assumption of 200 ppmu (pixels per manim unit) would suffice
|
||||
resample=self.resampling_algorithm,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1136,7 +1136,7 @@ class OpenGLMobject:
|
|||
x = OpenGLVGroup(s1, s2, s3, s4).set_x(0).arrange(buff=1.0)
|
||||
self.add(x)
|
||||
"""
|
||||
for m1, m2 in zip(self.submobjects, self.submobjects[1:], strict=False):
|
||||
for m1, m2 in zip(self.submobjects[:-1], self.submobjects[1:], strict=True):
|
||||
m2.next_to(m1, direction, **kwargs)
|
||||
if center:
|
||||
self.center()
|
||||
|
|
@ -2415,7 +2415,7 @@ class OpenGLMobject:
|
|||
mobs = self.submobjects
|
||||
new_colors = color_gradient(colors, len(mobs))
|
||||
|
||||
for mob, color in zip(mobs, new_colors, strict=False):
|
||||
for mob, color in zip(mobs, new_colors, strict=True):
|
||||
mob.set_color(color)
|
||||
return self
|
||||
|
||||
|
|
@ -2638,7 +2638,7 @@ class OpenGLMobject:
|
|||
return OpenGLGroup(
|
||||
*(
|
||||
template.copy().pointwise_become_partial(self, a1, a2)
|
||||
for a1, a2 in zip(alphas[:-1], alphas[1:], strict=False)
|
||||
for a1, a2 in zip(alphas[:-1], alphas[1:], strict=True)
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -2774,7 +2774,7 @@ class OpenGLMobject:
|
|||
mob1.add_n_more_submobjects(max(0, n2 - n1))
|
||||
mob2.add_n_more_submobjects(max(0, n1 - n2))
|
||||
# Recurse
|
||||
for sm1, sm2 in zip(mob1.submobjects, mob2.submobjects, strict=False):
|
||||
for sm1, sm2 in zip(mob1.submobjects, mob2.submobjects, strict=True):
|
||||
sm1.align_family(sm2)
|
||||
return self
|
||||
|
||||
|
|
@ -2801,7 +2801,7 @@ class OpenGLMobject:
|
|||
repeat_indices = (np.arange(target) * curr) // target
|
||||
split_factors = [(repeat_indices == i).sum() for i in range(curr)]
|
||||
new_submobs = []
|
||||
for submob, sf in zip(self.submobjects, split_factors, strict=False):
|
||||
for submob, sf in zip(self.submobjects, split_factors, strict=True):
|
||||
new_submobs.append(submob)
|
||||
for _ in range(1, sf):
|
||||
new_submob = submob.copy()
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
|||
from manim.utils.bezier import interpolate
|
||||
from manim.utils.color import (
|
||||
BLACK,
|
||||
PURE_YELLOW,
|
||||
WHITE,
|
||||
YELLOW,
|
||||
ParsableManimColor,
|
||||
color_gradient,
|
||||
color_to_rgba,
|
||||
|
|
@ -39,7 +39,7 @@ class OpenGLPMobject(OpenGLMobject):
|
|||
def __init__(
|
||||
self,
|
||||
stroke_width: float = 2.0,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
**kwargs,
|
||||
):
|
||||
self.stroke_width = stroke_width
|
||||
|
|
@ -69,7 +69,7 @@ class OpenGLPMobject(OpenGLMobject):
|
|||
Rgbas must be a Nx4 numpy array if it is not None.
|
||||
"""
|
||||
if rgbas is None and color is None:
|
||||
color = YELLOW
|
||||
color = PURE_YELLOW
|
||||
self.append_points(points)
|
||||
# rgbas array will have been resized with points
|
||||
if color is not None:
|
||||
|
|
|
|||
|
|
@ -2,19 +2,24 @@ from __future__ import annotations
|
|||
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from manim.constants import *
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.typing import Point3D_Array, Vector3D_Array
|
||||
from manim.utils.bezier import integer_interpolate, interpolate
|
||||
from manim.utils.color import *
|
||||
from manim.utils.images import change_to_rgba_array, get_full_raster_image_path
|
||||
from manim.utils.iterables import listify
|
||||
from manim.utils.space_ops import normalize_along_axis
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy.typing as npt
|
||||
|
||||
from manim.typing import Point3D_Array, Vector3D_Array
|
||||
|
||||
__all__ = ["OpenGLSurface", "OpenGLTexturedSurface"]
|
||||
|
||||
|
||||
|
|
@ -72,7 +77,7 @@ class OpenGLSurface(OpenGLMobject):
|
|||
# can crop up in the shaders.
|
||||
epsilon=1e-5,
|
||||
depth_test=True,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.passed_uv_func = uv_func
|
||||
self.u_range = u_range if u_range is not None else (0, 1)
|
||||
|
|
@ -249,7 +254,7 @@ class OpenGLTexturedSurface(OpenGLSurface):
|
|||
def __init__(
|
||||
self,
|
||||
uv_surface: OpenGLSurface,
|
||||
image_file: str | Path,
|
||||
image_file: str | Path | npt.NDArray,
|
||||
dark_image_file: str | Path = None,
|
||||
image_mode: str | Iterable[str] = "RGBA",
|
||||
**kwargs,
|
||||
|
|
@ -289,7 +294,7 @@ class OpenGLTexturedSurface(OpenGLSurface):
|
|||
self,
|
||||
image_file: str | Path,
|
||||
image_mode: str,
|
||||
):
|
||||
) -> Image.Image:
|
||||
image_file = get_full_raster_image_path(image_file)
|
||||
return Image.open(image_file).convert(image_mode)
|
||||
|
||||
|
|
|
|||
|
|
@ -310,7 +310,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
return self
|
||||
elif len(submobs2) == 0:
|
||||
submobs2 = [vmobject]
|
||||
for sm1, sm2 in zip(*make_even(submobs1, submobs2), strict=False):
|
||||
for sm1, sm2 in zip(*make_even(submobs1, submobs2), strict=True):
|
||||
sm1.match_style(sm2)
|
||||
return self
|
||||
|
||||
|
|
@ -493,7 +493,6 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
self.set_points([point])
|
||||
return self
|
||||
end = self.points[-1]
|
||||
alphas = np.linspace(0, 1, self.n_points_per_curve)
|
||||
if self.long_lines:
|
||||
halfway = interpolate(end, point, 0.5)
|
||||
points = [interpolate(end, halfway, t) for t in self._bezier_t_values] + [
|
||||
|
|
@ -663,9 +662,8 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
OpenGLVMobject
|
||||
For chaining purposes.
|
||||
"""
|
||||
assert (
|
||||
mode in ("jagged", "approx_smooth", "true_smooth"),
|
||||
'mode must be either "jagged", "approx_smooth" or "smooth"',
|
||||
assert mode in ("jagged", "approx_smooth", "true_smooth"), (
|
||||
'mode must be either "jagged", "approx_smooth" or "smooth"'
|
||||
)
|
||||
nppc = self.n_points_per_curve
|
||||
for submob in self.family_members_with_points():
|
||||
|
|
@ -1131,7 +1129,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
|
||||
s = self.get_start_anchors()
|
||||
e = self.get_end_anchors()
|
||||
return list(it.chain.from_iterable(zip(s, e, strict=False)))
|
||||
return list(it.chain.from_iterable(zip(s, e, strict=True)))
|
||||
|
||||
def get_points_without_null_curves(self, atol: float = 1e-9) -> Point3D_Array:
|
||||
nppc = self.n_points_per_curve
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import numpy as np
|
|||
import svgelements as se
|
||||
|
||||
from manim import config, logger
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
|
||||
from ...constants import RIGHT
|
||||
|
|
@ -24,7 +25,7 @@ from ..geometry.polygram import Polygon, Rectangle, RoundedRectangle
|
|||
__all__ = ["SVGMobject", "VMobjectFromSVGPath"]
|
||||
|
||||
|
||||
SVG_HASH_TO_MOB_MAP: dict[int, VMobject] = {}
|
||||
SVG_HASH_TO_MOB_MAP: dict[int, SVGMobject] = {}
|
||||
|
||||
|
||||
def _convert_point_to_3d(x: float, y: float) -> np.ndarray:
|
||||
|
|
@ -112,6 +113,7 @@ class SVGMobject(VMobject):
|
|||
self.svg_height = height
|
||||
self.svg_width = width
|
||||
self.opacity = opacity
|
||||
self.id_to_vgroup_dict: dict[str, VGroup] = {}
|
||||
|
||||
if svg_default is None:
|
||||
svg_default = {
|
||||
|
|
@ -149,6 +151,7 @@ class SVGMobject(VMobject):
|
|||
if hash_val in SVG_HASH_TO_MOB_MAP:
|
||||
mob = SVG_HASH_TO_MOB_MAP[hash_val].copy()
|
||||
self.add(*mob)
|
||||
self.id_to_vgroup_dict = mob.id_to_vgroup_dict
|
||||
return
|
||||
|
||||
self.generate_mobject()
|
||||
|
|
@ -182,8 +185,9 @@ class SVGMobject(VMobject):
|
|||
svg = se.SVG.parse(modified_file_path)
|
||||
modified_file_path.unlink()
|
||||
|
||||
mobjects = self.get_mobjects_from(svg)
|
||||
mobjects, mobject_dict = self.get_mobjects_from(svg)
|
||||
self.add(*mobjects)
|
||||
self.id_to_vgroup_dict = mobject_dict
|
||||
self.flip(RIGHT) # Flip y
|
||||
|
||||
def get_file_path(self) -> Path:
|
||||
|
|
@ -237,7 +241,9 @@ class SVGMobject(VMobject):
|
|||
result[svg_key] = str(svg_default_dict[style_key])
|
||||
return result
|
||||
|
||||
def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]:
|
||||
def get_mobjects_from(
|
||||
self, svg: se.SVG
|
||||
) -> tuple[list[VMobject], dict[str, VGroup]]:
|
||||
"""Convert the elements of the SVG to a list of mobjects.
|
||||
|
||||
Parameters
|
||||
|
|
@ -246,36 +252,77 @@ class SVGMobject(VMobject):
|
|||
The parsed SVG file.
|
||||
"""
|
||||
result: list[VMobject] = []
|
||||
for shape in svg.elements():
|
||||
# can we combine the two continue cases into one?
|
||||
if isinstance(shape, se.Group): # noqa: SIM114
|
||||
continue
|
||||
elif isinstance(shape, se.Path):
|
||||
mob: VMobject = self.path_to_mobject(shape)
|
||||
elif isinstance(shape, se.SimpleLine):
|
||||
mob = self.line_to_mobject(shape)
|
||||
elif isinstance(shape, se.Rect):
|
||||
mob = self.rect_to_mobject(shape)
|
||||
elif isinstance(shape, (se.Circle, se.Ellipse)):
|
||||
mob = self.ellipse_to_mobject(shape)
|
||||
elif isinstance(shape, se.Polygon):
|
||||
mob = self.polygon_to_mobject(shape)
|
||||
elif isinstance(shape, se.Polyline):
|
||||
mob = self.polyline_to_mobject(shape)
|
||||
elif isinstance(shape, se.Text):
|
||||
mob = self.text_to_mobject(shape)
|
||||
elif isinstance(shape, se.Use) or type(shape) is se.SVGElement:
|
||||
continue
|
||||
else:
|
||||
logger.warning(f"Unsupported element type: {type(shape)}")
|
||||
continue
|
||||
if mob is None or not mob.has_points():
|
||||
continue
|
||||
self.apply_style_to_mobject(mob, shape)
|
||||
if isinstance(shape, se.Transformable) and shape.apply:
|
||||
self.handle_transform(mob, shape.transform)
|
||||
result.append(mob)
|
||||
return result
|
||||
stack: list[tuple[se.SVGElement, int]] = []
|
||||
stack.append((svg, 1))
|
||||
group_id_number = 0
|
||||
vgroup_stack: list[str] = ["root"]
|
||||
vgroup_names: list[str] = ["root"]
|
||||
vgroups: dict[str, VGroup] = {"root": VGroup()}
|
||||
while len(stack) > 0:
|
||||
element, depth = stack.pop()
|
||||
# Reduce stack heights
|
||||
vgroup_stack = vgroup_stack[0:(depth)]
|
||||
try:
|
||||
group_name = str(element.values["id"])
|
||||
except Exception:
|
||||
group_name = f"numbered_group_{group_id_number}"
|
||||
group_id_number += 1
|
||||
vg = VGroup()
|
||||
vgroup_names.append(group_name)
|
||||
vgroup_stack.append(group_name)
|
||||
vgroups[group_name] = vg
|
||||
|
||||
if isinstance(element, (se.Group, se.Use)):
|
||||
stack.extend((subelement, depth + 1) for subelement in element[::-1])
|
||||
# Add element to the parent vgroup
|
||||
try:
|
||||
if isinstance(
|
||||
element,
|
||||
(
|
||||
se.Path,
|
||||
se.SimpleLine,
|
||||
se.Rect,
|
||||
se.Circle,
|
||||
se.Ellipse,
|
||||
se.Polygon,
|
||||
se.Polyline,
|
||||
se.Text,
|
||||
),
|
||||
):
|
||||
mob = self.get_mob_from_shape_element(element)
|
||||
if mob is not None:
|
||||
result.append(mob)
|
||||
for parent_name in vgroup_stack[:-1]:
|
||||
vgroups[parent_name].add(mob)
|
||||
except Exception as e:
|
||||
logger.error(f"Exception occurred in 'get_mobjects_from'. Details: {e}")
|
||||
|
||||
return result, vgroups
|
||||
|
||||
def get_mob_from_shape_element(self, shape: se.SVGElement) -> VMobject | None:
|
||||
if isinstance(shape, se.Path):
|
||||
mob: VMobject | None = self.path_to_mobject(shape)
|
||||
elif isinstance(shape, se.SimpleLine):
|
||||
mob = self.line_to_mobject(shape)
|
||||
elif isinstance(shape, se.Rect):
|
||||
mob = self.rect_to_mobject(shape)
|
||||
elif isinstance(shape, (se.Circle, se.Ellipse)):
|
||||
mob = self.ellipse_to_mobject(shape)
|
||||
elif isinstance(shape, se.Polygon):
|
||||
mob = self.polygon_to_mobject(shape)
|
||||
elif isinstance(shape, se.Polyline):
|
||||
mob = self.polyline_to_mobject(shape)
|
||||
elif isinstance(shape, se.Text):
|
||||
mob = self.text_to_mobject(shape)
|
||||
else:
|
||||
logger.warning(f"Unsupported element type: {type(shape)}")
|
||||
mob = None
|
||||
if mob is None or not mob.has_points():
|
||||
return mob
|
||||
self.apply_style_to_mobject(mob, shape)
|
||||
if isinstance(shape, se.Transformable) and shape.apply:
|
||||
self.handle_transform(mob, shape.transform)
|
||||
return mob
|
||||
|
||||
@staticmethod
|
||||
def handle_transform(mob: VMobject, matrix: se.Matrix) -> VMobject:
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ from ..animation.animation import Animation
|
|||
from ..animation.composition import AnimationGroup
|
||||
from ..animation.creation import Create, Write
|
||||
from ..animation.fading import FadeIn
|
||||
from ..utils.color import BLACK, YELLOW, ManimColor, ParsableManimColor
|
||||
from ..utils.color import BLACK, PURE_YELLOW, ManimColor, ParsableManimColor
|
||||
|
||||
|
||||
class Table(VGroup):
|
||||
|
|
@ -814,7 +814,10 @@ class Table(VGroup):
|
|||
return rec
|
||||
|
||||
def get_highlighted_cell(
|
||||
self, pos: Sequence[int] = (1, 1), color: ParsableManimColor = YELLOW, **kwargs
|
||||
self,
|
||||
pos: Sequence[int] = (1, 1),
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
**kwargs,
|
||||
) -> BackgroundRectangle:
|
||||
"""Returns a :class:`~.BackgroundRectangle` of the cell at the given position.
|
||||
|
||||
|
|
@ -850,7 +853,10 @@ class Table(VGroup):
|
|||
return bg_cell
|
||||
|
||||
def add_highlighted_cell(
|
||||
self, pos: Sequence[int] = (1, 1), color: ParsableManimColor = YELLOW, **kwargs
|
||||
self,
|
||||
pos: Sequence[int] = (1, 1),
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
**kwargs,
|
||||
) -> Table:
|
||||
"""Highlights one cell at a specific position on the table by adding a :class:`~.BackgroundRectangle`.
|
||||
|
||||
|
|
@ -1081,11 +1087,11 @@ class IntegerTable(Table):
|
|||
[[0,30,45,60,90],
|
||||
[90,60,45,30,0]],
|
||||
col_labels=[
|
||||
MathTex(r"\frac{\sqrt{0}}{2}"),
|
||||
MathTex(r"\frac{\sqrt{1}}{2}"),
|
||||
MathTex(r"\frac{\sqrt{2}}{2}"),
|
||||
MathTex(r"\frac{\sqrt{3}}{2}"),
|
||||
MathTex(r"\frac{\sqrt{4}}{2}")],
|
||||
MathTex(r"\frac{ \sqrt{0} }{2}"),
|
||||
MathTex(r"\frac{ \sqrt{1} }{2}"),
|
||||
MathTex(r"\frac{ \sqrt{2} }{2}"),
|
||||
MathTex(r"\frac{ \sqrt{3} }{2}"),
|
||||
MathTex(r"\frac{ \sqrt{4} }{2}")],
|
||||
row_labels=[MathTex(r"\sin"), MathTex(r"\cos")],
|
||||
h_buff=1,
|
||||
element_to_mobject_config={"unit": r"^{\circ}"})
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ r"""Mobjects representing text rendered using LaTeX.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from manim.utils.color import BLACK, ManimColor, ParsableManimColor
|
||||
from manim.utils.color import BLACK, ParsableManimColor
|
||||
|
||||
__all__ = [
|
||||
"SingleStringMathTex",
|
||||
|
|
@ -23,10 +23,9 @@ __all__ = [
|
|||
]
|
||||
|
||||
|
||||
import itertools as it
|
||||
import operator as op
|
||||
import re
|
||||
from collections.abc import Iterable, Sequence
|
||||
from collections.abc import Iterable
|
||||
from functools import reduce
|
||||
from textwrap import dedent
|
||||
from typing import Any, Self
|
||||
|
|
@ -35,10 +34,13 @@ from manim import config, logger
|
|||
from manim.constants import *
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim.mobject.svg.svg_mobject import SVGMobject
|
||||
from manim.utils.tex import TexTemplate
|
||||
from manim.utils.tex_file_writing import tex_to_svg_file
|
||||
|
||||
MATHTEX_SUBSTRING = "substring"
|
||||
|
||||
|
||||
class SingleStringMathTex(SVGMobject):
|
||||
"""Elementary building block for rendering text with LaTeX.
|
||||
|
|
@ -261,22 +263,30 @@ class MathTex(SingleStringMathTex):
|
|||
self.tex_template = kwargs.pop("tex_template", config["tex_template"])
|
||||
self.arg_separator = arg_separator
|
||||
self.substrings_to_isolate = (
|
||||
[] if substrings_to_isolate is None else substrings_to_isolate
|
||||
[] if substrings_to_isolate is None else list(substrings_to_isolate)
|
||||
)
|
||||
if tex_to_color_map is None:
|
||||
self.tex_to_color_map: dict[str, ParsableManimColor] = {}
|
||||
else:
|
||||
self.tex_to_color_map = tex_to_color_map
|
||||
self.substrings_to_isolate.extend(self.tex_to_color_map.keys())
|
||||
self.tex_environment = tex_environment
|
||||
self.brace_notation_split_occurred = False
|
||||
self.tex_strings = self._break_up_tex_strings(tex_strings)
|
||||
self.tex_strings = self._prepare_tex_strings(tex_strings)
|
||||
self.matched_strings_and_ids: list[tuple[str, str]] = []
|
||||
|
||||
try:
|
||||
joined_string = self._join_tex_strings_with_unique_deliminters(
|
||||
self.tex_strings, self.substrings_to_isolate
|
||||
)
|
||||
super().__init__(
|
||||
self.arg_separator.join(self.tex_strings),
|
||||
joined_string,
|
||||
tex_environment=self.tex_environment,
|
||||
tex_template=self.tex_template,
|
||||
**kwargs,
|
||||
)
|
||||
# Save the original tex_string
|
||||
self.tex_string = self.arg_separator.join(self.tex_strings)
|
||||
self._break_up_by_substrings()
|
||||
except ValueError:
|
||||
if self.brace_notation_split_occurred:
|
||||
|
|
@ -298,36 +308,109 @@ class MathTex(SingleStringMathTex):
|
|||
if self.organize_left_to_right:
|
||||
self._organize_submobjects_left_to_right()
|
||||
|
||||
def _break_up_tex_strings(self, tex_strings: Sequence[str]) -> list[str]:
|
||||
# Separate out anything surrounded in double braces
|
||||
pre_split_length = len(tex_strings)
|
||||
tex_strings_brace_splitted = [
|
||||
re.split("{{(.*?)}}", str(t)) for t in tex_strings
|
||||
def _prepare_tex_strings(self, tex_strings: Iterable[str]) -> list[str]:
|
||||
# Deal with the case where tex_strings contains integers instead
|
||||
# of strings.
|
||||
tex_strings_validated = [
|
||||
string if isinstance(string, str) else str(string) for string in tex_strings
|
||||
]
|
||||
tex_strings_combined = sum(tex_strings_brace_splitted, [])
|
||||
if len(tex_strings_combined) > pre_split_length:
|
||||
# Locate double curly bracers
|
||||
tex_strings_validated_two = []
|
||||
for tex_string in tex_strings_validated:
|
||||
split = re.split(r"{{|}}", tex_string)
|
||||
tex_strings_validated_two.extend(split)
|
||||
if len(tex_strings_validated_two) > len(tex_strings_validated):
|
||||
self.brace_notation_split_occurred = True
|
||||
return [string for string in tex_strings_validated_two if len(string) > 0]
|
||||
|
||||
# Separate out any strings specified in the isolate
|
||||
# or tex_to_color_map lists.
|
||||
patterns = []
|
||||
patterns.extend(
|
||||
[
|
||||
f"({re.escape(ss)})"
|
||||
for ss in it.chain(
|
||||
self.substrings_to_isolate,
|
||||
self.tex_to_color_map.keys(),
|
||||
def _join_tex_strings_with_unique_deliminters(
|
||||
self, tex_strings: list[str], substrings_to_isolate: Iterable[str]
|
||||
) -> str:
|
||||
joined_string = ""
|
||||
ssIdx = 0
|
||||
for idx, tex_string in enumerate(tex_strings):
|
||||
string_part = rf"\special{{dvisvgm:raw <g id='unique{idx:03d}'>}}"
|
||||
self.matched_strings_and_ids.append((tex_string, f"unique{idx:03d}"))
|
||||
|
||||
# Try to match with all substrings_to_isolate and apply the first match
|
||||
# then match again (on the rest of the string) and continue until no
|
||||
# characters are left in the string
|
||||
unprocessed_string = str(tex_string)
|
||||
processed_string = ""
|
||||
while len(unprocessed_string) > 0:
|
||||
first_match = self._locate_first_match(
|
||||
substrings_to_isolate, unprocessed_string
|
||||
)
|
||||
],
|
||||
|
||||
if first_match:
|
||||
processed, unprocessed_string = self._handle_match(
|
||||
ssIdx, first_match
|
||||
)
|
||||
processed_string = processed_string + processed
|
||||
ssIdx += 1
|
||||
else:
|
||||
processed_string = processed_string + unprocessed_string
|
||||
unprocessed_string = ""
|
||||
|
||||
string_part += processed_string
|
||||
if idx < len(tex_strings) - 1:
|
||||
string_part += self.arg_separator
|
||||
string_part += r"\special{dvisvgm:raw </g>}"
|
||||
joined_string = joined_string + string_part
|
||||
return joined_string
|
||||
|
||||
def _locate_first_match(
|
||||
self, substrings_to_isolate: Iterable[str], unprocessed_string: str
|
||||
) -> re.Match | None:
|
||||
first_match_start = len(unprocessed_string)
|
||||
first_match_length = 0
|
||||
first_match = None
|
||||
for substring in substrings_to_isolate:
|
||||
match = re.match(f"(.*?)({re.escape(substring)})(.*)", unprocessed_string)
|
||||
if match and len(match.group(1)) < first_match_start:
|
||||
first_match = match
|
||||
first_match_start = len(match.group(1))
|
||||
first_match_length = len(match.group(2))
|
||||
elif match and len(match.group(1)) == first_match_start:
|
||||
# Break ties by looking at length of matches.
|
||||
if first_match_length < len(match.group(2)):
|
||||
first_match = match
|
||||
first_match_start = len(match.group(1))
|
||||
first_match_length = len(match.group(2))
|
||||
return first_match
|
||||
|
||||
def _handle_match(self, ssIdx: int, first_match: re.Match) -> tuple[str, str]:
|
||||
pre_match = first_match.group(1)
|
||||
matched_string = first_match.group(2)
|
||||
post_match = first_match.group(3)
|
||||
pre_string = (
|
||||
rf"\special{{dvisvgm:raw <g id='unique{ssIdx:03d}{MATHTEX_SUBSTRING}'>}}"
|
||||
)
|
||||
pattern = "|".join(patterns)
|
||||
if pattern:
|
||||
pieces = []
|
||||
for s in tex_strings_combined:
|
||||
pieces.extend(re.split(pattern, s))
|
||||
else:
|
||||
pieces = tex_strings_combined
|
||||
return [p for p in pieces if p]
|
||||
post_string = r"\special{dvisvgm:raw </g>}"
|
||||
self.matched_strings_and_ids.append(
|
||||
(matched_string, f"unique{ssIdx:03d}{MATHTEX_SUBSTRING}")
|
||||
)
|
||||
processed_string = pre_match + pre_string + matched_string + post_string
|
||||
unprocessed_string = post_match
|
||||
return processed_string, unprocessed_string
|
||||
|
||||
@property
|
||||
def _substring_matches(self) -> list[tuple[str, str]]:
|
||||
"""Return only the 'ss' (substring_to_isolate) matches."""
|
||||
return [
|
||||
(tex, id_)
|
||||
for tex, id_ in self.matched_strings_and_ids
|
||||
if id_.endswith(MATHTEX_SUBSTRING)
|
||||
]
|
||||
|
||||
@property
|
||||
def _main_matches(self) -> list[tuple[str, str]]:
|
||||
"""Return only the main tex_string matches."""
|
||||
return [
|
||||
(tex, id_)
|
||||
for tex, id_ in self.matched_strings_and_ids
|
||||
if not id_.endswith(MATHTEX_SUBSTRING)
|
||||
]
|
||||
|
||||
def _break_up_by_substrings(self) -> Self:
|
||||
"""
|
||||
|
|
@ -336,25 +419,17 @@ class MathTex(SingleStringMathTex):
|
|||
of tex_strings)
|
||||
"""
|
||||
new_submobjects: list[VMobject] = []
|
||||
curr_index = 0
|
||||
for tex_string in self.tex_strings:
|
||||
sub_tex_mob = SingleStringMathTex(
|
||||
tex_string,
|
||||
tex_environment=self.tex_environment,
|
||||
tex_template=self.tex_template,
|
||||
try:
|
||||
for tex_string, tex_string_id in self._main_matches:
|
||||
mtp = MathTexPart()
|
||||
mtp.tex_string = tex_string
|
||||
mtp.add(*self.id_to_vgroup_dict[tex_string_id].submobjects)
|
||||
new_submobjects.append(mtp)
|
||||
except KeyError:
|
||||
logger.error(
|
||||
f"MathTex: Could not find SVG group for tex part '{tex_string}' (id: {tex_string_id}). Using fallback to root group."
|
||||
)
|
||||
num_submobs = len(sub_tex_mob.submobjects)
|
||||
new_index = (
|
||||
curr_index + num_submobs + len("".join(self.arg_separator.split()))
|
||||
)
|
||||
if num_submobs == 0:
|
||||
last_submob_index = min(curr_index, len(self.submobjects) - 1)
|
||||
sub_tex_mob.move_to(self.submobjects[last_submob_index], RIGHT)
|
||||
else:
|
||||
sub_tex_mob.submobjects = self.submobjects[curr_index:new_index]
|
||||
sub_tex_mob.note_changed_family()
|
||||
new_submobjects.append(sub_tex_mob)
|
||||
curr_index = new_index
|
||||
new_submobjects.append(self.id_to_vgroup_dict["root"])
|
||||
self.submobjects = new_submobjects
|
||||
|
||||
# 5 hours of work went into this line
|
||||
|
|
@ -364,30 +439,18 @@ class MathTex(SingleStringMathTex):
|
|||
|
||||
return self
|
||||
|
||||
def get_parts_by_tex(
|
||||
self, tex: str, substring: bool = True, case_sensitive: bool = True
|
||||
) -> VGroup:
|
||||
def test(tex1: str, tex2: str) -> bool:
|
||||
if not case_sensitive:
|
||||
tex1 = tex1.lower()
|
||||
tex2 = tex2.lower()
|
||||
if substring:
|
||||
return tex1 in tex2
|
||||
else:
|
||||
return tex1 == tex2
|
||||
|
||||
return VGroup(*(m for m in self.submobjects if test(tex, m.get_tex_string())))
|
||||
|
||||
def get_part_by_tex(self, tex: str, **kwargs: Any) -> MathTex | None:
|
||||
all_parts = self.get_parts_by_tex(tex, **kwargs)
|
||||
return all_parts[0] if all_parts else None
|
||||
def get_part_by_tex(self, tex: str, **kwargs: Any) -> VGroup | None:
|
||||
for tex_str, match_id in self.matched_strings_and_ids:
|
||||
if tex_str == tex:
|
||||
return self.id_to_vgroup_dict[match_id]
|
||||
return None
|
||||
|
||||
def set_color_by_tex(
|
||||
self, tex: str, color: ParsableManimColor, **kwargs: Any
|
||||
) -> Self:
|
||||
parts_to_color = self.get_parts_by_tex(tex, **kwargs)
|
||||
for part in parts_to_color:
|
||||
part.set_color(color)
|
||||
for tex_str, match_id in self.matched_strings_and_ids:
|
||||
if tex_str == tex:
|
||||
self.id_to_vgroup_dict[match_id].set_color(color)
|
||||
return self
|
||||
|
||||
def set_opacity_by_tex(
|
||||
|
|
@ -413,22 +476,18 @@ class MathTex(SingleStringMathTex):
|
|||
"""
|
||||
if remaining_opacity is not None:
|
||||
self.set_opacity(opacity=remaining_opacity)
|
||||
for part in self.get_parts_by_tex(tex):
|
||||
part.set_opacity(opacity)
|
||||
for tex_str, match_id in self.matched_strings_and_ids:
|
||||
if tex_str == tex:
|
||||
self.id_to_vgroup_dict[match_id].set_opacity(opacity)
|
||||
return self
|
||||
|
||||
def set_color_by_tex_to_color_map(
|
||||
self, texs_to_color_map: dict[str, ParsableManimColor], **kwargs: Any
|
||||
) -> Self:
|
||||
for texs, color in list(texs_to_color_map.items()):
|
||||
try:
|
||||
# If the given key behaves like tex_strings
|
||||
texs + ""
|
||||
self.set_color_by_tex(texs, ManimColor(color), **kwargs)
|
||||
except TypeError:
|
||||
# If the given key is a tuple
|
||||
for tex in texs:
|
||||
self.set_color_by_tex(tex, ManimColor(color), **kwargs)
|
||||
for match in self.matched_strings_and_ids:
|
||||
if match[0] == texs:
|
||||
self.id_to_vgroup_dict[match[1]].set_color(color)
|
||||
return self
|
||||
|
||||
def index_of_part(self, part: MathTex) -> int:
|
||||
|
|
@ -437,17 +496,18 @@ class MathTex(SingleStringMathTex):
|
|||
raise ValueError("Trying to get index of part not in MathTex")
|
||||
return split_self.index(part)
|
||||
|
||||
def index_of_part_by_tex(self, tex: str, **kwargs: Any) -> int:
|
||||
part = self.get_part_by_tex(tex, **kwargs)
|
||||
if part is None:
|
||||
return -1
|
||||
return self.index_of_part(part)
|
||||
|
||||
def sort_alphabetically(self) -> None:
|
||||
self.submobjects.sort(key=lambda m: m.get_tex_string())
|
||||
self.note_changed_family()
|
||||
|
||||
|
||||
class MathTexPart(VMobject):
|
||||
tex_string: str
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{type(self).__name__}({repr(self.tex_string)})"
|
||||
|
||||
|
||||
class Tex(MathTex):
|
||||
r"""A string compiled with LaTeX in normal mode.
|
||||
|
||||
|
|
|
|||
|
|
@ -809,8 +809,7 @@ class Text(SVGMobject):
|
|||
line_spacing /= TEXT2SVG_ADJUSTMENT_FACTOR
|
||||
|
||||
dir_name = config.get_dir("text_dir")
|
||||
if not dir_name.is_dir():
|
||||
dir_name.mkdir(parents=True)
|
||||
dir_name.mkdir(parents=True, exist_ok=True)
|
||||
hash_name = self._text2hash(color)
|
||||
file_name = dir_name / (hash_name + ".svg")
|
||||
|
||||
|
|
@ -1350,8 +1349,7 @@ class MarkupText(SVGMobject):
|
|||
line_spacing /= TEXT2SVG_ADJUSTMENT_FACTOR
|
||||
|
||||
dir_name = config.get_dir("text_dir")
|
||||
if not dir_name.is_dir():
|
||||
dir_name.mkdir(parents=True)
|
||||
dir_name.mkdir(parents=True, exist_ok=True)
|
||||
hash_name = self._text2hash(color)
|
||||
file_name = dir_name / (hash_name + ".svg")
|
||||
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ class Polyhedron(VGroup):
|
|||
"""Creates list of cyclic pairwise tuples."""
|
||||
edges: list[tuple[int, int]] = []
|
||||
for face in faces_list:
|
||||
edges += zip(face, face[1:] + face[:1], strict=False)
|
||||
edges += zip(face, face[1:] + face[:1], strict=True)
|
||||
return edges
|
||||
|
||||
def create_faces(
|
||||
|
|
|
|||
|
|
@ -17,7 +17,13 @@ from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
|||
from ... import config
|
||||
from ...constants import *
|
||||
from ...utils.bezier import interpolate
|
||||
from ...utils.color import WHITE, ManimColor, color_to_int_rgb
|
||||
from ...utils.color import (
|
||||
WHITE,
|
||||
YELLOW_C,
|
||||
ManimColor,
|
||||
ParsableManimColor,
|
||||
color_to_int_rgb,
|
||||
)
|
||||
from ...utils.images import change_to_rgba_array, get_full_raster_image_path
|
||||
|
||||
__all__ = ["ImageMobject", "ImageMobjectFromCamera"]
|
||||
|
|
@ -61,9 +67,14 @@ class AbstractImageMobject(Mobject):
|
|||
def get_pixel_array(self) -> PixelArray:
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_color(self, color, alpha=None, family=True):
|
||||
def set_color( # type: ignore[override]
|
||||
self,
|
||||
color: ParsableManimColor = YELLOW_C,
|
||||
alpha: Any = None,
|
||||
family: bool = True,
|
||||
) -> AbstractImageMobject:
|
||||
# Likely to be implemented in subclasses, but no obligation
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_resampling_algorithm(self, resampling_algorithm: int) -> Self:
|
||||
"""
|
||||
|
|
@ -86,18 +97,18 @@ class AbstractImageMobject(Mobject):
|
|||
* 'hamming'
|
||||
* 'lanczos' or 'antialias'
|
||||
"""
|
||||
if isinstance(resampling_algorithm, int):
|
||||
self.resampling_algorithm = resampling_algorithm
|
||||
else:
|
||||
if resampling_algorithm not in RESAMPLING_ALGORITHMS.values():
|
||||
raise ValueError(
|
||||
"resampling_algorithm has to be an int, one of the values defined in "
|
||||
"RESAMPLING_ALGORITHMS or a Pillow resampling filter constant. "
|
||||
"Available algorithms: 'bicubic', 'nearest', 'box', 'bilinear', "
|
||||
"'hamming', 'lanczos'.",
|
||||
"Available algorithms: 'bicubic' (or 'cubic'), 'nearest' (or 'none'), "
|
||||
"'bilinear' (or 'linear').",
|
||||
)
|
||||
|
||||
self.resampling_algorithm = resampling_algorithm
|
||||
return self
|
||||
|
||||
def reset_points(self) -> None:
|
||||
def reset_points(self) -> Self:
|
||||
"""Sets :attr:`points` to be the four image corners."""
|
||||
self.points = np.array(
|
||||
[
|
||||
|
|
@ -115,6 +126,7 @@ class AbstractImageMobject(Mobject):
|
|||
height = 3 # this is the case for ImageMobjectFromCamera
|
||||
self.stretch_to_fit_height(height)
|
||||
self.stretch_to_fit_width(height * w / h)
|
||||
return self
|
||||
|
||||
|
||||
class ImageMobject(AbstractImageMobject):
|
||||
|
|
@ -156,27 +168,18 @@ class ImageMobject(AbstractImageMobject):
|
|||
[0, 0, 0, 255]
|
||||
]))
|
||||
|
||||
img.height = 2
|
||||
img1 = img.copy()
|
||||
img2 = img.copy()
|
||||
img3 = img.copy()
|
||||
img4 = img.copy()
|
||||
img5 = img.copy()
|
||||
img.height = 3
|
||||
|
||||
img1.set_resampling_algorithm(RESAMPLING_ALGORITHMS["nearest"])
|
||||
img2.set_resampling_algorithm(RESAMPLING_ALGORITHMS["lanczos"])
|
||||
img3.set_resampling_algorithm(RESAMPLING_ALGORITHMS["linear"])
|
||||
img4.set_resampling_algorithm(RESAMPLING_ALGORITHMS["cubic"])
|
||||
img5.set_resampling_algorithm(RESAMPLING_ALGORITHMS["box"])
|
||||
img1.add(Text("nearest").scale(0.5).next_to(img1,UP))
|
||||
img2.add(Text("lanczos").scale(0.5).next_to(img2,UP))
|
||||
img3.add(Text("linear").scale(0.5).next_to(img3,UP))
|
||||
img4.add(Text("cubic").scale(0.5).next_to(img4,UP))
|
||||
img5.add(Text("box").scale(0.5).next_to(img5,UP))
|
||||
group = Group()
|
||||
algorithm_texts = ["nearest", "linear", "cubic"]
|
||||
for algorithm_text in algorithm_texts:
|
||||
algorithm = RESAMPLING_ALGORITHMS[algorithm_text]
|
||||
img_copy = img.copy().set_resampling_algorithm(algorithm)
|
||||
img_copy.add(Text(algorithm_text).scale(0.5).next_to(img_copy, UP))
|
||||
group.add(img_copy)
|
||||
|
||||
x= Group(img1,img2,img3,img4,img5)
|
||||
x.arrange()
|
||||
self.add(x)
|
||||
group.arrange()
|
||||
self.add(group)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -209,18 +212,23 @@ class ImageMobject(AbstractImageMobject):
|
|||
self.orig_alpha_pixel_array = self.pixel_array[:, :, 3].copy()
|
||||
super().__init__(scale_to_resolution, **kwargs)
|
||||
|
||||
def get_pixel_array(self):
|
||||
def get_pixel_array(self) -> PixelArray:
|
||||
"""A simple getter method."""
|
||||
return self.pixel_array
|
||||
|
||||
def set_color(self, color, alpha=None, family=True):
|
||||
def set_color( # type: ignore[override]
|
||||
self,
|
||||
color: ParsableManimColor = YELLOW_C,
|
||||
alpha: Any = None,
|
||||
family: bool = True,
|
||||
) -> Self:
|
||||
rgb = color_to_int_rgb(color)
|
||||
self.pixel_array[:, :, :3] = rgb
|
||||
if alpha is not None:
|
||||
self.pixel_array[:, :, 3] = int(255 * alpha)
|
||||
for submob in self.submobjects:
|
||||
submob.set_color(color, alpha, family)
|
||||
self.color = color
|
||||
self.color = ManimColor(color)
|
||||
return self
|
||||
|
||||
def set_opacity(self, alpha: float) -> Self:
|
||||
|
|
@ -252,7 +260,7 @@ class ImageMobject(AbstractImageMobject):
|
|||
return self
|
||||
|
||||
def interpolate_color(
|
||||
self, mobject1: ImageMobject, mobject2: ImageMobject, alpha: float
|
||||
self, mobject1: Mobject, mobject2: Mobject, alpha: float
|
||||
) -> None:
|
||||
"""Interpolates the array of pixel color values from one ImageMobject
|
||||
into an array of equal size in the target ImageMobject.
|
||||
|
|
@ -268,6 +276,8 @@ class ImageMobject(AbstractImageMobject):
|
|||
alpha
|
||||
Used to track the lerp relationship. Not opacity related.
|
||||
"""
|
||||
assert isinstance(mobject1, ImageMobject)
|
||||
assert isinstance(mobject2, ImageMobject)
|
||||
assert mobject1.pixel_array.shape == mobject2.pixel_array.shape, (
|
||||
f"Mobject pixel array shapes incompatible for interpolation.\n"
|
||||
f"Mobject 1 ({mobject1}) : {mobject1.pixel_array.shape}\n"
|
||||
|
|
@ -291,7 +301,7 @@ class ImageMobject(AbstractImageMobject):
|
|||
|
||||
def get_style(self) -> dict[str, Any]:
|
||||
return {
|
||||
"fill_color": ManimColor(self.color.get_rgb()).to_hex(),
|
||||
"fill_color": ManimColor(self.color.to_rgb()).to_hex(),
|
||||
"fill_opacity": self.fill_opacity,
|
||||
}
|
||||
|
||||
|
|
@ -320,7 +330,7 @@ class ImageMobjectFromCamera(AbstractImageMobject):
|
|||
super().__init__(scale_to_resolution=False, **kwargs)
|
||||
|
||||
# TODO: Get rid of this.
|
||||
def get_pixel_array(self):
|
||||
def get_pixel_array(self) -> PixelArray:
|
||||
self.pixel_array = self.camera.pixel_array
|
||||
return self.pixel_array
|
||||
|
||||
|
|
@ -331,7 +341,11 @@ class ImageMobjectFromCamera(AbstractImageMobject):
|
|||
self.add(self.display_frame)
|
||||
return self
|
||||
|
||||
def interpolate_color(self, mobject1, mobject2, alpha) -> None:
|
||||
def interpolate_color(
|
||||
self, mobject1: Mobject, mobject2: Mobject, alpha: float
|
||||
) -> None:
|
||||
assert isinstance(mobject1, ImageMobjectFromCamera)
|
||||
assert isinstance(mobject2, ImageMobjectFromCamera)
|
||||
assert mobject1.pixel_array.shape == mobject2.pixel_array.shape, (
|
||||
f"Mobject pixel array shapes incompatible for interpolation.\n"
|
||||
f"Mobject 1 ({mobject1}) : {mobject1.pixel_array.shape}\n"
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ from ...mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
|||
from ...utils.bezier import interpolate
|
||||
from ...utils.color import (
|
||||
BLACK,
|
||||
PURE_YELLOW,
|
||||
WHITE,
|
||||
YELLOW,
|
||||
ManimColor,
|
||||
ParsableManimColor,
|
||||
color_gradient,
|
||||
|
|
@ -109,7 +109,7 @@ class PMobject(Mobject):
|
|||
return self
|
||||
|
||||
def set_color(
|
||||
self, color: ParsableManimColor = YELLOW, family: bool = True
|
||||
self, color: ParsableManimColor = PURE_YELLOW, family: bool = True
|
||||
) -> Self:
|
||||
rgba = color_to_rgba(color)
|
||||
mobs = self.family_members_with_points() if family else [self]
|
||||
|
|
@ -129,7 +129,7 @@ class PMobject(Mobject):
|
|||
|
||||
def set_color_by_gradient(self, *colors: ParsableManimColor) -> Self:
|
||||
self.rgbas = np.array(
|
||||
list(map(color_to_rgba, color_gradient(*colors, len(self.points)))),
|
||||
list(map(color_to_rgba, color_gradient(colors, len(self.points)))),
|
||||
)
|
||||
return self
|
||||
|
||||
|
|
@ -171,7 +171,7 @@ class PMobject(Mobject):
|
|||
for mob in self.family_members_with_points():
|
||||
num_points = self.get_num_points()
|
||||
mob.apply_over_attr_arrays(
|
||||
lambda arr, n=num_points: arr[np.arange(0, n, factor)],
|
||||
lambda arr, n=num_points: arr[np.arange(0, n, factor)], # type: ignore[misc]
|
||||
)
|
||||
return self
|
||||
|
||||
|
|
@ -181,7 +181,7 @@ class PMobject(Mobject):
|
|||
"""Function is any map from R^3 to R"""
|
||||
for mob in self.family_members_with_points():
|
||||
indices = np.argsort(np.apply_along_axis(function, 1, mob.points))
|
||||
mob.apply_over_attr_arrays(lambda arr, idx=indices: arr[idx])
|
||||
mob.apply_over_attr_arrays(lambda arr, idx=indices: arr[idx]) # type: ignore[misc]
|
||||
return self
|
||||
|
||||
def fade_to(
|
||||
|
|
@ -198,7 +198,7 @@ class PMobject(Mobject):
|
|||
def ingest_submobjects(self) -> Self:
|
||||
attrs = self.get_array_attrs()
|
||||
arrays = list(map(self.get_merged_array, attrs))
|
||||
for attr, array in zip(attrs, arrays, strict=False):
|
||||
for attr, array in zip(attrs, arrays, strict=True):
|
||||
setattr(self, attr, array)
|
||||
self.submobjects = []
|
||||
self.note_changed_family()
|
||||
|
|
@ -359,7 +359,7 @@ class PointCloudDot(Mobject1D):
|
|||
radius: float = 2.0,
|
||||
stroke_width: int = 2,
|
||||
density: int = DEFAULT_POINT_DENSITY_1D,
|
||||
color: ManimColor = YELLOW,
|
||||
color: ManimColor = PURE_YELLOW,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.radius = radius
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ class VMobject(Mobject):
|
|||
rgbas: FloatRGBA_Array = np.array(
|
||||
[
|
||||
c.to_rgba_with_alpha(o)
|
||||
for c, o in zip(*make_even(colors, opacities), strict=False)
|
||||
for c, o in zip(*make_even(colors, opacities), strict=True)
|
||||
],
|
||||
)
|
||||
|
||||
|
|
@ -461,7 +461,7 @@ class VMobject(Mobject):
|
|||
return self
|
||||
elif len(submobs2) == 0:
|
||||
submobs2 = [vmobject]
|
||||
for sm1, sm2 in zip(*make_even(submobs1, submobs2), strict=False):
|
||||
for sm1, sm2 in zip(*make_even(submobs1, submobs2), strict=True):
|
||||
sm1.match_style(sm2)
|
||||
return self
|
||||
|
||||
|
|
@ -1349,7 +1349,7 @@ class VMobject(Mobject):
|
|||
split_indices = [0] + list(filtered) + [len(points)]
|
||||
return (
|
||||
points[i1:i2]
|
||||
for i1, i2 in zip(split_indices, split_indices[1:], strict=False)
|
||||
for i1, i2 in zip(split_indices[:-1], split_indices[1:], strict=True)
|
||||
if (i2 - i1) >= nppcc
|
||||
)
|
||||
|
||||
|
|
@ -1703,7 +1703,7 @@ class VMobject(Mobject):
|
|||
|
||||
s = self.get_start_anchors()
|
||||
e = self.get_end_anchors()
|
||||
return list(it.chain.from_iterable(zip(s, e, strict=False)))
|
||||
return list(it.chain.from_iterable(zip(s, e, strict=True)))
|
||||
|
||||
def get_points_defining_boundary(self) -> Point3D_Array:
|
||||
# Probably returns all anchors, but this is weird regarding the name of the method.
|
||||
|
|
|
|||
0
manim/py.typed
Normal file
0
manim/py.typed
Normal file
|
|
@ -44,9 +44,9 @@ from ..utils.color import (
|
|||
BLUE_D,
|
||||
GREEN_C,
|
||||
GREY,
|
||||
PURE_YELLOW,
|
||||
RED_C,
|
||||
WHITE,
|
||||
YELLOW,
|
||||
ManimColor,
|
||||
ParsableManimColor,
|
||||
)
|
||||
|
|
@ -181,7 +181,7 @@ class VectorScene(Scene):
|
|||
def add_vector(
|
||||
self,
|
||||
vector: Arrow | Vector3DLike,
|
||||
color: ParsableManimColor | Iterable[ParsableManimColor] = YELLOW,
|
||||
color: ParsableManimColor | Iterable[ParsableManimColor] = PURE_YELLOW,
|
||||
animate: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> Arrow:
|
||||
|
|
@ -817,7 +817,7 @@ class LinearTransformationScene(VectorScene):
|
|||
|
||||
def get_unit_square(
|
||||
self,
|
||||
color: ParsableManimColor | Iterable[ParsableManimColor] = YELLOW,
|
||||
color: ParsableManimColor | Iterable[ParsableManimColor] = PURE_YELLOW,
|
||||
opacity: float = 0.3,
|
||||
stroke_width: float = 3,
|
||||
) -> Rectangle:
|
||||
|
|
@ -884,7 +884,7 @@ class LinearTransformationScene(VectorScene):
|
|||
def add_vector(
|
||||
self,
|
||||
vector: Arrow | list | tuple | np.ndarray,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
animate: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> Arrow:
|
||||
|
|
|
|||
|
|
@ -1000,7 +1000,7 @@ def bezier_remap(
|
|||
|
||||
new_tuples = np.empty((new_number_of_curves, nppc, dim))
|
||||
index = 0
|
||||
for curve, sf in zip(bezier_tuples, split_factors, strict=False):
|
||||
for curve, sf in zip(bezier_tuples, split_factors, strict=True):
|
||||
new_tuples[index : index + sf] = subdivide_bezier(curve, sf).reshape(
|
||||
sf, nppc, dim
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1429,7 +1429,7 @@ def color_gradient(
|
|||
floors[-1] = num_colors - 2
|
||||
return [
|
||||
rgb_to_color((rgbs[i] * (1 - alpha)) + (rgbs[i + 1] * alpha))
|
||||
for i, alpha in zip(floors, alphas_mod1, strict=False)
|
||||
for i, alpha in zip(floors, alphas_mod1, strict=True)
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -1531,7 +1531,7 @@ class RandomColorGenerator:
|
|||
|
||||
>>> rnd = RandomColorGenerator(42)
|
||||
>>> rnd.next()
|
||||
ManimColor('#ECE7E2')
|
||||
ManimColor('#8B4513')
|
||||
>>> rnd.next()
|
||||
ManimColor('#BBBBBB')
|
||||
>>> rnd.next()
|
||||
|
|
@ -1541,7 +1541,7 @@ class RandomColorGenerator:
|
|||
|
||||
>>> rnd2 = RandomColorGenerator(42)
|
||||
>>> rnd2.next()
|
||||
ManimColor('#ECE7E2')
|
||||
ManimColor('#8B4513')
|
||||
>>> rnd2.next()
|
||||
ManimColor('#BBBBBB')
|
||||
>>> rnd2.next()
|
||||
|
|
|
|||
|
|
@ -102,6 +102,9 @@ These colors form Manim's default color space.
|
|||
"pure_red",
|
||||
"pure_green",
|
||||
"pure_blue",
|
||||
"pure_cyan",
|
||||
"pure_magenta",
|
||||
"pure_yellow",
|
||||
)
|
||||
|
||||
pure_lines = named_lines_group(
|
||||
|
|
@ -145,12 +148,17 @@ DARK_GRAY = ManimColor("#444444")
|
|||
DARK_GREY = ManimColor("#444444")
|
||||
DARKER_GRAY = ManimColor("#222222")
|
||||
DARKER_GREY = ManimColor("#222222")
|
||||
PURE_RED = ManimColor("#FF0000")
|
||||
PURE_GREEN = ManimColor("#00FF00")
|
||||
PURE_BLUE = ManimColor("#0000FF")
|
||||
PURE_CYAN = ManimColor("#00FFFF")
|
||||
PURE_MAGENTA = ManimColor("#FF00FF")
|
||||
PURE_YELLOW = ManimColor("#FFFF00")
|
||||
BLUE_A = ManimColor("#C7E9F1")
|
||||
BLUE_B = ManimColor("#9CDCEB")
|
||||
BLUE_C = ManimColor("#58C4DD")
|
||||
BLUE_D = ManimColor("#29ABCA")
|
||||
BLUE_E = ManimColor("#236B8E")
|
||||
PURE_BLUE = ManimColor("#0000FF")
|
||||
BLUE = ManimColor("#58C4DD")
|
||||
DARK_BLUE = ManimColor("#236B8E")
|
||||
TEAL_A = ManimColor("#ACEAD7")
|
||||
|
|
@ -164,14 +172,13 @@ GREEN_B = ManimColor("#A6CF8C")
|
|||
GREEN_C = ManimColor("#83C167")
|
||||
GREEN_D = ManimColor("#77B05D")
|
||||
GREEN_E = ManimColor("#699C52")
|
||||
PURE_GREEN = ManimColor("#00FF00")
|
||||
GREEN = ManimColor("#83C167")
|
||||
YELLOW_A = ManimColor("#FFF1B6")
|
||||
YELLOW_B = ManimColor("#FFEA94")
|
||||
YELLOW_C = ManimColor("#FFFF00")
|
||||
YELLOW_C = ManimColor("#F7D96F")
|
||||
YELLOW_D = ManimColor("#F4D345")
|
||||
YELLOW_E = ManimColor("#E8C11C")
|
||||
YELLOW = ManimColor("#FFFF00")
|
||||
YELLOW = ManimColor("#F7D96F")
|
||||
GOLD_A = ManimColor("#F7C797")
|
||||
GOLD_B = ManimColor("#F9B775")
|
||||
GOLD_C = ManimColor("#F0AC5F")
|
||||
|
|
@ -183,7 +190,6 @@ RED_B = ManimColor("#FF8080")
|
|||
RED_C = ManimColor("#FC6255")
|
||||
RED_D = ManimColor("#E65A4C")
|
||||
RED_E = ManimColor("#CF5044")
|
||||
PURE_RED = ManimColor("#FF0000")
|
||||
RED = ManimColor("#FC6255")
|
||||
MAROON_A = ManimColor("#ECABC1")
|
||||
MAROON_B = ManimColor("#EC92AB")
|
||||
|
|
|
|||
|
|
@ -153,15 +153,14 @@ def add_version_before_extension(file_name: Path) -> Path:
|
|||
|
||||
|
||||
def guarantee_existence(path: Path) -> Path:
|
||||
if not path.exists():
|
||||
path.mkdir(parents=True)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path.resolve(strict=True)
|
||||
|
||||
|
||||
def guarantee_empty_existence(path: Path) -> Path:
|
||||
if path.exists():
|
||||
shutil.rmtree(str(path))
|
||||
path.mkdir(parents=True)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path.resolve(strict=True)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ def adjacent_n_tuples(objects: Sequence[T], n: int) -> zip[tuple[T, ...]]:
|
|||
>>> list(adjacent_n_tuples([1, 2, 3, 4], 3))
|
||||
[(1, 2, 3), (2, 3, 4), (3, 4, 1), (4, 1, 2)]
|
||||
"""
|
||||
return zip(*([*objects[k:], *objects[:k]] for k in range(n)), strict=False)
|
||||
return zip(*([*objects[k:], *objects[:k]] for k in range(n)), strict=True)
|
||||
|
||||
|
||||
def adjacent_pairs(objects: Sequence[T]) -> zip[tuple[T, ...]]:
|
||||
|
|
|
|||
|
|
@ -9,13 +9,12 @@ __all__ = [
|
|||
"sigmoid",
|
||||
]
|
||||
|
||||
|
||||
import math
|
||||
from collections.abc import Callable
|
||||
from functools import lru_cache
|
||||
from typing import Any, Protocol, TypeVar
|
||||
|
||||
import numpy as np
|
||||
from scipy import special
|
||||
|
||||
|
||||
def binary_search(
|
||||
|
|
@ -87,9 +86,9 @@ def choose(n: int, k: int) -> int:
|
|||
References
|
||||
----------
|
||||
- https://en.wikipedia.org/wiki/Combination
|
||||
- https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.comb.html
|
||||
- https://docs.python.org/3/library/math.html#math.comb
|
||||
"""
|
||||
value: int = special.comb(n, k, exact=True)
|
||||
value: int = math.comb(n, k)
|
||||
return value
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -625,7 +625,7 @@ def find_intersection(
|
|||
# algorithm from https://en.wikipedia.org/wiki/Skew_lines#Nearest_points
|
||||
result = []
|
||||
|
||||
for p0, v0, p1, v1 in zip(p0s, v0s, p1s, v1s, strict=False):
|
||||
for p0, v0, p1, v1 in zip(p0s, v0s, p1s, v1s, strict=True):
|
||||
normal = cross(v1, cross(v0, v1))
|
||||
denom = max(np.dot(v0, normal), threshold)
|
||||
result += [p0 + np.dot(p1 - p0, normal) / denom * v0]
|
||||
|
|
@ -747,7 +747,10 @@ def earclip_triangulation(verts: np.ndarray, ring_ends: list) -> list:
|
|||
list
|
||||
A list of indices giving a triangulation of a polygon.
|
||||
"""
|
||||
rings = [list(range(e0, e1)) for e0, e1 in zip([0, *ring_ends], ring_ends)]
|
||||
rings = [
|
||||
list(range(e0, e1))
|
||||
for e0, e1 in zip([0, *ring_ends[:-1]], ring_ends, strict=True)
|
||||
]
|
||||
|
||||
def is_in(point, ring_id):
|
||||
return (
|
||||
|
|
@ -758,7 +761,7 @@ def earclip_triangulation(verts: np.ndarray, ring_ends: list) -> list:
|
|||
def ring_area(ring_id):
|
||||
ring = rings[ring_id]
|
||||
s = 0
|
||||
for i, j in zip(ring[1:], ring):
|
||||
for i, j in zip(ring[1:], ring[:-1], strict=True):
|
||||
s += cross2d(verts[i], verts[j])
|
||||
return abs(s) / 2
|
||||
|
||||
|
|
|
|||
|
|
@ -103,8 +103,7 @@ def generate_tex_file(
|
|||
output = tex_template.get_texcode_for_expression(expression)
|
||||
|
||||
tex_dir = config.get_dir("tex_dir")
|
||||
if not tex_dir.exists():
|
||||
tex_dir.mkdir()
|
||||
tex_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
result = tex_dir / (tex_hash(output) + ".tex")
|
||||
if not result.exists():
|
||||
|
|
|
|||
112
mypy.ini
112
mypy.ini
|
|
@ -67,21 +67,6 @@ ignore_errors = True
|
|||
[mypy-manim.animation.creation]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.animation.fading]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.animation.growing]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.animation.indication]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.animation.movement]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.animation.numbers]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.animation.speedmodifier]
|
||||
ignore_errors = True
|
||||
|
||||
|
|
@ -94,31 +79,7 @@ ignore_errors = True
|
|||
[mypy-manim.animation.updaters.mobject_update_utils]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.camera.camera]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.cli.checkhealth.commands]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.manager]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.frame]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.geometry.arc]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.geometry.labeled]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.geometry.line]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.geometry.polygram]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.geometry.shape_matchers]
|
||||
[mypy-manim.camera.mapping_camera]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.graphing.coordinate_systems]
|
||||
|
|
@ -142,21 +103,6 @@ ignore_errors = True
|
|||
[mypy-manim.mobject.mobject]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.opengl.dot_cloud]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.opengl.opengl_compatibility]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.opengl.opengl_geometry]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.opengl.opengl_image_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.opengl.opengl_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.opengl.opengl_point_cloud_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
|
|
@ -178,27 +124,6 @@ ignore_errors = True
|
|||
[mypy-manim.mobject.table]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.text.code_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.text.tex_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.text.text_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.three_d.polyhedra]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.three_d.three_dimensions]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.types.image_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.types.point_cloud_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.types.vectorized_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
|
|
@ -220,44 +145,9 @@ ignore_errors = True
|
|||
[mypy-manim.renderer.shader_wrapper]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.renderer.vectorized_mobject_rendering]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.scene.scene]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.scene.vector_space_scene]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.scene.zoomed_scene]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.utils.caching]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.utils.color.core]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.utils.debug]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.utils.directories]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.utils.family_ops]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.utils.hashing]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.utils.progressbar]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.utils.space_ops]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.utils.testing._test_class_makers]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.utils.testing.frames_comparison]
|
||||
ignore_errors = True
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "manim"
|
||||
version = "0.19.2"
|
||||
version = "0.20.0"
|
||||
description = "Animation engine for explanatory math videos."
|
||||
authors = [
|
||||
{name = "The Manim Community Developers", email = "contact@manim.community"},
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ def main():
|
|||
sys.exit(1)
|
||||
npz_file = sys.argv[1]
|
||||
output_folder = pathlib.Path(sys.argv[2])
|
||||
if not output_folder.exists():
|
||||
output_folder.mkdir(parents=True)
|
||||
output_folder.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
data = np.load(npz_file)
|
||||
if "frame_data" not in data:
|
||||
|
|
|
|||
|
|
@ -346,7 +346,12 @@ def cli(ctx: click.Context, dry_run: bool) -> None:
|
|||
)
|
||||
@click.option("--head", default="main", help="Head ref for comparison (default: main)")
|
||||
@click.option("--title", help="Custom changelog title (default: vX.Y.Z)")
|
||||
@click.option("--update-citation", is_flag=True, help="Also update CITATION.cff")
|
||||
@click.option(
|
||||
"--update-citation",
|
||||
"also_update_citation",
|
||||
is_flag=True,
|
||||
help="Also update CITATION.cff",
|
||||
)
|
||||
@click.pass_context
|
||||
def changelog(
|
||||
ctx: click.Context,
|
||||
|
|
@ -354,7 +359,7 @@ def changelog(
|
|||
version: str,
|
||||
head: str,
|
||||
title: str | None,
|
||||
update_citation: bool,
|
||||
also_update_citation: bool,
|
||||
) -> None:
|
||||
"""Generate changelog for an upcoming release.
|
||||
|
||||
|
|
@ -375,7 +380,7 @@ def changelog(
|
|||
click.echo()
|
||||
click.secho("[DRY RUN]", fg="yellow", bold=True)
|
||||
click.echo(f" Would save: {CHANGELOG_DIR / f'{version}-changelog.md'}")
|
||||
if update_citation:
|
||||
if also_update_citation:
|
||||
click.echo(f" Would update: {CITATION_FILE}")
|
||||
click.echo()
|
||||
click.echo("--- Generated changelog ---")
|
||||
|
|
@ -385,7 +390,7 @@ def changelog(
|
|||
filepath = save_changelog(version, content)
|
||||
click.secho(f" ✓ Saved: {filepath}", fg="green")
|
||||
|
||||
if update_citation:
|
||||
if also_update_citation:
|
||||
citation_path = update_citation(version)
|
||||
click.secho(f" ✓ Updated: {citation_path}", fg="green")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{"levelname": "INFO", "module": "logger_utils", "message": "Log file will be saved in <>"}
|
||||
{"levelname": "INFO", "module": "tex_file_writing", "message": "Writing <> to <>"}
|
||||
{"levelname": "ERROR", "module": "tex_file_writing", "message": "LaTeX compilation error: LaTeX Error: File `notapackage.sty' not found.\n"}
|
||||
{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\frac{1}{0}\n"}
|
||||
{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\special{dvisvgm:raw <g id='unique000'>}\\frac{1}{0}\\special{dvisvgm:raw </g>}\n"}
|
||||
{"levelname": "INFO", "module": "tex_file_writing", "message": "You do not have package notapackage.sty installed."}
|
||||
{"levelname": "INFO", "module": "tex_file_writing", "message": "Install notapackage.sty it using your LaTeX package manager, or check for typos."}
|
||||
{"levelname": "ERROR", "module": "tex_file_writing", "message": "LaTeX compilation error: Emergency stop.\n"}
|
||||
{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\frac{1}{0}\n"}
|
||||
{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\special{dvisvgm:raw <g id='unique000'>}\\frac{1}{0}\\special{dvisvgm:raw </g>}\n"}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ def set_test_scene(scene_object: type[Scene], module_name: str, config):
|
|||
tests_directory = Path(__file__).absolute().parent.parent
|
||||
path_control_data = Path(tests_directory) / "control_data" / "graphical_units_data"
|
||||
path = Path(path_control_data) / module_name
|
||||
if not path.is_dir():
|
||||
path.mkdir(parents=True)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
np.savez_compressed(path / str(manager.scene), frame_data=data)
|
||||
logger.info(f"Test data for {str(manager.scene)} saved in {path}\n")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from manim import Circle, FadeIn
|
||||
from manim import UP, Circle, Dot, FadeIn
|
||||
from manim.animation.updaters.mobject_update_utils import turn_animation_into_updater
|
||||
|
||||
|
||||
|
|
@ -56,3 +56,14 @@ def test_turn_animation_into_updater_positive_run_time_persists():
|
|||
current_updaters = mobject.get_updaters()
|
||||
assert len(current_updaters) == len(original_updaters) + 1
|
||||
assert updater in current_updaters
|
||||
|
||||
|
||||
def test_always():
|
||||
d = Dot()
|
||||
circ = Circle()
|
||||
d.always.next_to(circ, UP)
|
||||
assert len(d.updaters) == 1
|
||||
# we should be able to chain updaters
|
||||
d2 = Dot()
|
||||
d.always.next_to(d2, UP).next_to(circ, UP)
|
||||
assert len(d.updaters) == 3
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ def test_Polygram_get_vertex_groups():
|
|||
for vertex_groups in vertex_groups_arr:
|
||||
polygram = Polygram(*vertex_groups)
|
||||
poly_vertex_groups = polygram.get_vertex_groups()
|
||||
for poly_group, group in zip(poly_vertex_groups, vertex_groups, strict=False):
|
||||
for poly_group, group in zip(poly_vertex_groups, vertex_groups, strict=True):
|
||||
np.testing.assert_array_equal(poly_group, group)
|
||||
|
||||
# If polygram is a Polygram of a vertex group containing the start vertex N times,
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ def test_add_labels():
|
|||
expected_label_length = 6
|
||||
num_line = NumberLine(x_range=[-4, 4])
|
||||
num_line.add_labels(
|
||||
dict(zip(list(range(-3, 3)), [Integer(m) for m in range(-1, 5)], strict=False)),
|
||||
dict(zip(list(range(-3, 3)), [Integer(m) for m in range(-1, 5)], strict=True)),
|
||||
)
|
||||
actual_label_length = len(num_line.labels)
|
||||
assert actual_label_length == expected_label_length, (
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ def test_bracelabel_copy(tmp_path, config):
|
|||
config["text_dir"] = str(mediadir.joinpath("Text"))
|
||||
config["tex_dir"] = str(mediadir.joinpath("Tex"))
|
||||
for el in ["text_dir", "tex_dir"]:
|
||||
Path(config[el]).mkdir(parents=True)
|
||||
Path(config[el]).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Before the refactoring of Mobject.copy(), the class BraceLabel was the
|
||||
# only one to have a non-trivial definition of copy. Here we test that it
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from manim import MathTex, SingleStringMathTex, Tex, TexTemplate, tempconfig
|
|||
|
||||
def test_MathTex(config):
|
||||
MathTex("a^2 + b^2 = c^2")
|
||||
assert Path(config.media_dir, "Tex", "e4be163a00cf424f.svg").exists()
|
||||
assert Path(config.media_dir, "Tex", "05bb0a41ed575f00.svg").exists()
|
||||
|
||||
|
||||
def test_SingleStringMathTex(config):
|
||||
|
|
@ -29,7 +29,7 @@ def test_double_braces_testing(text_input, length_sub):
|
|||
|
||||
def test_tex(config):
|
||||
Tex("The horse does not eat cucumber salad.")
|
||||
assert Path(config.media_dir, "Tex", "c3945e23e546c95a.svg").exists()
|
||||
assert Path(config.media_dir, "Tex", "5384b41741a246bd.svg").exists()
|
||||
|
||||
|
||||
def test_tex_temp_directory(tmpdir, monkeypatch):
|
||||
|
|
@ -38,16 +38,16 @@ def test_tex_temp_directory(tmpdir, monkeypatch):
|
|||
# tempconfig to change media directory to temporary directory by default
|
||||
# we partially, revert that change here.
|
||||
monkeypatch.chdir(tmpdir)
|
||||
Path(tmpdir, "media").mkdir()
|
||||
Path(tmpdir, "media").mkdir(exist_ok=True)
|
||||
with tempconfig({"media_dir": "media"}):
|
||||
Tex("The horse does not eat cucumber salad.")
|
||||
assert Path("media", "Tex").exists()
|
||||
assert Path("media", "Tex", "c3945e23e546c95a.svg").exists()
|
||||
assert Path("media", "Tex", "5384b41741a246bd.svg").exists()
|
||||
|
||||
|
||||
def test_percent_char_rendering(config):
|
||||
Tex(r"\%")
|
||||
assert Path(config.media_dir, "Tex", "4a583af4d19a3adf.tex").exists()
|
||||
assert Path(config.media_dir, "Tex", "32509dd0ea993961.tex").exists()
|
||||
|
||||
|
||||
def test_tex_whitespace_arg():
|
||||
|
|
@ -108,7 +108,7 @@ def test_multi_part_tex_with_empty_parts():
|
|||
for one_part_glyph, multi_part_glyph in zip(
|
||||
one_part_fomula.family_members_with_points(),
|
||||
multi_part_formula.family_members_with_points(),
|
||||
strict=False,
|
||||
strict=True,
|
||||
):
|
||||
np.testing.assert_allclose(one_part_glyph.points, multi_part_glyph.points)
|
||||
|
||||
|
|
@ -215,14 +215,14 @@ def test_tempconfig_resetting_tex_template(config):
|
|||
|
||||
def test_tex_garbage_collection(tmpdir, monkeypatch, config):
|
||||
monkeypatch.chdir(tmpdir)
|
||||
Path(tmpdir, "media").mkdir()
|
||||
Path(tmpdir, "media").mkdir(exist_ok=True)
|
||||
config.media_dir = "media"
|
||||
|
||||
tex_without_log = Tex("Hello World!") # d771330b76d29ffb.tex
|
||||
assert Path("media", "Tex", "d771330b76d29ffb.tex").exists()
|
||||
assert not Path("media", "Tex", "d771330b76d29ffb.log").exists()
|
||||
tex_without_log = Tex("Hello World!") # 058a4e242c57db6d.tex
|
||||
assert Path("media", "Tex", "058a4e242c57db6d.tex").exists()
|
||||
assert not Path("media", "Tex", "058a4e242c57db6d.log").exists()
|
||||
|
||||
config.no_latex_cleanup = True
|
||||
|
||||
tex_with_log = Tex("Hello World, again!") # da27670a37b08799.tex
|
||||
assert Path("media", "Tex", "da27670a37b08799.log").exists()
|
||||
tex_with_log = Tex("Hello World, again!") # 45b4e7819cc20cb1.tex
|
||||
assert Path("media", "Tex", "45b4e7819cc20cb1.log").exists()
|
||||
|
|
|
|||
|
|
@ -408,13 +408,7 @@ def test_vdict_init():
|
|||
# Test VDict made from a python dict
|
||||
VDict({"a": VMobject(), "b": VMobject(), "c": VMobject()})
|
||||
# Test VDict made using zip
|
||||
VDict(
|
||||
zip(
|
||||
["a", "b", "c"],
|
||||
[VMobject(), VMobject(), VMobject()],
|
||||
strict=False,
|
||||
)
|
||||
)
|
||||
VDict(zip(["a", "b", "c"], [VMobject(), VMobject(), VMobject()], strict=True))
|
||||
# If the value is of type Mobject, must raise a TypeError
|
||||
with pytest.raises(TypeError):
|
||||
VDict({"a": Mobject()})
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -32,7 +32,7 @@ def test_custom_coordinates(scene: Scene) -> None:
|
|||
ax = Axes(x_range=[0, 10])
|
||||
|
||||
ax.add_coordinates(
|
||||
dict(zip(list(range(1, 10)), [Tex("str") for _ in range(1, 10)], strict=False)),
|
||||
dict(zip(list(range(1, 10)), [Tex("str") for _ in range(1, 10)], strict=True)),
|
||||
)
|
||||
scene.add(ax)
|
||||
|
||||
|
|
@ -226,7 +226,7 @@ def test_get_area(scene: Scene) -> None:
|
|||
area2 = ax.get_area(
|
||||
curve1,
|
||||
x_range=(-4.5, -2),
|
||||
color=(RED, YELLOW),
|
||||
color=(RED, PURE_YELLOW),
|
||||
opacity=0.2,
|
||||
bounded_graph=curve2,
|
||||
)
|
||||
|
|
@ -266,7 +266,7 @@ def test_get_riemann_rectangles(scene: Scene, use_vectorized: bool) -> None:
|
|||
quadratic,
|
||||
x_range=[-1.5, 1.5],
|
||||
dx=0.15,
|
||||
color=YELLOW,
|
||||
color=PURE_YELLOW,
|
||||
)
|
||||
|
||||
bounding_line = ax.plot(lambda x: 1.5 * x, color=BLUE_B, x_range=[3.3, 6])
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ def test_line_graph(scene):
|
|||
first_line = plane.plot_line_graph(
|
||||
x_values=[-3, 1],
|
||||
y_values=[-2, 2],
|
||||
line_color=YELLOW,
|
||||
line_color=PURE_YELLOW,
|
||||
)
|
||||
second_line = plane.plot_line_graph(
|
||||
x_values=[0, 2, 2, 4],
|
||||
|
|
@ -75,7 +75,7 @@ def test_plot_surface_colorscale(scene):
|
|||
param_trig,
|
||||
u_range=(-3, 3),
|
||||
v_range=(-3, 3),
|
||||
colorscale=[BLUE, GREEN, YELLOW, ORANGE, RED],
|
||||
colorscale=[BLUE, GREEN, PURE_YELLOW, ORANGE, RED],
|
||||
)
|
||||
|
||||
scene.add(axes, trig_plane)
|
||||
|
|
@ -156,7 +156,7 @@ def test_gradient_line_graph_x_axis(scene):
|
|||
curve = axes.plot(
|
||||
lambda x: 0.1 * x**3,
|
||||
x_range=(-3, 3, 0.001),
|
||||
colorscale=[BLUE, GREEN, YELLOW, ORANGE, RED],
|
||||
colorscale=[BLUE, GREEN, PURE_YELLOW, ORANGE, RED],
|
||||
colorscale_axis=0,
|
||||
)
|
||||
|
||||
|
|
@ -172,7 +172,7 @@ def test_gradient_line_graph_y_axis(scene):
|
|||
curve = axes.plot(
|
||||
lambda x: 0.1 * x**3,
|
||||
x_range=(-3, 3, 0.001),
|
||||
colorscale=[BLUE, GREEN, YELLOW, ORANGE, RED],
|
||||
colorscale=[BLUE, GREEN, PURE_YELLOW, ORANGE, RED],
|
||||
colorscale_axis=1,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -268,21 +268,16 @@ def test_ImageInterpolation(scene):
|
|||
img = ImageMobject(
|
||||
np.uint8([[63, 0, 0, 0], [0, 127, 0, 0], [0, 0, 191, 0], [0, 0, 0, 255]]),
|
||||
)
|
||||
img.height = 2
|
||||
img1 = img.copy()
|
||||
img2 = img.copy()
|
||||
img3 = img.copy()
|
||||
img4 = img.copy()
|
||||
img5 = img.copy()
|
||||
img.height = 3
|
||||
|
||||
img1.set_resampling_algorithm(RESAMPLING_ALGORITHMS["nearest"])
|
||||
img2.set_resampling_algorithm(RESAMPLING_ALGORITHMS["lanczos"])
|
||||
img3.set_resampling_algorithm(RESAMPLING_ALGORITHMS["linear"])
|
||||
img4.set_resampling_algorithm(RESAMPLING_ALGORITHMS["cubic"])
|
||||
img5.set_resampling_algorithm(RESAMPLING_ALGORITHMS["box"])
|
||||
algorithm_texts = ["nearest", "linear", "cubic"]
|
||||
for i, algorithm_text in enumerate(algorithm_texts):
|
||||
algorithm = RESAMPLING_ALGORITHMS[algorithm_text]
|
||||
img_copy = img.copy().set_resampling_algorithm(algorithm)
|
||||
position = img.height * (i - (len(algorithm_texts) - 1) / 2) * RIGHT
|
||||
img_copy.move_to(position)
|
||||
scene.add(img_copy)
|
||||
|
||||
scene.add(img1, img2, img3, img4, img5)
|
||||
[s.shift(4 * LEFT + pos * 2 * RIGHT) for pos, s in enumerate(scene.mobjects)]
|
||||
scene.wait()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ def test_become(scene):
|
|||
.set_opacity(0.25)
|
||||
.set_color(GREEN)
|
||||
)
|
||||
s3 = s.copy().become(d, stretch=True).set_opacity(0.25).set_color(YELLOW)
|
||||
s3 = s.copy().become(d, stretch=True).set_opacity(0.25).set_color(PURE_YELLOW)
|
||||
|
||||
scene.add(s, d, s1, s2, s3)
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ def test_become_no_color_linking(scene):
|
|||
scene.add(b)
|
||||
b.become(a)
|
||||
b.shift(1 * RIGHT)
|
||||
b.set_stroke(YELLOW, opacity=1)
|
||||
b.set_stroke(PURE_YELLOW, opacity=1)
|
||||
|
||||
|
||||
@frames_comparison
|
||||
|
|
@ -59,7 +59,7 @@ def test_vmobject_joint_types(scene):
|
|||
]
|
||||
)
|
||||
lines = VGroup(*[angled_line.copy() for _ in range(len(LineJointType))])
|
||||
for line, joint_type in zip(lines, LineJointType, strict=False):
|
||||
for line, joint_type in zip(lines, LineJointType, strict=True):
|
||||
line.joint_type = joint_type
|
||||
|
||||
lines.arrange(RIGHT, buff=1)
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ __module_test__ = "modifier_methods"
|
|||
|
||||
@frames_comparison
|
||||
def test_Gradient(scene):
|
||||
c = Circle(fill_opacity=1).set_color(color=[YELLOW, GREEN])
|
||||
c = Circle(fill_opacity=1).set_color(color=[PURE_YELLOW, GREEN])
|
||||
scene.add(c)
|
||||
|
||||
|
||||
@frames_comparison
|
||||
def test_GradientRotation(scene):
|
||||
c = Circle(fill_opacity=1).set_color(color=[YELLOW, GREEN]).rotate(PI)
|
||||
c = Circle(fill_opacity=1).set_color(color=[PURE_YELLOW, GREEN]).rotate(PI)
|
||||
scene.add(c)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import pytest
|
|||
from manim.constants import LEFT
|
||||
from manim.mobject.graphing.probability import BarChart
|
||||
from manim.mobject.text.tex_mobject import MathTex
|
||||
from manim.utils.color import BLUE, GREEN, RED, WHITE, YELLOW
|
||||
from manim.utils.color import BLUE, GREEN, PURE_YELLOW, RED, WHITE
|
||||
from manim.utils.testing.frames_comparison import frames_comparison
|
||||
|
||||
__module_test__ = "probability"
|
||||
|
|
@ -72,13 +72,13 @@ def test_advanced_customization(scene):
|
|||
chart = BarChart(values=[10, 40, 10, 20], bar_names=["one", "two", "three", "four"])
|
||||
|
||||
c_x_lbls = chart.x_axis.labels
|
||||
c_x_lbls.set_color_by_gradient(GREEN, RED, YELLOW)
|
||||
c_x_lbls.set_color_by_gradient(GREEN, RED, PURE_YELLOW)
|
||||
|
||||
c_y_nums = chart.y_axis.numbers
|
||||
c_y_nums.set_color_by_gradient(BLUE, WHITE).shift(LEFT)
|
||||
|
||||
c_y_axis = chart.y_axis
|
||||
c_y_axis.ticks.set_color(YELLOW)
|
||||
c_y_axis.ticks.set_color(PURE_YELLOW)
|
||||
|
||||
c_bar_lbls = chart.get_bar_labels()
|
||||
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ def test_SurfaceColorscale(scene: Scene) -> None:
|
|||
u_range=[-3, 3],
|
||||
)
|
||||
trig_plane.set_fill_by_value(
|
||||
axes=axes, colorscale=[BLUE, GREEN, YELLOW, ORANGE, RED]
|
||||
axes=axes, colorscale=[BLUE, GREEN, PURE_YELLOW, ORANGE, RED]
|
||||
)
|
||||
scene.add(axes, trig_plane)
|
||||
|
||||
|
|
@ -170,7 +170,7 @@ def test_Y_Direction(scene: Scene) -> None:
|
|||
)
|
||||
surface_plane.set_style(fill_opacity=1)
|
||||
surface_plane.set_fill_by_value(
|
||||
axes=axes, colorscale=[(RED, -0.4), (YELLOW, 0), (GREEN, 0.4)], axis=1
|
||||
axes=axes, colorscale=[(RED, -0.4), (PURE_YELLOW, 0), (GREEN, 0.4)], axis=1
|
||||
)
|
||||
scene.add(axes, surface_plane)
|
||||
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ def test_AnimationBuilder(scene):
|
|||
|
||||
@frames_comparison(last_frame=False)
|
||||
def test_ReplacementTransform(scene):
|
||||
yellow = Square(fill_opacity=1.0, fill_color=YELLOW)
|
||||
yellow = Square(fill_opacity=1.0, fill_color=PURE_YELLOW)
|
||||
yellow.move_to([0, 0.75, 0])
|
||||
|
||||
green = Square(fill_opacity=1.0, fill_color=GREEN)
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ def create_plugin(tmp_path, python_version, random_string):
|
|||
def _create_plugin(entry_point, class_name, function_name, all_dec=""):
|
||||
entry_point = entry_point.format(plugin_name=plugin_name)
|
||||
module_dir = plugin_dir / plugin_name
|
||||
module_dir.mkdir(parents=True)
|
||||
module_dir.mkdir(parents=True, exist_ok=True)
|
||||
(module_dir / "__init__.py").write_text(
|
||||
plugin_init_template.format(
|
||||
class_name=class_name,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ def _check_logs(reference_logfile_path: Path, generated_logfile_path: Path) -> N
|
|||
msg_assert += f"\nPath of reference log: {reference_logfile}\nPath of generated logs: {generated_logfile}"
|
||||
pytest.fail(msg_assert)
|
||||
|
||||
for index, ref, gen in zip(itertools.count(), reference_logs, generated_logs):
|
||||
for index, ref, gen in zip(
|
||||
itertools.count(), reference_logs, generated_logs, strict=False
|
||||
):
|
||||
# As they are string, we only need to check if they are equal. If they are not, we then compute a more precise difference, to debug.
|
||||
if ref == gen:
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ def check_video_data(path_control_data: Path, path_video_gen: Path) -> None:
|
|||
f"expected {len(sec_index_exp)} sections ({', '.join([el['name'] for el in sec_index_exp])}), but {len(sec_index_gen)} ({', '.join([el['name'] for el in sec_index_gen])}) got generated (in '{path_sec_index_gen}')"
|
||||
)
|
||||
# check individual sections
|
||||
for sec_gen, sec_exp in zip(sec_index_gen, sec_index_exp, strict=False):
|
||||
for sec_gen, sec_exp in zip(sec_index_gen, sec_index_exp, strict=True):
|
||||
assert_shallow_dict_compare(
|
||||
sec_gen,
|
||||
sec_exp,
|
||||
|
|
|
|||
170
uv.lock
generated
170
uv.lock
generated
|
|
@ -1396,7 +1396,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "manim"
|
||||
version = "0.19.2"
|
||||
version = "0.20.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "audioop-lts", marker = "python_full_version >= '3.13'" },
|
||||
|
|
@ -1873,7 +1873,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "nbconvert"
|
||||
version = "7.16.6"
|
||||
version = "7.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "beautifulsoup4" },
|
||||
|
|
@ -1891,9 +1891,9 @@ dependencies = [
|
|||
{ name = "pygments" },
|
||||
{ name = "traitlets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/59/f28e15fc47ffb73af68a8d9b47367a8630d76e97ae85ad18271b9db96fdf/nbconvert-7.16.6.tar.gz", hash = "sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582", size = 857715, upload-time = "2025-01-28T09:29:14.724Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/47/81f886b699450d0569f7bc551df2b1673d18df7ff25cc0c21ca36ed8a5ff/nbconvert-7.17.0.tar.gz", hash = "sha256:1b2696f1b5be12309f6c7d707c24af604b87dfaf6d950794c7b07acab96dda78", size = 862855, upload-time = "2026-01-29T16:37:48.478Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl", hash = "sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b", size = 258525, upload-time = "2025-01-28T09:29:12.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl", hash = "sha256:4f99a63b337b9a23504347afdab24a11faa7d86b405e5c8f9881cd313336d518", size = 261510, upload-time = "2026-01-29T16:37:46.322Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2095,89 +2095,89 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "12.1.0"
|
||||
version = "12.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057, upload-time = "2026-01-02T09:10:46.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811, upload-time = "2026-01-02T09:10:49.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243, upload-time = "2026-01-02T09:10:51.62Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", size = 8037872, upload-time = "2026-01-02T09:10:53.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", size = 6345398, upload-time = "2026-01-02T09:10:55.426Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", size = 7034667, upload-time = "2026-01-02T09:10:57.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", size = 6458743, upload-time = "2026-01-02T09:10:59.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", size = 7159342, upload-time = "2026-01-02T09:11:01.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/40/50d86571c9e5868c42b81fe7da0c76ca26373f3b95a8dd675425f4a92ec1/pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64", size = 6328655, upload-time = "2026-01-02T09:11:04.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75", size = 7031469, upload-time = "2026-01-02T09:11:06.538Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/36/d5716586d887fb2a810a4a61518a327a1e21c8b7134c89283af272efe84b/pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304", size = 2452515, upload-time = "2026-01-02T09:11:08.226Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605, upload-time = "2026-01-02T09:13:14.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245, upload-time = "2026-01-02T09:13:15.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593, upload-time = "2026-01-02T09:13:17.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", size = 6989008, upload-time = "2026-01-02T09:13:20.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", size = 5313824, upload-time = "2026-01-02T09:13:22.405Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", size = 5963278, upload-time = "2026-01-02T09:13:24.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue