initial
This commit is contained in:
+33
@@ -0,0 +1,33 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# IDE/Editor specific
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Build/Distribution
|
||||
dist/
|
||||
build/
|
||||
*.min.js
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Cache
|
||||
.cache/
|
||||
.temp/
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 codeprimate
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,48 @@
|
||||
# ComfyUI Mask Contour Processor
|
||||
|
||||
A ComfyUI node that improves inpainting results by extending mask boundaries with geometric patterns, helping create smoother transitions and better context for AI-driven image completion.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Navigate to your ComfyUI custom nodes directory:
|
||||
```bash
|
||||
cd ComfyUI/custom_nodes/
|
||||
git clone https://github.com/codeprimate/ComfyUI-MaskContourProcessor
|
||||
cd ComfyUI-MaskContourProcessor
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Parameters
|
||||
|
||||
- **Line Length** (0.0 - 3.0): Controls pattern extension distance. Start with 1.0
|
||||
- **Line Count** (1 - 40): Sets pattern density. Recommended: 20-30
|
||||
- **Line Width** (0.0 - 0.1): Adjusts pattern thickness. Start with 0.01
|
||||
|
||||
### Recommended Settings
|
||||
|
||||
**Large Areas:**
|
||||
- Line Length: 1.5-2.0
|
||||
- Line Count: 30-40
|
||||
- Line Width: 0.015-0.02
|
||||
|
||||
**Detail Work:**
|
||||
- Line Length: 0.5-1.0
|
||||
- Line Count: 25-35
|
||||
- Line Width: 0.005-0.01
|
||||
|
||||
## Tips
|
||||
|
||||
- Start with conservative settings
|
||||
- Adjust Line Length based on image scale
|
||||
- Use higher Line Count for complex textures
|
||||
- Maximum recommended image size: 4096x4096
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) file.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions welcome. Please open an issue first for major changes.
|
||||
@@ -0,0 +1,6 @@
|
||||
from .nodes.nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
|
||||
# Uncomment to print loading message
|
||||
print("\033[34mMask Contour Processor: \033[92mLoaded\033[0m")
|
||||
+450
@@ -0,0 +1,450 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
from skimage.draw import line_aa, circle_perimeter_aa
|
||||
|
||||
class MaskContourProcessor:
|
||||
"""A class that adds decorative flame-like contour effects to binary masks.
|
||||
|
||||
This processor detects mask edges and generates flame-ray effects along the contour,
|
||||
creating visually appealing decorative patterns that radiate from the mask's edges.
|
||||
"""
|
||||
|
||||
# Effect generation constants
|
||||
AMPLITUDE_INITIAL_MOD = 0.2
|
||||
AMPLITUDE_DECAY_MOD = 0.8
|
||||
MIN_LINE_WIDTH = 1.0
|
||||
DEFAULT_STROKE_COLOR = 'black'
|
||||
DEFAULT_ELEMENT_COLOR = 'black'
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"mask": ("MASK",),
|
||||
"line_length": ("FLOAT", {
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 3.0,
|
||||
"step": 0.01
|
||||
}),
|
||||
"line_count": ("INT", {
|
||||
"default": 20,
|
||||
"min": 1,
|
||||
"max": 40,
|
||||
"step": 1
|
||||
}),
|
||||
"line_width": ("FLOAT", {
|
||||
"default": 0.01,
|
||||
"min": 0.0,
|
||||
"max": 0.1,
|
||||
"step": 0.001
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MASK",)
|
||||
FUNCTION = "process_mask"
|
||||
CATEGORY = "mask"
|
||||
|
||||
def __init__(self):
|
||||
self.stroke_color = self.DEFAULT_STROKE_COLOR
|
||||
self.element_color = self.DEFAULT_ELEMENT_COLOR
|
||||
|
||||
def set_colors(self, stroke_color, element_color):
|
||||
"""Configure the colors used for rendering effects.
|
||||
|
||||
Args:
|
||||
stroke_color (str): Color to use for the main flame strokes
|
||||
element_color (str): Color to use for decorative elements
|
||||
"""
|
||||
self.stroke_color = stroke_color
|
||||
self.element_color = element_color
|
||||
|
||||
def calculate_mask_centroid(self, mask_array):
|
||||
"""Calculate the center of mass (centroid) of a binary mask.
|
||||
|
||||
Args:
|
||||
mask_array (ndarray): Binary mask array with values between 0 and 1
|
||||
|
||||
Returns:
|
||||
tuple: (x, y) coordinates of the mask's centroid
|
||||
"""
|
||||
# Create binary mask by thresholding at 0.5
|
||||
binary_mask = (mask_array >= 0.5).astype(np.float32)
|
||||
|
||||
# Find coordinates of mask pixels
|
||||
y_coords, x_coords = np.nonzero(binary_mask)
|
||||
|
||||
if len(x_coords) == 0: # Handle empty mask case
|
||||
return (mask_array.shape[1] // 2, mask_array.shape[0] // 2)
|
||||
|
||||
# Calculate average x and y coordinates
|
||||
center_x = np.mean(x_coords)
|
||||
center_y = np.mean(y_coords)
|
||||
|
||||
return (center_x, center_y)
|
||||
|
||||
def detect_edge_points(self, mask_array, center):
|
||||
"""Detect and sort edge points of a mask in clockwise order.
|
||||
|
||||
Args:
|
||||
mask_array (ndarray): Binary mask array
|
||||
center (tuple): (x, y) coordinates of the mask's centroid
|
||||
|
||||
Returns:
|
||||
list: Edge points as [(x, y), ...] sorted clockwise around the center
|
||||
"""
|
||||
edges = set() # Use set to avoid duplicates
|
||||
height, width = mask_array.shape
|
||||
|
||||
# Scan horizontally (left to right)
|
||||
for y in range(height):
|
||||
last_pixel = 0 # Start with black
|
||||
for x in range(width):
|
||||
current_pixel = mask_array[y, x]
|
||||
if last_pixel != current_pixel:
|
||||
edges.add((x, y))
|
||||
last_pixel = current_pixel
|
||||
|
||||
# Scan vertically (top to bottom)
|
||||
for x in range(width):
|
||||
last_pixel = 0 # Start with black
|
||||
for y in range(height):
|
||||
current_pixel = mask_array[y, x]
|
||||
if last_pixel != current_pixel:
|
||||
edges.add((x, y))
|
||||
last_pixel = current_pixel
|
||||
|
||||
# Convert to list and sort points clockwise around center
|
||||
edge_points = list(edges)
|
||||
center_x, center_y = center
|
||||
|
||||
# Sort points clockwise using arctan2
|
||||
edge_points.sort(key=lambda p: np.arctan2(p[1] - center_y, p[0] - center_x))
|
||||
|
||||
return edge_points
|
||||
|
||||
def redistribute_points(self, points, target_count):
|
||||
"""Redistribute points evenly along a contour path.
|
||||
|
||||
Args:
|
||||
points (list): List of (x, y) points defining the contour
|
||||
target_count (int): Desired number of evenly spaced points
|
||||
|
||||
Returns:
|
||||
list: New list of (x, y) points evenly distributed along the contour
|
||||
"""
|
||||
# Calculate total perimeter length
|
||||
total_length = 0
|
||||
segments = []
|
||||
|
||||
for i in range(len(points)):
|
||||
next_point = points[(i + 1) % len(points)]
|
||||
length = np.sqrt(
|
||||
(next_point[0] - points[i][0])**2 +
|
||||
(next_point[1] - points[i][1])**2
|
||||
)
|
||||
total_length += length
|
||||
segments.append({
|
||||
'start': points[i],
|
||||
'end': next_point,
|
||||
'length': length
|
||||
})
|
||||
|
||||
# Redistribute points evenly
|
||||
spacing = total_length / target_count
|
||||
new_points = []
|
||||
current_dist = 0
|
||||
current_segment = 0
|
||||
|
||||
for i in range(target_count):
|
||||
# Skip to next segment if we've exceeded current segment length
|
||||
while current_dist >= segments[current_segment]['length']:
|
||||
current_dist -= segments[current_segment]['length']
|
||||
current_segment = (current_segment + 1) % len(segments)
|
||||
|
||||
# Calculate interpolation factor for current position
|
||||
seg = segments[current_segment]
|
||||
t = current_dist / seg['length']
|
||||
|
||||
# Interpolate position
|
||||
new_points.append((
|
||||
seg['start'][0] + (seg['end'][0] - seg['start'][0]) * t,
|
||||
seg['start'][1] + (seg['end'][1] - seg['start'][1]) * t
|
||||
))
|
||||
|
||||
current_dist += spacing
|
||||
|
||||
return new_points
|
||||
|
||||
def calculate_base_line_width(self, edge_points, line_width):
|
||||
"""Calculate the base width for effect lines based on contour size.
|
||||
|
||||
Args:
|
||||
edge_points (list): List of (x, y) edge points
|
||||
line_width (float): Width parameter for scaling
|
||||
|
||||
Returns:
|
||||
float: Calculated base line width
|
||||
"""
|
||||
if not edge_points:
|
||||
return 10 # Default fallback value
|
||||
|
||||
# Calculate circumference by summing distances between consecutive points
|
||||
circumference = 0
|
||||
for i in range(len(edge_points)):
|
||||
current = edge_points[i]
|
||||
next_point = edge_points[(i + 1) % len(edge_points)]
|
||||
|
||||
distance = np.sqrt(
|
||||
(next_point[0] - current[0])**2 +
|
||||
(next_point[1] - current[1])**2
|
||||
)
|
||||
circumference += distance
|
||||
|
||||
# Return max of 1 or circumference * line_width
|
||||
return max(1.0, circumference * line_width)
|
||||
|
||||
def generate_flame_ray_effect(self, point, next_point, line_length, center, base_line_width):
|
||||
"""Generate data for a flame-like ray effect emanating from an edge point.
|
||||
|
||||
Args:
|
||||
point (tuple): (x, y) starting point on the contour
|
||||
next_point (tuple): (x, y) next point on the contour
|
||||
line_length (float): Length of the flame effect
|
||||
center (tuple): (x, y) mask centroid
|
||||
base_line_width (float): Base width for effect lines
|
||||
|
||||
Returns:
|
||||
dict: Effect data containing path segments and decorative elements
|
||||
"""
|
||||
# Calculate direction vectors for current point
|
||||
dx1 = point[0] - center[0]
|
||||
dy1 = point[1] - center[1]
|
||||
length1 = np.sqrt(dx1 * dx1 + dy1 * dy1)
|
||||
normalized_dx1 = dx1 / length1
|
||||
normalized_dy1 = dy1 / length1
|
||||
|
||||
# Calculate direction vectors for next point
|
||||
dx2 = next_point[0] - center[0]
|
||||
dy2 = next_point[1] - center[1]
|
||||
length2 = np.sqrt(dx2 * dx2 + dy2 * dy2)
|
||||
normalized_dx2 = dx2 / length2
|
||||
normalized_dy2 = dy2 / length2
|
||||
|
||||
# Calculate midpoint between current and next point
|
||||
mid_point = {
|
||||
'x': (point[0] + next_point[0]) / 2,
|
||||
'y': (point[1] + next_point[1]) / 2
|
||||
}
|
||||
|
||||
num_waves = max(5, line_length / 5)
|
||||
initial_amplitude = line_length * self.AMPLITUDE_INITIAL_MOD
|
||||
min_line_width = self.MIN_LINE_WIDTH
|
||||
|
||||
segments = []
|
||||
decorative_elements = []
|
||||
|
||||
# Generate path segments for main flame rays
|
||||
for i in range(int(num_waves)):
|
||||
t = i / num_waves
|
||||
next_t = (i + 1) / num_waves
|
||||
|
||||
# Calculate current wave points
|
||||
wave_x = point[0] + normalized_dx1 * line_length * t
|
||||
wave_y = point[1] + normalized_dy1 * line_length * t
|
||||
next_wave_x = point[0] + normalized_dx1 * line_length * next_t
|
||||
next_wave_y = point[1] + normalized_dy1 * line_length * next_t
|
||||
|
||||
# Calculate wave amplitudes and offsets
|
||||
current_amplitude = initial_amplitude * (1 - t * self.AMPLITUDE_DECAY_MOD)
|
||||
offset_x = -normalized_dy1 * np.sin(t * np.pi * 2) * current_amplitude
|
||||
offset_y = normalized_dx1 * np.sin(t * np.pi * 2) * current_amplitude
|
||||
next_amplitude = initial_amplitude * (1 - next_t * self.AMPLITUDE_DECAY_MOD)
|
||||
next_offset_x = -normalized_dy1 * np.sin(next_t * np.pi * 2) * next_amplitude
|
||||
next_offset_y = normalized_dx1 * np.sin(next_t * np.pi * 2) * next_amplitude
|
||||
|
||||
# Interpolate between direction vectors for virtual flame
|
||||
interpolation_factor = 0.5
|
||||
virtual_dx = normalized_dx1 * (1 - interpolation_factor) + normalized_dx2 * interpolation_factor
|
||||
virtual_dy = normalized_dy1 * (1 - interpolation_factor) + normalized_dy2 * interpolation_factor
|
||||
|
||||
# Normalize interpolated direction
|
||||
virtual_length = np.sqrt(virtual_dx * virtual_dx + virtual_dy * virtual_dy)
|
||||
normalized_virtual_dx = virtual_dx / virtual_length
|
||||
normalized_virtual_dy = virtual_dy / virtual_length
|
||||
|
||||
# Calculate virtual flame point
|
||||
virtual_flame_x = mid_point['x'] + normalized_virtual_dx * line_length * t
|
||||
virtual_flame_y = mid_point['y'] + normalized_virtual_dy * line_length * t
|
||||
|
||||
# Add sinusoidal pattern to virtual flame
|
||||
virtual_amplitude = current_amplitude
|
||||
virtual_offset_x = -normalized_virtual_dy * np.sin(t * np.pi * 2) * virtual_amplitude
|
||||
virtual_offset_y = normalized_virtual_dx * np.sin(t * np.pi * 2) * virtual_amplitude
|
||||
|
||||
segments.append({
|
||||
'startPoint': {
|
||||
'x': wave_x + offset_x,
|
||||
'y': wave_y + offset_y
|
||||
},
|
||||
'endPoint': {
|
||||
'x': next_wave_x + next_offset_x,
|
||||
'y': next_wave_y + next_offset_y
|
||||
},
|
||||
'properties': {
|
||||
'width': base_line_width - (base_line_width - min_line_width) * t,
|
||||
'strokeColor': self.stroke_color
|
||||
}
|
||||
})
|
||||
|
||||
# Add decorative elements along virtual flame path
|
||||
start_step = 2
|
||||
if i >= start_step and i % 3 == 0:
|
||||
progress = (i - start_step) / (num_waves - start_step)
|
||||
min_radius = min_line_width / 2
|
||||
max_radius = base_line_width * 0.6
|
||||
current_radius = min_radius + (max_radius - min_radius) * progress
|
||||
|
||||
decorative_elements.append({
|
||||
'position': {
|
||||
'x': virtual_flame_x + virtual_offset_x,
|
||||
'y': virtual_flame_y + virtual_offset_y
|
||||
},
|
||||
'type': 'circle',
|
||||
'properties': {
|
||||
'radius': current_radius,
|
||||
'color': self.element_color
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
'origin': {'x': point[0], 'y': point[1]},
|
||||
'path': {
|
||||
'direction': {'dx': normalized_dx1, 'dy': normalized_dy1},
|
||||
'length': line_length,
|
||||
'segments': segments
|
||||
},
|
||||
'decorativeElements': decorative_elements
|
||||
}
|
||||
|
||||
def render_effect_to_mask(self, mask_array, effect_data):
|
||||
"""Render a flame effect onto a mask using anti-aliased drawing.
|
||||
|
||||
Args:
|
||||
mask_array (ndarray): Target mask array
|
||||
effect_data (dict): Effect specification data
|
||||
|
||||
Returns:
|
||||
ndarray: Updated mask with rendered effect
|
||||
"""
|
||||
# Create a temporary canvas for anti-aliased drawing
|
||||
height, width = mask_array.shape
|
||||
canvas = np.zeros((height, width), dtype=np.float32)
|
||||
|
||||
# Render path segments
|
||||
for segment in effect_data['path']['segments']:
|
||||
start = segment['startPoint']
|
||||
end = segment['endPoint']
|
||||
width = segment['properties']['width']
|
||||
|
||||
# Draw anti-aliased line
|
||||
rr, cc, val = line_aa(
|
||||
int(start['y']), int(start['x']),
|
||||
int(end['y']), int(end['x'])
|
||||
)
|
||||
|
||||
# Filter valid coordinates
|
||||
mask = (rr >= 0) & (rr < height) & (cc >= 0) & (cc < width)
|
||||
rr, cc, val = rr[mask], cc[mask], val[mask]
|
||||
|
||||
# Apply line width
|
||||
canvas[rr, cc] = np.maximum(canvas[rr, cc], val * width)
|
||||
|
||||
# Render decorative elements
|
||||
if 'decorativeElements' in effect_data:
|
||||
for element in effect_data['decorativeElements']:
|
||||
if element['type'] == 'circle':
|
||||
pos = element['position']
|
||||
radius = element['properties']['radius']
|
||||
|
||||
# Draw anti-aliased circle
|
||||
rr, cc, val = circle_perimeter_aa(
|
||||
int(pos['y']), int(pos['x']),
|
||||
int(radius)
|
||||
)
|
||||
|
||||
# Filter valid coordinates
|
||||
mask = (rr >= 0) & (rr < height) & (cc >= 0) & (cc < width)
|
||||
rr, cc, val = rr[mask], cc[mask], val[mask]
|
||||
|
||||
canvas[rr, cc] = np.maximum(canvas[rr, cc], val)
|
||||
|
||||
# Combine with original mask
|
||||
return np.maximum(mask_array, canvas)
|
||||
|
||||
def process_mask(self, mask, line_length, line_count, line_width):
|
||||
"""Process a mask by adding flame-like contour effects.
|
||||
|
||||
Args:
|
||||
mask (tensor): Input mask tensor
|
||||
line_length (float): Relative length of flame effects
|
||||
line_count (int): Number of flame effects to generate
|
||||
line_width (float): Width of effect lines
|
||||
|
||||
Returns:
|
||||
tuple: Single-element tuple containing the processed mask tensor
|
||||
"""
|
||||
# Convert mask to numpy
|
||||
mask_np = mask.cpu().numpy() if isinstance(mask, torch.Tensor) else np.array(mask)
|
||||
|
||||
# Create output mask
|
||||
output_mask = mask_np.copy()
|
||||
|
||||
# Calculate centroid
|
||||
center = self.calculate_mask_centroid(mask_np)
|
||||
|
||||
# Detect edge points
|
||||
edge_points = self.detect_edge_points(mask_np, center)
|
||||
|
||||
# Calculate base line width
|
||||
base_line_width = self.calculate_base_line_width(edge_points, line_width)
|
||||
|
||||
# Select evenly distributed points
|
||||
selected_points = self.redistribute_points(edge_points, line_count)
|
||||
|
||||
# Generate all effects data first
|
||||
effects_data = []
|
||||
for i in range(len(selected_points)):
|
||||
point = selected_points[i]
|
||||
next_point = selected_points[(i + 1) % len(selected_points)]
|
||||
|
||||
# Calculate effect length based on distance from center
|
||||
dx = point[0] - center[0]
|
||||
dy = point[1] - center[1]
|
||||
distance_from_center = np.sqrt(dx*dx + dy*dy)
|
||||
effect_length = distance_from_center * line_length
|
||||
|
||||
effect = self.generate_flame_ray_effect(
|
||||
point,
|
||||
next_point,
|
||||
effect_length,
|
||||
center,
|
||||
base_line_width
|
||||
)
|
||||
effects_data.append(effect)
|
||||
|
||||
# Render all effects
|
||||
for effect in effects_data:
|
||||
output_mask = self.render_effect_to_mask(output_mask, effect)
|
||||
|
||||
return (torch.from_numpy(output_mask),)
|
||||
|
||||
# Node class mappings
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"MaskContourProcessor": MaskContourProcessor
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"MaskContourProcessor": "Mask Contour Processor"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
numpy>=1.20.0
|
||||
torch>=1.7.0
|
||||
scikit-image
|
||||
@@ -0,0 +1,54 @@
|
||||
!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/
|
||||
!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/
|
||||
!_TAG_PROGRAM_AUTHOR Darren Hiebert /dhiebert@users.sourceforge.net/
|
||||
!_TAG_PROGRAM_NAME Exuberant Ctags //
|
||||
!_TAG_PROGRAM_URL http://ctags.sourceforge.net /official site/
|
||||
!_TAG_PROGRAM_VERSION 5.8 //
|
||||
AMPLITUDE_DECAY_MOD ./nodes/nodes.py /^ AMPLITUDE_DECAY_MOD = 0.8$/;" v class:MaskContourProcessor
|
||||
AMPLITUDE_INITIAL_MOD ./nodes/nodes.py /^ AMPLITUDE_INITIAL_MOD = 0.2$/;" v class:MaskContourProcessor
|
||||
CATEGORY ./nodes/nodes.py /^ CATEGORY = "mask"$/;" v class:MaskContourProcessor
|
||||
DEFAULT_ELEMENT_COLOR ./nodes/nodes.py /^ DEFAULT_ELEMENT_COLOR = 'black'$/;" v class:MaskContourProcessor
|
||||
DEFAULT_STROKE_COLOR ./nodes/nodes.py /^ DEFAULT_STROKE_COLOR = 'black'$/;" v class:MaskContourProcessor
|
||||
FUNCTION ./nodes/nodes.py /^ FUNCTION = "process_mask"$/;" v class:MaskContourProcessor
|
||||
HTMLCanvasElement ./web/js/maskProcessor.test.js /^HTMLCanvasElement.prototype.getContext = () => mockContext;$/;" c
|
||||
HTMLCanvasElement.getContext ./web/js/maskProcessor.test.js /^HTMLCanvasElement.prototype.getContext = () => mockContext;$/;" m
|
||||
INPUT_TYPES ./nodes/nodes.py /^ def INPUT_TYPES(s):$/;" m class:MaskContourProcessor
|
||||
MIN_LINE_WIDTH ./nodes/nodes.py /^ MIN_LINE_WIDTH = 1.0$/;" v class:MaskContourProcessor
|
||||
MaskContourProcessor ./nodes/nodes.py /^class MaskContourProcessor:$/;" c
|
||||
NODE_CLASS_MAPPINGS ./__init__.py /^from .nodes.nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS$/;" i
|
||||
NODE_CLASS_MAPPINGS ./nodes/nodes.py /^NODE_CLASS_MAPPINGS = {$/;" v
|
||||
NODE_DISPLAY_NAME_MAPPINGS ./__init__.py /^from .nodes.nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS$/;" i
|
||||
NODE_DISPLAY_NAME_MAPPINGS ./nodes/nodes.py /^NODE_DISPLAY_NAME_MAPPINGS = {$/;" v
|
||||
RETURN_TYPES ./nodes/nodes.py /^ RETURN_TYPES = ("MASK",)$/;" v class:MaskContourProcessor
|
||||
__all__ ./__init__.py /^__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']$/;" v
|
||||
__init__ ./nodes/nodes.py /^ def __init__(self):$/;" m class:MaskContourProcessor
|
||||
calculate_base_line_width ./nodes/nodes.py /^ def calculate_base_line_width(self, edge_points, line_width):$/;" m class:MaskContourProcessor
|
||||
calculate_mask_centroid ./nodes/nodes.py /^ def calculate_mask_centroid(self, mask_array):$/;" m class:MaskContourProcessor
|
||||
circle_perimeter_aa ./nodes/nodes.py /^from skimage.draw import line_aa, circle_perimeter_aa$/;" i
|
||||
const.clearRect ./web/js/maskProcessor.test.js /^ clearRect: jest.fn(),$/;" p
|
||||
const.fillStyle ./web/js/maskProcessor.test.js /^ fillStyle: null,$/;" p
|
||||
const.height ./web/js/maskProcessor.test.js /^ height: 0,$/;" p
|
||||
const.lineCap ./web/js/maskProcessor.test.js /^ lineCap: null,$/;" p
|
||||
const.lineWidth ./web/js/maskProcessor.test.js /^ lineWidth: null,$/;" p
|
||||
const.strokeStyle ./web/js/maskProcessor.test.js /^ strokeStyle: null,$/;" p
|
||||
const.style ./web/js/maskProcessor.test.js /^ style: {$/;" p
|
||||
const.width ./web/js/maskProcessor.test.js /^ width: 0,$/;" p
|
||||
const.x ./web/js/maskProcessor.js /^ x: (point.x + nextPoint.x) \/ 2,$/;" p
|
||||
detect_edge_points ./nodes/nodes.py /^ def detect_edge_points(self, mask_array, center):$/;" m class:MaskContourProcessor
|
||||
downloadCanvas ./web/js/index.js /^function downloadCanvas(type) {$/;" f
|
||||
generate_flame_ray_effect ./nodes/nodes.py /^ def generate_flame_ray_effect(self, point, next_point, line_length, center, base_line_width):$/;" m class:MaskContourProcessor
|
||||
line_aa ./nodes/nodes.py /^from skimage.draw import line_aa, circle_perimeter_aa$/;" i
|
||||
maskInfo.center ./web/js/maskProcessor.js /^ center: center,$/;" p
|
||||
maskInfo.center ./web/js/maskProcessor.js /^ center: { x: centerX, y: centerY },$/;" p
|
||||
maskInfo.edgePoints ./web/js/maskProcessor.js /^ edgePoints: this.redistributePoints(edges, 100) \/\/ Maintain consistent point count$/;" p
|
||||
np ./nodes/nodes.py /^import numpy as np$/;" i
|
||||
process_mask ./nodes/nodes.py /^ def process_mask(self, mask, line_length, line_count, line_width):$/;" m class:MaskContourProcessor
|
||||
redistribute_points ./nodes/nodes.py /^ def redistribute_points(self, points, target_count):$/;" m class:MaskContourProcessor
|
||||
regenerateMask ./web/js/index.js /^function regenerateMask() {$/;" f
|
||||
render_effect_to_mask ./nodes/nodes.py /^ def render_effect_to_mask(self, mask_array, effect_data):$/;" m class:MaskContourProcessor
|
||||
set_colors ./nodes/nodes.py /^ def set_colors(self, stroke_color, element_color):$/;" m class:MaskContourProcessor
|
||||
torch ./nodes/nodes.py /^import torch$/;" i
|
||||
updateControl ./web/js/index.js /^function updateControl(controlId, value, processorProperty, formatValue = v => v) {$/;" f
|
||||
updateLineLength ./web/js/index.js /^function updateLineLength(value) {$/;" f
|
||||
updateLineWidth ./web/js/index.js /^function updateLineWidth(value) {$/;" f
|
||||
updateNumLines ./web/js/index.js /^function updateNumLines(value) {$/;" f
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mask Contour Processor v0.5</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
@keyframes errorFlash {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
|
||||
50% { box-shadow: 0 0 40px 10px rgba(239, 68, 68, 0.7); }
|
||||
}
|
||||
@keyframes successFlash {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
|
||||
50% { box-shadow: 0 0 20px 0 rgba(34, 197, 94, 0.5); }
|
||||
}
|
||||
.error-flash {
|
||||
animation: errorFlash 0.8s ease-out;
|
||||
}
|
||||
.success-flash {
|
||||
animation: successFlash 0.8s ease-out;
|
||||
}
|
||||
|
||||
#dropZone {
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
#dropZone:hover,
|
||||
#dropZone.drag-active {
|
||||
opacity: 1;
|
||||
border-color: #3b82f6; /* Tailwind's blue-500 */
|
||||
box-shadow: inset 0 0 0 2px rgba(59, 130, 246, 0.1); /* Subtle inner glow */
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.download-btn:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
@keyframes explode {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.exploding {
|
||||
animation: explode 0.5s ease-out forwards;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="p-5 font-sans bg-gray-100">
|
||||
<main class="container max-w-[850px] mx-auto">
|
||||
<!-- Controls Section -->
|
||||
<section class="flex items-center gap-4 mb-4">
|
||||
<button
|
||||
onclick="regenerateMask()"
|
||||
class="px-5 py-2.5 bg-blue-600 text-white rounded hover:bg-blue-700 text-base cursor-pointer transition-colors"
|
||||
>
|
||||
Generate New Mask
|
||||
</button>
|
||||
|
||||
<!-- Line Length Control -->
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="lineLength" class="text-sm text-gray-700">Line Length:</label>
|
||||
<input
|
||||
type="range"
|
||||
id="lineLength"
|
||||
min="0"
|
||||
max="3"
|
||||
step="0.01"
|
||||
value="1"
|
||||
class="w-32 cursor-pointer"
|
||||
oninput="updateLineLength(this.value)"
|
||||
>
|
||||
<span id="lineLengthValue" class="text-sm text-gray-700 font-mono">1</span>
|
||||
</div>
|
||||
|
||||
<!-- Line Spacing Control -->
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="numLines" class="text-sm text-gray-700">Line Count:</label>
|
||||
<input
|
||||
type="range"
|
||||
id="numLines"
|
||||
min="1"
|
||||
max="40"
|
||||
value="10"
|
||||
class="w-32 cursor-pointer"
|
||||
oninput="updateNumLines(this.value)"
|
||||
>
|
||||
<span id="numLinesValue" class="text-sm text-gray-700 font-mono">10</span>
|
||||
</div>
|
||||
|
||||
<!-- Line Width Control -->
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="lineWidth" class="text-sm text-gray-700">Line Width:</label>
|
||||
<input
|
||||
type="range"
|
||||
id="lineWidth"
|
||||
min="0"
|
||||
max="100"
|
||||
value="1"
|
||||
step="0.1"
|
||||
class="w-32 cursor-pointer"
|
||||
oninput="updateLineWidth(this.value)"
|
||||
>
|
||||
<span id="lineWidthValue" class="text-sm text-gray-700 font-mono">0.010</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Canvas Section -->
|
||||
<section class="flex gap-5">
|
||||
<div class="relative group">
|
||||
<canvas
|
||||
id="inputCanvas"
|
||||
width="400"
|
||||
height="400"
|
||||
class="bg-white border border-gray-300 rounded shadow-sm"
|
||||
aria-label="Input mask canvas"
|
||||
></canvas>
|
||||
<div
|
||||
id="dropZone"
|
||||
class="absolute inset-0 rounded flex items-center justify-center"
|
||||
>
|
||||
<div class="text-center">
|
||||
<svg class="w-8 h-8 mx-auto mb-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||
</svg>
|
||||
<p class="text-sm text-gray-600">Drop image here</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative group">
|
||||
<canvas
|
||||
id="outputCanvas"
|
||||
width="400"
|
||||
height="400"
|
||||
class="bg-white border border-gray-300 rounded shadow-sm"
|
||||
aria-label="Output mask canvas with contour lines"
|
||||
></canvas>
|
||||
<button
|
||||
onclick="downloadCanvas('output')"
|
||||
class="download-btn opacity-0 group-hover:opacity-100"
|
||||
aria-label="Download output canvas"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="js/maskProcessor.js"></script>
|
||||
<script src="js/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
// Initialize processor
|
||||
const processor = new MaskProcessor(400, 400, 1.0, 20, 0.01);
|
||||
|
||||
// Control handlers
|
||||
function updateControl(controlId, value, processorProperty, formatValue = v => v) {
|
||||
document.getElementById(`${controlId}Value`).textContent = formatValue(value);
|
||||
processor[processorProperty] = value;
|
||||
processor.processMask();
|
||||
}
|
||||
|
||||
function updateLineLength(value) {
|
||||
updateControl('lineLength', value, 'lineLengthRatio', v => (v/1.0).toFixed(2));
|
||||
}
|
||||
|
||||
function updateNumLines(value) {
|
||||
updateControl('numLines', value, 'numLines', v => parseInt(v));
|
||||
}
|
||||
|
||||
function updateLineWidth(value) {
|
||||
updateControl('lineWidth', (value/1000).toFixed(3), 'lineWidthRatio', v => (v*1000));
|
||||
}
|
||||
|
||||
function regenerateMask() {
|
||||
console.log('regenerateMask called');
|
||||
try {
|
||||
processor.generateRandomMask();
|
||||
processor.processMask();
|
||||
} catch (error) {
|
||||
console.error('Error in regenerateMask:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mask generation
|
||||
console.log('Running initial mask generation');
|
||||
regenerateMask();
|
||||
|
||||
// Drag and drop handlers
|
||||
const dropZone = document.getElementById('dropZone');
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('drag-active');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', () => {
|
||||
dropZone.classList.remove('drag-active');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', async (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('drag-active');
|
||||
|
||||
// Remove any existing animation classes
|
||||
dropZone.classList.remove('error-flash');
|
||||
dropZone.classList.remove('success-flash');
|
||||
|
||||
// Force a reflow to reset the animation
|
||||
void dropZone.offsetWidth;
|
||||
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (!file || !file.type.startsWith('image/')) {
|
||||
dropZone.classList.add('error-flash');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const img = new Image();
|
||||
img.src = URL.createObjectURL(file);
|
||||
await img.decode();
|
||||
|
||||
// Use the new loadMask method
|
||||
processor.loadMask(img);
|
||||
|
||||
dropZone.classList.add('success-flash');
|
||||
} catch (error) {
|
||||
console.error('Error loading image:', error);
|
||||
dropZone.classList.add('error-flash');
|
||||
}
|
||||
});
|
||||
|
||||
// Show drop zone on page load
|
||||
dropZone.style.display = 'flex';
|
||||
|
||||
// Add download functionality
|
||||
function downloadCanvas(type) {
|
||||
const button = event.currentTarget;
|
||||
const canvas = type === 'input' ? processor.domInputCanvas : processor.domOutputCanvas;
|
||||
|
||||
// Create and trigger download
|
||||
const link = document.createElement('a');
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
link.download = `mask-${type}-${timestamp}.png`;
|
||||
link.href = canvas.toDataURL('image/png');
|
||||
|
||||
// Add explosion animation
|
||||
button.classList.add('exploding');
|
||||
|
||||
// Remove animation class after it completes
|
||||
button.addEventListener('animationend', () => {
|
||||
button.classList.remove('exploding');
|
||||
}, { once: true });
|
||||
|
||||
link.click();
|
||||
}
|
||||
@@ -0,0 +1,806 @@
|
||||
/**
|
||||
* @typedef {Object} Point
|
||||
* @property {number} x - X coordinate
|
||||
* @property {number} y - Y coordinate
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PathSegment
|
||||
* @property {Point} startPoint - Starting point of the segment
|
||||
* @property {Point} endPoint - Ending point of the segment
|
||||
* @property {Object} properties - Segment properties
|
||||
* @property {number} properties.width - Line width
|
||||
* @property {string} properties.strokeColor - Stroke color
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} DecorativeElement
|
||||
* @property {Point} position - Element position
|
||||
* @property {string} type - Element type (e.g., 'circle', 'dot')
|
||||
* @property {Object} properties - Element properties
|
||||
* @property {number} properties.radius - Element radius
|
||||
* @property {string} properties.color - Element color
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} RadiatingEffect
|
||||
* @property {Point} origin - Starting point on the boundary
|
||||
* @property {Object} path - Path information
|
||||
* @property {Object} path.direction - Direction vector
|
||||
* @property {number} path.direction.dx - X component of direction
|
||||
* @property {number} path.direction.dy - Y component of direction
|
||||
* @property {number} path.length - Path length
|
||||
* @property {PathSegment[]} path.segments - Path segments
|
||||
* @property {DecorativeElement[]} [decorativeElements] - Optional decorative elements
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} MaskInfo
|
||||
* @property {Point} center - Centroid of the mask
|
||||
* @property {Point[]} edgePoints - Array of points along the mask's edge
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} OverlayEffect
|
||||
* @property {Object} baseShape - Base shape information
|
||||
* @property {Point} baseShape.center - Center point
|
||||
* @property {Point[]} baseShape.boundaryPoints - Boundary points
|
||||
* @property {Object} baseShape.bounds - Shape bounds
|
||||
* @property {number} baseShape.bounds.width - Width
|
||||
* @property {number} baseShape.bounds.height - Height
|
||||
* @property {RadiatingEffect[]} radiatingEffects - Array of radiating effects
|
||||
*/
|
||||
|
||||
/**
|
||||
* @class MaskProcessor
|
||||
* @description Processes bitmap masks to generate decorative flame-like effects radiating from edges.
|
||||
*/
|
||||
class MaskProcessor {
|
||||
/**
|
||||
* @constructor
|
||||
* @param {number} width - Canvas width in pixels
|
||||
* @param {number} height - Canvas height in pixels
|
||||
* @param {number} [lineLengthRatio=1.0] - Ratio controlling the length of radiating lines
|
||||
* @param {number} [numLines=10] - Number of radiating lines to draw
|
||||
* @param {number} [lineWidthRatio=0.01] - Ratio of line width to contour circumference
|
||||
*/
|
||||
constructor(width, height, lineLengthRatio = 1.0, numLines = 20, lineWidthRatio = 0.01) {
|
||||
this.debug = false;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
||||
this.inputCanvas = document.createElement('canvas');
|
||||
this.outputCanvas = document.createElement('canvas');
|
||||
|
||||
this.inputCanvas.width = width;
|
||||
this.inputCanvas.height = height;
|
||||
this.outputCanvas.width = width;
|
||||
this.outputCanvas.height = height;
|
||||
|
||||
this.inputCtx = this.inputCanvas.getContext('2d');
|
||||
this.outputCtx = this.outputCanvas.getContext('2d');
|
||||
this.maskInfo = null;
|
||||
this.lineLengthRatio = lineLengthRatio;
|
||||
this.numLines = numLines;
|
||||
this.lineWidthRatio = lineWidthRatio;
|
||||
this.strokeColor = 'black';
|
||||
this.elementColor = 'black';
|
||||
|
||||
this.domInputCanvas = document.getElementById('inputCanvas');
|
||||
this.domOutputCanvas = document.getElementById('outputCanvas');
|
||||
|
||||
if (!this.domInputCanvas || !this.domOutputCanvas) {
|
||||
console.error('Failed to find canvas elements');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @method generateRandomMask
|
||||
* @description Generates a random organic mask shape using quadratic curves and enhanced bulge calculations
|
||||
* @returns {MaskInfo} Contains center point and edge points of the generated mask
|
||||
*/
|
||||
generateRandomMask() {
|
||||
this.inputCanvas.width = 400;
|
||||
this.inputCanvas.height = 400;
|
||||
this.width = 400;
|
||||
this.height = 400;
|
||||
|
||||
// Reset DOM canvas dimensions
|
||||
this.domInputCanvas.width = 400;
|
||||
this.domInputCanvas.height = 400;
|
||||
this.domOutputCanvas.width = 400;
|
||||
this.domOutputCanvas.height = 400;
|
||||
|
||||
// Reset display styles
|
||||
this.domInputCanvas.style.width = '400px';
|
||||
this.domInputCanvas.style.height = '400px';
|
||||
this.domOutputCanvas.style.width = '400px';
|
||||
this.domOutputCanvas.style.height = '400px';
|
||||
|
||||
// Clear canvas
|
||||
this.inputCtx.fillStyle = 'white';
|
||||
this.inputCtx.fillRect(0, 0, this.width, this.height);
|
||||
|
||||
const centerX = this.width / 2;
|
||||
const centerY = this.height / 2;
|
||||
const baseRadius = Math.min(this.width, this.height) * 0.15;
|
||||
const numPoints = 6 + Math.floor(Math.random() * 3);
|
||||
const points = [];
|
||||
|
||||
// Generate base points with smoother distribution
|
||||
for (let i = 0; i < numPoints; i++) {
|
||||
const angle = (i / numPoints) * Math.PI * 2;
|
||||
// Smoother radius variation
|
||||
const radiusVariation = baseRadius * (0.8 + Math.random() * 0.4);
|
||||
const x = centerX + Math.cos(angle) * radiusVariation;
|
||||
const y = centerY + Math.sin(angle) * radiusVariation;
|
||||
points.push({ x, y });
|
||||
}
|
||||
|
||||
// Draw the mask with enhanced curves
|
||||
this.inputCtx.fillStyle = 'black';
|
||||
|
||||
this.inputCtx.beginPath();
|
||||
this.inputCtx.moveTo(points[0].x, points[0].y);
|
||||
|
||||
const curvePoints = [];
|
||||
const numCurvePoints = 240; // Increased for smoother sampling
|
||||
|
||||
// Draw and sample points along curves between vertices
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const current = points[i];
|
||||
const next = points[(i + 1) % points.length];
|
||||
|
||||
// Enhanced bulge calculation
|
||||
const midX = (current.x + next.x) / 2;
|
||||
const midY = (current.y + next.y) / 2;
|
||||
const bulgeX = midX - centerX;
|
||||
const bulgeY = midY - centerY;
|
||||
const bulgeLength = Math.sqrt(bulgeX * bulgeX + bulgeY * bulgeY);
|
||||
const segmentLength = Math.sqrt(
|
||||
Math.pow(next.x - current.x, 2) +
|
||||
Math.pow(next.y - current.y, 2)
|
||||
);
|
||||
// Scale bulge factor based on segment length
|
||||
const bulgeFactor = baseRadius * (0.2 + Math.random() * 0.2) *
|
||||
(segmentLength / baseRadius) * 0.5;
|
||||
const controlX = midX + (bulgeX / bulgeLength) * bulgeFactor;
|
||||
const controlY = midY + (bulgeY / bulgeLength) * bulgeFactor;
|
||||
|
||||
// Sample points with adaptive density
|
||||
const pointsPerSegment = Math.ceil((numCurvePoints / points.length) *
|
||||
(segmentLength / (baseRadius * 2)));
|
||||
for (let t = 0; t < 1; t += 1/pointsPerSegment) {
|
||||
const t1 = 1 - t;
|
||||
const x = t1 * t1 * current.x + 2 * t1 * t * controlX + t * t * next.x;
|
||||
const y = t1 * t1 * current.y + 2 * t1 * t * controlY + t * t * next.y;
|
||||
curvePoints.push({ x, y });
|
||||
}
|
||||
|
||||
this.inputCtx.quadraticCurveTo(controlX, controlY, next.x, next.y);
|
||||
}
|
||||
|
||||
this.inputCtx.closePath();
|
||||
this.inputCtx.fill();
|
||||
|
||||
// Store mask info with evenly distributed points
|
||||
this.maskInfo = {
|
||||
center: { x: centerX, y: centerY },
|
||||
edgePoints: this.redistributePoints(curvePoints, 100)
|
||||
};
|
||||
|
||||
return this.maskInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method redistributePoints
|
||||
* @description Redistributes points evenly along a path defined by a series of points
|
||||
* @param {Point[]} points - Original array of points defining the path
|
||||
* @param {number} targetCount - Desired number of evenly spaced points
|
||||
* @returns {Point[]} Array of evenly distributed points
|
||||
* @private
|
||||
*/
|
||||
redistributePoints(points, targetCount) {
|
||||
// Calculate total perimeter length
|
||||
let totalLength = 0;
|
||||
const segments = [];
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const next = points[(i + 1) % points.length];
|
||||
const length = Math.sqrt(
|
||||
Math.pow(next.x - points[i].x, 2) +
|
||||
Math.pow(next.y - points[i].y, 2)
|
||||
);
|
||||
totalLength += length;
|
||||
segments.push({ start: points[i], end: next, length });
|
||||
}
|
||||
|
||||
// Redistribute points evenly
|
||||
const spacing = totalLength / targetCount;
|
||||
const newPoints = [];
|
||||
let currentDist = 0;
|
||||
let currentSegment = 0;
|
||||
let segmentPos = 0;
|
||||
|
||||
for (let i = 0; i < targetCount; i++) {
|
||||
while (currentDist >= segments[currentSegment].length) {
|
||||
currentDist -= segments[currentSegment].length;
|
||||
currentSegment = (currentSegment + 1) % segments.length;
|
||||
}
|
||||
|
||||
const seg = segments[currentSegment];
|
||||
const t = currentDist / seg.length;
|
||||
newPoints.push({
|
||||
x: seg.start.x + (seg.end.x - seg.start.x) * t,
|
||||
y: seg.start.y + (seg.end.y - seg.start.y) * t
|
||||
});
|
||||
|
||||
currentDist += spacing;
|
||||
}
|
||||
|
||||
return newPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @method drawRadiatingLine
|
||||
* @description Draws a single radiating line from a point on the mask's edge
|
||||
* @param {Object} point - The starting point on the mask's edge
|
||||
* @param {number} lineLength - The length of the line to draw
|
||||
*/
|
||||
drawRadiatingLine(point, lineLength) {
|
||||
const dx = point.x - this.maskInfo.center.x;
|
||||
const dy = point.y - this.maskInfo.center.y;
|
||||
const length = Math.sqrt(dx * dx + dy * dy);
|
||||
const normalizedDx = dx / length;
|
||||
const normalizedDy = dy / length;
|
||||
|
||||
this.outputCtx.beginPath();
|
||||
this.outputCtx.moveTo(point.x, point.y);
|
||||
this.outputCtx.lineTo(
|
||||
point.x + normalizedDx * lineLength,
|
||||
point.y + normalizedDy * lineLength
|
||||
);
|
||||
this.outputCtx.stroke();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @method calculateBaseLineWidth
|
||||
* @description Calculates the base line width based on the contour circumference
|
||||
* @returns {number} Calculated base line width
|
||||
*/
|
||||
calculateBaseLineWidth() {
|
||||
if (!this.maskInfo || !this.maskInfo.edgePoints) {
|
||||
return 10; // Default fallback value
|
||||
}
|
||||
|
||||
let circumference = 0;
|
||||
const points = this.maskInfo.edgePoints;
|
||||
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const current = points[i];
|
||||
const next = points[(i + 1) % points.length];
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(next.x - current.x, 2) +
|
||||
Math.pow(next.y - current.y, 2)
|
||||
);
|
||||
circumference += distance;
|
||||
}
|
||||
|
||||
return Math.max(1, circumference * this.lineWidthRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* @method generateFlameRayEffect
|
||||
* @description Generates data for a flame-like effect radiating from an edge point
|
||||
* @param {Point} point - Starting point on the mask's edge
|
||||
* @param {Point} nextPoint - Next point on the mask's edge
|
||||
* @param {number} lineLength - Length of the flame effect
|
||||
* @returns {RadiatingEffect} Generated flame effect data
|
||||
* @private
|
||||
*/
|
||||
generateFlameRayEffect(point, nextPoint, lineLength) {
|
||||
const amplitudeInitialMod = 0.2;
|
||||
const amplitudeDecayMod = 0.8;
|
||||
const baseLineWidth = this.calculateBaseLineWidth();
|
||||
|
||||
// Calculate direction vectors for both current and next points
|
||||
const dx1 = point.x - this.maskInfo.center.x;
|
||||
const dy1 = point.y - this.maskInfo.center.y;
|
||||
const length1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
|
||||
const normalizedDx1 = dx1 / length1;
|
||||
const normalizedDy1 = dy1 / length1;
|
||||
|
||||
const dx2 = nextPoint.x - this.maskInfo.center.x;
|
||||
const dy2 = nextPoint.y - this.maskInfo.center.y;
|
||||
const length2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
|
||||
const normalizedDx2 = dx2 / length2;
|
||||
const normalizedDy2 = dy2 / length2;
|
||||
|
||||
// Calculate midpoint between current and next flame
|
||||
const midPoint = {
|
||||
x: (point.x + nextPoint.x) / 2,
|
||||
y: (point.y + nextPoint.y) / 2
|
||||
};
|
||||
|
||||
const numWaves = Math.max(5, lineLength / 5);
|
||||
const initialAmplitude = lineLength * amplitudeInitialMod;
|
||||
const minLineWidth = 1;
|
||||
|
||||
const segments = [];
|
||||
const decorativeElements = [];
|
||||
|
||||
// Generate path segments for the main flame rays
|
||||
for (let i = 0; i < numWaves; i++) {
|
||||
const t = i / numWaves;
|
||||
const nextT = (i + 1) / numWaves;
|
||||
|
||||
// Calculate current wave points for both flames
|
||||
const waveX = point.x + normalizedDx1 * lineLength * t;
|
||||
const waveY = point.y + normalizedDy1 * lineLength * t;
|
||||
const nextWaveX = point.x + normalizedDx1 * lineLength * nextT;
|
||||
const nextWaveY = point.y + normalizedDy1 * lineLength * nextT;
|
||||
|
||||
// Calculate wave amplitudes and offsets
|
||||
const currentAmplitude = initialAmplitude * (1 - t * amplitudeDecayMod);
|
||||
const offsetX = -normalizedDy1 * Math.sin(t * Math.PI * 2) * currentAmplitude;
|
||||
const offsetY = normalizedDx1 * Math.sin(t * Math.PI * 2) * currentAmplitude;
|
||||
const nextAmplitude = initialAmplitude * (1 - nextT * amplitudeDecayMod);
|
||||
const nextOffsetX = -normalizedDy1 * Math.sin(nextT * Math.PI * 2) * nextAmplitude;
|
||||
const nextOffsetY = normalizedDx1 * Math.sin(nextT * Math.PI * 2) * nextAmplitude;
|
||||
|
||||
// Interpolate between the two direction vectors for virtual flame
|
||||
const interpolationFactor = 0.5; // Can be adjusted for different effects
|
||||
const virtualDx = normalizedDx1 * (1 - interpolationFactor) + normalizedDx2 * interpolationFactor;
|
||||
const virtualDy = normalizedDy1 * (1 - interpolationFactor) + normalizedDy2 * interpolationFactor;
|
||||
|
||||
// Normalize the interpolated direction
|
||||
const virtualLength = Math.sqrt(virtualDx * virtualDx + virtualDy * virtualDy);
|
||||
const normalizedVirtualDx = virtualDx / virtualLength;
|
||||
const normalizedVirtualDy = virtualDy / virtualLength;
|
||||
|
||||
// Calculate virtual flame point with interpolated direction and sinusoidal pattern
|
||||
const virtualFlameX = midPoint.x + normalizedVirtualDx * lineLength * t;
|
||||
const virtualFlameY = midPoint.y + normalizedVirtualDy * lineLength * t;
|
||||
|
||||
// Add sinusoidal pattern to virtual flame position
|
||||
const virtualAmplitude = currentAmplitude;
|
||||
const virtualOffsetX = -normalizedVirtualDy * Math.sin(t * Math.PI * 2) * virtualAmplitude;
|
||||
const virtualOffsetY = normalizedVirtualDx * Math.sin(t * Math.PI * 2) * virtualAmplitude;
|
||||
|
||||
segments.push({
|
||||
startPoint: {
|
||||
x: waveX + offsetX,
|
||||
y: waveY + offsetY
|
||||
},
|
||||
endPoint: {
|
||||
x: nextWaveX + nextOffsetX,
|
||||
y: nextWaveY + nextOffsetY
|
||||
},
|
||||
properties: {
|
||||
width: baseLineWidth - (baseLineWidth - minLineWidth) * t,
|
||||
strokeColor: this.strokeColor
|
||||
}
|
||||
});
|
||||
|
||||
// Add decorative elements along virtual flame path with sinusoidal pattern
|
||||
const startStep = 2;
|
||||
if (i >= startStep && i % 3 === 0) {
|
||||
const progress = (i - startStep) / (numWaves - startStep);
|
||||
const minRadius = minLineWidth / 2;
|
||||
const maxRadius = baseLineWidth * 0.6;
|
||||
const currentRadius = minRadius + (maxRadius - minRadius) * progress;
|
||||
|
||||
decorativeElements.push({
|
||||
position: {
|
||||
x: virtualFlameX + virtualOffsetX,
|
||||
y: virtualFlameY + virtualOffsetY
|
||||
},
|
||||
type: 'circle',
|
||||
properties: {
|
||||
radius: currentRadius,
|
||||
color: this.elementColor
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
origin: point,
|
||||
path: {
|
||||
direction: { dx: normalizedDx1, dy: normalizedDy1 },
|
||||
length: lineLength,
|
||||
segments
|
||||
},
|
||||
decorativeElements
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @method renderFlameRayEffect
|
||||
* @description Renders a flame ray effect including path segments and decorative elements
|
||||
* @param {RadiatingEffect} effect - The effect data to render
|
||||
* @private
|
||||
*/
|
||||
renderFlameRayEffect(effect) {
|
||||
// Render main path segments
|
||||
effect.path.segments.forEach(segment => {
|
||||
this.outputCtx.beginPath();
|
||||
this.outputCtx.lineWidth = segment.properties.width;
|
||||
this.outputCtx.strokeStyle = segment.properties.strokeColor || this.strokeColor;
|
||||
this.outputCtx.moveTo(segment.startPoint.x, segment.startPoint.y);
|
||||
this.outputCtx.lineTo(segment.endPoint.x, segment.endPoint.y);
|
||||
this.outputCtx.stroke();
|
||||
});
|
||||
|
||||
// Render decorative elements
|
||||
if (effect.decorativeElements) {
|
||||
effect.decorativeElements.forEach(element => {
|
||||
if (element.type === 'circle') {
|
||||
this.outputCtx.fillStyle = element.properties.color || this.elementColor;
|
||||
this.outputCtx.beginPath();
|
||||
this.outputCtx.arc(
|
||||
element.position.x,
|
||||
element.position.y,
|
||||
element.properties.radius,
|
||||
0,
|
||||
Math.PI * 2
|
||||
);
|
||||
this.outputCtx.fill();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @method setupOutputCanvas
|
||||
* @description Prepares the output canvas for drawing
|
||||
*/
|
||||
setupOutputCanvas() {
|
||||
this.outputCtx.fillStyle = 'white';
|
||||
this.outputCtx.fillRect(0, 0, this.width, this.height);
|
||||
this.outputCtx.drawImage(this.inputCanvas, 0, 0);
|
||||
|
||||
this.outputCtx.strokeStyle = 'black';
|
||||
this.outputCtx.lineCap = 'round';
|
||||
// Line width will be set per segment in renderFlameRayEffect
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @method copyToDOM
|
||||
* @description Copies the internal canvases to the DOM canvases
|
||||
*/
|
||||
copyToDOM() {
|
||||
const domInputCtx = this.domInputCanvas.getContext('2d');
|
||||
const domOutputCtx = this.domOutputCanvas.getContext('2d');
|
||||
|
||||
domInputCtx.clearRect(0, 0, this.width, this.height);
|
||||
domOutputCtx.clearRect(0, 0, this.width, this.height);
|
||||
|
||||
domInputCtx.drawImage(this.inputCanvas, 0, 0);
|
||||
domOutputCtx.drawImage(this.outputCanvas, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @method processMask
|
||||
* @description Generates decorative effects based on the analyzed mask.
|
||||
*/
|
||||
processMask() {
|
||||
if (!this.maskInfo) {
|
||||
this.generateRandomMask();
|
||||
}
|
||||
|
||||
this.setupOutputCanvas();
|
||||
|
||||
// Debug visualization
|
||||
if (this.debug) {
|
||||
this.outputCtx.fillStyle = 'red';
|
||||
this.maskInfo.edgePoints.forEach(point => {
|
||||
this.outputCtx.beginPath();
|
||||
this.outputCtx.arc(point.x, point.y, 2, 0, Math.PI * 2);
|
||||
this.outputCtx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
// Select evenly distributed points around the contour
|
||||
const selectedPoints = this.selectEvenlyDistributedPoints(
|
||||
this.maskInfo.edgePoints,
|
||||
this.numLines
|
||||
);
|
||||
|
||||
// Process each selected point
|
||||
for (let i = 0; i < selectedPoints.length; i++) {
|
||||
const point = selectedPoints[i];
|
||||
const nextPoint = selectedPoints[(i + 1) % selectedPoints.length];
|
||||
const lineLength = Math.sqrt(
|
||||
Math.pow(point.x - this.maskInfo.center.x, 2) +
|
||||
Math.pow(point.y - this.maskInfo.center.y, 2)
|
||||
) * this.lineLengthRatio;
|
||||
|
||||
const effect = this.generateFlameRayEffect(point, nextPoint, lineLength);
|
||||
this.renderFlameRayEffect(effect);
|
||||
}
|
||||
|
||||
this.copyToDOM();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @method selectEvenlyDistributedPoints
|
||||
* @description Selects points that are evenly distributed along the contour
|
||||
* @param {Point[]} points - Array of points along the contour
|
||||
* @param {number} count - Number of points to select
|
||||
* @returns {Point[]} Array of selected points
|
||||
*/
|
||||
selectEvenlyDistributedPoints(points, count) {
|
||||
if (points.length <= count) return points;
|
||||
|
||||
// Calculate the total contour length
|
||||
let totalLength = 0;
|
||||
const distances = [];
|
||||
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const current = points[i];
|
||||
const next = points[(i + 1) % points.length];
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(next.x - current.x, 2) +
|
||||
Math.pow(next.y - current.y, 2)
|
||||
);
|
||||
distances.push(distance);
|
||||
totalLength += distance;
|
||||
}
|
||||
|
||||
// Calculate the ideal spacing between points
|
||||
const spacing = totalLength / count;
|
||||
const selectedPoints = [];
|
||||
let currentDistance = 0;
|
||||
let currentIndex = 0;
|
||||
let accumulatedDistance = 0;
|
||||
|
||||
// Select points based on accumulated distance
|
||||
for (let i = 0; i < count; i++) {
|
||||
const targetDistance = (i * totalLength) / count;
|
||||
|
||||
// Find the point closest to the target distance
|
||||
while (accumulatedDistance < targetDistance && currentIndex < distances.length) {
|
||||
accumulatedDistance += distances[currentIndex];
|
||||
currentIndex++;
|
||||
}
|
||||
|
||||
// Get the actual point
|
||||
selectedPoints.push(points[currentIndex % points.length]);
|
||||
}
|
||||
|
||||
return selectedPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method detectMaskFromBitmap
|
||||
* @description Analyzes bitmap data to extract mask information
|
||||
* @returns {MaskInfo} Analyzed mask data including center and edge points
|
||||
*/
|
||||
detectMaskFromBitmap() {
|
||||
const imageData = this.inputCtx.getImageData(0, 0, this.width, this.height);
|
||||
const pixels = imageData.data;
|
||||
|
||||
// Find mask boundaries and center
|
||||
const center = this.calculateMaskCentroid(pixels);
|
||||
const edges = this.detectEdgePoints(pixels, center);
|
||||
|
||||
// Store and return mask info
|
||||
this.maskInfo = {
|
||||
center: center,
|
||||
edgePoints: this.redistributePoints(edges, 100) // Maintain consistent point count
|
||||
};
|
||||
|
||||
return this.maskInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @method calculateMaskCentroid
|
||||
* @description Calculates the centroid (center of mass) of the mask
|
||||
* @param {Uint8ClampedArray} pixels - Canvas pixel data
|
||||
* @returns {Object} Center point {x, y}
|
||||
*/
|
||||
calculateMaskCentroid(pixels) {
|
||||
let sumX = 0, sumY = 0, total = 0;
|
||||
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
const idx = (y * this.width + x) * 4;
|
||||
// Check if pixel is black (part of mask)
|
||||
if (pixels[idx] === 0) {
|
||||
sumX += x;
|
||||
sumY += y;
|
||||
total++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
x: sumX / total,
|
||||
y: sumY / total
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @method detectEdgePoints
|
||||
* @description Detects edge points of the mask using a simple edge detection algorithm
|
||||
* @param {Uint8ClampedArray} pixels - Canvas pixel data
|
||||
* @returns {Array<Object>} Array of edge points
|
||||
*/
|
||||
detectEdgePoints(pixels, center) {
|
||||
const edges = [];
|
||||
|
||||
// Scan from each direction to find edges
|
||||
// Top to bottom
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
let lastPixel = 255; // Start with white
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
const idx = (y * this.width + x) * 4;
|
||||
const currentPixel = pixels[idx];
|
||||
if (lastPixel !== currentPixel) {
|
||||
edges.push({ x, y });
|
||||
}
|
||||
lastPixel = currentPixel;
|
||||
}
|
||||
}
|
||||
|
||||
// Left to right
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
let lastPixel = 255; // Start with white
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
const idx = (y * this.width + x) * 4;
|
||||
const currentPixel = pixels[idx];
|
||||
if (lastPixel !== currentPixel) {
|
||||
edges.push({ x, y });
|
||||
}
|
||||
lastPixel = currentPixel;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
const uniqueEdges = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const point of edges) {
|
||||
const key = `${point.x},${point.y}`;
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
uniqueEdges.push(point);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort points clockwise around the center
|
||||
uniqueEdges.sort((a, b) => {
|
||||
const angleA = Math.atan2(a.y - center.y, a.x - center.x);
|
||||
const angleB = Math.atan2(b.y - center.y, b.x - center.x);
|
||||
return angleA - angleB;
|
||||
});
|
||||
|
||||
return uniqueEdges;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method updateCanvasDimensions
|
||||
* @description Updates the dimensions of both internal and DOM canvases
|
||||
* @param {number} width - New width for the canvases
|
||||
* @param {number} height - New height for the canvases
|
||||
*/
|
||||
updateCanvasDimensions(width, height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
||||
// Update internal canvases
|
||||
this.inputCanvas.width = width;
|
||||
this.inputCanvas.height = height;
|
||||
this.outputCanvas.width = width;
|
||||
this.outputCanvas.height = height;
|
||||
|
||||
// Update DOM canvases
|
||||
this.domInputCanvas.width = width;
|
||||
this.domInputCanvas.height = height;
|
||||
this.domOutputCanvas.width = width;
|
||||
this.domOutputCanvas.height = height;
|
||||
|
||||
// Update display styles
|
||||
this.domInputCanvas.style.width = `${width}px`;
|
||||
this.domInputCanvas.style.height = `${height}px`;
|
||||
this.domOutputCanvas.style.width = `${width}px`;
|
||||
this.domOutputCanvas.style.height = `${height}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method loadMask
|
||||
* @description Primary entry point for processing an existing mask image.
|
||||
* Handles the complete workflow of loading, detecting edges, and generating effects.
|
||||
* @param {HTMLImageElement} img - The mask image to process
|
||||
*/
|
||||
loadMask(img) {
|
||||
// Update canvas dimensions
|
||||
this.updateCanvasDimensions(img.width, img.height);
|
||||
|
||||
// Clear the input canvas
|
||||
this.inputCtx.clearRect(0, 0, this.width, this.height);
|
||||
|
||||
// Draw the image onto the input canvas
|
||||
this.inputCtx.drawImage(img, 0, 0, img.width, img.height);
|
||||
|
||||
// Detect the mask from the bitmap
|
||||
this.detectMaskFromBitmap();
|
||||
|
||||
// Process the mask
|
||||
this.processMask();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @private
|
||||
// * @method generateOverlayData
|
||||
// * @description Generates complete overlay effect data structure
|
||||
// * @returns {OverlayEffect} Generated overlay effect data
|
||||
// * @private
|
||||
// */
|
||||
// generateOverlayData() {
|
||||
// if (!this.maskInfo) {
|
||||
// this.generateRandomMask();
|
||||
// }
|
||||
|
||||
// const totalEdgePoints = this.maskInfo.edgePoints.length;
|
||||
// const step = totalEdgePoints >= this.numLines ?
|
||||
// Math.floor(totalEdgePoints / this.numLines) : 1;
|
||||
|
||||
// const radiatingEffects = [];
|
||||
|
||||
// // Generate data for each radiating effect
|
||||
// for (let i = 0; i < totalEdgePoints; i += step) {
|
||||
// const point = this.maskInfo.edgePoints[i];
|
||||
// const dx = point.x - this.maskInfo.center.x;
|
||||
// const dy = point.y - this.maskInfo.center.y;
|
||||
// const length = Math.sqrt(dx * dx + dy * dy) * this.lineLengthRatio;
|
||||
|
||||
// radiatingEffects.push({
|
||||
// origin: point,
|
||||
// path: {
|
||||
// direction: {
|
||||
// dx: dx / length,
|
||||
// dy: dy / length
|
||||
// },
|
||||
// length: length,
|
||||
// segments: [] // Would be populated based on effect type
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// return {
|
||||
// baseShape: {
|
||||
// center: this.maskInfo.center,
|
||||
// boundaryPoints: this.maskInfo.edgePoints,
|
||||
// bounds: {
|
||||
// width: this.width,
|
||||
// height: this.height
|
||||
// }
|
||||
// },
|
||||
// radiatingEffects
|
||||
// };
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * @method setColors
|
||||
// * @description Sets the base colors for strokes and decorative elements
|
||||
// * @param {string} strokeColor - Color for line strokes
|
||||
// * @param {string} elementColor - Color for decorative elements
|
||||
// */
|
||||
// setColors(strokeColor, elementColor) {
|
||||
// this.strokeColor = strokeColor;
|
||||
// this.elementColor = elementColor;
|
||||
// }
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = MaskProcessor;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user