accel + alternate mode

This commit is contained in:
siraxe
2025-10-13 14:50:32 +01:00
parent a9c53ca21f
commit e124dd3811
10 changed files with 156 additions and 208 deletions
-9
View File
@@ -1,9 +0,0 @@
{
"permissions": {
"allow": [
"Bash(python:*)"
],
"deny": [],
"ask": []
}
}
+2 -1
View File
@@ -5,4 +5,5 @@ nodes/__pycache__/
.env2
.env 22
wanvideo_cached_samples.pt
ref_image.jpg
ref_image.jpg
QWEN.md
-170
View File
@@ -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`)
+1 -1
View File
@@ -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
View File
@@ -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
)
+10 -1
View File
@@ -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
View File
@@ -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;
}
+5 -3
View File
@@ -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 -2
View File
@@ -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;