753 lines
21 KiB
Python
753 lines
21 KiB
Python
import contextlib
|
|
import functools
|
|
import math
|
|
import os
|
|
import shlex
|
|
import shutil
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import uuid
|
|
from pathlib import Path
|
|
from typing import List, Optional, Union
|
|
|
|
import folder_paths
|
|
import numpy as np
|
|
import requests
|
|
import torch
|
|
from PIL import Image
|
|
|
|
from .install import pip_map
|
|
|
|
try:
|
|
from .log import log
|
|
except ImportError:
|
|
try:
|
|
from log import log
|
|
|
|
log.warn("Imported log without relative path")
|
|
except ImportError:
|
|
import logging
|
|
|
|
log = logging.getLogger("comfy mtb utils")
|
|
log.warn("[comfy mtb] You probably called the file outside a module.")
|
|
|
|
|
|
# region SANITY_CHECK Utilities
|
|
|
|
|
|
def make_report():
|
|
pass
|
|
|
|
|
|
# endregion
|
|
|
|
|
|
# region SERVER Utilities
|
|
class IPChecker:
|
|
def __init__(self):
|
|
self.ips = list(self.get_local_ips())
|
|
log.debug(f"Found {len(self.ips)} local ips")
|
|
self.checked_ips = set()
|
|
|
|
def get_working_ip(self, test_url_template):
|
|
for ip in self.ips:
|
|
if ip not in self.checked_ips:
|
|
self.checked_ips.add(ip)
|
|
test_url = test_url_template.format(ip)
|
|
if self._test_url(test_url):
|
|
return ip
|
|
return None
|
|
|
|
@staticmethod
|
|
def get_local_ips(prefix="192.168."):
|
|
hostname = socket.gethostname()
|
|
log.debug(f"Getting local ips for {hostname}")
|
|
for info in socket.getaddrinfo(hostname, None):
|
|
# Filter out IPv6 addresses if you only want IPv4
|
|
log.debug(info)
|
|
# if info[1] == socket.SOCK_STREAM and
|
|
if info[0] == socket.AF_INET and info[4][0].startswith(prefix):
|
|
yield info[4][0]
|
|
|
|
def _test_url(self, url):
|
|
try:
|
|
response = requests.get(url)
|
|
return response.status_code == 200
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
@functools.lru_cache(maxsize=1)
|
|
def get_server_info():
|
|
from comfy.cli_args import args
|
|
|
|
ip_checker = IPChecker()
|
|
base_url = args.listen
|
|
if base_url == "0.0.0.0":
|
|
log.debug("Server set to 0.0.0.0, we will try to resolve the host IP")
|
|
base_url = ip_checker.get_working_ip(f"http://{{}}:{args.port}/history")
|
|
log.debug(f"Setting ip to {base_url}")
|
|
return (base_url, args.port)
|
|
|
|
|
|
# endregion
|
|
|
|
|
|
# region MISC Utilities
|
|
def backup_file(
|
|
fp: Path,
|
|
target: Optional[Path] = None,
|
|
backup_dir: str = ".bak",
|
|
suffix: Optional[str] = None,
|
|
prefix: Optional[str] = None,
|
|
):
|
|
if not fp.exists():
|
|
raise FileNotFoundError(f"No file found at {fp}")
|
|
|
|
backup_directory = target or fp.parent / backup_dir
|
|
backup_directory.mkdir(parents=True, exist_ok=True)
|
|
|
|
stem = fp.stem
|
|
|
|
if suffix or prefix:
|
|
new_stem = f"{prefix or ''}{stem}{suffix or ''}"
|
|
else:
|
|
new_stem = f"{stem}_{uuid.uuid4()}"
|
|
|
|
backup_file_path = backup_directory / f"{new_stem}{fp.suffix}"
|
|
|
|
# Perform the backup
|
|
shutil.copy(fp, backup_file_path)
|
|
log.debug(f"File backed up to {backup_file_path}")
|
|
|
|
|
|
def hex_to_rgb(hex_color):
|
|
try:
|
|
hex_color = hex_color.lstrip("#")
|
|
return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
|
|
except ValueError:
|
|
log.error(f"Invalid hex color: {hex_color}")
|
|
return (0, 0, 0)
|
|
|
|
|
|
def add_path(path, prepend=False):
|
|
if isinstance(path, list):
|
|
for p in path:
|
|
add_path(p, prepend)
|
|
return
|
|
|
|
if isinstance(path, Path):
|
|
path = path.resolve().as_posix()
|
|
|
|
if path not in sys.path:
|
|
if prepend:
|
|
sys.path.insert(0, path)
|
|
else:
|
|
sys.path.append(path)
|
|
|
|
|
|
def run_command(cmd, ignored_lines_start=None):
|
|
if ignored_lines_start is None:
|
|
ignored_lines_start = []
|
|
|
|
if isinstance(cmd, str):
|
|
shell_cmd = cmd
|
|
elif isinstance(cmd, list):
|
|
shell_cmd = " ".join(
|
|
arg.as_posix() if isinstance(arg, Path) else shlex.quote(str(arg))
|
|
for arg in cmd
|
|
)
|
|
else:
|
|
raise ValueError(
|
|
"Invalid 'cmd' argument. It must be a string or a list of arguments."
|
|
)
|
|
|
|
try:
|
|
_run_command(shell_cmd, ignored_lines_start)
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"Command failed with return code: {e.returncode}", file=sys.stderr)
|
|
print(e.stderr.strip(), file=sys.stderr)
|
|
|
|
except KeyboardInterrupt:
|
|
print("Command execution interrupted.")
|
|
|
|
|
|
def _run_command(shell_cmd, ignored_lines_start):
|
|
log.debug(f"Running {shell_cmd}")
|
|
|
|
result = subprocess.run(
|
|
shell_cmd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
shell=True,
|
|
check=True,
|
|
)
|
|
|
|
stdout_lines = result.stdout.strip().split("\n")
|
|
stderr_lines = result.stderr.strip().split("\n")
|
|
|
|
# Print stdout, skipping ignored lines
|
|
for line in stdout_lines:
|
|
if not any(line.startswith(ign) for ign in ignored_lines_start):
|
|
print(line)
|
|
|
|
# Print stderr
|
|
for line in stderr_lines:
|
|
print(line, file=sys.stderr)
|
|
|
|
print("Command executed successfully!")
|
|
|
|
|
|
# todo use the requirements library
|
|
reqs_map = {value: key for key, value in pip_map.items()}
|
|
|
|
import importlib
|
|
|
|
|
|
def import_install(package_name):
|
|
package_spec = reqs_map.get(package_name, package_name)
|
|
|
|
try:
|
|
importlib.import_module(package_name)
|
|
|
|
except Exception: # (ImportError, ModuleNotFoundError):
|
|
run_command(
|
|
[Path(sys.executable).as_posix(), "-m", "pip", "install", package_spec]
|
|
)
|
|
importlib.import_module(package_name)
|
|
|
|
|
|
# endregion
|
|
|
|
|
|
# region GLOBAL VARIABLES
|
|
# - detect mode
|
|
comfy_mode = None
|
|
if os.environ.get("COLAB_GPU"):
|
|
comfy_mode = "colab"
|
|
elif "python_embeded" in sys.executable:
|
|
comfy_mode = "embeded"
|
|
elif ".venv" in sys.executable:
|
|
comfy_mode = "venv"
|
|
|
|
# - Get the absolute path of the parent directory of the current script
|
|
here = Path(__file__).parent.absolute()
|
|
|
|
# - Construct the absolute path to the ComfyUI directory
|
|
comfy_dir = Path(folder_paths.base_path)
|
|
models_dir = Path(folder_paths.models_dir)
|
|
styles_dir = comfy_dir / "styles"
|
|
|
|
# - Construct the path to the font file
|
|
font_path = here / "font.ttf"
|
|
|
|
# - Add extern folder to path
|
|
extern_root = here / "extern"
|
|
add_path(extern_root)
|
|
for pth in extern_root.iterdir():
|
|
if pth.is_dir():
|
|
add_path(pth)
|
|
|
|
# - Add the ComfyUI directory and custom nodes path to the sys.path list
|
|
add_path(comfy_dir)
|
|
add_path((comfy_dir / "custom_nodes"))
|
|
|
|
PIL_FILTER_MAP = {
|
|
"nearest": Image.Resampling.NEAREST,
|
|
"box": Image.Resampling.BOX,
|
|
"bilinear": Image.Resampling.BILINEAR,
|
|
"hamming": Image.Resampling.HAMMING,
|
|
"bicubic": Image.Resampling.BICUBIC,
|
|
"lanczos": Image.Resampling.LANCZOS,
|
|
}
|
|
# endregion
|
|
|
|
|
|
# region TENSOR Utilities
|
|
def tensor2pil(image: torch.Tensor) -> List[Image.Image]:
|
|
batch_count = image.size(0) if len(image.shape) > 3 else 1
|
|
if batch_count > 1:
|
|
out = []
|
|
for i in range(batch_count):
|
|
out.extend(tensor2pil(image[i]))
|
|
return out
|
|
|
|
return [
|
|
Image.fromarray(
|
|
np.clip(255.0 * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)
|
|
)
|
|
]
|
|
|
|
|
|
def pil2tensor(image: Union[Image.Image, List[Image.Image]]) -> torch.Tensor:
|
|
if isinstance(image, list):
|
|
return torch.cat([pil2tensor(img) for img in image], dim=0)
|
|
|
|
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
|
|
|
|
|
|
def np2tensor(img_np: Union[np.ndarray, List[np.ndarray]]) -> torch.Tensor:
|
|
if isinstance(img_np, list):
|
|
return torch.cat([np2tensor(img) for img in img_np], dim=0)
|
|
|
|
return torch.from_numpy(img_np.astype(np.float32) / 255.0).unsqueeze(0)
|
|
|
|
|
|
def tensor2np(tensor: torch.Tensor) -> List[np.ndarray]:
|
|
batch_count = tensor.size(0) if len(tensor.shape) > 3 else 1
|
|
if batch_count > 1:
|
|
out = []
|
|
for i in range(batch_count):
|
|
out.extend(tensor2np(tensor[i]))
|
|
return out
|
|
|
|
return [np.clip(255.0 * tensor.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)]
|
|
|
|
|
|
def pad(img, left, right, top, bottom):
|
|
pad_width = np.array(((0, 0), (top, bottom), (left, right)))
|
|
print(f"pad_width: {pad_width}, shape: {pad_width.shape}") # Debugging line
|
|
return np.pad(img, pad_width, mode="wrap")
|
|
|
|
|
|
def tiles_infer(tiles, ort_session, progress_callback=None):
|
|
"""Infer each tile with the given model. progress_callback will be called with
|
|
arguments : current tile idx and total tiles amount (used to show progress on
|
|
cursor in Blender)."""
|
|
|
|
out_channels = 3 # normal map RGB channels
|
|
tiles_nb = tiles.shape[0]
|
|
pred_tiles = np.empty((tiles_nb, out_channels, tiles.shape[2], tiles.shape[3]))
|
|
|
|
for i in range(tiles_nb):
|
|
if progress_callback != None:
|
|
progress_callback(i + 1, tiles_nb)
|
|
pred_tiles[i] = ort_session.run(
|
|
None, {"input": tiles[i : i + 1].astype(np.float32)}
|
|
)[0]
|
|
|
|
return pred_tiles
|
|
|
|
|
|
def generate_mask(tile_size, stride_size):
|
|
"""Generates a pyramidal-like mask. Used for mixing overlapping predicted tiles."""
|
|
|
|
tile_h, tile_w = tile_size
|
|
stride_h, stride_w = stride_size
|
|
ramp_h = tile_h - stride_h
|
|
ramp_w = tile_w - stride_w
|
|
|
|
mask = np.ones((tile_h, tile_w))
|
|
|
|
# ramps in width direction
|
|
mask[ramp_h:-ramp_h, :ramp_w] = np.linspace(0, 1, num=ramp_w)
|
|
mask[ramp_h:-ramp_h, -ramp_w:] = np.linspace(1, 0, num=ramp_w)
|
|
# ramps in height direction
|
|
mask[:ramp_h, ramp_w:-ramp_w] = np.transpose(
|
|
np.linspace(0, 1, num=ramp_h)[None], (1, 0)
|
|
)
|
|
mask[-ramp_h:, ramp_w:-ramp_w] = np.transpose(
|
|
np.linspace(1, 0, num=ramp_h)[None], (1, 0)
|
|
)
|
|
|
|
# Assume tiles are squared
|
|
assert ramp_h == ramp_w
|
|
# top left corner
|
|
corner = np.rot90(corner_mask(ramp_h), 2)
|
|
mask[:ramp_h, :ramp_w] = corner
|
|
# top right corner
|
|
corner = np.flip(corner, 1)
|
|
mask[:ramp_h, -ramp_w:] = corner
|
|
# bottom right corner
|
|
corner = np.flip(corner, 0)
|
|
mask[-ramp_h:, -ramp_w:] = corner
|
|
# bottom right corner
|
|
corner = np.flip(corner, 1)
|
|
mask[-ramp_h:, :ramp_w] = corner
|
|
|
|
return mask
|
|
|
|
|
|
def corner_mask(side_length):
|
|
"""Generates the corner part of the pyramidal-like mask.
|
|
Currently, only for square shapes."""
|
|
|
|
corner = np.zeros([side_length, side_length])
|
|
|
|
for h in range(0, side_length):
|
|
for w in range(0, side_length):
|
|
if h >= w:
|
|
sh = h / (side_length - 1)
|
|
corner[h, w] = 1 - sh
|
|
if h <= w:
|
|
sw = w / (side_length - 1)
|
|
corner[h, w] = 1 - sw
|
|
|
|
return corner - 0.25 * scaling_mask(side_length)
|
|
|
|
|
|
def scaling_mask(side_length):
|
|
scaling = np.zeros([side_length, side_length])
|
|
|
|
for h in range(0, side_length):
|
|
for w in range(0, side_length):
|
|
sh = h / (side_length - 1)
|
|
sw = w / (side_length - 1)
|
|
if h >= w and h <= side_length - w:
|
|
scaling[h, w] = sw
|
|
if h <= w and h <= side_length - w:
|
|
scaling[h, w] = sh
|
|
if h >= w and h >= side_length - w:
|
|
scaling[h, w] = 1 - sh
|
|
if h <= w and h >= side_length - w:
|
|
scaling[h, w] = 1 - sw
|
|
|
|
return 2 * scaling
|
|
|
|
|
|
def tiles_merge(tiles, stride_size, img_size, paddings):
|
|
"""Merges the list of tiles into one image. img_size is the original size, before
|
|
padding."""
|
|
|
|
_, tile_h, tile_w = tiles[0].shape
|
|
pad_left, pad_right, pad_top, pad_bottom = paddings
|
|
height = img_size[1] + pad_top + pad_bottom
|
|
width = img_size[2] + pad_left + pad_right
|
|
stride_h, stride_w = stride_size
|
|
|
|
# stride must be even
|
|
assert (stride_h % 2 == 0) and (stride_w % 2 == 0)
|
|
# stride must be greater or equal than half tile
|
|
assert (stride_h >= tile_h / 2) and (stride_w >= tile_w / 2)
|
|
# stride must be smaller or equal tile size
|
|
assert (stride_h <= tile_h) and (stride_w <= tile_w)
|
|
|
|
merged = np.zeros((img_size[0], height, width))
|
|
mask = generate_mask((tile_h, tile_w), stride_size)
|
|
|
|
h_range = ((height - tile_h) // stride_h) + 1
|
|
w_range = ((width - tile_w) // stride_w) + 1
|
|
|
|
idx = 0
|
|
for h in range(0, h_range):
|
|
for w in range(0, w_range):
|
|
h_from, h_to = h * stride_h, h * stride_h + tile_h
|
|
w_from, w_to = w * stride_w, w * stride_w + tile_w
|
|
merged[:, h_from:h_to, w_from:w_to] += tiles[idx] * mask
|
|
idx += 1
|
|
|
|
return merged[:, pad_top:-pad_bottom, pad_left:-pad_right]
|
|
|
|
|
|
def tiles_split(img, tile_size, stride_size):
|
|
"""Returns list of tiles from the given image and the padding used to fit the tiles
|
|
in it. Input image must have dimension C,H,W."""
|
|
log.debug(f"Splitting img: tile {tile_size}, stride {stride_size} ")
|
|
tile_h, tile_w = tile_size
|
|
stride_h, stride_w = stride_size
|
|
img_h, img_w = img.shape[0], img.shape[1]
|
|
|
|
# stride must be even
|
|
assert (stride_h % 2 == 0) and (stride_w % 2 == 0)
|
|
# stride must be greater or equal than half tile
|
|
assert (stride_h >= tile_h / 2) and (stride_w >= tile_w / 2)
|
|
# stride must be smaller or equal tile size
|
|
assert (stride_h <= tile_h) and (stride_w <= tile_w)
|
|
|
|
# find total height & width padding sizes
|
|
pad_h, pad_w = 0, 0
|
|
remainer_h = (img_h - tile_h) % stride_h
|
|
remainer_w = (img_w - tile_w) % stride_w
|
|
if remainer_h != 0:
|
|
pad_h = stride_h - remainer_h
|
|
if remainer_w != 0:
|
|
pad_w = stride_w - remainer_w
|
|
|
|
# if tile bigger than image, pad image to tile size
|
|
if tile_h > img_h:
|
|
pad_h = tile_h - img_h
|
|
if tile_w > img_w:
|
|
pad_w = tile_w - img_w
|
|
|
|
# pad image, add extra stride to padding to avoid pyramid
|
|
# weighting leaking onto the valid part of the picture
|
|
pad_left = pad_w // 2 + stride_w
|
|
pad_right = pad_left if pad_w % 2 == 0 else pad_left + 1
|
|
pad_top = pad_h // 2 + stride_h
|
|
pad_bottom = pad_top if pad_h % 2 == 0 else pad_top + 1
|
|
img = pad(img, pad_left, pad_right, pad_top, pad_bottom)
|
|
img_h, img_w = img.shape[1], img.shape[2]
|
|
|
|
# extract tiles
|
|
h_range = ((img_h - tile_h) // stride_h) + 1
|
|
w_range = ((img_w - tile_w) // stride_w) + 1
|
|
tiles = np.empty([h_range * w_range, img.shape[0], tile_h, tile_w])
|
|
idx = 0
|
|
for h in range(0, h_range):
|
|
for w in range(0, w_range):
|
|
h_from, h_to = h * stride_h, h * stride_h + tile_h
|
|
w_from, w_to = w * stride_w, w * stride_w + tile_w
|
|
tiles[idx] = img[:, h_from:h_to, w_from:w_to]
|
|
idx += 1
|
|
|
|
return tiles, (pad_left, pad_right, pad_top, pad_bottom)
|
|
|
|
|
|
# endregion
|
|
|
|
|
|
# region MODEL Utilities
|
|
def download_antelopev2():
|
|
antelopev2_url = "https://drive.google.com/uc?id=18wEUfMNohBJ4K3Ly5wpTejPfDzp-8fI8"
|
|
|
|
try:
|
|
import gdown
|
|
|
|
log.debug("Loading antelopev2 model")
|
|
|
|
dest = get_model_path("insightface")
|
|
archive = dest / "antelopev2.zip"
|
|
final_path = dest / "models" / "antelopev2"
|
|
if not final_path.exists():
|
|
log.info(f"antelopev2 not found, downloading to {dest}")
|
|
gdown.download(
|
|
antelopev2_url,
|
|
archive.as_posix(),
|
|
resume=True,
|
|
)
|
|
|
|
log.info(f"Unzipping antelopev2 to {final_path}")
|
|
|
|
if archive.exists():
|
|
# we unzip it
|
|
import zipfile
|
|
|
|
with zipfile.ZipFile(archive.as_posix(), "r") as zip_ref:
|
|
zip_ref.extractall(final_path.parent.as_posix())
|
|
|
|
except Exception as e:
|
|
log.error(
|
|
f"Could not load or download antelopev2 model, download it manually from {antelopev2_url}"
|
|
)
|
|
raise e
|
|
|
|
|
|
def get_model_path(fam, model=None):
|
|
log.debug(f"Requesting {fam} with model {model}")
|
|
res = None
|
|
if model:
|
|
res = folder_paths.get_full_path(fam, model)
|
|
else:
|
|
# this one can raise errors...
|
|
with contextlib.suppress(KeyError):
|
|
res = folder_paths.get_folder_paths(fam)
|
|
|
|
if res:
|
|
if isinstance(res, list):
|
|
if len(res) > 1:
|
|
log.warning(
|
|
f"Found multiple match, we will pick the first {res[0]}\n{res}"
|
|
)
|
|
res = res[0]
|
|
res = Path(res)
|
|
log.debug(f"Resolved model path from folder_paths: {res}")
|
|
else:
|
|
res = models_dir / fam
|
|
if model:
|
|
res /= model
|
|
|
|
return res
|
|
|
|
|
|
# endregion
|
|
|
|
|
|
# region UV Utilities
|
|
|
|
|
|
def create_uv_map_tensor(width=512, height=512):
|
|
u = torch.linspace(0.0, 1.0, steps=width)
|
|
v = torch.linspace(0.0, 1.0, steps=height)
|
|
|
|
U, V = torch.meshgrid(u, v)
|
|
|
|
uv_map = torch.zeros(height, width, 3, dtype=torch.float32)
|
|
uv_map[:, :, 0] = U.t()
|
|
uv_map[:, :, 1] = V.t()
|
|
|
|
return uv_map.unsqueeze(0)
|
|
|
|
|
|
# endregion
|
|
|
|
|
|
# region ANIMATION Utilities
|
|
def apply_easing(value, easing_type):
|
|
if easing_type == "Linear":
|
|
return value
|
|
|
|
# Back easing functions
|
|
def easeInBack(t):
|
|
s = 1.70158
|
|
return t * t * ((s + 1) * t - s)
|
|
|
|
def easeOutBack(t):
|
|
s = 1.70158
|
|
return ((t - 1) * t * ((s + 1) * t + s)) + 1
|
|
|
|
def easeInOutBack(t):
|
|
s = 1.70158 * 1.525
|
|
if t < 0.5:
|
|
return (t * t * (t * (s + 1) - s)) * 2
|
|
return ((t - 2) * t * ((s + 1) * t + s) + 2) * 2
|
|
|
|
# Elastic easing functions
|
|
def easeInElastic(t):
|
|
if t == 0:
|
|
return 0
|
|
if t == 1:
|
|
return 1
|
|
p = 0.3
|
|
s = p / 4
|
|
return -(math.pow(2, 10 * (t - 1)) * math.sin((t - 1 - s) * (2 * math.pi) / p))
|
|
|
|
def easeOutElastic(t):
|
|
if t == 0:
|
|
return 0
|
|
if t == 1:
|
|
return 1
|
|
p = 0.3
|
|
s = p / 4
|
|
return math.pow(2, -10 * t) * math.sin((t - s) * (2 * math.pi) / p) + 1
|
|
|
|
def easeInOutElastic(t):
|
|
if t == 0:
|
|
return 0
|
|
if t == 1:
|
|
return 1
|
|
p = 0.3 * 1.5
|
|
s = p / 4
|
|
t = t * 2
|
|
if t < 1:
|
|
return -0.5 * (
|
|
math.pow(2, 10 * (t - 1)) * math.sin((t - 1 - s) * (2 * math.pi) / p)
|
|
)
|
|
return (
|
|
0.5 * math.pow(2, -10 * (t - 1)) * math.sin((t - 1 - s) * (2 * math.pi) / p)
|
|
+ 1
|
|
)
|
|
|
|
# Bounce easing functions
|
|
def easeInBounce(t):
|
|
return 1 - easeOutBounce(1 - t)
|
|
|
|
def easeOutBounce(t):
|
|
if t < (1 / 2.75):
|
|
return 7.5625 * t * t
|
|
elif t < (2 / 2.75):
|
|
t -= 1.5 / 2.75
|
|
return 7.5625 * t * t + 0.75
|
|
elif t < (2.5 / 2.75):
|
|
t -= 2.25 / 2.75
|
|
return 7.5625 * t * t + 0.9375
|
|
else:
|
|
t -= 2.625 / 2.75
|
|
return 7.5625 * t * t + 0.984375
|
|
|
|
def easeInOutBounce(t):
|
|
if t < 0.5:
|
|
return easeInBounce(t * 2) * 0.5
|
|
return easeOutBounce(t * 2 - 1) * 0.5 + 0.5
|
|
|
|
# Quart easing functions
|
|
def easeInQuart(t):
|
|
return t * t * t * t
|
|
|
|
def easeOutQuart(t):
|
|
t -= 1
|
|
return -(t**2 * t * t - 1)
|
|
|
|
def easeInOutQuart(t):
|
|
t *= 2
|
|
if t < 1:
|
|
return 0.5 * t * t * t * t
|
|
t -= 2
|
|
return -0.5 * (t**2 * t * t - 2)
|
|
|
|
# Cubic easing functions
|
|
def easeInCubic(t):
|
|
return t * t * t
|
|
|
|
def easeOutCubic(t):
|
|
t -= 1
|
|
return t**2 * t + 1
|
|
|
|
def easeInOutCubic(t):
|
|
t *= 2
|
|
if t < 1:
|
|
return 0.5 * t * t * t
|
|
t -= 2
|
|
return 0.5 * (t**2 * t + 2)
|
|
|
|
# Circ easing functions
|
|
def easeInCirc(t):
|
|
return -(math.sqrt(1 - t * t) - 1)
|
|
|
|
def easeOutCirc(t):
|
|
t -= 1
|
|
return math.sqrt(1 - t**2)
|
|
|
|
def easeInOutCirc(t):
|
|
t *= 2
|
|
if t < 1:
|
|
return -0.5 * (math.sqrt(1 - t**2) - 1)
|
|
t -= 2
|
|
return 0.5 * (math.sqrt(1 - t**2) + 1)
|
|
|
|
# Sine easing functions
|
|
def easeInSine(t):
|
|
return -math.cos(t * (math.pi / 2)) + 1
|
|
|
|
def easeOutSine(t):
|
|
return math.sin(t * (math.pi / 2))
|
|
|
|
def easeInOutSine(t):
|
|
return -0.5 * (math.cos(math.pi * t) - 1)
|
|
|
|
easing_functions = {
|
|
"Sine In": easeInSine,
|
|
"Sine Out": easeOutSine,
|
|
"Sine In/Out": easeInOutSine,
|
|
"Quart In": easeInQuart,
|
|
"Quart Out": easeOutQuart,
|
|
"Quart In/Out": easeInOutQuart,
|
|
"Cubic In": easeInCubic,
|
|
"Cubic Out": easeOutCubic,
|
|
"Cubic In/Out": easeInOutCubic,
|
|
"Circ In": easeInCirc,
|
|
"Circ Out": easeOutCirc,
|
|
"Circ In/Out": easeInOutCirc,
|
|
"Back In": easeInBack,
|
|
"Back Out": easeOutBack,
|
|
"Back In/Out": easeInOutBack,
|
|
"Elastic In": easeInElastic,
|
|
"Elastic Out": easeOutElastic,
|
|
"Elastic In/Out": easeInOutElastic,
|
|
"Bounce In": easeInBounce,
|
|
"Bounce Out": easeOutBounce,
|
|
"Bounce In/Out": easeInOutBounce,
|
|
}
|
|
|
|
function_ease = easing_functions.get(easing_type)
|
|
if function_ease:
|
|
return function_ease(value)
|
|
|
|
log.error(f"Unknown easing type: {easing_type}")
|
|
log.error(f"Available easing types: {list(easing_functions.keys())}")
|
|
raise ValueError(f"Unknown easing type: {easing_type}")
|
|
|
|
|
|
# endregion
|