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()
|
self.is_finished = threading.Event()
|
||||||
|
|
||||||
def tick(self):
|
def tick(self):
|
||||||
|
"""
|
||||||
|
returns dt since previous tock
|
||||||
|
"""
|
||||||
self.iterations += 1
|
self.iterations += 1
|
||||||
self.snapshot()
|
self.snapshot()
|
||||||
|
if len(self.tocs) > 1:
|
||||||
|
return float(self.tocs[-1][0] - self.tocs[-2][0])
|
||||||
|
else:
|
||||||
|
return 0.
|
||||||
|
|
||||||
def snapshot(self):
|
def snapshot(self):
|
||||||
self.tocs.append((time.perf_counter(), self.iterations))
|
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 __future__ import annotations
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
import copy
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum, IntEnum
|
from enum import Enum, IntEnum
|
||||||
import math
|
import math
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Tuple
|
import time
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
import shapely
|
||||||
|
import shapely.ops
|
||||||
from simplification.cutil import simplify_coords_idx, simplify_coords_vw_idx
|
from simplification.cutil import simplify_coords_idx, simplify_coords_vw_idx
|
||||||
import svgpathtools
|
import svgpathtools
|
||||||
|
|
||||||
|
|
||||||
|
from noise import snoise2
|
||||||
|
|
||||||
|
from trap.utils import exponentialDecayRounded, inv_lerp
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
See [notebook](../test_path_transforms.ipynb) for examples
|
See [notebook](../test_path_transforms.ipynb) for examples
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RenderablePosition = Tuple[float,float]
|
RenderablePosition = Tuple[float,float]
|
||||||
|
Coordinate = Tuple[float, float]
|
||||||
|
DeltaT = float # delta_t in seconds
|
||||||
|
|
||||||
class CoordinateSpace(IntEnum):
|
class CoordinateSpace(IntEnum):
|
||||||
CAMERA = 1
|
CAMERA = 1
|
||||||
|
|
@ -35,6 +48,10 @@ class SrgbaColor():
|
||||||
def as_faded(self, alpha: float) -> SrgbaColor:
|
def as_faded(self, alpha: float) -> SrgbaColor:
|
||||||
return SrgbaColor(self.red, self.green, self.blue, self.alpha * alpha)
|
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
|
@dataclass
|
||||||
class RenderablePoint():
|
class RenderablePoint():
|
||||||
position: RenderablePosition
|
position: RenderablePosition
|
||||||
|
|
@ -63,6 +80,9 @@ class SimplifyMethod(Enum):
|
||||||
class RenderableLine():
|
class RenderableLine():
|
||||||
points: List[RenderablePoint]
|
points: List[RenderablePoint]
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.points)
|
||||||
|
|
||||||
def as_simplified(self, method: SimplifyMethod = SimplifyMethod.RDP, factor = SIMPLIFY_FACTOR_RDP):
|
def as_simplified(self, method: SimplifyMethod = SimplifyMethod.RDP, factor = SIMPLIFY_FACTOR_RDP):
|
||||||
linestring = [p.position for p in self.points]
|
linestring = [p.position for p in self.points]
|
||||||
if method == SimplifyMethod.RDP:
|
if method == SimplifyMethod.RDP:
|
||||||
|
|
@ -72,6 +92,32 @@ class RenderableLine():
|
||||||
points = [self.points[i] for i in indexes]
|
points = [self.points[i] for i in indexes]
|
||||||
return RenderableLine(points)
|
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
|
@dataclass
|
||||||
class RenderableLines():
|
class RenderableLines():
|
||||||
|
|
@ -137,7 +183,7 @@ def cross_points(cx, cy, r, c: SrgbaColor):
|
||||||
return [path, path2]
|
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 = []
|
lines = []
|
||||||
paths, attributes = svgpathtools.svg2paths(svg_path)
|
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
|
# Create LineString from coordinates
|
||||||
if len(coordinates) > 1:
|
if len(coordinates) > 1:
|
||||||
coordinates = (np.array(coordinates) / scale).tolist()
|
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)
|
line = RenderableLine(points)
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
# linestring = shapely.geometry.LineString(coordinates)
|
|
||||||
# linestrings.append(linestring)
|
# linestrings.append(linestring)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error processing path: {e}")
|
print(f"Error processing path: {e}")
|
||||||
return lines
|
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._prev_loop_time = 0
|
||||||
|
|
||||||
|
self.dt_since_last_tick = 0
|
||||||
|
|
||||||
self.setup()
|
self.setup()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -29,7 +31,7 @@ class Node():
|
||||||
return logging.getLogger(f"trap.{cls.__name__}")
|
return logging.getLogger(f"trap.{cls.__name__}")
|
||||||
|
|
||||||
def tick(self):
|
def tick(self):
|
||||||
self.fps_counter.tick()
|
self.dt_since_last_tick = self.fps_counter.tick()
|
||||||
# with self.fps_counter.get_lock():
|
# with self.fps_counter.get_lock():
|
||||||
# self.fps_counter.value+=1
|
# 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