accel + alternate mode
This commit is contained in:
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(python:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -5,4 +5,5 @@ nodes/__pycache__/
|
||||
.env2
|
||||
.env 22
|
||||
wanvideo_cached_samples.pt
|
||||
ref_image.jpg
|
||||
ref_image.jpg
|
||||
QWEN.md
|
||||
@@ -1,170 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Overview
|
||||
|
||||
ComfyUI-WanVideoWrapper_QQ is a custom node collection for ComfyUI that provides utilities and nodes for WanVideo video generation workflows. It includes video manipulation, LoRA loading, path animation, image encoding, and visual drawing tools.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Node Registration System
|
||||
|
||||
The node system uses **dynamic class discovery** via `__init__.py`:
|
||||
- Classes are imported from `/nodes/*.py` modules
|
||||
- Any class with `INPUT_TYPES`, `FUNCTION`, and `CATEGORY` attributes is automatically registered
|
||||
- Display names are auto-generated from class names (e.g., `WanVideoSpeed` → "Wan Video Speed (QQ)")
|
||||
- Classes containing "Math" in their name are explicitly excluded (line 41 in `__init__.py:41`)
|
||||
- Manual override via `DISPLAY_NAME` class attribute if custom naming is needed
|
||||
|
||||
The registration creates two dictionaries:
|
||||
- `NODE_CLASS_MAPPINGS`: Maps node class names to Python classes
|
||||
- `NODE_DISPLAY_NAME_MAPPINGS`: Maps node class names to human-readable display names
|
||||
|
||||
### Frontend Integration
|
||||
|
||||
The repository includes browser-based UI components in `/web/`:
|
||||
- **Power LoRA Loader**: Custom widget system (`/web/power_lora_loader_js/`) with rgthree-style UI
|
||||
- **Spline Editors**: Canvas-based path drawing (`/web/spline_editor/`, `/web/power_spline_editor/`)
|
||||
- **Static Assets**: External libraries in `/kjweb_async/` (served via aiohttp route at `/kjweb_async`)
|
||||
|
||||
The `__init__.py` sets up an aiohttp static route to serve `/kjweb_async` content to the web interface.
|
||||
|
||||
### Data Flow Patterns
|
||||
|
||||
**1. Video Tensor Format**
|
||||
- ComfyUI uses BHWC format: (Batch, Height, Width, Channels)
|
||||
- Internal processing often converts to CHW or NTCHW for PyTorch operations
|
||||
- Always verify tensor dimensions when working with video nodes
|
||||
|
||||
**2. Coordinate System**
|
||||
- Path coordinates are stored in JSON format with metadata: `{"coordinates": [...], "start_p_frames": N, "end_p_frames": M}`
|
||||
- Two coordinate types:
|
||||
- `coordinates`: Animated paths (interpolated over frames)
|
||||
- `p_coordinates`: Static points (rendered on all frames, optionally driven by driver path)
|
||||
- Supports scaling via `coord_width`/`coord_height` metadata when canvas size differs from output size
|
||||
|
||||
**3. LoRA Loading Architecture**
|
||||
- `WanVideoPowerLoraLoader` (nodes/power_lora.py) uses `FlexibleOptionalInputType` for unlimited dynamic inputs
|
||||
- Returns `WANVIDLORA` type containing list of LoRA configurations with paths, strengths, and block selections
|
||||
- Supports "High/Low" LoRA pair detection: automatically identifies matching low-res variants (e.g., `foo-High.safetensors` → `foo-Low.safetensors`)
|
||||
- JavaScript widget manages UI state; Python backend processes the serialized widget values
|
||||
- Properties like `low_mem_load` and `merge_loras` are passed via `OptionsWidget` in the workflow JSON
|
||||
|
||||
## Key Node Categories
|
||||
|
||||
### Video Manipulation Nodes (nodes/nodes.py)
|
||||
|
||||
- **WanFrames**: Adjusts frame counts (divisible by 4)
|
||||
- **WanVideoMerge**: Blends 2-3 videos with customizable transition frames
|
||||
- **WanVideoSpeed**: Adjusts playback speed by resampling frames
|
||||
- **WanVideoExtractFrame**: Extracts frames from video tensors
|
||||
- **WanReplaceFirstFrame**: Replaces first frame with provided image
|
||||
|
||||
### Image-to-Video Encoding (nodes/image_to_video.py)
|
||||
|
||||
Contains the TAEHV (Tiny AutoEncoder for Hunyuan Video) implementation:
|
||||
- **Memory Management**: Supports parallel (fast, high memory) and sequential (slow, low memory) processing modes
|
||||
- **MemBlocks**: Temporal memory blocks that reference previous frames during encoding/decoding
|
||||
- **TPool/TGrow**: Temporal pooling and growth layers for frame compression/expansion
|
||||
- **WanVideoImageToVideoEncode_v2**: Main encoding node with support for start/mid/end image conditioning
|
||||
|
||||
Key features:
|
||||
- Temporal feathering for smooth frame transitions (cosine interpolation)
|
||||
- Configurable latent strength multipliers for start/mid/end frames
|
||||
- Support for dual end frames (one at custom position, one at final frame)
|
||||
- Noise augmentation for improved motion in I2V tasks
|
||||
|
||||
### Path Animation & Drawing (nodes/draw_shapes.py)
|
||||
|
||||
- **PathFrameConfig**: Defines easing functions, frame counts, and timing for animations
|
||||
- **DrawShapeOnPath**: Renders shapes (circle/square/triangle) along coordinate paths
|
||||
- Supports multiple paths with per-path pause frames
|
||||
- Interpolation modes: linear, cardinal (Catmull-Rom), basis (B-spline), points (no interpolation)
|
||||
- Driver offset system: Apply movement from one path to transform another path
|
||||
- Handles "highlighted" control points for hard corners in spline interpolation
|
||||
|
||||
### Spline Editor Nodes (nodes/spline_editor.py, nodes/power_spline_editor.py)
|
||||
|
||||
- SplineEditor2: Single-path editor with driver offset, rotation, and smoothing
|
||||
- PowerSplineEditor: Multi-layer path editor with per-layer controls
|
||||
- Both output coordinate metadata in JSON format for downstream nodes
|
||||
- Frontend canvas editors in `/web/` directories
|
||||
|
||||
### Utility Functions (utility/)
|
||||
|
||||
- `utility.py`: Tensor/PIL/numpy conversion utilities (pil2tensor, tensor2pil, tensor2np, np2tensor)
|
||||
- `draw_utils.py`: Path interpolation, easing functions, and drawing helpers
|
||||
- `driver_utils.py`: Driver path offset calculations
|
||||
- `numerical.py`, `magictex.py`, `fluid.py`: Mathematical/visual effects utilities
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Adding a New Node
|
||||
|
||||
1. Create a new class in an appropriate file under `/nodes/` or create a new file
|
||||
2. Define required class attributes:
|
||||
```python
|
||||
class MyNewNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {...}, "optional": {...}}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("output",)
|
||||
FUNCTION = "process"
|
||||
CATEGORY = "WanVideoWrapper_QQ"
|
||||
DESCRIPTION = "..."
|
||||
|
||||
def process(self, ...):
|
||||
return (result,)
|
||||
```
|
||||
3. The node will auto-register on ComfyUI reload (no manual registration needed)
|
||||
4. Add to NODE_CLASS_MAPPINGS/NODE_DISPLAY_NAME_MAPPINGS at end of file if creating a new module
|
||||
|
||||
### Modifying Video Processing
|
||||
|
||||
- Always clone tensors before modification to avoid in-place operations: `output_video = video.clone()`
|
||||
- Use `torch.clamp(tensor, 0.0, 1.0)` to ensure valid pixel values
|
||||
- For frame-by-frame processing, consider using `concurrent.futures.ThreadPoolExecutor` (see draw_shapes.py)
|
||||
- Ensure tensor devices match before operations: `tensor.to(device=target.device, dtype=target.dtype)`
|
||||
|
||||
### Working with Coordinates
|
||||
|
||||
- Coordinate JSON format: `{"coordinates": [[{x, y}, ...]], "start_p_frames": {...}, "end_p_frames": {...}}`
|
||||
- Pause frames can be:
|
||||
- Single value: applied to all paths
|
||||
- List: per-path values
|
||||
- Dict with "p" and "c" keys: separate for p_coordinates and coordinates
|
||||
- Interpolation helpers in `draw_utils.py`:
|
||||
- `interpolate_points()`: Spline interpolation (cardinal/basis)
|
||||
- `InterpMath.interpolate_or_downsample_path()`: Resampling to target frame count with easing
|
||||
|
||||
### Frontend Widget Development
|
||||
|
||||
- Custom widgets extend `RgthreeBaseWidget` (see power_lora_loader.js)
|
||||
- Widgets must implement:
|
||||
- `draw(ctx, node, width, posY, height)`: Render the widget
|
||||
- `serializeValue(node, index)`: Save widget state to workflow JSON
|
||||
- `mouse(event, pos, node)`: Handle mouse interactions
|
||||
- Use hit areas for clickable regions: `this.hitAreas.myButton = {bounds: [x, width], onClick: this.onButtonClick}`
|
||||
- Widget state is stored in `widget.value` and must be serializable to JSON
|
||||
|
||||
### Testing Workflow
|
||||
|
||||
1. Load ComfyUI with this custom node installed
|
||||
2. Press `R` to refresh node definitions after code changes
|
||||
3. Test with example workflows (if available in repository)
|
||||
4. For LoRA nodes: Ensure LoRA files are in ComfyUI's `models/loras/` directory
|
||||
5. For video nodes: Test with batch IMAGE tensors (BHWC format)
|
||||
|
||||
## Important Notes
|
||||
|
||||
- This is a **ComfyUI custom node package**, not a standalone application
|
||||
- Must be installed in `ComfyUI/custom_nodes/` directory
|
||||
- Requires ComfyUI's server and frontend to function
|
||||
- Python nodes run in ComfyUI's backend process
|
||||
- JavaScript files are loaded by ComfyUI's frontend
|
||||
- The "(QQ)" suffix in node names distinguishes these nodes from similar nodes by other authors
|
||||
- VAE operations expect specific stride values: `VAE_STRIDE = (4, 8, 8)` for temporal/spatial compression
|
||||
- Image-to-video encoding uses 4-frame temporal grouping (num_frames must satisfy `(n-1) % 4 == 0`)
|
||||
@@ -110,7 +110,7 @@ https://github.com/user-attachments/assets/5689c666-924a-4362-9788-d8f662c0d925
|
||||
- (EXPERIMENTAL) Maybe there's native ways to do this , but it seems to work
|
||||
- **Multi-frame I2V**: Start/mid/end image conditioning with position control
|
||||
- **Temporal feathering**: Cosine interpolation for smooth frame transitions
|
||||
- **Latent strength**: Per-frame multipliers (start/mid/end) for motion control
|
||||
- **Latent strength**: Per-frame multipliers (start/mid/end) for motion control (default setting work but can be adjusted)
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
+10
-2
@@ -435,7 +435,7 @@ Locations are center locations. Allows coordinates outside the frame for 'fly-in
|
||||
coordinates_data = json.dumps(parsed["coordinates"])
|
||||
if "p_coordinates" in parsed:
|
||||
p_coordinates_data = json.dumps(parsed["p_coordinates"])
|
||||
for k in ("start_p_frames", "end_p_frames", "offsets", "interpolations", "easing_functions", "easing_paths", "easing_strengths", "scales", "drivers", "p_coordinates_use_driver", "p_driver_path", "p_driver_smooth", "coord_width", "coord_height"):
|
||||
for k in ("start_p_frames", "end_p_frames", "offsets", "interpolations", "easing_functions", "easing_paths", "easing_strengths", "accelerations", "scales", "drivers", "p_coordinates_use_driver", "p_driver_path", "p_driver_smooth", "coord_width", "coord_height"):
|
||||
if k in parsed:
|
||||
metadata[k] = parsed[k]
|
||||
else:
|
||||
@@ -693,7 +693,7 @@ Locations are center locations. Allows coordinates outside the frame for 'fly-in
|
||||
start_p_frames_meta, end_p_frames_meta,
|
||||
offsets_meta, interpolations_meta, drivers_meta,
|
||||
easing_functions_meta, easing_paths_meta, easing_strengths_meta,
|
||||
scales_meta,
|
||||
scales_meta, accelerations_meta=None,
|
||||
coord_width: Optional[float] = None, coord_height: Optional[float] = None,
|
||||
frame_width: int = 512, frame_height: int = 512) -> Tuple[List[Path], List[Tuple[int, int]], List[Optional[Dict[str, Any]]], List[float]]:
|
||||
"""
|
||||
@@ -714,6 +714,7 @@ Locations are center locations. Allows coordinates outside the frame for 'fly-in
|
||||
easing_functions_list = self._normalize_easing_lists(num_paths, easing_functions_meta, "easing_function")
|
||||
easing_paths_list = self._normalize_easing_lists(num_paths, easing_paths_meta, "easing_path")
|
||||
easing_strengths_list = self._normalize_easing_lists(num_paths, easing_strengths_meta, "easing_strength")
|
||||
accelerations_list = self._normalize_easing_lists(num_paths, accelerations_meta, 0.00)
|
||||
scales_list = self._normalize_easing_lists(num_paths, scales_meta, 1.0)
|
||||
|
||||
processed_coords_list: List[Path] = []
|
||||
@@ -769,6 +770,11 @@ Locations are center locations. Allows coordinates outside the frame for 'fly-in
|
||||
processed_path = draw_utils.InterpMath.interpolate_or_downsample_path(
|
||||
interpolated_path, path_animation_frames, path_easing_function, effective_easing_path, bounce_between=0.0, easing_strength=path_easing_strength, interpolation=path_interpolation
|
||||
)
|
||||
|
||||
# Apply acceleration remapping if acceleration is not zero
|
||||
path_acceleration = float(accelerations_list[i]) if i < len(accelerations_list) else 0.00
|
||||
if abs(path_acceleration) > 0.001: # Only apply if acceleration is not close to zero
|
||||
processed_path = draw_utils.InterpMath.apply_acceleration_remapping(processed_path, path_acceleration)
|
||||
|
||||
# Prepare per-path driver interpolation (for per-frame offsets)
|
||||
driver_info_for_frame = None
|
||||
@@ -1007,6 +1013,7 @@ Locations are center locations. Allows coordinates outside the frame for 'fly-in
|
||||
easing_functions_meta = meta.get("easing_functions", "in_out")
|
||||
easing_paths_meta = meta.get("easing_paths", "full")
|
||||
easing_strengths_meta = meta.get("easing_strengths", 1.0)
|
||||
accelerations_meta = meta.get("accelerations", 0.00)
|
||||
|
||||
processed_coords_list, path_pause_frames, coords_driver_info_list, scales_list = self._build_interpolated_paths(
|
||||
coords_list_raw, total_frames,
|
||||
@@ -1017,6 +1024,7 @@ Locations are center locations. Allows coordinates outside the frame for 'fly-in
|
||||
easing_paths_meta,
|
||||
easing_strengths_meta,
|
||||
meta.get("scales", 1.0),
|
||||
accelerations_meta,
|
||||
coord_width, coord_height, frame_width, frame_height
|
||||
)
|
||||
|
||||
|
||||
@@ -401,6 +401,8 @@ class PowerSplineEditor:
|
||||
all_coord_easing_paths = [] # Initialize list for coordinates easing paths
|
||||
all_p_easing_strengths = [] # Initialize list for p_coordinates easing strengths
|
||||
all_coord_easing_strengths = [] # Initialize list for coordinates easing strengths
|
||||
all_p_accelerations = [] # Initialize list for p_coordinates accelerations
|
||||
all_coord_accelerations = [] # Initialize list for coordinates accelerations
|
||||
all_p_scales = [] # Initialize list for p_coordinates scales
|
||||
all_coord_scales = [] # Initialize list for coordinates scales
|
||||
all_p_drivers = [] # Driver info for p_coordinates
|
||||
@@ -456,9 +458,10 @@ class PowerSplineEditor:
|
||||
|
||||
# Get easing parameters
|
||||
easing_function = spline_data.get('easing', 'in_out') # Get easing function using simple name
|
||||
easing_config = spline_data.get('easingConfig', {'path': 'full', 'strength': 1.0}) # Get easing config
|
||||
easing_config = spline_data.get('easingConfig', {'path': 'full', 'strength': 1.0, 'acceleration': 0.00}) # Get easing config
|
||||
easing_path = easing_config.get('path', 'full') # Get easing path ('each' or 'full')
|
||||
easing_strength = easing_config.get('strength', 1.0) # Get easing strength
|
||||
acceleration = easing_config.get('acceleration', 0.00) # Get acceleration value
|
||||
|
||||
# Get scale parameter
|
||||
scale = spline_data.get('scale', 1.00) # Get scale value
|
||||
@@ -553,6 +556,7 @@ class PowerSplineEditor:
|
||||
all_p_easing_functions.append(easing_function) # Collect easing function
|
||||
all_p_easing_paths.append(easing_path) # Collect easing path
|
||||
all_p_easing_strengths.append(easing_strength) # Collect easing strength
|
||||
all_p_accelerations.append(acceleration) # Collect acceleration value
|
||||
all_p_scales.append(scale) # Collect scale value
|
||||
all_p_drivers.append(driver_info_for_layer) # Collect driver info (None if no driver)
|
||||
else:
|
||||
@@ -564,6 +568,7 @@ class PowerSplineEditor:
|
||||
all_coord_easing_functions.append(easing_function) # Collect easing function
|
||||
all_coord_easing_paths.append(easing_path) # Collect easing path
|
||||
all_coord_easing_strengths.append(easing_strength) # Collect easing strength
|
||||
all_coord_accelerations.append(acceleration) # Collect acceleration value
|
||||
all_coord_scales.append(scale) # Collect scale value
|
||||
all_coord_drivers.append(driver_info_for_layer) # Collect driver info (None if no driver)
|
||||
|
||||
@@ -622,6 +627,7 @@ class PowerSplineEditor:
|
||||
coord_out_data["easing_functions"] = {"p": all_p_easing_functions, "c": all_coord_easing_functions}
|
||||
coord_out_data["easing_paths"] = {"p": all_p_easing_paths, "c": all_coord_easing_paths}
|
||||
coord_out_data["easing_strengths"] = {"p": all_p_easing_strengths, "c": all_coord_easing_strengths}
|
||||
coord_out_data["accelerations"] = {"p": all_p_accelerations, "c": all_coord_accelerations}
|
||||
coord_out_data["scales"] = {"p": all_p_scales, "c": all_coord_scales}
|
||||
coord_out_data["drivers"] = {"p": all_p_drivers, "c": all_coord_drivers}
|
||||
elif all_p_paths:
|
||||
@@ -633,6 +639,7 @@ class PowerSplineEditor:
|
||||
coord_out_data["easing_functions"] = {"p": all_p_easing_functions, "c": []}
|
||||
coord_out_data["easing_paths"] = {"p": all_p_easing_paths, "c": []}
|
||||
coord_out_data["easing_strengths"] = {"p": all_p_easing_strengths, "c": []}
|
||||
coord_out_data["accelerations"] = {"p": all_p_accelerations, "c": []}
|
||||
coord_out_data["scales"] = {"p": all_p_scales, "c": []}
|
||||
coord_out_data["drivers"] = {"p": all_p_drivers, "c": []}
|
||||
elif all_coord_paths:
|
||||
@@ -644,6 +651,7 @@ class PowerSplineEditor:
|
||||
coord_out_data["easing_functions"] = {"p": [], "c": all_coord_easing_functions}
|
||||
coord_out_data["easing_paths"] = {"p": [], "c": all_coord_easing_paths}
|
||||
coord_out_data["easing_strengths"] = {"p": [], "c": all_coord_easing_strengths}
|
||||
coord_out_data["accelerations"] = {"p": [], "c": all_coord_accelerations}
|
||||
coord_out_data["scales"] = {"p": [], "c": all_coord_scales}
|
||||
coord_out_data["drivers"] = {"p": [], "c": all_coord_drivers}
|
||||
else:
|
||||
@@ -655,6 +663,7 @@ class PowerSplineEditor:
|
||||
coord_out_data["easing_functions"] = [] # Default empty list for easing functions
|
||||
coord_out_data["easing_paths"] = [] # Default empty list for easing paths
|
||||
coord_out_data["easing_strengths"] = [] # Default empty list for easing strengths
|
||||
coord_out_data["accelerations"] = [] # Default empty list for accelerations
|
||||
coord_out_data["scales"] = [] # Default empty list for scales
|
||||
coord_out_data["drivers"] = [] # Default empty list
|
||||
print("Warning: No paths to output")
|
||||
|
||||
+126
-2
@@ -48,6 +48,16 @@ class InterpMath:
|
||||
}
|
||||
apply_easing = easing_map.get(method, lambda t, s=1.0: t)
|
||||
|
||||
# Create inverted easing functions for "alternate" mode
|
||||
inverted_easing_map = {
|
||||
"linear": lambda t, s=1.0: t,
|
||||
"in": lambda t, s=easing_strength: InterpMath._ease_out(t, s),
|
||||
"out": lambda t, s=easing_strength: InterpMath._ease_in(t, s),
|
||||
"in_out": lambda t, s=easing_strength: InterpMath._ease_out_in(t, s),
|
||||
"out_in": lambda t, s=easing_strength: InterpMath._ease_in_out(t, s)
|
||||
}
|
||||
apply_inverted_easing = inverted_easing_map.get(method, lambda t, s=1.0: t)
|
||||
|
||||
total_path_length = 0
|
||||
segment_lengths = []
|
||||
for i in range(n_points - 1):
|
||||
@@ -81,6 +91,17 @@ class InterpMath:
|
||||
elif easing_path == 'full':
|
||||
if interpolation != 'linear':
|
||||
control_point_indices = [i for i, p in enumerate(points) if p.get('highlighted')]
|
||||
elif easing_path == 'alternate':
|
||||
# For alternate mode, use the same segment boundaries as 'each' mode
|
||||
if interpolation == 'basis':
|
||||
control_point_indices = [i for i, p in enumerate(points) if p.get('highlighted')]
|
||||
# If no highlighted points for basis+each, we let control_point_indices be empty,
|
||||
# which causes a fallback to 'full' path easing later.
|
||||
else:
|
||||
control_point_indices = [i for i, p in enumerate(points) if p.get('is_control')]
|
||||
if not control_point_indices:
|
||||
# Fallback for non-basis interpolations
|
||||
control_point_indices = list(range(n_points))
|
||||
|
||||
if len(control_point_indices) > 1:
|
||||
if control_point_indices[0] != 0:
|
||||
@@ -119,7 +140,8 @@ class InterpMath:
|
||||
segment_frame_ranges.append({
|
||||
'segment': seg,
|
||||
'start_frame': frames_allocated,
|
||||
'frame_count': num_frames
|
||||
'frame_count': num_frames,
|
||||
'segment_idx': idx # Track the segment index for alternating easing
|
||||
})
|
||||
frames_allocated += num_frames
|
||||
|
||||
@@ -145,7 +167,16 @@ class InterpMath:
|
||||
else:
|
||||
t_segment_local = 0.0
|
||||
|
||||
t_segment_eased = apply_easing(t_segment_local, easing_strength)
|
||||
# Apply normal easing or inverted easing based on the segment index for 'alternate' mode
|
||||
if easing_path == 'alternate':
|
||||
# Alternate between normal and inverted easing based on segment index
|
||||
if target_segment_info['segment_idx'] % 2 == 0: # Even index (0, 2, 4, ...)
|
||||
t_segment_eased = apply_easing(t_segment_local, easing_strength)
|
||||
else: # Odd index (1, 3, 5, ...)
|
||||
t_segment_eased = apply_inverted_easing(t_segment_local, easing_strength)
|
||||
else:
|
||||
# For 'each' and 'full' modes, use the normal easing
|
||||
t_segment_eased = apply_easing(t_segment_local, easing_strength)
|
||||
|
||||
dist_before_segment = 0
|
||||
for seg in major_segments:
|
||||
@@ -185,6 +216,99 @@ class InterpMath:
|
||||
|
||||
return output_points
|
||||
|
||||
@staticmethod
|
||||
def apply_acceleration_remapping(points, acceleration):
|
||||
"""
|
||||
Apply acceleration-based timing remapping to a sequence of points.
|
||||
|
||||
For positive acceleration (> 0.00):
|
||||
- Movement is faster at the end by squishing end coordinates and stretching starting ones
|
||||
|
||||
For negative acceleration (< 0.00):
|
||||
- Movement is slower at the end by stretching end coordinates (and compressing starting ones)
|
||||
|
||||
For zero acceleration (0.00):
|
||||
- Skip the function (return original points)
|
||||
|
||||
Args:
|
||||
points: List of points with x, y coordinates
|
||||
acceleration: Acceleration value from -1.0 to 1.0
|
||||
|
||||
Returns:
|
||||
List of remapped points
|
||||
"""
|
||||
if abs(acceleration) < 0.001: # Close to zero, skip remapping
|
||||
return points
|
||||
|
||||
if len(points) <= 2:
|
||||
return points
|
||||
|
||||
n_points = len(points)
|
||||
output_points = []
|
||||
|
||||
for i in range(n_points):
|
||||
# Calculate normalized position along the path (0 to 1)
|
||||
t = i / (n_points - 1) if n_points > 1 else 0
|
||||
|
||||
# Apply acceleration remapping with clamped values to prevent math errors
|
||||
# Convert acceleration to proper easing behavior:
|
||||
# Positive acceleration should create ease-out (start fast, end slow)
|
||||
# Negative acceleration should create ease-in (start slow, end fast)
|
||||
clamped_acceleration = max(-0.99, min(0.99, acceleration))
|
||||
|
||||
if clamped_acceleration > 0:
|
||||
# For positive acceleration, create ease-out behavior (start fast, end slow)
|
||||
# Use exponent < 1 to make time progress faster at the start and slower at the end
|
||||
exponent = max(0.01, 1.0 - clamped_acceleration)
|
||||
else:
|
||||
# For negative acceleration, create ease-in behavior (start slow, end fast)
|
||||
# Use exponent > 1 to make time progress slower at the start and faster at the end
|
||||
# abs(negative_acceleration) is positive, so 1.0 + positive_value > 1
|
||||
exponent = 1.0 + abs(clamped_acceleration)
|
||||
|
||||
# Handle edge cases to avoid math errors
|
||||
if t == 0.0:
|
||||
remap_t = 0.0
|
||||
elif t == 1.0:
|
||||
remap_t = 1.0
|
||||
else:
|
||||
remap_t = pow(t, exponent)
|
||||
|
||||
# Ensure remap_t stays within bounds
|
||||
remap_t = max(0.0, min(1.0, remap_t))
|
||||
|
||||
# Map remap_t back to original point index
|
||||
original_index = remap_t * (n_points - 1)
|
||||
index_low = int(original_index)
|
||||
index_high = min(index_low + 1, n_points - 1)
|
||||
t_local = original_index - index_low
|
||||
|
||||
# Linear interpolation between the two nearest points
|
||||
if index_low == index_high:
|
||||
output_points.append(points[index_low].copy())
|
||||
else:
|
||||
p1 = points[index_low]
|
||||
p2 = points[index_high]
|
||||
|
||||
x = p1['x'] * (1.0 - t_local) + p2['x'] * t_local
|
||||
y = p1['y'] * (1.0 - t_local) + p2['y'] * t_local
|
||||
|
||||
new_point = {'x': x, 'y': y}
|
||||
|
||||
# Preserve any additional properties from the original points
|
||||
for key in p1:
|
||||
if key not in ['x', 'y']:
|
||||
new_point[key] = p1[key]
|
||||
|
||||
output_points.append(new_point)
|
||||
|
||||
# Ensure the first and last points match the original to maintain path integrity
|
||||
if output_points and len(output_points) > 1:
|
||||
output_points[0] = points[0].copy()
|
||||
output_points[-1] = points[-1].copy()
|
||||
|
||||
return output_points
|
||||
|
||||
def interpolate_points(points, interpolation, easing_path='full', steps_per_segment=3):
|
||||
if not points or len(points) < 2:
|
||||
return points
|
||||
|
||||
@@ -472,7 +472,6 @@ class OptionsWidget extends RgthreeBaseWidget {
|
||||
merge_loras: node.properties['merge_loras'] !== false ? true : false,
|
||||
overwrite_duplicates: node.properties['overwrite_duplicates'] || false
|
||||
};
|
||||
console.log(`[JS] OptionsWidget serializing: `, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -988,22 +987,6 @@ class PowerLoraLoaderWidget extends RgthreeBaseWidget {
|
||||
v.low_strength = 1;
|
||||
}
|
||||
|
||||
// Keep the original selected LoRA - let Python backend handle the outputs
|
||||
if (v.is_low && v.low_variant_name) {
|
||||
console.log(`[JS] High LoRA '${v.lora}' has low variant '${v.low_variant_name}' - sending both to backend`);
|
||||
console.log(`[JS] High strength: ${v.strength}, Low strength: ${v.low_strength}`);
|
||||
}
|
||||
|
||||
// Enhanced debug logging
|
||||
console.log(`[JS] serializeValue for ${this.name}:`, {
|
||||
final_lora: v.lora,
|
||||
final_strength: v.strength,
|
||||
original_low_strength: v.low_strength,
|
||||
is_low: v.is_low,
|
||||
low_variant_name: v.low_variant_name,
|
||||
full_object: v
|
||||
});
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
|
||||
@@ -507,11 +507,11 @@ export function showCustomEasingMenu(event, widget, position) {
|
||||
font-size: 11px !important;
|
||||
`;
|
||||
|
||||
// Create grid container (2 columns: Path | Strength)
|
||||
// Create grid container (3 columns: Path | Strength | Accel)
|
||||
const container = document.createElement('div');
|
||||
container.style.cssText = `
|
||||
display: grid !important;
|
||||
grid-template-columns: 1fr 1fr !important;
|
||||
grid-template-columns: 1fr 1fr 1fr !important;
|
||||
grid-template-rows: auto auto !important;
|
||||
gap: 2px !important;
|
||||
width: 100% !important;
|
||||
@@ -535,10 +535,12 @@ export function showCustomEasingMenu(event, widget, position) {
|
||||
// Row 1: Headers
|
||||
container.appendChild(createHeader('Path:'));
|
||||
container.appendChild(createHeader('Strength:'));
|
||||
container.appendChild(createHeader('Accel:'));
|
||||
|
||||
// Row 2: Controls
|
||||
container.appendChild(createDropdown(['each', 'full'], configObj.path, widget, 'path', updateEasingConfigValue));
|
||||
container.appendChild(createDropdown(['each', 'full', 'alternate'], configObj.path, widget, 'path', updateEasingConfigValue));
|
||||
container.appendChild(createEasingNumberInput(widget, configObj, 'strength', 0.0, 2.0, 0.1, 1));
|
||||
container.appendChild(createEasingNumberInput(widget, configObj, 'acceleration', -1.0, 1.0, 0.01, 2));
|
||||
|
||||
menu.appendChild(container);
|
||||
document.body.appendChild(menu);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { app } from '../../../scripts/app.js';
|
||||
|
||||
// == DEFAULTS ---
|
||||
const DRIVEN_DEFAULTS = { driver: "", rotate: 0, d_scale: 1.0 };
|
||||
const EASING_DEFAULTS = { path: "each", strength: 1.0 };
|
||||
const EASING_DEFAULTS = { path: "each", strength: 1.0, acceleration: 0.00 };
|
||||
const SCALE_DEFAULT = 1.00;
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ export function initializeEasingConfig(widgetValue, loadedValue = null) {
|
||||
// Ensure the easingConfig object exists and apply defaults over the loaded data.
|
||||
// Handle case where easingConfig might not exist at all (new layers)
|
||||
if (!widgetValue.easingConfig || typeof widgetValue.easingConfig !== 'object') {
|
||||
widgetValue.easingConfig = { path: "each", strength: 1.0 };
|
||||
widgetValue.easingConfig = { path: "each", strength: 1.0, acceleration: 0.00 };
|
||||
}
|
||||
|
||||
const loadedConfig = widgetValue.easingConfig;
|
||||
|
||||
Reference in New Issue
Block a user