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