Rework stage line animations
This commit is contained in:
parent
bcb2369046
commit
c4cdda64e1
5 changed files with 2020 additions and 1069 deletions
|
|
@ -37,8 +37,15 @@ class CounterFpsSender():
|
|||
self.is_finished = threading.Event()
|
||||
|
||||
def tick(self):
|
||||
"""
|
||||
returns dt since previous tock
|
||||
"""
|
||||
self.iterations += 1
|
||||
self.snapshot()
|
||||
if len(self.tocs) > 1:
|
||||
return float(self.tocs[-1][0] - self.tocs[-2][0])
|
||||
else:
|
||||
return 0.
|
||||
|
||||
def snapshot(self):
|
||||
self.tocs.append((time.perf_counter(), self.iterations))
|
||||
|
|
|
|||
683
trap/lines.py
683
trap/lines.py
|
|
@ -1,20 +1,33 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, IntEnum
|
||||
import math
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple
|
||||
import time
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import numpy as np
|
||||
|
||||
import shapely
|
||||
import shapely.ops
|
||||
from simplification.cutil import simplify_coords_idx, simplify_coords_vw_idx
|
||||
import svgpathtools
|
||||
|
||||
|
||||
from noise import snoise2
|
||||
|
||||
from trap.utils import exponentialDecayRounded, inv_lerp
|
||||
|
||||
|
||||
"""
|
||||
See [notebook](../test_path_transforms.ipynb) for examples
|
||||
"""
|
||||
|
||||
RenderablePosition = Tuple[float,float]
|
||||
Coordinate = Tuple[float, float]
|
||||
DeltaT = float # delta_t in seconds
|
||||
|
||||
class CoordinateSpace(IntEnum):
|
||||
CAMERA = 1
|
||||
|
|
@ -35,6 +48,10 @@ class SrgbaColor():
|
|||
def as_faded(self, alpha: float) -> SrgbaColor:
|
||||
return SrgbaColor(self.red, self.green, self.blue, self.alpha * alpha)
|
||||
|
||||
def __eq__(self, other):
|
||||
return math.isclose(self.red, other.red) and math.isclose(self.green, other.green) and math.isclose(self.blue, other.blue) and math.isclose(self.alpha, other.alpha)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RenderablePoint():
|
||||
position: RenderablePosition
|
||||
|
|
@ -63,6 +80,9 @@ class SimplifyMethod(Enum):
|
|||
class RenderableLine():
|
||||
points: List[RenderablePoint]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.points)
|
||||
|
||||
def as_simplified(self, method: SimplifyMethod = SimplifyMethod.RDP, factor = SIMPLIFY_FACTOR_RDP):
|
||||
linestring = [p.position for p in self.points]
|
||||
if method == SimplifyMethod.RDP:
|
||||
|
|
@ -72,6 +92,32 @@ class RenderableLine():
|
|||
points = [self.points[i] for i in indexes]
|
||||
return RenderableLine(points)
|
||||
|
||||
def as_linestring(self):
|
||||
return shapely.geometry.LineString([p.position for p in self.points])
|
||||
|
||||
@classmethod
|
||||
def from_multilinestring(cls, mls: shapely.geometry.MultiLineString, color: SrgbaColor) -> RenderableLine:
|
||||
points: List[RenderablePoint] = []
|
||||
for i, line in enumerate(mls.geoms):
|
||||
if len(line.coords) < 2:
|
||||
continue
|
||||
|
||||
is_first = i == 0
|
||||
is_last = i == (len(mls.geoms) - 1)
|
||||
|
||||
# blank point
|
||||
if not is_first:
|
||||
points.append(RenderablePoint(line.coords[0], color.as_faded(0)))
|
||||
|
||||
points.extend(
|
||||
[RenderablePoint(pos, color) for pos in line.coords]
|
||||
)
|
||||
|
||||
# blank point
|
||||
if not is_last:
|
||||
points.append(RenderablePoint(line.coords[-1], color.as_faded(0)))
|
||||
return RenderableLine(points)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RenderableLines():
|
||||
|
|
@ -137,7 +183,7 @@ def cross_points(cx, cy, r, c: SrgbaColor):
|
|||
return [path, path2]
|
||||
|
||||
|
||||
def load_lines_from_svg(svg_path: Path, scale: float, c: SrgbaColor) -> List[RenderableLine]:
|
||||
def load_lines_from_svg(svg_path: Path, scale: float, c: SrgbaColor, max_len = 0.3) -> List[RenderableLine]:
|
||||
|
||||
lines = []
|
||||
paths, attributes = svgpathtools.svg2paths(svg_path)
|
||||
|
|
@ -181,12 +227,641 @@ def load_lines_from_svg(svg_path: Path, scale: float, c: SrgbaColor) -> List[Ren
|
|||
# Create LineString from coordinates
|
||||
if len(coordinates) > 1:
|
||||
coordinates = (np.array(coordinates) / scale).tolist()
|
||||
points = [RenderablePoint(pos, c) for pos in coordinates]
|
||||
|
||||
# cut into smaller segments, so the laser corrections apply nicely
|
||||
linestring = shapely.geometry.LineString(coordinates)
|
||||
linestring = shapely.segmentize(linestring, max_len)
|
||||
|
||||
|
||||
points = [RenderablePoint(pos, c) for pos in linestring.coords]
|
||||
line = RenderableLine(points)
|
||||
lines.append(line)
|
||||
# linestring = shapely.geometry.LineString(coordinates)
|
||||
# linestrings.append(linestring)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing path: {e}")
|
||||
return lines
|
||||
|
||||
|
||||
|
||||
class LineGenerator(ABC):
|
||||
@abstractmethod
|
||||
def update_drawn_positions(self, dt: DeltaT):
|
||||
pass
|
||||
|
||||
def extend(self, *args):
|
||||
self.points.extend(args)
|
||||
|
||||
|
||||
def as_renderable(self, dt: DeltaT, color: SrgbaColor) -> RenderableLines:
|
||||
points = [RenderablePoint(p, color) for p in self.get_drawn_points(dt)]
|
||||
lines = [RenderableLine(points)]
|
||||
return RenderableLines(lines)
|
||||
|
||||
class StaticLine(LineGenerator):
|
||||
def __init__(self, points: Optional[List[Coordinate]] = None):
|
||||
self.target_points: List[Coordinate] = points if points is not None else []
|
||||
self._drawn_points = points
|
||||
|
||||
def get_drawn_points(self, dt):
|
||||
return self._drawn_points
|
||||
|
||||
|
||||
def update_drawn_positions(self, dt):
|
||||
# nothing to update
|
||||
pass
|
||||
|
||||
class AppendableLine(LineGenerator):
|
||||
"""
|
||||
A line generator that allows for points to be added over time.
|
||||
Simply use `line.points.extend([p1, p2])` and it will animate in
|
||||
"""
|
||||
def __init__(self, points: Optional[List[Coordinate]] = None, draw_decay_speed = 25.):
|
||||
self.target_points: List[Coordinate] = points if points is not None else [] # when providing [] as default, it messes up instancing by reusing the same list
|
||||
self.drawn_points = []
|
||||
self.ready = len(self.target_points) == 0
|
||||
self.draw_decay_speed = draw_decay_speed
|
||||
|
||||
def nr_of_passed_points(self):
|
||||
"""The number of points passed in the animation"""
|
||||
return len(self.drawn_points) - 1
|
||||
|
||||
def update_drawn_positions(self, dt: DeltaT):
|
||||
if len(self.target_points) == 0:
|
||||
# nothing to draw yet
|
||||
return
|
||||
|
||||
# self._drawn_points = self.points
|
||||
|
||||
if len(self.drawn_points) == 0:
|
||||
# create origin
|
||||
self.drawn_points.append(self.target_points[0])
|
||||
# and drawing head
|
||||
self.drawn_points.append(self.target_points[0])
|
||||
|
||||
idx = len(self.drawn_points) - 1
|
||||
target = self.target_points[idx]
|
||||
|
||||
if np.isclose(self.drawn_points[-1], target, atol=.05).all():
|
||||
# TODO: might want to migrate to np.isclose()
|
||||
if len(self.drawn_points) == len(self.target_points):
|
||||
self.ready = True
|
||||
return # done until a new point is added
|
||||
|
||||
# add new point as drawing head
|
||||
self.drawn_points.append(self.drawn_points[-1])
|
||||
self.ready = False
|
||||
|
||||
x = exponentialDecayRounded(self.drawn_points[-1][0], target[0], self.draw_decay_speed, dt, .05)
|
||||
y = exponentialDecayRounded(self.drawn_points[-1][1], target[1], self.draw_decay_speed, dt, .05)
|
||||
self.drawn_points[-1] = (float(x), float(y))
|
||||
|
||||
class ProceduralChain(LineGenerator):
|
||||
"""A line that can be 'dragged' to a target. In which
|
||||
it disappears."""
|
||||
MOVE_DECAY_SPEED = 80 # speed at which the drawing head should approach the next point
|
||||
VELOCITY_DAMPING = 10
|
||||
VELOCITY_FACTOR = 2
|
||||
link_size = .1 # 10cm
|
||||
# angle_constraint = 5
|
||||
|
||||
def __init__(self, joints: List[Coordinate], use_velocity = False):
|
||||
self.joints: List[Coordinate] = joints
|
||||
self.target: Coordinate = joints[-1]
|
||||
self.ready = False
|
||||
self.move_decay_speed = self.MOVE_DECAY_SPEED
|
||||
|
||||
self.use_velocity = use_velocity
|
||||
if self.use_velocity:
|
||||
if len(self.joints) > 1:
|
||||
self.v = np.array(self.joints[-2]) - np.array(self.joints[-1])
|
||||
self.v /= np.linalg.norm(self.v) / 10
|
||||
else:
|
||||
self.v = np.array([0,0])
|
||||
|
||||
@classmethod
|
||||
def from_appendable_line(cls, al: AppendableLine) -> ProceduralChain:
|
||||
# TODO: create more segments:
|
||||
# last added points becomes the head of the chain
|
||||
points = list(reversed(al.target_points))
|
||||
linestring = shapely.LineString(points)
|
||||
linestring = linestring.segmentize(cls.link_size)
|
||||
joints = list(linestring.coords)
|
||||
|
||||
return cls(joints)
|
||||
|
||||
def update_drawn_positions(self, dt: DeltaT):
|
||||
if self.ready:
|
||||
return
|
||||
# direction = np.array(self.joints[-1] - self.target)
|
||||
|
||||
# TODO: check self.joints empty, and stop then
|
||||
if self.use_velocity:
|
||||
vx = exponentialDecayRounded(self.v[0], self.target[0] - self.joints[0][0], self.VELOCITY_DAMPING, dt, .05)
|
||||
vy = exponentialDecayRounded(self.v[1], self.target[1] - self.joints[0][1], self.VELOCITY_DAMPING, dt, .05)
|
||||
self.v = np.array([vx, vy])
|
||||
self.joints[0] = (float(self.joints[0][0] + self.v[0] * dt * self.VELOCITY_FACTOR), float(self.joints[0][1] + self.v[1] * dt * self.VELOCITY_FACTOR))
|
||||
else:
|
||||
x = exponentialDecayRounded(self.joints[0][0], self.target[0], self.move_decay_speed, dt, .05)
|
||||
y = exponentialDecayRounded(self.joints[0][1], self.target[1], self.move_decay_speed, dt, .05)
|
||||
self.joints[0] = (float(x), float(y))
|
||||
|
||||
# Loop inspired by: https://github.com/argonautcode/animal-proc-anim/blob/main/Chain.pde
|
||||
# see that code for angle constrains.
|
||||
for i, (joint, prev_joint) in enumerate(zip(self.joints[1:], self.joints), start=1):
|
||||
diff = np.array(prev_joint) - np.array(joint)
|
||||
direction = diff / np.linalg.norm(diff)
|
||||
self.joints[i] = prev_joint - direction * self.link_size
|
||||
|
||||
if np.isclose(self.joints[0], self.target, atol=.05).all():
|
||||
# self.ready = True
|
||||
# TODO: smooth transition instead of cutting off
|
||||
self.joints.pop(0)
|
||||
if len(self.joints) == 0:
|
||||
self.ready = True
|
||||
|
||||
self._drawn_points = self.joints
|
||||
|
||||
# class AnimatedLineMask():
|
||||
# """
|
||||
# given a line, create an animation by masking/skewing it
|
||||
# """
|
||||
# def __init__(self, line: LineGenerator):
|
||||
# self.line = line
|
||||
# self.start_at = time.time()
|
||||
|
||||
# @abstractmethod
|
||||
# def apply(self, dt: DeltaT, color: SrgbaColor) -> RenderableLines:
|
||||
# pass
|
||||
|
||||
# class FadeoutMask(AnimatedLineMask):
|
||||
# def __init__(self, line: LineGenerator, max_len = 200, fade_len = 20):
|
||||
# self.line = LineGenerator
|
||||
|
||||
# def apply(self, lines: RenderableLines, factor):
|
||||
# for line in lines.lines:
|
||||
# for point in line.points:
|
||||
# point.color = point.color.as_faded(factor)
|
||||
|
||||
|
||||
# class FadeoutTailMask(AnimatedLineMask):
|
||||
# def __init__(self, line: LineGenerator, max_len = 200, fade_len = 20):
|
||||
# self.line = LineGenerator
|
||||
|
||||
class StaticLine():
|
||||
"""
|
||||
Always a continuous line of monotonous color
|
||||
"""
|
||||
def __init__(self, points = None, color: SrgbaColor = None):
|
||||
self.points: List[Coordinate] = points if points is not None else [] # when providing [] as default, it messes up instancing by reusing the same list
|
||||
self.color = color if color else SrgbaColor(0,0,0,1)
|
||||
|
||||
def get_positions(self):
|
||||
return self.points
|
||||
|
||||
def __len__(self):
|
||||
return len(self.points)
|
||||
|
||||
def extend(self, coords: List[Coordinate]):
|
||||
self.points.extend(coords)
|
||||
|
||||
def as_renderable_line(self, dt: DeltaT) -> RenderableLine:
|
||||
points = [RenderablePoint(p, self.color) for p in self.points]
|
||||
line = RenderableLine(points)
|
||||
return line
|
||||
|
||||
def is_ready(self):
|
||||
return True
|
||||
|
||||
def start(self):
|
||||
return True
|
||||
|
||||
|
||||
|
||||
class LineAnimator(StaticLine):
|
||||
"""
|
||||
Animate a line, can/should be nested. Always derives from a StaticLine
|
||||
"""
|
||||
def __init__(self, target_line: Optional[StaticLine] = None):
|
||||
# print(self, target_line, bool(target_line), target_line is not None)
|
||||
self.target = target_line if target_line is not None else StaticLine()
|
||||
self.ready = len(self.target) == 0
|
||||
self.start_t = time.time()
|
||||
self.skip = False
|
||||
|
||||
def extend(self, coords):
|
||||
return self.target.extend(coords)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.target)
|
||||
|
||||
def as_renderable_line(self, dt: DeltaT) -> RenderableLine:
|
||||
target = self.target.as_renderable_line(dt)
|
||||
if self.skip:
|
||||
return target
|
||||
|
||||
return self.apply(target, dt)
|
||||
|
||||
def apply(self, target_line: RenderableLine, dt: DeltaT) -> RenderableLine:
|
||||
raise RuntimeError("Not Implemented")
|
||||
|
||||
def is_ready(self):
|
||||
return self.ready and self.target.is_ready()
|
||||
|
||||
def start(self):
|
||||
self.target.start()
|
||||
|
||||
self.start_t = time.time()
|
||||
return True
|
||||
|
||||
def running_for(self):
|
||||
if self.start:
|
||||
return time.time() - self.start_t
|
||||
return 0.
|
||||
|
||||
|
||||
|
||||
class AppendableLineAnimator(LineAnimator):
|
||||
"""
|
||||
Animate a line, can/should be nested. Always derives from a StaticLine
|
||||
"""
|
||||
def __init__(self, target_line = None, draw_decay_speed: float = 25., draw_decay_speed_init = 1000, transition_in_on_init = True):
|
||||
super().__init__(target_line)
|
||||
self.drawn_points: List[RenderablePoint] = []
|
||||
self.draw_decay_speed = draw_decay_speed
|
||||
self.draw_decay_speed_init = draw_decay_speed_init # faster drawing until catching up
|
||||
self.transition_in_on_init = transition_in_on_init # when False, immediately draw the full line
|
||||
self.is_init = False
|
||||
|
||||
|
||||
def apply(self, target_line, dt: DeltaT) -> RenderableLine:
|
||||
if len(target_line) == 0:
|
||||
# nothing to draw yet
|
||||
return RenderableLine()
|
||||
|
||||
|
||||
|
||||
if len(self.drawn_points) == 0:
|
||||
if self.transition_in_on_init:
|
||||
# create origin
|
||||
self.drawn_points.append(target_line.points[0])
|
||||
else:
|
||||
self.drawn_points.extend(target_line.points[:-1])
|
||||
# and a copy as drawing head
|
||||
self.drawn_points.append(copy.deepcopy(self.drawn_points[-1]))
|
||||
|
||||
idx = len(self.drawn_points) - 1
|
||||
target = target_line.points[idx]
|
||||
|
||||
if np.isclose(self.drawn_points[-1].position, target.position, atol=.05).all():
|
||||
# TODO: might want to migrate to np.isclose()
|
||||
if len(self.drawn_points) == len(target_line):
|
||||
self.ready = True
|
||||
self.is_init = True
|
||||
return RenderableLine(self.drawn_points) # done until a new point is added
|
||||
|
||||
# add new point as drawing head, by duplicating the current point
|
||||
self.drawn_points.append(copy.deepcopy(self.drawn_points[-1]) )
|
||||
self.drawn_points[-1].color = target.color # set to color of the next point
|
||||
self.ready = False
|
||||
|
||||
decay_speed = self.draw_decay_speed if self.is_init else self.draw_decay_speed_init
|
||||
x = exponentialDecayRounded(self.drawn_points[-1].position[0], target.position[0], decay_speed, dt, .05)
|
||||
y = exponentialDecayRounded(self.drawn_points[-1].position[1], target.position[1], decay_speed, dt, .05)
|
||||
|
||||
# handle color gradient:
|
||||
# if self.drawn_points[-1].color != target.color:
|
||||
# # t
|
||||
# tx = inv_lerp(self.drawn_points[-2].position[0], target.position[0], x)
|
||||
# ty = inv_lerp(self.drawn_points[-2].position[1], target.position[1], y)
|
||||
# t = (tx+ty) / 2
|
||||
# TODO: this should set color to intermediate color, but this is hardly used, so skip it for sake of speed
|
||||
|
||||
self.drawn_points[-1].position = (float(x), float(y))
|
||||
|
||||
|
||||
|
||||
return RenderableLine(self.drawn_points)
|
||||
|
||||
|
||||
class FadeOutLine(LineAnimator):
|
||||
"""
|
||||
Fade the line providing an alpha, 1 by default
|
||||
"""
|
||||
def __init__(self, target_line = None):
|
||||
super().__init__(target_line)
|
||||
self.alpha = 1
|
||||
self.ready = True # filter holds no state, so always ready
|
||||
|
||||
def set_alpha(self, alpha: float):
|
||||
self.alpha = min(1, max(0, alpha))
|
||||
|
||||
|
||||
def apply(self, target_line: RenderableLine, dt: DeltaT) -> RenderableLine:
|
||||
|
||||
for point in target_line.points:
|
||||
point.color = point.color.as_faded(self.alpha)
|
||||
|
||||
return target_line
|
||||
|
||||
|
||||
class CropLine(LineAnimator):
|
||||
"""
|
||||
Crop the line at a max nr of points (thus not actual lenght!)
|
||||
Keeps the tail, removes the start
|
||||
"""
|
||||
def __init__(self, target_line = None, max_points = 200):
|
||||
super().__init__(target_line)
|
||||
self.max_points = max_points
|
||||
self.ready = True # static filter, always ready
|
||||
|
||||
def apply(self, target_line: RenderableLine, dt: DeltaT) -> RenderableLine:
|
||||
target_line.points = target_line.points[-1 * self.max_points:]
|
||||
return target_line
|
||||
|
||||
class FadedTailLine(LineAnimator):
|
||||
"""
|
||||
Fade the tail of the line, proving a max length
|
||||
"""
|
||||
def __init__(self, target_line = None, fade_after: int = 170, fade_frames: int = 30):
|
||||
super().__init__(target_line)
|
||||
self.fade_after = fade_after
|
||||
self.fade_frames = fade_frames
|
||||
self.frame_offset = 0
|
||||
|
||||
def set_frame_offset(self, frame_offset: int):
|
||||
"""
|
||||
This can be used to provide an additional offset between fade_frames and the lenght of
|
||||
the actual track. Can e.g. be used to process the diff between last tracking time and
|
||||
actual wall time (in case a track is lost, it keeps getting 'eaten')
|
||||
"""
|
||||
self.frame_offset = frame_offset
|
||||
|
||||
|
||||
def apply(self, target_line: RenderableLine, dt: DeltaT) -> RenderableLine:
|
||||
l = len(target_line.points)
|
||||
points = []
|
||||
for i, point in enumerate(target_line.points):
|
||||
reverse_i = l - i
|
||||
fade_i = reverse_i - self.fade_after + self.frame_offset # -90
|
||||
fade_i /= self.fade_frames # / 10
|
||||
# fade_i = np.clip(fade_i, 0, 1)
|
||||
alpha = 1 - fade_i
|
||||
if alpha >= 0:
|
||||
alpha = min(1, alpha)
|
||||
point.color = point.color.as_faded(alpha)
|
||||
points.append(point)
|
||||
|
||||
self.ready = not bool(len(points))
|
||||
|
||||
return RenderableLine(points)
|
||||
|
||||
|
||||
class NoiseLine(LineAnimator):
|
||||
"""
|
||||
Apply animated noise to line normals
|
||||
"""
|
||||
def __init__(self, target_line = None, amplitude=.3, frequency=.02, t_factor = 1.0):
|
||||
super().__init__(target_line)
|
||||
self.amplitude = amplitude
|
||||
self.frequency = frequency
|
||||
self.t_factor = t_factor
|
||||
self.t = 0
|
||||
self.ready = True # static filter, always ready
|
||||
|
||||
# Gemma3:27b prompt: "python. Given a list of coordinates, that describes a line: `drawable_points: List[Tuple[float,float]]` apply perlin noise over the normal of the line, that changes over time `dt`."
|
||||
@classmethod
|
||||
def apply_perlin_noise_to_line_normal(cls, drawable_points: np.ndarray, t: float, amplitude: float = 1.0, frequency: float = 1.0, fade_over_n_points = 8) -> List[RenderablePosition]:
|
||||
"""
|
||||
Applies Perlin noise to the normals of a line described by a list of coordinates, changing over time.
|
||||
|
||||
Args:
|
||||
drawable_points: A list of (x, y) tuples representing the points of the line.
|
||||
dt: The time delta, used to animate the Perlin noise.
|
||||
amplitude: The strength of the Perlin noise effect.
|
||||
frequency: The frequency of the Perlin noise (how many waves per unit).
|
||||
|
||||
Returns:
|
||||
A new list of (x, y) tuples representing the line with Perlin noise applied to the normals. If drawable_points
|
||||
has fewer than 2 points, it returns the original list unchanged.
|
||||
|
||||
Raises:
|
||||
TypeError: If drawable_points is not a list or dt is not a float.
|
||||
ValueError: If the input points are not tuples of length 2.
|
||||
"""
|
||||
|
||||
if len(drawable_points) < 2:
|
||||
return drawable_points # Nothing to do with fewer than 2 points
|
||||
|
||||
# for point in drawable_points:
|
||||
# if not isinstance(point, tuple) or len(point) != 2:
|
||||
# raise ValueError("Each point in drawable_points must be a tuple of length 2.")
|
||||
|
||||
|
||||
# noise = PerlinNoise(octaves=4) # You can adjust octaves for different noise patterns
|
||||
|
||||
new_points = []
|
||||
for i in range(len(drawable_points)):
|
||||
x, y = copy.deepcopy(drawable_points[i])
|
||||
|
||||
# Calculate the normal vector. We'll approximate it using the previous and next points.
|
||||
if i == 0:
|
||||
# For the first point, use the next point to estimate the normal
|
||||
next_x, next_y = drawable_points[i + 1]
|
||||
normal_x = next_y - y
|
||||
normal_y = -(next_x - x)
|
||||
elif i == len(drawable_points) - 1:
|
||||
# For the last point, use the previous point
|
||||
prev_x, prev_y = drawable_points[i - 1]
|
||||
normal_x = y - prev_y
|
||||
normal_y = -(x - prev_x)
|
||||
else:
|
||||
prev_x, prev_y = drawable_points[i - 1]
|
||||
next_x, next_y = drawable_points[i + 1]
|
||||
normal_x = next_y - prev_y
|
||||
normal_y = -(next_x - prev_x)
|
||||
|
||||
# Normalize the normal vector
|
||||
norm = np.sqrt(normal_x**2 + normal_y**2)
|
||||
if norm > 0:
|
||||
normal_x /= norm
|
||||
normal_y /= norm
|
||||
|
||||
# Apply Perlin noise to the normal
|
||||
# noise_x = noise([x * frequency, (y + dt) * frequency]) * amplitude * normal_x
|
||||
# noise_y = noise([x * frequency, (y + dt) * frequency]) * amplitude * normal_y
|
||||
noise = snoise2(i * frequency, t % 1000, octaves=4)
|
||||
|
||||
use_amp = amplitude
|
||||
if fade_over_n_points > 0:
|
||||
rev_step = len(drawable_points) - i
|
||||
amp_factor = rev_step / fade_over_n_points
|
||||
if amp_factor < 1:
|
||||
use_amp *= amp_factor
|
||||
|
||||
noise_x = noise * use_amp * normal_x
|
||||
noise_y = noise * use_amp * normal_y
|
||||
|
||||
# print(noise_x, noise_y, dt, frequency, i, dt, snoise2(i * frequency, dt % 1000, octaves=4))
|
||||
|
||||
|
||||
# Add the noise to the point's coordinates
|
||||
new_x = x + noise_x
|
||||
new_y = y + noise_y
|
||||
|
||||
new_points.append((new_x, new_y))
|
||||
|
||||
|
||||
return new_points
|
||||
|
||||
def apply(self, target_line: RenderableLine, dt: DeltaT) -> RenderableLine:
|
||||
|
||||
self.t += dt
|
||||
|
||||
if self.amplitude < 0.01:
|
||||
return target_line
|
||||
|
||||
positions = np.array([p.position for p in target_line.points])
|
||||
new_positions = self.apply_perlin_noise_to_line_normal(
|
||||
positions,
|
||||
self.t * self.t_factor,
|
||||
self.amplitude,
|
||||
self.frequency
|
||||
)
|
||||
|
||||
points = []
|
||||
for point, pos in zip(target_line.points, new_positions):
|
||||
p = copy.deepcopy(point)
|
||||
p.position = pos
|
||||
points.append(p)
|
||||
|
||||
|
||||
return RenderableLine(points)
|
||||
|
||||
class SegmentLine(LineAnimator):
|
||||
def __init__(self, target_line = None):
|
||||
super().__init__(target_line)
|
||||
|
||||
def apply(self, target_line: RenderableLine, dt: DeltaT):
|
||||
if len(target_line) < 2:
|
||||
return target_line
|
||||
i = self.running_for()
|
||||
ls = target_line.as_linestring()
|
||||
|
||||
return super().apply(target_line, dt)
|
||||
|
||||
class DashedLine(LineAnimator):
|
||||
"""
|
||||
Dashed line
|
||||
"""
|
||||
def __init__(self, target_line = None, dash_len: float = 1., gap_len: float = .5, t_factor: float = 1., loop_offset: bool = False):
|
||||
super().__init__(target_line)
|
||||
self.dash_len = dash_len
|
||||
self.gap_len = gap_len
|
||||
self.loop_offset = loop_offset
|
||||
self.t_factor = t_factor
|
||||
|
||||
# def set_offset_t(self, dt: DeltaT):
|
||||
# self.offset_t = dt
|
||||
|
||||
|
||||
@classmethod
|
||||
def dashed_line(cls, line: shapely.geometry.LineString, dash_len: float, gap_len: float, offset: float = 0, loop_offset = True) -> shapely.geometry.MultiLineString:
|
||||
total_length = line.length
|
||||
|
||||
# index_helper = LineStringIncrementingDistanceOffset(line)
|
||||
|
||||
segments = []
|
||||
|
||||
if loop_offset:
|
||||
# by default, prepend skipped gap
|
||||
pos = offset % (dash_len + gap_len)
|
||||
|
||||
if pos > gap_len:
|
||||
# TODO: use index_helper.get_index_and_offset to lerp the colors of all points on substring
|
||||
segments.append(shapely.ops.substring(line, 0, pos - gap_len))
|
||||
else:
|
||||
pos = offset
|
||||
|
||||
while pos < total_length:
|
||||
end = min(pos + dash_len, total_length)
|
||||
if pos < end:
|
||||
# TODO: use index_helper.get_index_and_offset to lerp the colors of all points on substring
|
||||
dash = shapely.ops.substring(line, pos, end)
|
||||
segments.append(dash)
|
||||
pos += dash_len + gap_len
|
||||
|
||||
# TODO: return all color together with the points
|
||||
return shapely.geometry.MultiLineString(segments)
|
||||
|
||||
|
||||
def apply(self, target_line: RenderableLine, dt: DeltaT) -> RenderableLine:
|
||||
"""
|
||||
warning, dashing (for now) removes all color
|
||||
"""
|
||||
|
||||
if len(target_line) < 2:
|
||||
self.ready = True
|
||||
return target_line
|
||||
|
||||
ls = target_line.as_linestring()
|
||||
multilinestring = self.dashed_line(ls, self.dash_len, self.gap_len, self.t_factor * self.running_for(), self.loop_offset)
|
||||
|
||||
|
||||
self.ready = not bool(len(multilinestring.geoms))
|
||||
|
||||
color = target_line.points[0].color
|
||||
|
||||
return RenderableLine.from_multilinestring(multilinestring, color)
|
||||
|
||||
|
||||
IndexAndOffset = Tuple[int, float]
|
||||
|
||||
class LineStringIncrementingDistanceOffset():
|
||||
"""
|
||||
Helper for linestrings: find the index + lerp-offset, ASAP provided that
|
||||
distance to look for is always increasing
|
||||
"""
|
||||
def __init__(self, ls: shapely.geometry.LineString):
|
||||
self.ls = ls
|
||||
self.gen = self.index_distances(self.ls)
|
||||
self.current_range = next(self.gen)
|
||||
self.pos = 0
|
||||
|
||||
def get_index_and_offset(self, length: float) -> IndexAndOffset:
|
||||
if length > self.pos:
|
||||
raise RuntimeError("Cannot reverse")
|
||||
self.pos = length
|
||||
|
||||
while True:
|
||||
index_range = self.current_range[0]
|
||||
start_distance = self.current_range[1][0]
|
||||
end_distance = self.current_range[1][1]
|
||||
|
||||
if length < end_distance:
|
||||
# fill here
|
||||
|
||||
segment_distance = length - start_distance
|
||||
segment_length = end_distance - start_distance
|
||||
|
||||
lerp = segment_distance / segment_length
|
||||
|
||||
return index_range[0], lerp
|
||||
else:
|
||||
try:
|
||||
self.current_range = next(self.gen)
|
||||
except StopIteration as e:
|
||||
# when exhausting the list return the last index
|
||||
return len(self.ls.coords) - 1, 0.
|
||||
|
||||
@classmethod
|
||||
def index_distances(cls, line: shapely.geometry.LineString):
|
||||
"""
|
||||
Build a list of segments with the distance range they cover
|
||||
"""
|
||||
cumulative_length = 0
|
||||
for i in range(len(line.coords) - 1):
|
||||
segment_length = line.coords[i+1].distance(line.coords[i])
|
||||
start_at = cumulative_length
|
||||
cumulative_length += float(segment_length)
|
||||
yield (i, i+1), (start_at, cumulative_length)
|
||||
|
|
@ -22,6 +22,8 @@ class Node():
|
|||
|
||||
self._prev_loop_time = 0
|
||||
|
||||
self.dt_since_last_tick = 0
|
||||
|
||||
self.setup()
|
||||
|
||||
@classmethod
|
||||
|
|
@ -29,7 +31,7 @@ class Node():
|
|||
return logging.getLogger(f"trap.{cls.__name__}")
|
||||
|
||||
def tick(self):
|
||||
self.fps_counter.tick()
|
||||
self.dt_since_last_tick = self.fps_counter.tick()
|
||||
# with self.fps_counter.get_lock():
|
||||
# self.fps_counter.value+=1
|
||||
|
||||
|
|
|
|||
1356
trap/stage.py
1356
trap/stage.py
File diff suppressed because it is too large
Load diff
1009
trap/stage_old.py
Normal file
1009
trap/stage_old.py
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue