This commit is contained in:
ryanontheinstide
2024-08-23 22:16:23 -04:00
parent 17724fdc05
commit b4361a39b9
35 changed files with 2922 additions and 714 deletions
+147 -40
View File
@@ -1,3 +1,45 @@
from .node_configs import CombinedMeta
from collections import OrderedDict
class RyanOnTheInside(metaclass=CombinedMeta):
@classmethod
def get_description(cls):
# Get the display name from NODE_DISPLAY_NAME_MAPPINGS, fallback to cls.__name__ if not found
display_name = NODE_DISPLAY_NAME_MAPPINGS.get(cls.__name__, cls.__name__)
footer = "For more information, visit [RyanOnTheInside GitHub](https://github.com/ryanontheinside)."
# Remove the " | RyanOnTheInside" suffix if present
display_name = display_name.replace(" | RyanOnTheInside", "")
desc = f"# {display_name}\n\n"
if hasattr(cls, 'DESCRIPTION'):
desc += f"{cls.DESCRIPTION}\n\n{footer}"
return desc
if hasattr(cls, 'TOP_DESCRIPTION'):
desc += f"{cls.TOP_DESCRIPTION}\n\n"
if hasattr(cls, "BASE_DESCRIPTION"):
desc += cls.BASE_DESCRIPTION + "\n\n"
# Use OrderedDict to maintain order and avoid duplication
additional_info = OrderedDict()
for c in cls.mro()[::-1]: # Reverse method resolution order
if hasattr(c, 'ADDITIONAL_INFO'):
info = c.ADDITIONAL_INFO.strip()
# Use the class name as a key to avoid duplication
additional_info[c.__name__] = info
# Join all unique ADDITIONAL_INFO entries
if additional_info:
desc += "\n\n".join(additional_info.values()) + "\n\n"
if hasattr(cls, 'BOTTOM_DESCRIPTION'):
desc += f"{cls.BOTTOM_DESCRIPTION}\n\n"
desc += footer
return desc
from .nodes.masks.temporal_masks import (
MaskMorph,
MaskTransform,
@@ -5,23 +47,34 @@ from .nodes.masks.temporal_masks import (
MaskRings,
MaskWarp,
)
from .nodes.audio.audio_nodes import (
AudioSeparator,
AudioFeatureVisualizer,
FrequencyFilterCustom,
FrequencyFilterPreset,
AudioFilter,
)
from .nodes.masks.audio_masks import (
AudioMaskMorph,
AudioMaskWarp,
AudioMaskTransform,
AudioMaskMath
from .nodes.flex.feature_extractors import(
MIDILoadAndExtract,
AudioFeatureExtractor,
TimeFeatureNode,
DepthFeatureNode,
)
from .nodes.masks.flex_masks import (
FlexMaskMorph,
FlexMaskWarp,
FlexMaskTransform,
FlexMaskMath
)
from .nodes.masks.optical_flow_masks import (
OpticalFlowMaskModulation,
OpticalFlowDirectionMask,
OpticalFlowParticleSystem
OpticalFlowParticleSystem,
)
from .nodes.masks.particle_system_masks import (
@@ -37,7 +90,21 @@ from .nodes.masks.particle_system_masks import (
ParticleSpeedModulation,
)
from .nodes.images.image_utility_nodes import DyeImage, ColorPicker
from .nodes.masks.utility_nodes import _mfc, TextMaskNode, MovingShape
import os
import folder_paths
# Get the directory of the current file
current_dir = os.path.dirname(os.path.realpath(__file__))
# Register the midi_files directory
midi_path = os.path.join(current_dir, "data/midi_files")
folder_paths.add_model_folder_path("midi_files", midi_path)
# Ensure the MIDI files directory exists
os.makedirs(midi_path, exist_ok=True)
NODE_CLASS_MAPPINGS = {
"MaskMorph": MaskMorph,
@@ -64,55 +131,95 @@ NODE_CLASS_MAPPINGS = {
"ParticleSpeedModulation":ParticleSpeedModulation,
"AudioMaskMorph":AudioMaskMorph,
"AudioMaskWarp": AudioMaskWarp,
"AudioMaskTransform":AudioMaskTransform,
"AudioMaskMath": AudioMaskMath,
"FlexMaskMorph": FlexMaskMorph,
"FlexMaskWarp": FlexMaskWarp,
"FlexMaskTransform": FlexMaskTransform,
"FlexMaskMath": FlexMaskMath,
"AudioSeparator": AudioSeparator,
"AudioFeatureExtractor":AudioFeatureExtractor,
#"AudioFeatureVisualizer":AudioFeatureVisualizer,
"FrequencyFilterCustom": FrequencyFilterCustom,
"FrequencyFilterPreset": FrequencyFilterPreset,
"AudioFilter":AudioFilter,
"MIDILoadAndExtract":MIDILoadAndExtract,
"TimeFeatureNode":TimeFeatureNode,
"DepthFeatureNode": DepthFeatureNode ,
"MovingShape": MovingShape,
"_mfc":_mfc,
"TextMaskNode":TextMaskNode,
"ColorPicker": ColorPicker,
"DyeImage": DyeImage,
}
WEB_DIRECTORY = "./web/js"
NODE_DISPLAY_NAME_MAPPINGS = {
"MaskMorph": "Temporal Mask Morph | RyanOnTheInside",
"MaskTransform":"Temporal Mask Transform | RyanOnTheInside",
"MaskMath":"Temporal Mask Math | RyanOnTheInside",
"MaskRings":"Temporal Mask Rings | RyanOnTheInside",
"MaskWarp":"Temporal Mask Warp | RyanOnTheInside",
"MaskMorph": "Temporal Mask Morph",
"MaskTransform":"Temporal Mask Transform",
"MaskMath":"Temporal Mask Math",
"MaskRings":"Temporal Mask Rings",
"MaskWarp":"Temporal Mask Warp",
"OpticalFlowMaskModulation": "Optical Flow Mask Modulation | RyanOnTheInside",
"OpticalFlowParticleSystem":"Optical Flow Particle System | RyanOnTheInside",
#"OpticalFlowDirectionMask":"Optical Flow Direction Mask | RyanOnTheInside",
"OpticalFlowMaskModulation": "Optical Flow Mask Modulation",
"OpticalFlowParticleSystem":"Optical Flow Particle System",
#"OpticalFlowDirectionMask":"Optical Flow Direction Mask",
"ParticleEmissionMask":"Particle Emission Mask | RyanOnTheInside",
"Vortex": "Vortex | RyanOnTheInside",
"GravityWell":"Gravity Well | RyanOnTheInside",
"ParticleEmitter": "Particle Emitter | RyanOnTheInside",
"EmitterMovement":"Emitter Movement | RyanOnTheInside",
"SpringJointSetting":"Spring Joint Setting | RyanOnTheInside",
"StaticBody":"Static Body | RyanOnTheInside",
"ParticleColorModulation":"Particle Color Modulation | RyanOnTheInside",
"ParticleSizeModulation": "Particle Size Modulation | RyanOnTheInside",
"ParticleSpeedModulation":"Particle Speed Modulation | RyanOnTheInside",
"ParticleEmissionMask":"Particle Emission Mask",
"Vortex": "Vortex",
"GravityWell":"Gravity Well",
"ParticleEmitter": "Particle Emitter",
"EmitterMovement":"Emitter Movement",
"SpringJointSetting":"Spring Joint Setting",
"StaticBody":"Static Body",
"ParticleColorModulation":"Particle Color Modulation",
"ParticleSizeModulation": "Particle Size Modulation",
"ParticleSpeedModulation":"Particle Speed Modulation",
"AudioMaskMorph":"Audio Mask Morph | RyanOnTheInside",
"AudioMaskWarp": "Audio Mask Warp | RyanOnTheInside",
"AudioMaskTransform":"Audio Mask Transform | RyanOnTheInside",
"AudioMaskMath": "Audio Mask Math | RyanOnTheInside",
"AudioSeparator": "Audio Separator | RyanOnTheInside",
#"AudioFeatureVisualizer": "Audio Feature Visualizer | RyanOnTheInside" ,
"Frequency Filter Custom": "Frequency Filter Custom | RyanOnTheInside",
"Frequency Filter Preset": "Frequency Filter Preset | RyanOnTheInside",
"AudioFilter": "Audio Filter | RyanOnTheInside",
"AudioMaskMorph":"Audio Mask Morph",
"AudioMaskWarp": "Audio Mask Warp",
"AudioMaskTransform":"Audio Mask Transform",
"AudioMaskMath": "Audio Mask Math",
"AudioSeparator": "Audio Separator",
"MovingShape": "Moving Shape | RyanOnTheInside",
"TextMaskNode":"Text Mask Node | RyanOnTheInside"
}
#"AudioFeatureVisualizer": "Audio Feature Visualizer" ,
"Frequency Filter Custom": "Frequency Filter Custom",
"Frequency Filter Preset": "Frequency Filter Preset",
"AudioFilter": "Audio Filter",
"MIDILoadAndExtract": "MIDI Load And Feature Extract",
"AudioFeatureExtractor": "Audio Feature Extractor",
"TimeFeatureNode": "Time Feature Node ",
"DepthFeatureNode":"Depth Feature Node",
"MovingShape": "Moving Shape",
"TextMaskNode":"Text Mask Node",
"DyeImage" : "Dye Image"
}
suffix = " | RyanOnTheInside"
NODE_DISPLAY_NAME_MAPPINGS = {
key: value if value.endswith(suffix) else value + suffix
for key, value in NODE_DISPLAY_NAME_MAPPINGS.items()
}
from aiohttp import web
from server import PromptServer
from pathlib import Path
if hasattr(PromptServer, "instance"):
# NOTE: we add an extra static path to avoid comfy mechanism
# that loads every script in web. KJNodes
PromptServer.instance.app.add_routes(
[web.static("/ryanontheinside_web_async", (Path(__file__).parent.absolute() / "ryanontheinside_web_async").as_posix())]
)
for node_name, node_class in NODE_CLASS_MAPPINGS.items():
if hasattr(node_class, 'get_description'):
desc = node_class.get_description()
node_class.DESCRIPTION = desc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+544
View File
@@ -0,0 +1,544 @@
def add_node_config(node_name, config):
"""
Adds a new node configuration to NODE_CONFIGS.
:param node_name: Name of the node to add or update.
:param config: Dictionary containing the configuration for the node.
"""
NODE_CONFIGS[node_name] = config
NODE_CONFIGS = {
"MaskBase":{
"BASE_DESCRIPTION" : """
##Parameters
- `Masks`: Input mask or sequence of masks to be processed. (you can pass in a blank mask if you want)
- `Strength`: Controls the intensity of the effect (0.0 to 1.0). Higher values make the mask operation more pronounced.
- `Invert`: When enabled, reverses the mask, turning black areas white and vice versa.
- `Subtract Original`: Removes a portion of the original mask from the result (0.0 to 1.0). Higher values create more pronounced edge effects.
- `Grow with Blur`: Expands the mask edges (0.0 to 10.0). Higher values create softer, more expanded edges.
"""
},
"TemporalMaskBase":{
"ADDITIONAL_INFO":"""- `Start Frame`: The frame number where the effect begins (0 to 1000).
- `End Frame`: The frame number where the effect ends (0 to 1000). If set to 0, continues until the last frame.
- `Effect Duration`: Number of frames over which the effect is applied (0 to 1000). If 0, uses (End Frame - Start Frame).
- `Temporal Easing`: Controls how the effect strength changes over time, affecting the smoothness of transitions.
- Options: "ease_in_out", "linear", "bounce", "elastic", "none"
- `Palindrome`: When enabled, the effect plays forward then reverses within the specified duration, creating a back-and-forth motion."""
},
"ParticleSystemMaskBase":{
"ADDITIONAL_INFO":"""
- `particle_count`: Total number of particles in the system (1 to 10000). More particles create denser effects.
- `particle_lifetime`: How long each particle exists in seconds (0.1 to 10.0). Longer lifetimes create more persistent effects.
- `wind_strength`: Power of the wind effect (-100.0 to 100.0). Positive values blow right, negative left.
- `wind_direction`: Angle of the wind in degrees (0.0 to 360.0). 0 is right, 90 is up, etc.
- `gravity`: Strength of downward pull (-1000.0 to 1000.0). Negative values make particles float up.
- `start_frame`: Frame to begin the particle effect (0 to 1000).
- `end_frame`: Frame to stop the particle effect (0 to 1000).
- `respect_mask_boundary`: When enabled, particles stay within the mask's shape.
Optional inputs:
- `emitters`: Particle emitter configurations (PARTICLE_EMITTER type). Define where particles originate.
- `vortices`: Optional vortex configurations (VORTEX type). Create swirling effects.
- `wells`: Optional gravity well configurations (GRAVITY_WELL type). Create areas that attract or repel particles.
- `well_strength_multiplier`: Amplifies the power of gravity wells (0.0 to 10.0). Higher values create stronger attraction/repulsion.
"""
},
"ParticleEmissionMask": {
"TOP_DESCRIPTION": "This is the main node for particle simulations. It creates dynamic, fluid-like effects through particle simulation. Supports multiple particle emitters, force fields (Gravity Well, Vortex), and allows for complex particle behaviors including boundary-respecting particles and static body interactions.",
"ADDITIONAL_INFO": """
- `emission_strength`: Strength of particle emission effect (0.0 to 1.0), basically opacity
- `draw_modifiers`: Visibility of vortices and gravity wells (0.0 to 1.0)
"""
},
"OpticalFlowMaskBase":{
"ADDITIONAL_INFO":"""- `Images`: Input image sequence for optical flow calculation
- `Masks`: Input mask sequence to be processed
- `Strength`: Overall intensity of the effect (0.0 to 1.0). Higher values create more pronounced motion-based effects.
- `Flow Method`: Technique used for optical flow calculation. Each method has different speed/accuracy tradeoffs.
- Options: "Farneback", "LucasKanade", "PyramidalLK"
- `Flow Threshold`: Minimum motion required to trigger the effect (0.0 to 1.0). Higher values ignore subtle movements.
- `Magnitude Threshold`: Relative threshold for flow magnitude (0.0 to 1.0). Higher values focus on areas of stronger motion."""
},
"OpticalFlowMaskModulation":{
"TOP_DESCRIPTION":"""This is currently the main Optical Flow node. Use it to make motion based effects.
""",
"ADDITIONAL_INFO":"""- `Modulation Strength`: Intensity of the modulation effect (0.0 to 5.0). Higher values create more pronounced motion trails.
- `Blur Radius`: Smoothing applied to the flow magnitude (0 to 20 pixels). Larger values create smoother trails.
- `Trail Length`: Number of frames for the trail effect (1 to 20). Longer trails last longer.
- `Decay Factor`: Rate of trail decay over time (0.1 to 1.0). Lower values make trails fade faster.
- `Decay Style`: Method of trail decay.
- Options: "fade" (opacity reduction), "thickness" (width reduction)
- `Max Thickness`: Maximum trail thickness for thickness-based decay (1 to 50 pixels). Larger values create thicker trails."""
},
"OpticalFlowDirectionMask":{
"TOP_DESCRIPTION":"Work in progress"
},
"OpticalFlowParticleSystem":{
"TOP_DESCRIPTION":"Work in progress"
},
"MaskTransform":{
"ADDITIONAL_INFO":"""- `Transform Type`: The type of transformation to apply.
- Options: "translate", "rotate", "scale"
- `X Value`: Horizontal component of the transformation (-1000 to 1000). Positive values move right, negative left.
- `Y Value`: Vertical component of the transformation (-1000 to 1000). Positive values move up, negative down.
""",
"TOP_DESCRIPTION":"""Applies geometric transformations to the mask over time."""},
"MaskMorph": {
"TOP_DESCRIPTION": "Applies morphological operations to the mask, changing its shape over time.",
"ADDITIONAL_INFO": """- `Morph Type`: The type of morphological operation to apply.
- Options: "erode", "dilate", "open", "close"
- `Max Kernel Size`: Maximum size of the morphological kernel (3 to 21, odd numbers only). Larger values create more pronounced effects.
- `Max Iterations`: Maximum number of times to apply the operation (1 to 50). More iterations create more extreme effects."""
},
"MaskMath":{
"TOP_DESCRIPTION":""""Combines two masks using various mathematical operations.""",
"ADDITIONAL_INFO":""""- `Mask B`: Second mask to combine with the input mask.
- `Combination Method`: Mathematical operation to apply.
- Options: "add", "subtract", "multiply", "minimum", "maximum" """
},
"MaskRings":{
"TOP_DESCRIPTION":"""Creates concentric ring patterns based on the distance from the mask edges.""",
"ADDITIONAL_INFO":"""- `Num Rings`: Number of rings to generate (1 to 50). More rings create more detailed patterns.
- `Max Ring Width`: Maximum width of each ring as a fraction of the total distance (0.01 to 0.5). Larger values create wider rings."""
},
"MaskWarp":{
"ADDITIONAL_INFO":"""- `Warp Type`: The type of warping effect to apply. Each creates a different distortion pattern.
- Options: "perlin" (noise-based), "radial" (circular), "swirl" (spiral)
- `Frequency`: Controls the scale of the warping effect (0.01 to 1.0). Higher values create more rapid changes in the warp pattern.
- `Amplitude`: Controls the strength of the warping effect (0.1 to 500.0). Higher values create more extreme distortions.
- `Octaves`: For noise-based warps, adds detail at different scales (1 to 8). More octaves create more complex, detailed patterns.""",
"TOP_DESCRIPTION":"""Applies various warping effects to the mask, creating distortions and movement."""
},
"BaseFeatureNode" : {
"BASE_DESCRIPTION" : "Provides a foundation for creating features that control mask modulation. Features are used to dynamically adjust mask operations over time or based on various inputs."
},
"MIDILoadAndExtract":{
"TOP_DESCRIPTION": "Loads a MIDI file and extracts specified features for mask modulation. To use this, select the notes on the piano that you want to use to control modulations. Many of the different types of information in the notes can be chosen as the driving feature.",
"ADDITIONAL_INFO": """
- `midi_file`: Path to the MIDI file to load and analyze
- `track_selection`: Which track(s) to analyze ("all" or specific track number)
- `attribute`: MIDI attribute to extract (e.g., "Note On/Off", "Pitchbend", "Pitch", "Aftertouch")
- `frame_rate`: Frame rate of the video to sync MIDI data with
- `video_frames`: Corresponding video frames (IMAGE type)
- `chord_only`: When true, only considers full chords (BOOLEAN)
- `notes`: Specific notes to consider (comma-separated string of MIDI note numbers, e.g., "60,64,67" for C major triad)
"""
},
"AudioFilter": {
"TOP_DESCRIPTION": "Applies frequency filters to audio for targeted sound processing.",
"ADDITIONAL_INFO": """
- `audio`: Input audio to be filtered (AUDIO type)
- `filters`: Frequency filters to be applied (FREQUENCY_FILTER type). These determine which frequencies are emphasized or reduced.
"""
},
"FrequencyFilterPreset": {
"TOP_DESCRIPTION": "Creates preset filter chains for common audio processing tasks, simplifying complex audio manipulations.",
"ADDITIONAL_INFO": """
- `preset`: Preset to use (e.g., "isolate_kick_drum" emphasizes low frequencies, "isolate_vocals" focuses on mid-range, "remove_rumble" cuts low frequencies)
Optional inputs:
- `previous_filter`: Previous filter chain to append to, allowing for cumulative effects
"""
},
"FrequencyFilterCustom": {
"TOP_DESCRIPTION": "Creates custom frequency filters.",
"ADDITIONAL_INFO": """
- `filter_type`: Type of filter ("lowpass", "highpass", "bandpass")
- `order`: Filter order (1 to 10)
- `cutoff`: Cutoff frequency (20 to 20000 Hz)
Optional inputs:
- `previous_filter`: Previous filter chain to append to
"""
},
"AudioFeatureVisualizer": {
"TOP_DESCRIPTION": "Visualizes various audio features, creating visual representations of sound characteristics.",
"ADDITIONAL_INFO": """
- `audio`: Input audio to visualize (AUDIO type)
- `video_frames`: Corresponding video frames to overlay visualizations on (IMAGE type)
- `visualization_type`: Type of visualization to generate:
- "waveform": Shows amplitude over time
- "spectrogram": Displays frequency content over time
- "mfcc": Mel-frequency cepstral coefficients, useful for speech recognition
- "chroma": Represents pitch classes, useful for harmonic analysis
- "tonnetz": Tonal space representation
- "spectral_centroid": Shows the "center of mass" of the spectrum over time
- `frame_rate`: Frame rate of the video for synchronization
"""
},
"AudioSeparator":{
"TOP_DESCRIPTION": "Separates an input audio track into its component parts using the Open-Unmix model.",
"ADDITIONAL_INFO": """
- `audio`: Input audio to be separated (AUDIO type)
- `video_frames`: Corresponding video frames (IMAGE type)
- `frame_rate`: Frame rate of the video for synchronization
Outputs:
- Original audio
- Isolated drums track
- Isolated vocals track
- Isolated bass track
- Isolated other instruments track
- FeaturePipe containing frame information
This node uses the Open-Unmix model to separate the input audio into four stems: drums, vocals, bass, and other instruments. Each separated track is returned as an individual AUDIO type output, along with the original audio and a FeaturePipe for further processing.
"""
},
"SpringJointSetting": {
"TOP_DESCRIPTION": "Defines the behavior of spring joints attached to particles.",
"ADDITIONAL_INFO": """
- `stiffness`: Stiffness of the spring (0.0 to 1000.0). Higher values create stronger connections.
- `damping`: Damping factor of the spring (0.0 to 100.0). Higher values create more resistance to motion.
- `rest_length`: Rest length of the spring (0.0 to 100.0). Longer springs allow for more stretching.
- `max_distance`: Maximum distance the spring can stretch (0.0 to 500.0). Larger values allow for more elasticity.
"""
},
"StaticBody": {
"TOP_DESCRIPTION": "Defines static bodies in the simulation that particles can interact with (think walls, barrier, ramps, etc.).",
"ADDITIONAL_INFO": """
- `shape_type`: Type of shape ("segment" or "polygon")
- `x1`, `y1`, `x2`, `y2`: Coordinates defining the shape
- `elasticity`: Bounciness of the static body (0.0 to 1.0). Higher values create more bouncy collisions.
- `friction`: Friction of the static body (0.0 to 1.0). Higher values create more resistance to motion.
- `draw`: Whether to visualize the static body and how thick
- `color`: Color of the static body (RGB tuple)
"""
},
"GravityWell": {
"TOP_DESCRIPTION": "An optional input for a simulation space. These can be chained together to add many to a simulation.",
"ADDITIONAL_INFO": """
- `x`: X-coordinate of the gravity well (0.0 to 1.0)
- `y`: Y-coordinate of the gravity well (0.0 to 1.0)
- `strength`: Strength of the gravity well. Higher values create stronger attraction or repulsion.
- `radius`: Radius of effect for the gravity well. Larger values affect a wider area.
- `type`: Type of gravity well ('attract' or 'repel'). Attract pulls particles in, repel pushes them away.
- `color`: Color of the gravity well visualization (RGB tuple)
- `draw`: Thickness of the gravity well visualization (0.0 to 1.0)
"""
},
"Vortex": {
"TOP_DESCRIPTION": "An optional input for a simulation space. These can be chained together to add many to a simulation",
"ADDITIONAL_INFO": """
- `x`: X-coordinate of the vortex center (0.0 to 1.0)
- `y`: Y-coordinate of the vortex center (0.0 to 1.0)
- `strength`: Strength of the vortex effect (0.0 to 1000.0). Higher values create stronger swirling motion.
- `radius`: Radius of effect for the vortex (10.0 to 500.0). Larger values create wider swirling areas.
- `inward_factor`: Factor controlling how quickly particles are pulled towards the center (0.0 to 1.0). Higher values create tighter spirals.
- `movement_speed`: Speed of movement of the vortex object (0.0 to 10.0). Higher values make the vortex move faster in the simulation space.
- `color`: Color of the vortex visualization (RGB tuple)
- `draw`: Thickness of the vortex visualization (0.0 to 1.0)
"""
},
"ParticleModulationBase": {
"TOP_DESCRIPTION": "Base class for particle modulation settings.",
"ADDITIONAL_INFO": """
- `start_frame`: Frame to start the modulation effect (0 to 1000)
- `end_frame`: Frame to end the modulation effect (0 to 1000)
- `effect_duration`: Duration of the modulation effect in frames (0 to 1000)
- `temporal_easing`: Easing function for the modulation effect ("ease_in_out", "linear", "bounce", "elastic", "none")
- `palindrome`: Whether to reverse the modulation effect after completion (True/False)
"""
},
"ParticleSizeModulation": {
"TOP_DESCRIPTION": "Modulates particle size over time.",
"ADDITIONAL_INFO": """
- `target_size`: Target size for particles at the end of the modulation (0.0 to 400.0)
"""
},
"ParticleSpeedModulation": {
"TOP_DESCRIPTION": "Modulates particle speed over time.",
"ADDITIONAL_INFO": """
- `target_speed`: Target speed for particles at the end of the modulation (0.0 to 1000.0)
"""
},
"ParticleColorModulation": {
"TOP_DESCRIPTION": "Modulates particle color over time.",
"ADDITIONAL_INFO": """
- `target_color`: Target color for particles at the end of the modulation (RGB tuple)
"""
},
"FlexMaskBase": {
"BASE_DESCRIPTION": """
Provides a base for mask operations modulated by various features.
## Common Parameters
- `feature`: The feature used to modulate the mask operation (FEATURE type)
- `feature_pipe`: The feature pipe containing frame information (FEATURE_PIPE type)
- `feature_threshold`: Threshold for feature activation (0.0 to 1.0)
Optional inputs:
- `feature`: The feature used to modulate the mask operation (FEATURE type)
- `feature_pipe`: The feature pipe containing frame information (FEATURE_PIPE type)
"""
},
"FlexMaskMorph": {
"TOP_DESCRIPTION": "Applies morphological operations to the mask, modulated by a selected feature.",
"ADDITIONAL_INFO": """
- `morph_type`: The type of morphological operation to apply.
- Options: "erode", "dilate", "open", "close"
- `max_kernel_size`: Maximum size of the morphological kernel (3 to 21, odd numbers only). Larger values create more pronounced effects.
- `max_iterations`: Maximum number of times to apply the operation (1 to 50). More iterations create more extreme effects.
The strength of the morphological operation is determined by the selected feature's value at each frame.
"""
},
"FlexMaskWarp": {
"TOP_DESCRIPTION": "Applies warping effects to the mask, modulated by a selected feature.",
"ADDITIONAL_INFO": """
- `warp_type`: The type of warping effect to apply. Each creates a different distortion pattern.
- Options: "perlin" (noise-based), "radial" (circular), "swirl" (spiral)
- `frequency`: Controls the scale of the warping effect (0.01 to 1.0). Higher values create more rapid changes in the warp pattern.
- `max_amplitude`: Maximum amplitude of the warping effect (0.1 to 500.0). Higher values create more extreme distortions.
- `octaves`: For noise-based warps, adds detail at different scales (1 to 8). More octaves create more complex, detailed patterns.
The intensity of the warping effect is determined by the selected feature's value at each frame.
"""
},
"FlexMaskTransform": {
"TOP_DESCRIPTION": "Applies geometric transformations to the mask, modulated by a selected feature.",
"ADDITIONAL_INFO": """
- `transform_type`: The type of transformation to apply.
- Options: "translate", "rotate", "scale"
- `max_x_value`: Maximum horizontal component of the transformation (-1000.0 to 1000.0). Positive values move right, negative left.
- `max_y_value`: Maximum vertical component of the transformation (-1000.0 to 1000.0). Positive values move up, negative down.
The extent of the transformation is determined by the selected feature's value at each frame.
"""
},
"FlexMaskMath": {
"TOP_DESCRIPTION": "Performs mathematical operations between two masks, modulated by a selected feature.",
"ADDITIONAL_INFO": """
- `mask_b`: Second mask to combine with the input mask.
- `combination_method`: Mathematical operation to apply.
- Options: "add", "subtract", "multiply", "minimum", "maximum"
The strength of the combination is determined by the selected feature's value at each frame.
"""
},
"BaseFeatureNode": {
"BASE_DESCRIPTION": """
Features are used to modulate mask operations in FlexMask nodes.
## Common Parameters
- `frame_rate`: Frame rate of the video
- `frame_count`: Total number of frames
"""
},
"TimeFeatureNode": {
"TOP_DESCRIPTION": "Produces a feature that changes over time based on the selected effect type. This can be used to create dynamic, time-varying mask modulations.",
"ADDITIONAL_INFO": """
- `effect_type`: Type of time-based pattern to apply.
- Options: "smooth" (gradual), "accelerate" (speeds up), "pulse" (rhythmic), "sawtooth" (repeating ramp), "bounce" (up and down)
- `speed`: How quickly the effect progresses (0.1 to 10.0, default: 1.0). Higher values create faster changes.
- `offset`: Shifts the starting point of the effect (0.0 to 1.0, default: 0.0). Useful for staggering multiple effects.
"""
},
"AudioFeatureNode": {
"TOP_DESCRIPTION": "Analyzes the input audio to extract the specified feature. This feature can then be used to modulate masks based on audio characteristics.",
"ADDITIONAL_INFO": """
- `audio`: Input audio to analyze (AUDIO type)
- `feature_type`: Type of audio feature to extract.
- Options: "amplitude_envelope", "rms_energy", "spectral_centroid", "onset_detection", "chroma_features"
"""
},
"DepthFeatureNode": {
"TOP_DESCRIPTION": "Analyzes the input depth maps to extract the specified depth-related feature. This feature can be used to modulate masks based on depth information in the scene.",
"ADDITIONAL_INFO": """
- `depth_maps`: Input depth maps to analyze (IMAGE type)
- `feature_type`: Type of depth feature to extract.
- Options: "mean_depth", "depth_variance", "depth_range", "gradient_magnitude", "foreground_ratio", "midground_ratio", "background_ratio"
"""
},
# "FeatureExtractorBase": {
# "BASE_DESCRIPTION": """
# These nodes are responsible for extracting or generating features that can be used to modulate masks in FlexMask nodes.
# """
# },
"AudioFeatureExtractor": {
"TOP_DESCRIPTION": "Analyzes the input audio to extract the specified feature. The resulting feature can be used to modulate masks based on audio characteristics.",
"ADDITIONAL_INFO": """
- `audio`: Input audio to analyze (AUDIO type)
- `feature_pipe`: Feature pipe for frame information (FEATURE_PIPE type)
- `feature_type`: Type of audio feature to extract
- Options: "amplitude_envelope", "rms_energy", "spectral_centroid", "onset_detection", "chroma_features"
"""
},
"TimeFeatureNode": {
"TOP_DESCRIPTION": "Generates time-based features for mask modulation.",
"ADDITIONAL_INFO": """
- `video_frames`: Input video frames (IMAGE type)
- `frame_rate`: Frame rate of the video
- `effect_type`: Type of time-based pattern to apply
- Options: "smooth" (gradual), "accelerate" (speeds up), "pulse" (rhythmic), "sawtooth" (repeating ramp), "bounce" (up and down)
- `speed`: How quickly the effect progresses (0.1 to 10.0, default: 1.0). Higher values create faster changes.
- `offset`: Shifts the starting point of the effect (0.0 to 1.0, default: 0.0). Useful for staggering multiple effects.
Generates a feature that changes over time based on the selected effect type. This can be used to create dynamic, time-varying mask modulations.
"""
},
"DepthFeatureNode": {
"TOP_DESCRIPTION": "Extracts depth-related features from depth maps for mask modulation.",
"ADDITIONAL_INFO": """
- `video_frames`: Input video frames (IMAGE type)
- `frame_rate`: Frame rate of the video
- `depth_maps`: Input depth maps to analyze (IMAGE type)
- `feature_type`: Type of depth feature to extract
- Options: "mean_depth", "depth_variance", "depth_range", "gradient_magnitude", "foreground_ratio", "midground_ratio", "background_ratio"
Analyzes the input depth maps to extract the specified depth-related feature. This feature can be used to modulate masks based on depth information in the scene.
"""
},
"EmitterMovement": {
"TOP_DESCRIPTION": """These parameters work together to create complex, periodic movements for particle emitters.
By adjusting frequencies and amplitudes, you can achieve various patterns like circles,
figure-eights, or more chaotic motions. The direction parameters add extra dynamism by
altering the angle of particle emission over time.""",
"ADDITIONAL_INFO": """
Position Control:
- `emitter_x_frequency`: How quickly the emitter moves horizontally (0.0 to 10.0). Higher values create faster side-to-side motion.
- `emitter_x_amplitude`: Maximum horizontal distance the emitter moves (0.0 to 0.5). Larger values create wider movements.
- `emitter_y_frequency`: How quickly the emitter moves vertically (0.0 to 10.0). Higher values create faster up-and-down motion.
- `emitter_y_amplitude`: Maximum vertical distance the emitter moves (0.0 to 0.5). Larger values create taller movements.
Direction Control:
- `direction_frequency`: How quickly the emission angle changes (0.0 to 10.0). Higher values create more rapid direction changes.
- `direction_amplitude`: Maximum angle change in degrees (0.0 to 180.0). Larger values allow for more extreme direction shifts.
"""
},
"ParticleEmitter": {
"TOP_DESCRIPTION": "This node creates a particle emitter with the specified properties. It can be used in conjunction with particle system mask nodes to create complex particle effects. They can be chained together to add many to a given simulation.",
"ADDITIONAL_INFO": """
- `emitter_x`: X-coordinate of the emitter (0.0 to 1.0, left to right)
- `emitter_y`: Y-coordinate of the emitter (0.0 to 1.0, up to down)
- `particle_direction`: Direction of particle emission in degrees (0.0 to 360.0, clockwise)
- `particle_spread`: Spread angle of particle emission in degrees (0.0 to 360.0, clockwise)
- `particle_size`: Size of emitted particles (1.0 to 400.0)
- `particle_speed`: Speed of emitted particles (1.0 to 1000.0)
- `emission_rate`: Rate of particle emission (0.1 to 100.0)
- `color`: Color of emitted particles (RGB string)
- `initial_plume`: Initial burst of particles (0.0 to 1.0)
- `start_frame`: Frame to start the emission (0 to 10000)
- `end_frame`: Frame to end the emission (0 to 10000)
Optional inputs:
- `emitter_movement`: Movement settings for the emitter (EMITTER_MOVEMENT type)
- `spring_joint_setting`: Spring joint configuration for particles (SPRING_JOINT_SETTING type)
- `particle_modulation`: Modulation settings for particle properties over time (PARTICLE_MODULATION type)
"""
},
"MovingShape": {
"TOP_DESCRIPTION": "Generate animated mask sequences featuring a moving shape with customizable parameters.",
"ADDITIONAL_INFO": """
- `frame_width`: Width of each frame (1-3840 pixels)
- `frame_height`: Height of each frame (1-2160 pixels)
- `num_frames`: Number of frames in the sequence (1-120)
- `rgb`: Color of the shape in RGB format, e.g., "(255,255,255)"
- `shape`: Shape type ("square", "circle", or "triangle")
- `shape_width_percent`: Width of the shape as a percentage of frame width (0-100%)
- `shape_height_percent`: Height of the shape as a percentage of frame height (0-100%)
- `shape_start_position_x`: Starting X position of the shape (-100 to 100)
- `shape_start_position_y`: Starting Y position of the shape (-100 to 100)
- `shape_end_position_x`: Ending X position of the shape (-100 to 100)
- `shape_end_position_y`: Ending Y position of the shape (-100 to 100)
- `movement_type`: Type of movement ("linear", "ease_in_out", "bounce", or "elastic")
- `grow`: Growth factor of the shape during animation (0-100)
- `palindrome`: Whether to reverse the animation sequence (True/False)
- `delay`: Number of static frames at the start (0-60)
This node creates a mask sequence with a moving shape, allowing for various animations and transformations.
"""
},
"TextMaskNode": {
"TOP_DESCRIPTION": "Generate mask and image sequences featuring customizable text.",
"ADDITIONAL_INFO": """
- `width`: Width of the output image (1-8192 pixels)
- `height`: Height of the output image (1-8192 pixels)
- `text`: The text to be rendered
- `font`: Font to be used (selectable from system fonts)
- `font_size`: Size of the font (1-1000)
- `font_color`: Color of the text in RGB format, e.g., "(255,255,255)"
- `background_color`: Color of the background in RGB format, e.g., "(0,0,0)"
- `x_position`: Horizontal position of the text (0.0-1.0, where 0.5 is center)
- `y_position`: Vertical position of the text (0.0-1.0, where 0.5 is center)
- `rotation`: Rotation angle of the text (0-360 degrees)
- `max_width_ratio`: Maximum width of text as a ratio of image width (0.1-1.0)
- `batch_size`: Number of images to generate in the batch (1-10000)
"""
},
"_mfc": {
"TOP_DESCRIPTION": "Basic mask from color.",
"ADDITIONAL_INFO": """
This is an abstract base class that provides common functionality for mask function components.
It is not meant to be used directly but serves as a foundation for other mask-related nodes.
Key features:
- Implements common methods for mask operations
- Provides a structure for derived classes to follow
- Ensures consistency across different mask function components
"""}
}
from abc import ABCMeta
class NodeConfigMeta(type):
def __new__(cls, name, bases, attrs):
new_class = super().__new__(cls, name, bases, attrs)
if name in NODE_CONFIGS:
for key, value in NODE_CONFIGS[name].items():
setattr(new_class, key, value)
return new_class
class CombinedMeta(NodeConfigMeta, ABCMeta):
pass
+23 -10
View File
@@ -9,8 +9,16 @@ from .audio_utils import AudioVisualizer
from scipy import signal
import logging
from ... import RyanOnTheInside
from ..flex.feature_pipe import FeaturePipe
from ... import RyanOnTheInside
class AudioSeparator:
class AudioNodeBase(RyanOnTheInside):
CATEGORY= "RyanOnTheInside/Audio"
class AudioSeparator(AudioNodeBase):
@classmethod
def INPUT_TYPES(cls):
return {
@@ -21,11 +29,10 @@ class AudioSeparator:
}
}
RETURN_TYPES = ("AUDIO", "IMAGE", "AUDIO", "AUDIO", "AUDIO","AUDIO")
RETURN_NAMES = ("audio", "video_frames","drums_audio", "vocals_audio", "bass_audio", "other_audio")
RETURN_TYPES = ("AUDIO", "AUDIO", "AUDIO", "AUDIO", "AUDIO", "FEATURE_PIPE")
RETURN_NAMES = ("audio", "drums_audio", "vocals_audio", "bass_audio", "other_audio", "feature_pipe")
FUNCTION = "process_audio"
def __init__(self):
self.separator = openunmix.umxl(targets=['drums', 'vocals', 'bass', 'other'], device='cpu')
@@ -58,27 +65,34 @@ class AudioSeparator:
'frame_rate': frame_rate
}
# Create FeaturePipe
feature_pipe = FeaturePipe(frame_rate, video_frames)
return (
audio,
video_frames,
isolated_audio['drums'],
isolated_audio['vocals'],
isolated_audio['bass'],
isolated_audio['other'],
feature_pipe,
)
class AudioFilter:
class AudioFilter(AudioNodeBase):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"audio": ("AUDIO",),
"filters": ("FREQUENCY_FILTER",),
"descrip": ("STRING", {"default":cls.DESCRIPTION})
},
}
RETURN_TYPES = ("AUDIO",)
FUNCTION = "apply_filters"
DESCRIPTION = """Applies frequency filters to audio:
- `audio`: Input audio to be filtered
- `filters`: Frequency filters to be applied (FREQUENCY_FILTER type)"""
def apply_filters(self, audio, filters):
audio_np = audio['waveform'].cpu().numpy().squeeze(0)
@@ -119,7 +133,7 @@ class AudioFilter:
return signal.lfilter(b, a, audio)
class FrequencyFilterPreset:
class FrequencyFilterPreset(AudioNodeBase):
@classmethod
def INPUT_TYPES(cls):
return {
@@ -193,7 +207,7 @@ class FrequencyFilterPreset:
else:
raise ValueError(f"Unknown preset: {preset}")
class FrequencyFilterCustom:
class FrequencyFilterCustom(AudioNodeBase):
@classmethod
def INPUT_TYPES(cls):
return {
@@ -226,8 +240,7 @@ class FrequencyFilterCustom:
else:
return ([filter_params],)
class AudioFeatureVisualizer:
class AudioFeatureVisualizer(AudioNodeBase):
@classmethod
def INPUT_TYPES(cls):
return {
+10 -1
View File
@@ -4,6 +4,15 @@ import librosa
import cv2
import matplotlib.pyplot as plt
import numpy as np
import librosa
import torch
import cv2
import matplotlib.pyplot as plt
class BaseAudioProcessor:
def __init__(self, audio, num_frames, height, width, frame_rate):
self.audio = audio['waveform'].squeeze(0).mean(axis=0).cpu().numpy() # Convert to mono and numpy array
@@ -174,7 +183,7 @@ class AudioFeatureExtractor(BaseAudioProcessor):
raise ValueError("Unsupported feature type")
#TODO HANDLE NO MASKS
#TODO HANDLE FRAME MASK COUNT MISMATCH OR GET RID OF FRAMES
#TODO HANDLE FRAME MASK COUNT MISMATCH
def _amplitude_envelope(self):
return np.array([np.max(np.abs(self._get_audio_frame(i))) for i in range(self.num_frames)])
View File
View File
+239
View File
@@ -0,0 +1,239 @@
import mido
import os
import json
from .feature_pipe import FeaturePipe
import folder_paths
from server import PromptServer
from aiohttp import web
import shutil
import numpy as np
from ... import RyanOnTheInside
from .features import AudioFeature, TimeFeature, DepthFeature
from .midi_feature import MIDIFeature
class FeatureExtractorBase(RyanOnTheInside):
CATEGORY="RyanOnTheInside/FlexFeatures"
class AudioFeatureExtractor(FeatureExtractorBase):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"audio": ("AUDIO",),
"feature_pipe": ("FEATURE_PIPE",),
"feature_type": (["amplitude_envelope", "spectral_centroid", "onset_detection", "chroma_features"],),
}
}
RETURN_TYPES = ("FEATURE", "FEATURE_PIPE")
FUNCTION = "extract_feature"
def extract_feature(self, audio, feature_pipe, feature_type):
feature = AudioFeature(feature_type, audio, feature_pipe.frame_count, feature_pipe.frame_rate, feature_type)
return (feature, feature_pipe)
class MIDILoadAndExtract(FeatureExtractorBase):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"midi_file": (folder_paths.get_filename_list("midi_files"),),
"track_selection": (["all"],), # This will be dynamically updated
"attribute": (MIDIFeature.get_attribute_names(), {"default": "Note On/Off"}),
"frame_rate": ("FLOAT", {"default": 30, "min": 0.1, "max": 120, "step": 0.1}),
"video_frames": ("IMAGE",),
"chord_only": ("BOOLEAN", {"default": False}),
"notes": ("STRING", {"default": ""}),
},
}
RETURN_TYPES = ("MIDI", "FEATURE", "FEATURE_PIPE")
FUNCTION = "process_midi"
CATEGORY = "RyanOnTheInside/Audio"
def process_midi(self, midi_file, track_selection, notes, attribute, frame_rate, video_frames, chord_only=False):
try:
midi_path = folder_paths.get_full_path("midi_files", midi_file)
if not midi_path or not os.path.exists(midi_path):
raise FileNotFoundError(f"MIDI file not found: {midi_file}")
midi_data = mido.MidiFile(midi_path)
selected_notes = [int(n.strip()) for n in notes.split(',') if n.strip().isdigit()]
feature_pipe = FeaturePipe(frame_rate, video_frames)
# Convert friendly attribute name to internal attribute name
internal_attribute = MIDIFeature.get_attribute_value(attribute)
feature = MIDIFeature(f"midi_{internal_attribute}", midi_data, internal_attribute,
feature_pipe.frame_rate, feature_pipe.frame_count,
notes=selected_notes, chord_only=chord_only)
feature.extract()
return (midi_data, feature, feature_pipe)
except Exception as e:
# error_msg = f"Error in MIDILoadAndExtract.process_midi: {type(e).__name__}: {str(e)}\n"
# error_msg += traceback.format_exc()
# print(error_msg)
raise RuntimeError(f"Error processing MIDI file: {type(e).__name__}: {str(e)}")
@classmethod
def analyze_midi(cls, midi_path):
midi_data = mido.MidiFile(midi_path)
tracks = ["all"]
all_notes = set()
track_notes = {}
for i, track in enumerate(midi_data.tracks):
track_notes[str(i)] = set()
for msg in track:
if msg.type == 'note_on':
track_notes[str(i)].add(msg.note)
all_notes.add(msg.note)
if len(track_notes[str(i)]) == 0:
tracks.append(f"{i}: (Empty)")
else:
tracks.append(f"{i}: {track.name or f'Track {i}'}")
return {
"tracks": tracks,
"all_notes": ",".join(map(str, sorted(set(all_notes)))),
"track_notes": {k: ",".join(map(str, sorted(v))) for k, v in track_notes.items()}
}
@classmethod
def VALIDATE_INPUTS(cls, midi_file, track_selection, notes, attribute, frame_rate, video_frames):
if not folder_paths.exists_and_is_file(midi_file):
return "MIDI file not found: {}".format(midi_file)
if notes != "all":
try:
note_list = [int(n.strip()) for n in notes.split(',') if n.strip()]
if not all(0 <= n <= 127 for n in note_list):
return "Invalid note value. All notes must be between 0 and 127."
except ValueError:
return "Invalid notes format. Please provide comma-separated integers or 'all'."
return True
class FirstFeature(FeatureExtractorBase):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"video_frames": ("IMAGE",),
"frame_rate": ("FLOAT", {"default": 30.0, "min": 1.0, "max": 120.0, "step": 0.1}),
}
}
RETURN_TYPES = ("FEATURE", "FEATURE_PIPE")
FUNCTION = "create_feature"
class TimeFeatureNode(FirstFeature):
@classmethod
def INPUT_TYPES(cls):
return {
**super().INPUT_TYPES(),
"required": {
**super().INPUT_TYPES()["required"],
"effect_type": (["smooth", "accelerate", "pulse", "sawtooth", "bounce"],),
"speed": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1}),
"offset": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
}
}
def create_feature(self, effect_type, speed, offset, video_frames, frame_rate):
feature_pipe = FeaturePipe(frame_rate, video_frames)
time_feature = TimeFeature("time_effect", feature_pipe.frame_rate, feature_pipe.frame_count,
effect_type=effect_type, speed=speed, offset=offset)
return (time_feature, feature_pipe)
class DepthFeatureNode(FirstFeature):
@classmethod
def INPUT_TYPES(cls):
return {
**super().INPUT_TYPES(),
"required": {
**super().INPUT_TYPES()["required"],
"depth_maps": ("IMAGE",),
"feature_type": (["mean_depth", "depth_variance", "depth_range", "gradient_magnitude", "foreground_ratio", "midground_ratio", "background_ratio"],),
}
}
def create_feature(self, depth_maps, frame_rate, video_frames, feature_type):
feature_pipe = FeaturePipe(frame_rate, video_frames)
depth_feature = DepthFeature("depth_feature", feature_pipe.frame_rate, feature_pipe.frame_count, depth_maps, feature_type)
return (depth_feature, feature_pipe)
routes = PromptServer.instance.routes
@PromptServer.instance.routes.post('/get_track_notes')
async def get_track_notes(request):
data = await request.json()
midi_file = data.get('midi_file')
if not midi_file:
return web.json_response({"error": "Missing required parameters"}, status=400)
midi_path = folder_paths.get_full_path("midi_files", midi_file)
if not midi_path or not os.path.exists(midi_path):
return web.json_response({"error": "MIDI file not found"}, status=404)
analysis = MIDILoadAndExtract.analyze_midi(midi_path)
return web.json_response(analysis)
@routes.post('/upload_midi')
async def upload_midi(request):
data = await request.post()
midi_file = data['file']
if midi_file and midi_file.filename:
safe_filename = os.path.basename(midi_file.filename)
midi_dir = folder_paths.get_folder_paths("midi_files")[0]
if not os.path.exists(midi_dir):
os.makedirs(midi_dir, exist_ok=True)
midi_path = os.path.join(midi_dir, safe_filename)
with open(midi_path, 'wb') as f:
shutil.copyfileobj(midi_file.file, f)
midi_files = folder_paths.get_filename_list("midi_files")
analysis = MIDILoadAndExtract.analyze_midi(midi_path)
return web.json_response({
"status": "success",
"uploaded_file": safe_filename,
"midi_files": midi_files,
"analysis": analysis
})
else:
return web.json_response({"status": "error", "message": "No file uploaded"}, status=400)
@PromptServer.instance.routes.post('/refresh_midi_data')
async def refresh_midi_data(request):
data = await request.json()
midi_file = data.get('midi_file')
track_selection = data.get('track_selection')
if not midi_file:
return web.json_response({"error": "Missing required parameters"}, status=400)
midi_path = folder_paths.get_full_path("midi_files", midi_file)
if not midi_path or not os.path.exists(midi_path):
return web.json_response({"error": "MIDI file not found"}, status=404)
analysis = MIDILoadAndExtract.analyze_midi(midi_path)
# Filter notes based on track selection
if track_selection != "all":
track_index = track_selection.split(':')[0]
analysis['all_notes'] = analysis['track_notes'].get(track_index, "")
return web.json_response(analysis)
+11
View File
@@ -0,0 +1,11 @@
class FeaturePipe:
def __init__(self, frame_rate, video_frames):
self.frame_rate = frame_rate
self.frame_count = video_frames.shape[0]
self.height = video_frames.shape[1]
self.width = video_frames.shape[2]
def update(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
return self
+179
View File
@@ -0,0 +1,179 @@
from abc import ABC, abstractmethod
import numpy as np
import librosa
from scipy import ndimage
import torch
import torch.nn.functional as F
class BaseFeature(ABC):
def __init__(self, name, feature_type, frame_rate, frame_count):
self.name = name
self.type = feature_type
self.frame_rate = frame_rate
self.frame_count = frame_count
self.data = None
@abstractmethod
def extract(self):
pass
@abstractmethod
def get_value_at_frame(self, frame_index):
pass
def normalize(self):
if self.data is not None:
self.data = (self.data - np.min(self.data)) / (np.max(self.data) - np.min(self.data))
return self
class TimeFeature(BaseFeature):
def __init__(self, name, frame_rate, frame_count, effect_type='smooth', speed=1.0, offset=0.0):
super().__init__(name, "time", frame_rate, frame_count)
self.effect_type = effect_type
self.speed = speed
self.offset = offset
def extract(self):
t = np.linspace(0, self.frame_count / self.frame_rate, self.frame_count)
t = (t * self.speed + self.offset) % 1
if self.effect_type == 'smooth':
self.data = t
elif self.effect_type == 'accelerate':
self.data = t ** 2
elif self.effect_type == 'pulse':
self.data = (np.sin(2 * np.pi * t) + 1) / 2
elif self.effect_type == 'sawtooth':
self.data = t
elif self.effect_type == 'bounce':
self.data = 1 - np.abs(1 - 2 * t)
else:
raise ValueError("Unsupported effect type")
return self.normalize()
def get_value_at_frame(self, frame_index):
if self.data is None:
self.extract()
return self.data[frame_index]
class AudioFeature(BaseFeature):
def __init__(self, name, audio, num_frames, frame_rate, feature_type='amplitude_envelope'):
super().__init__(name, "audio", frame_rate,num_frames)
self.audio = audio['waveform'].squeeze(0).mean(axis=0).cpu().numpy()
self.sample_rate = audio['sample_rate']
self.num_frames = num_frames
self.feature_type = feature_type
self.frame_duration = 1 / self.frame_rate if self.frame_rate > 0 else len(self.audio) / (self.sample_rate * self.num_frames)
def extract(self):
if self.feature_type == 'amplitude_envelope':
self.data = self._amplitude_envelope()
elif self.feature_type == 'rms_energy':
self.data = self._rms_energy()
elif self.feature_type == 'spectral_centroid':
self.data = self._spectral_centroid()
elif self.feature_type == 'onset_detection':
self.data = self._onset_detection()
elif self.feature_type == 'chroma_features':
self.data = self._chroma_features()
else:
raise ValueError("Unsupported feature type")
return self.normalize()
def get_value_at_frame(self, frame_index):
if self.data is None:
self.extract()
return self.data[frame_index]
def _get_audio_frame(self, frame_index):
start_time = frame_index * self.frame_duration
end_time = (frame_index + 1) * self.frame_duration
start_sample = int(start_time * self.sample_rate)
end_sample = int(end_time * self.sample_rate)
return self.audio[start_sample:end_sample]
def _amplitude_envelope(self):
return np.array([np.max(np.abs(self._get_audio_frame(i))) for i in range(self.num_frames)])
def _rms_energy(self):
return np.array([np.sqrt(np.mean(self._get_audio_frame(i)**2)) for i in range(self.num_frames)])
def _spectral_centroid(self):
return np.array([np.mean(librosa.feature.spectral_centroid(y=self._get_audio_frame(i), sr=self.sample_rate)[0]) for i in range(self.num_frames)])
def _onset_detection(self):
return np.array([np.mean(librosa.onset.onset_strength(y=self._get_audio_frame(i), sr=self.sample_rate)) for i in range(self.num_frames)])
def _chroma_features(self):
return np.array([np.mean(librosa.feature.chroma_stft(y=self._get_audio_frame(i), sr=self.sample_rate), axis=1) for i in range(self.num_frames)])
class DepthFeature(BaseFeature):
def __init__(self, name, frame_rate, frame_count, depth_maps, feature_name='mean_depth'):
super().__init__(name, "depth", frame_rate, frame_count)
self.depth_maps = depth_maps # Expected shape: (B, H, W, C)
self.features = None
self.feature_name = feature_name
self.available_features = [
"mean_depth", "depth_variance", "depth_range", "gradient_magnitude",
"foreground_ratio", "midground_ratio", "background_ratio"
]
def extract(self):
self.features = {feature: [] for feature in self.available_features}
combined_depth = torch.mean(self.depth_maps, dim=-1)
for depth_map in combined_depth:
depth_min = torch.min(depth_map)
depth_max = torch.max(depth_map)
if depth_max > depth_min:
normalized_depth = (depth_map - depth_min) / (depth_max - depth_min)
else:
normalized_depth = torch.zeros_like(depth_map)
self.features['mean_depth'].append(torch.mean(normalized_depth).item())
self.features['depth_variance'].append(torch.var(normalized_depth).item())
self.features['depth_range'].append((depth_max - depth_min).item())
grad_y, grad_x = torch.gradient(normalized_depth)
gradient_magnitude = torch.sqrt(grad_x**2 + grad_y**2)
self.features['gradient_magnitude'].append(torch.mean(gradient_magnitude).item())
total_pixels = normalized_depth.numel()
foreground = torch.sum(normalized_depth < 0.33).item() / total_pixels
midground = torch.sum((normalized_depth >= 0.33) & (normalized_depth < 0.66)).item() / total_pixels
background = torch.sum(normalized_depth >= 0.66).item() / total_pixels
self.features['foreground_ratio'].append(foreground)
self.features['midground_ratio'].append(midground)
self.features['background_ratio'].append(background)
for key in self.features:
feature_tensor = torch.tensor(self.features[key])
feature_min = torch.min(feature_tensor)
feature_max = torch.max(feature_tensor)
if feature_max > feature_min:
self.features[key] = ((feature_tensor - feature_min) / (feature_max - feature_min)).tolist()
else:
self.features[key] = torch.zeros_like(feature_tensor).tolist()
return self
def get_value_at_frame(self, frame_index):
if self.features is None:
self.extract()
return self.features[self.feature_name][frame_index]
def get_feature_sequence(self, feature_name=None):
if self.features is None:
self.extract()
if feature_name is None:
feature_name = self.feature_name
return self.features.get(feature_name, None)
def set_active_feature(self, feature_name):
if feature_name in self.available_features:
self.feature_name = feature_name
else:
raise ValueError(f"Invalid feature name. Available features are: {', '.join(self.available_features)}")
+209
View File
@@ -0,0 +1,209 @@
from .features import BaseFeature
import numpy as np
class MIDIFeature(BaseFeature):
ATTRIBUTE_MAP = {
"Velocity": "velocity",
"Pitch": "pitch",
"Note On/Off": "on_off",
"Note Duration": "duration",
"Note Density": "density",
"Pitchbend": "pitchbend",
"Aftertouch": "aftertouch",
"Poly Pressure": "poly_pressure",
"Modulation (CC1)": "cc1",
"Breath (CC2)": "cc2",
"Foot Controller (CC4)": "cc4",
"Volume (CC7)": "cc7",
"Balance (CC8)": "cc8",
"Pan (CC10)": "cc10",
"Expression (CC11)": "cc11",
"Sustain (CC64)": "cc64"
}
def __init__(self, name, midi_data, attribute, frame_rate, frame_count, notes=None, chord_only=False):
super().__init__(name, "midi", frame_rate, frame_count)
self.midi_data = midi_data
self.attribute = attribute
self.notes = set(notes) if notes is not None else set()
self.chord_only = chord_only
self.total_time = self.calculate_total_time()
self.data = None
self.time_points = None
self.cc_data = {cc: [] for cc in [1, 2, 4, 7, 8, 10, 11, 64]}
self.pitchbend_data = []
self.aftertouch_data = []
self.poly_pressure_data = []
def extract(self):
try:
self.data = self.extract_attribute()
except ValueError as e:
print(f"Warning: {str(e)}. Using default values.")
self.data = np.zeros(self.frame_count)
return self.normalize()
def get_value_at_frame(self, frame_index):
if self.data is None:
self.extract()
return self.data[frame_index] if len(self.data) > 0 else 0
def calculate_total_time(self):
total_time = sum(msg.time for msg in self.midi_data if not msg.is_meta)
return max(total_time, self.frame_count / self.frame_rate)
def extract_attribute(self):
attribute_values = []
time_points = []
current_time = 0
note_on_times = {}
active_notes = set()
note_counts = np.zeros(self.frame_count)
modulation_data = []
for msg in self.midi_data:
if msg.is_meta:
continue
current_time += msg.time
frame_index = int(current_time * self.frame_rate)
if frame_index >= self.frame_count:
break
value = self.process_message(msg, current_time, note_on_times, active_notes, note_counts, modulation_data)
if value is not None:
attribute_values.append(value)
time_points.append(current_time)
return self.finalize_attribute(attribute_values, time_points, note_counts, modulation_data)
def process_message(self, msg, current_time, note_on_times, active_notes, note_counts, modulation_data):
frame_index = int(current_time * self.frame_rate)
if msg.type == 'control_change':
if msg.control in self.cc_data:
self.cc_data[msg.control].append((current_time, msg.value / 127.0))
if msg.control == 1:
modulation_data.append((current_time, msg.value / 127.0))
elif msg.type == 'pitchwheel':
self.pitchbend_data.append((current_time, (msg.pitch + 8192) / 16383.0))
elif msg.type == 'aftertouch':
self.aftertouch_data.append((current_time, msg.value / 127.0))
elif msg.type == 'polytouch':
self.poly_pressure_data.append((current_time, msg.value / 127.0))
if msg.type == 'note_on' and (not self.notes or msg.note in self.notes):
note_on_times[msg.note] = current_time
active_notes.add(msg.note)
note_counts[frame_index:] += 1
if not self.chord_only or (self.chord_only and active_notes == self.notes):
return self.get_attribute_value_for_message(msg, 'on')
elif msg.type == 'note_off' and msg.note in note_on_times:
if msg.note in active_notes:
active_notes.remove(msg.note)
if not self.chord_only or (self.chord_only and len(active_notes) == 0):
value = self.get_attribute_value_for_message(msg, 'off', current_time - note_on_times[msg.note])
note_counts[frame_index:] -= 1
del note_on_times[msg.note]
return value
return None
def get_attribute_value_for_message(self, msg, state, duration=None):
if self.attribute == 'velocity' and state == 'on':
return msg.velocity / 127.0
elif self.attribute == 'pitch' and state == 'on':
return msg.note / 127.0
elif self.attribute == 'on_off':
return 1.0 if state == 'on' else 0.0
elif self.attribute == 'duration' and state == 'off':
return duration
elif self.attribute == 'pitchbend':
return (msg.pitch + 8192) / 16383.0 if msg.type == 'pitchwheel' else None
elif self.attribute == 'aftertouch':
return msg.value / 127.0 if msg.type == 'aftertouch' else None
elif self.attribute == 'poly_pressure':
return msg.value / 127.0 if msg.type == 'polytouch' else None
elif self.attribute.startswith('cc'):
cc_number = int(self.attribute[2:])
return msg.value / 127.0 if msg.type == 'control_change' and msg.control == cc_number else None
return None
def finalize_attribute(self, attribute_values, time_points, note_counts, modulation_data):
if not time_points:
return np.zeros(self.frame_count)
self.time_points = np.array(time_points)
attribute_values = np.array(attribute_values)
if len(self.time_points) < 2:
self.time_points = np.array([0, self.total_time])
attribute_values = np.array([0, 0])
frame_times = np.linspace(0, self.total_time, self.frame_count)
if self.attribute == 'density':
max_count = np.max(note_counts)
return note_counts / max_count if max_count > 0 else np.zeros(self.frame_count)
elif self.attribute.startswith('cc'):
cc_number = int(self.attribute[2:])
return self.interpolate_cc_data(cc_number, frame_times)
elif self.attribute == 'pitchbend':
return self.interpolate_data(self.pitchbend_data, frame_times)
elif self.attribute == 'aftertouch':
return self.interpolate_data(self.aftertouch_data, frame_times)
elif self.attribute == 'poly_pressure':
return self.interpolate_data(self.poly_pressure_data, frame_times)
elif self.attribute == 'modulation':
if modulation_data:
mod_times, mod_values = zip(*modulation_data)
return np.interp(frame_times, mod_times, mod_values)
return np.zeros(self.frame_count)
else:
interpolated = np.interp(frame_times, self.time_points, attribute_values)
interpolated[note_counts == 0] = 0
return self.apply_modulation(interpolated, frame_times, modulation_data)
def interpolate_cc_data(self, cc_number, frame_times):
if self.cc_data[cc_number]:
cc_times, cc_values = zip(*self.cc_data[cc_number])
return np.interp(frame_times, cc_times, cc_values)
return np.zeros(self.frame_count)
def interpolate_data(self, data, frame_times):
if data:
times, values = zip(*data)
return np.interp(frame_times, times, values)
return np.zeros(self.frame_count)
def apply_modulation(self, interpolated, frame_times, modulation_data):
if modulation_data and self.attribute != 'modulation':
mod_times, mod_values = zip(*modulation_data)
modulation = np.interp(frame_times, mod_times, mod_values)
interpolated = interpolated * (1 + modulation * 0.2)
return np.clip(interpolated, 0, 1)
return interpolated
def normalize(self):
if self.data is not None and len(self.data) > 0:
min_val = np.min(self.data)
max_val = np.max(self.data)
if max_val > min_val:
self.data = (self.data - min_val) / (max_val - min_val)
else:
self.data = np.zeros_like(self.data)
return self
@classmethod
def get_attribute_names(cls):
return list(cls.ATTRIBUTE_MAP.keys())
@classmethod
def get_attribute_value(cls, friendly_name):
return cls.ATTRIBUTE_MAP.get(friendly_name)
+58
View File
@@ -0,0 +1,58 @@
import torch
import numpy as np
from ..node_utilities import string_to_rgb
class DyeImage:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"source_rgb": ("STRING", {"default": "255,255,255"}),
"target_rgb": ("STRING", {"default": "0,0,0"}),
"tolerance": ("FLOAT", {"default": 0.01, "min": 0.0, "max": 1.0, "step": 0.001})
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "dye_image"
CATEGORY = "image/color"
def dye_image(self, image, source_rgb, target_rgb, tolerance):
source = torch.tensor(string_to_rgb(source_rgb), dtype=torch.float32, device=image.device)
target = torch.tensor(string_to_rgb(target_rgb), dtype=torch.float32, device=image.device)
color_distance = torch.sum((image - source.view(1, 1, 1, 3)).abs(), dim=-1)
mask = color_distance <= (tolerance * 3)
result = image.clone()
result[mask] = target.view(1, 1, 1, 3).expand_as(result)[mask]
return (result,)
class ColorPicker:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"color": ("COLOR", {"default": "#FF0000"}),
}
}
RETURN_TYPES = ("IMAGE", "STRING")
FUNCTION = "pick_color"
CATEGORY = "image/color"
def pick_color(self, color):
# Convert hex color to RGB
r, g, b = tuple(int(color[1:][i:i+2], 16) for i in (0, 2, 4))
# Create a 1x1x3 image tensor with the selected color
color_tensor = torch.tensor([[[r, g, b]]], dtype=torch.float32) / 255.0
# Create the RGB string
rgb_string = f"{r},{g},{b}"
return (color_tensor, rgb_string)
-95
View File
@@ -1,95 +0,0 @@
from .mask_base import AudioMaskBase
from .mask_utils import morph_mask, warp_mask, transform_mask, combine_masks
import numpy as np
import torch
class AudioMaskMorph(AudioMaskBase):
@classmethod
def INPUT_TYPES(cls):
return {
**super().INPUT_TYPES(),
"required": {
**super().INPUT_TYPES()["required"],
"morph_type": (["erode", "dilate", "open", "close"],),
"max_kernel_size": ("INT", {"default": 5, "min": 3, "max": 21, "step": 2}),
"max_iterations": ("INT", {"default": 10, "min": 1, "max": 50, "step": 1}),
}
}
def process_single_frame(self, mask: np.ndarray, strength: float, morph_type: str, max_kernel_size: int, max_iterations: int, **kwargs) -> np.ndarray:
kernel_size = max(3, int(3 + (max_kernel_size - 3) * strength))
iterations = max(1, int(max_iterations * strength))
return morph_mask(mask, morph_type, kernel_size, iterations)
def main_function(self, masks, audio, video_frames, strength, audio_feature, feature_threshold, frame_rate, morph_type, max_kernel_size, max_iterations, **kwargs):
return super().main_function(masks, audio, video_frames, strength, audio_feature, feature_threshold, frame_rate,
morph_type=morph_type, max_kernel_size=max_kernel_size, max_iterations=max_iterations, **kwargs)
class AudioMaskWarp(AudioMaskBase):
@classmethod
def INPUT_TYPES(cls):
return {
**super().INPUT_TYPES(),
"required": {
**super().INPUT_TYPES()["required"],
"warp_type": (["perlin", "radial", "swirl"],),
"frequency": ("FLOAT", {"default": 0.1, "min": 0.01, "max": 1.0, "step": 0.01}),
"max_amplitude": ("FLOAT", {"default": 30.0, "min": 0.1, "max": 500.0, "step": 0.1}),
"octaves": ("INT", {"default": 3, "min": 1, "max": 8, "step": 1}),
}
}
def process_single_frame(self, mask: np.ndarray, strength: float, warp_type: str, frequency: float, max_amplitude: float, octaves: int, **kwargs) -> np.ndarray:
amplitude = max_amplitude * strength
return warp_mask(mask, warp_type, frequency, amplitude, octaves)
def main_function(self, masks, audio, video_frames, strength, audio_feature, feature_threshold, frame_rate, warp_type, frequency, max_amplitude, octaves, **kwargs):
return super().main_function(masks, audio, video_frames, strength, audio_feature, feature_threshold, frame_rate,
warp_type=warp_type, frequency=frequency, max_amplitude=max_amplitude, octaves=octaves, **kwargs)
class AudioMaskTransform(AudioMaskBase):
@classmethod
def INPUT_TYPES(cls):
return {
**super().INPUT_TYPES(),
"required": {
**super().INPUT_TYPES()["required"],
"transform_type": (["translate", "rotate", "scale"],),
"max_x_value": ("FLOAT", {"default": 10.0, "min": -1000.0, "max": 1000.0, "step": 0.1}),
"max_y_value": ("FLOAT", {"default": 10.0, "min": -1000.0, "max": 1000.0, "step": 0.1}),
}
}
def process_single_frame(self, mask: np.ndarray, strength: float, transform_type: str, max_x_value: float, max_y_value: float, **kwargs) -> np.ndarray:
x_value = max_x_value * strength
y_value = max_y_value * strength
return transform_mask(mask, transform_type, x_value, y_value)
def main_function(self, masks, audio, video_frames, strength, audio_feature, feature_threshold, frame_rate, transform_type, max_x_value, max_y_value, **kwargs):
return super().main_function(masks, audio, video_frames, strength, audio_feature, feature_threshold, frame_rate,
transform_type=transform_type, max_x_value=max_x_value, max_y_value=max_y_value, **kwargs)
class AudioMaskMath(AudioMaskBase):
@classmethod
def INPUT_TYPES(cls):
return {
**super().INPUT_TYPES(),
"required": {
**super().INPUT_TYPES()["required"],
"mask_b": ("MASK",),
"combination_method": (["add", "subtract", "multiply", "minimum", "maximum"],),
}
}
def process_single_frame(self, mask: np.ndarray, strength: float, mask_b: np.ndarray, combination_method: str, frame_index: int = None, **kwargs) -> np.ndarray:
if frame_index is not None:
mask_b_frame = mask_b[frame_index]
else:
mask_b_frame = mask_b
return combine_masks(mask, mask_b_frame, combination_method, strength)
def main_function(self, masks, audio, video_frames, strength, audio_feature, feature_threshold, frame_rate, mask_b, combination_method, **kwargs):
mask_b_np = mask_b.cpu().numpy() if isinstance(mask_b, torch.Tensor) else mask_b
return super().main_function(masks, audio, video_frames, strength, audio_feature, feature_threshold, frame_rate,
mask_b=mask_b_np, combination_method=combination_method, **kwargs)
+89
View File
@@ -0,0 +1,89 @@
from .mask_base import FlexMaskBase
from .mask_utils import morph_mask, warp_mask, transform_mask, combine_masks
import numpy as np
class FlexMaskMorph(FlexMaskBase):
@classmethod
def INPUT_TYPES(cls):
return {
**super().INPUT_TYPES(),
"required": {
**super().INPUT_TYPES()["required"],
"morph_type": (["erode", "dilate", "open", "close"],),
"max_kernel_size": ("INT", {"default": 5, "min": 3, "max": 21, "step": 2}),
"max_iterations": ("INT", {"default": 10, "min": 1, "max": 50, "step": 1}),
}
}
def process_mask(self, mask: np.ndarray, feature_value: float, strength: float, morph_type: str, max_kernel_size: int, max_iterations: int, **kwargs) -> np.ndarray:
kernel_size = max(3, int(3 + (max_kernel_size - 3) * feature_value * strength))
iterations = max(1, int(max_iterations * feature_value * strength))
return morph_mask(mask, morph_type, kernel_size, iterations)
def main_function(self, masks, feature, feature_pipe, strength, feature_threshold, invert, subtract_original, grow_with_blur, morph_type, max_kernel_size, max_iterations, **kwargs):
return (self.apply_mask_operation(masks, feature, feature_pipe, strength, feature_threshold, invert, subtract_original, grow_with_blur,
morph_type=morph_type, max_kernel_size=max_kernel_size, max_iterations=max_iterations, **kwargs),)
class FlexMaskWarp(FlexMaskBase):
@classmethod
def INPUT_TYPES(cls):
return {
**super().INPUT_TYPES(),
"required": {
**super().INPUT_TYPES()["required"],
"warp_type": (["perlin", "radial", "swirl"],),
"frequency": ("FLOAT", {"default": 0.1, "min": 0.01, "max": 1.0, "step": 0.01}),
"max_amplitude": ("FLOAT", {"default": 30.0, "min": 0.1, "max": 500.0, "step": 0.1}),
"octaves": ("INT", {"default": 3, "min": 1, "max": 8, "step": 1}),
}
}
def process_mask(self, mask: np.ndarray, feature_value: float, strength: float, warp_type: str, frequency: float, max_amplitude: float, octaves: int, **kwargs) -> np.ndarray:
amplitude = max_amplitude * feature_value * strength
return warp_mask(mask, warp_type, frequency, amplitude, octaves)
def main_function(self, masks, feature, feature_pipe, strength, feature_threshold, invert, subtract_original, grow_with_blur, warp_type, frequency, max_amplitude, octaves, **kwargs):
return (self.apply_mask_operation(masks, feature, feature_pipe, strength, feature_threshold, invert, subtract_original, grow_with_blur,
warp_type=warp_type, frequency=frequency, max_amplitude=max_amplitude, octaves=octaves, **kwargs),)
class FlexMaskTransform(FlexMaskBase):
@classmethod
def INPUT_TYPES(cls):
return {
**super().INPUT_TYPES(),
"required": {
**super().INPUT_TYPES()["required"],
"transform_type": (["translate", "rotate", "scale"],),
"max_x_value": ("FLOAT", {"default": 10.0, "min": -1000.0, "max": 1000.0, "step": 0.1}),
"max_y_value": ("FLOAT", {"default": 10.0, "min": -1000.0, "max": 1000.0, "step": 0.1}),
}
}
def process_mask(self, mask: np.ndarray, feature_value: float, strength: float, transform_type: str, max_x_value: float, max_y_value: float, **kwargs) -> np.ndarray:
x_value = max_x_value * feature_value * strength
y_value = max_y_value * feature_value * strength
return transform_mask(mask, transform_type, x_value, y_value)
def main_function(self, masks, feature, feature_pipe, strength, feature_threshold, invert, subtract_original, grow_with_blur, transform_type, max_x_value, max_y_value, **kwargs):
return (self.apply_mask_operation(masks, feature, feature_pipe, strength, feature_threshold, invert, subtract_original, grow_with_blur,
transform_type=transform_type, max_x_value=max_x_value, max_y_value=max_y_value, **kwargs),)
class FlexMaskMath(FlexMaskBase):
@classmethod
def INPUT_TYPES(cls):
return {
**super().INPUT_TYPES(),
"required": {
**super().INPUT_TYPES()["required"],
"mask_b": ("MASK",),
"combination_method": (["add", "subtract", "multiply", "minimum", "maximum"],),
}
}
def process_mask(self, mask: np.ndarray, feature_value: float, strength: float, mask_b: np.ndarray, combination_method: str, **kwargs) -> np.ndarray:
return combine_masks(mask, mask_b, combination_method, feature_value * strength)
def main_function(self, masks, feature, feature_pipe, strength, feature_threshold, invert, subtract_original, grow_with_blur, mask_b, combination_method, **kwargs):
return (self.apply_mask_operation(masks, feature, feature_pipe, strength, feature_threshold, invert, subtract_original, grow_with_blur,
mask_b=mask_b, combination_method=combination_method, **kwargs),)
+73 -63
View File
@@ -19,8 +19,10 @@ from typing import List, Tuple
import pymunk
import cv2
from ..audio.audio_utils import AudioFeatureExtractor
from ... import RyanOnTheInside
class MaskBase(ABC):
class MaskBase(RyanOnTheInside, ABC):
@classmethod
def INPUT_TYPES(cls):
return {
@@ -33,8 +35,9 @@ class MaskBase(ABC):
}
}
CATEGORY="RyanOnTheInside/Masks"
CATEGORY = "RyanOnTheInside/Masks"
def __init__(self):
self.pre_processors = []
self.post_processors = []
@@ -152,7 +155,7 @@ class TemporalMaskBase(MaskBase, ABC):
}
CATEGORY="RyanOnTheInside/TemporalMasks"
def __init__(self):
super().__init__()
@@ -223,6 +226,7 @@ class ParticleSystemMaskBase(MaskBase, ABC):
"wind_strength": ("FLOAT", {"default": 0.0, "min": -100.0, "max": 100.0, "step": 1.0}),
"wind_direction": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 360.0, "step": 1.0}),
"gravity": ("FLOAT", {"default": 0.0, "min": -1000.0, "max": 1000.0, "step": 1.0}),
"warmup_period": ("INT", {"default": 0, "min": 0, "max": 1000, "step": 1}),
"start_frame": ("INT", {"default": 0, "min": 0, "max": 1000, "step": 1}),
"end_frame": ("INT", {"default": 0, "min": 0, "max": 1000, "step": 1}),
"respect_mask_boundary": ("BOOLEAN", {"default": False}),
@@ -275,12 +279,18 @@ class ParticleSystemMaskBase(MaskBase, ABC):
start_frame = kwargs.get('start_frame', 0)
end_frame = kwargs.get('end_frame', num_frames)
end_frame = end_frame if end_frame > 0 else num_frames
warmup_period = kwargs.get('warmup_period', 0)
self.setup_particle_system(width, height, **kwargs)
respect_mask_boundary = kwargs.get('respect_mask_boundary', False)
self.start_progress(num_frames, desc="Processing particle system mask")
self.start_progress(num_frames + warmup_period, desc="Processing particle system mask")
# Run warmup period
for _ in range(warmup_period):
self.update_particle_system(1.0 / 30.0, masks_np[0], respect_mask_boundary, -1)
self.update_progress()
mask_result = []
image_result = []
@@ -303,6 +313,7 @@ class ParticleSystemMaskBase(MaskBase, ABC):
return torch.from_numpy(np.stack(mask_result)).float(), torch.from_numpy(np.stack(image_result)).float()
def setup_particle_system(self, width: int, height: int, **kwargs):
self.space.gravity = pymunk.Vec2d(float(kwargs['wind_strength']), float(kwargs['gravity']))
@@ -575,15 +586,13 @@ class ParticleSystemMaskBase(MaskBase, ABC):
elif modulation['type'] == 'ParticleColorModulation':
self.apply_color_modulation(particle, modulation, progress)
def calculate_modulation_progress(self, current_frame, modulation, particle_age):
def calculate_modulation_progress(self, current_frame, modulation, particle_creation_frame):
start_frame = modulation['start_frame']
duration = modulation['effect_duration']
progress = (current_frame - start_frame) / duration
progress = (max(0, current_frame) - start_frame) / duration
if modulation['palindrome']:
# For plindrome, we need to adjust the progress
if progress <= 0.5:
progress = progress * 2
else:
@@ -637,14 +646,16 @@ class ParticleSystemMaskBase(MaskBase, ABC):
for _ in range(sub_steps):
for i, emitter in enumerate(self.emitters):
emission_rate = emitter['emission_rate'] * 10 # Increased sensitivity
self.particles_to_emit[i] += emission_rate * sub_dt
while self.particles_to_emit[i] >= 1 and self.total_particles_emitted < self.max_particles:
self.emit_particle(emitter, height, width, i, frame_index)
self.particles_to_emit[i] -= 1
# Check if the emitter is active in the current frame
if emitter['start_frame'] <= max(0, frame_index) and (emitter['end_frame'] == 0 or max(0, frame_index) < emitter['end_frame']):
emission_rate = emitter['emission_rate'] * 10 # Increased sensitivity
self.particles_to_emit[i] += emission_rate * sub_dt
while self.particles_to_emit[i] >= 1 and self.total_particles_emitted < self.max_particles:
self.emit_particle(emitter, height, width, i, frame_index)
self.particles_to_emit[i] -= 1
particles_before = len(self.particles)
self.particles = [p for p in self.particles if frame_index - p.creation_frame < self.particle_lifetime * 30] # Assuming 30 fps
self.particles = [p for p in self.particles if max(0, frame_index) - p.creation_frame < self.particle_lifetime * 30] # Assuming 30 fps
particles_after = len(self.particles)
particles_removed = particles_before - particles_after
@@ -653,7 +664,7 @@ class ParticleSystemMaskBase(MaskBase, ABC):
self.apply_gravity_well_force(particle)
# Apply modulations before updating position
self.apply_particle_modulations(particle, frame_index)
self.apply_particle_modulations(particle, max(0, frame_index))
old_pos = particle.position
new_pos = old_pos + particle.velocity * sub_dt
@@ -702,12 +713,11 @@ class ParticleSystemMaskBase(MaskBase, ABC):
particle = pymunk.Body(mass, moment)
particle.position = emitter_pos
particle.velocity = velocity
#particle.creation_time = self.total_time
particle.lifetime = self.particle_lifetime
particle.size = particle_size
particle.color = emitter['color']
particle.emitter_index = emitter_index
particle.creation_frame = frame_index
particle.creation_frame = max(0, frame_index) # Ensure non-negative creation frame
shape = pymunk.Circle(particle, radius)
shape.elasticity = 0.9
@@ -786,7 +796,7 @@ class ParticleSystemMaskBase(MaskBase, ABC):
def main_function(self, masks, strength, invert, subtract_original, grow_with_blur, emitters, **kwargs):
self.initialize()
self.total_frames = masks.shape[0]
self.total_frames = masks.shape[0] + kwargs.get('warmup_period', 0) # Include warmup period in total frames
for emitter in emitters:
emitter['initial_x'] = emitter['emitter_x']
emitter['initial_y'] = emitter['emitter_y']
@@ -796,10 +806,24 @@ class ParticleSystemMaskBase(MaskBase, ABC):
original_masks = masks
processed_masks, processed_images = self.process_mask(masks, emitters=self.emitters, **kwargs)
if subtract_original > 0:
original_masks_np = original_masks.cpu().numpy()
for i in range(self.total_frames):
mask_8bit = (original_masks_np[i] * 255).astype(np.uint8)
dist_transform = create_distance_transform(mask_8bit)
dist_transform = normalize_array(dist_transform)
threshold = 1 - subtract_original
subtraction_mask = dist_transform > threshold
processed_images[i][subtraction_mask] = 0
result_masks = self.apply_mask_operation(processed_masks, original_masks, strength, invert, subtract_original, grow_with_blur, **kwargs)
return (result_masks, processed_images,)
class OpticalFlowMaskBase(MaskBase, ABC):
@classmethod
def INPUT_TYPES(cls):
@@ -862,67 +886,53 @@ class OpticalFlowMaskBase(MaskBase, ABC):
processed_masks = np.stack(result)
return self.apply_mask_operation(processed_masks, masks, strength, **kwargs)
class AudioMaskBase(MaskBase):
#TODO check if input mask is blank and just skip
class FlexMaskBase(MaskBase):
@classmethod
def INPUT_TYPES(cls):
return {
**super().INPUT_TYPES(),
"required": {
"frame_rate": ("FLOAT", {"default": 30, "min": 0.1, "max": 120, "step": 0.1}),
**super().INPUT_TYPES()["required"],
"audio": ("AUDIO",),
"video_frames": ("IMAGE",),
"audio_feature": (["amplitude_envelope", "rms_energy", "spectral_centroid", "onset_detection", "chroma_features"],),
"feature": ("FEATURE",),
"feature_pipe": ("FEATURE_PIPE",),
"feature_threshold": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
}
}
RETURN_TYPES = ("MASK","AUDIO")
CATEGORY = "RyanOnTheInside/AudioMasks"
RETURN_TYPES = ("MASK",)
FUNCTION = "main_function"
CATEGORY="RyanOnTheInside/AudioMasks"
def __init__(self):
super().__init__()
def initialize(self, audio, audio_feature, num_frames, frame_rate):
self.audio_feature_extractor = AudioFeatureExtractor(audio, num_frames, frame_rate, feature_type=audio_feature)
@abstractmethod
def process_mask(self, mask: np.ndarray, feature_value: float, strength: float, **kwargs) -> np.ndarray:
pass
def process_mask(self, mask: np.ndarray, strength: float, audio: dict, video_frames: torch.Tensor, audio_feature: str, feature_threshold: float, frame_rate: float, **kwargs) -> np.ndarray:
feature = self.audio_feature_extractor.extract()
normalized_feature = normalize_array(feature)
num_frames = mask.shape[0]
if len(normalized_feature) != num_frames:
normalized_feature = np.interp(np.linspace(0, 1, num_frames), np.linspace(0, 1, len(normalized_feature)), normalized_feature)
processed_masks = []
def apply_mask_operation(self, masks, feature, feature_pipe, strength, feature_threshold, invert, subtract_original, grow_with_blur, **kwargs):
num_frames = feature_pipe.frame_count
original_masks = masks.clone()
self.start_progress(num_frames, desc="Applying flex mask operation")
result = []
for i in range(num_frames):
if normalized_feature[i] < feature_threshold:
processed_masks.append(mask[i])
mask = masks[i].numpy()
feature_value = feature.get_value_at_frame(i)
if feature_value >= feature_threshold:
processed_mask = self.process_mask(mask, feature_value, strength, **kwargs)
else:
frame_strength = strength * (1 + normalized_feature[i])
processed_mask = self.process_single_frame(mask[i], frame_strength, frame_index=i, **kwargs)
processed_masks.append(processed_mask)
return np.stack(processed_masks)
processed_mask = mask
def process_single_frame(self, mask: np.ndarray, strength: float, frame_index: int = None, **kwargs) -> np.ndarray:
"""
Process a single frame of the mask.
This method should be implemented by subclasses.
"""
raise NotImplementedError("Subclasses must implement process_single_frame method")
result.append(processed_mask)
self.update_progress()
def apply_audio_driven_mask(self, masks, audio, video_frames, strength, audio_feature, feature_threshold, frame_rate, **kwargs):
processed_masks = self.process_mask(masks.numpy(), strength, audio, video_frames, audio_feature, feature_threshold, frame_rate, **kwargs)
return self.apply_mask_operation(torch.from_numpy(processed_masks), masks, strength, **kwargs)
self.end_progress()
def main_function(self, masks, audio, video_frames, strength, audio_feature, feature_threshold, frame_rate, **kwargs):
num_frames = masks.shape[0]
self.initialize(audio, audio_feature, num_frames, frame_rate)
return self.apply_audio_driven_mask(masks, audio, video_frames, strength, audio_feature, feature_threshold, frame_rate, **kwargs), audio
processed_masks = torch.from_numpy(np.stack(result)).float()
return super().apply_mask_operation(processed_masks, original_masks, strength, invert, subtract_original, grow_with_blur, **kwargs)
@abstractmethod
def main_function(self, masks, feature, feature_pipe, strength, feature_threshold, invert, subtract_original, grow_with_blur, **kwargs):
pass
+18 -10
View File
@@ -2,7 +2,7 @@ import numpy as np
import cv2
from scipy import ndimage
from scipy.ndimage import gaussian_filter
from ..node_utilities import string_to_rgb
def apply_easing(t, easing_type):
if easing_type == 'linear':
@@ -55,9 +55,23 @@ def translate_mask(mask: np.ndarray, x_value: float, y_value: float) -> np.ndarr
def rotate_mask(mask: np.ndarray, angle: float) -> np.ndarray:
height, width = mask.shape[:2]
center = (width // 2, height // 2)
M = cv2.getRotationMatrix2D(center, angle, 1)
return warp_affine(mask, M)
rows = np.any(mask, axis=1)
cols = np.any(mask, axis=0)
ymin, ymax = np.where(rows)[0][[0, -1]] if np.any(rows) else (0, height-1)
xmin, xmax = np.where(cols)[0][[0, -1]] if np.any(cols) else (0, width-1)
center_y = int((ymin + ymax) / 2)
center_x = int((xmin + xmax) / 2)
M = cv2.getRotationMatrix2D((center_x, center_y), angle, 1.0)
rotated = np.zeros_like(mask)
mask_rotated = cv2.warpAffine(mask, M, (width, height), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT)
rotated = np.maximum(rotated, mask_rotated)
return rotated
def scale_mask(mask: np.ndarray, scale_x: float, scale_y: float) -> np.ndarray:
height, width = mask.shape[:2]
@@ -215,9 +229,3 @@ def calculate_optical_flow(frame1, frame2, flow_method):
else:
raise ValueError(f"Unknown flow method: {flow_method}")
def string_to_rgb(color_string):
if isinstance(color_string, tuple):
return color_string
color_values = color_string.strip('()').split(',')
return tuple(int(value.strip()) / 255.0 for value in color_values)
+21 -10
View File
@@ -2,7 +2,7 @@ import numpy as np
from .mask_base import ParticleSystemMaskBase
from typing import List, Tuple
import cv2
import inspect
from ... import RyanOnTheInside
class ParticleEmissionMask(ParticleSystemMaskBase):
@classmethod
@@ -23,6 +23,7 @@ class ParticleEmissionMask(ParticleSystemMaskBase):
RETURN_TYPES = ("MASK","IMAGE")
FUNCTION = "main_function"
def process_single_mask(self, mask: np.ndarray, frame_index: int, emission_strength: float, **kwargs) -> Tuple[np.ndarray, np.ndarray]:
self.modulate_parameters(frame_index, mask)
@@ -56,7 +57,12 @@ class ParticleEmissionMask(ParticleSystemMaskBase):
emission_strength=emission_strength,
static_bodies=static_bodies, **kwargs)
class Vortex:
class ParticleSystemModulatorBase(RyanOnTheInside):
CATEGORY="RyanOnTheInside/ParticleSystemMasks"
class Vortex(ParticleSystemModulatorBase):
@classmethod
def INPUT_TYPES(cls):
return {
@@ -97,7 +103,7 @@ class Vortex:
return (vortex_list,)
class GravityWell:
class GravityWell(ParticleSystemModulatorBase):
@classmethod
def INPUT_TYPES(cls):
return {
@@ -136,7 +142,7 @@ class GravityWell:
return (well_list,)
class ParticleEmitter:
class ParticleEmitter(ParticleSystemModulatorBase):
@classmethod
def INPUT_TYPES(cls):
return {
@@ -150,6 +156,8 @@ class ParticleEmitter:
"emission_rate": ("FLOAT", {"default": 10.0, "min": 0.1, "max": 100.0, "step": 0.1}),
"color": ("STRING", {"default": "(255,255,255)"}),
"initial_plume": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"start_frame": ("INT", {"default": 0, "min": 0, "max": 10000, "step": 1}),
"end_frame": ("INT", {"default": 0, "min": 0, "max": 10000, "step": 1}),
},
"optional": {
"previous_emitter": ("PARTICLE_EMITTER",),
@@ -164,7 +172,8 @@ class ParticleEmitter:
def create_emitter(self, emitter_x, emitter_y, particle_direction, particle_spread,
particle_size, particle_speed, emission_rate, color, initial_plume,
previous_emitter=None, emitter_movement=None, spring_joint_setting=None, particle_modulation=None):
start_frame, end_frame, previous_emitter=None, emitter_movement=None,
spring_joint_setting=None, particle_modulation=None):
emitter = {
"emitter_x": emitter_x,
"emitter_y": emitter_y,
@@ -175,6 +184,8 @@ class ParticleEmitter:
"emission_rate": emission_rate,
"color": color,
"initial_plume": initial_plume,
"start_frame": start_frame,
"end_frame": end_frame,
}
if emitter_movement:
@@ -193,7 +204,7 @@ class ParticleEmitter:
return (emitter_list,)
class SpringJointSetting:
class SpringJointSetting(ParticleSystemModulatorBase):
@classmethod
def INPUT_TYPES(cls):
return {
@@ -216,7 +227,7 @@ class SpringJointSetting:
"max_distance": max_distance,
},)
class EmitterMovement:
class EmitterMovement(ParticleSystemModulatorBase):
@classmethod
def INPUT_TYPES(cls):
return {
@@ -245,7 +256,7 @@ class EmitterMovement:
}
return (movement,)
class StaticBody:
class StaticBody(ParticleSystemModulatorBase):
@classmethod
def INPUT_TYPES(cls):
return {
@@ -290,7 +301,7 @@ class StaticBody:
return (body_list,)
class ParticleModulationBase:
class ParticleModulationBase(ParticleSystemModulatorBase):
@classmethod
def INPUT_TYPES(cls):
return {
@@ -305,7 +316,7 @@ class ParticleModulationBase:
"previous_modulation": ("PARTICLE_MODULATION",),
}
}
def create_modulation(self, start_frame, end_frame, effect_duration, temporal_easing, palindrome, previous_modulation=None):
modulation = {
-108
View File
@@ -1,108 +0,0 @@
"""
Mask Operation Inheritance Structure
Other types can be extrapolated from this temporal mask structure (ie, the particle system nodes).
1. MaskBase (Abstract Base Class)
- Location:
- Inherits from: ABC (Abstract Base Class)
- Key features:
- Defines basic structure for all mask operations
- Provides common methods: pre_process, post_process, start_progress, update_progress, end_progress
- Defines abstract method: process_mask
- Implements apply_mask_operation method
- Abstract methods that must be implemented by subclasses:
- process_mask
- main_function
2. TemporalMaskBase (Abstract Base Class)
- Location:
- Inherits from: MaskBase, ABC
- Key features:
- Adds temporal-specific inputs (start_frame, end_frame, effect_duration, temporal_easing, palindrome)
- Implements apply_temporal_mask_operation method
- Overrides process_mask to delegate to process_single_mask
- Implements main_function to handle temporal operations
- Abstract methods that must be implemented by subclasses:
- process_single_mask
3. Specific Mask Operation Classes (e.g., MaskMorph, MaskTransform)
- Location:
- Inherit from: TemporalMaskBase
- Key features:
- Implement process_single_mask method with specific mask operation logic
- May override main_function if additional processing is needed
- Define operation-specific inputs
Inheritance Structure:
ABC
|
+--> MaskBase (ABC)
|
+--> TemporalMaskBase (ABC)
|
+--> MaskMorph
|
+--> MaskTransform
|
+--> (Other temporal mask operations)
Responsibilities:
1. MaskBase:
- Defines the basic structure for all mask operations
- Handles progress tracking and reporting
- Provides pre- and post-processing capabilities
- Defines the interface for mask processing (process_mask)
2. TemporalMaskBase:
- Adds temporal functionality to mask operations
- Handles frame-by-frame processing with temporal effects
- Provides easing and palindrome capabilities
- Defines the interface for single frame processing (process_single_mask)
3. Specific Mask Operation Classes:
- Implement the actual mask processing logic
- Define operation-specific parameters
- May provide additional customization of the temporal processing
When creating a new mask operation:
- For non-temporal operations, inherit directly from MaskBase
- For temporal operations, inherit from TemporalMaskBase
- Implement the required abstract methods
- Add any operation-specific inputs and logic
"""
# Summary of the incoming audio object
audio = {
'waveform': torch.Tensor, # Shape: (1, 2, num_samples)
'sample_rate': int # Typically 44100 Hz
}
# Detailed breakdown:
# 1. 'waveform' key:
# - Contains the audio data as a PyTorch tensor
# - Shape: (1, 2, num_samples)
# - 1: Batch dimension (always 1 in this case)
# - 2: Number of channels (stereo audio)
# - num_samples: Number of audio samples (e.g., 38220 in the previous example)
# - Data type: torch.float32
# - Value range: Typically between -1.0 and 1.0
# 2. 'sample_rate' key:
# - Integer representing the number of samples per second
# - Typically 44100 Hz (CD-quality audio)
# Example usage:
audio_np = audio['waveform'].cpu().numpy() # Convert to numpy array
sample_rate = audio['sample_rate']
# Converting to mono:
audio_mono = audio_np.squeeze(0).mean(axis=0)
# Result: 1D numpy array with shape (num_samples,)
+5
View File
@@ -5,6 +5,7 @@ import torch
import cv2
from scipy.ndimage import distance_transform_edt
class MaskMorph(TemporalMaskBase):
@classmethod
def INPUT_TYPES(cls):
@@ -21,6 +22,7 @@ class MaskMorph(TemporalMaskBase):
RETURN_TYPES = ("MASK",)
FUNCTION = "apply_mask_morph"
def process_single_mask(self, mask: np.ndarray, strength: float, morph_type: str, max_kernel_size: int, max_iterations: int, **kwargs) -> np.ndarray:
# Scale kernel size and iterations based on strength
kernel_size = max(3, int(3 + (max_kernel_size - 3) * strength))
@@ -47,6 +49,7 @@ class MaskTransform(TemporalMaskBase):
RETURN_TYPES = ("MASK",)
FUNCTION = "apply_mask_transform"
def process_single_mask(self, mask: np.ndarray, strength: float, transform_type: str, x_value: float, y_value: float, **kwargs) -> np.ndarray:
transformed_mask = transform_mask(mask, transform_type, x_value * strength, y_value * strength)
return transformed_mask
@@ -69,6 +72,7 @@ class MaskMath(TemporalMaskBase):
RETURN_TYPES = ("MASK",)
FUNCTION = "apply_mask_math"
def process_single_mask(self, mask: np.ndarray, strength: float, mask_b: np.ndarray, combination_method: str, frame_index: int, **kwargs) -> np.ndarray:
mask_b_frame = mask_b[frame_index]
return combine_masks(mask, mask_b_frame, combination_method, strength)
@@ -93,6 +97,7 @@ class MaskRings(TemporalMaskBase):
RETURN_TYPES = ("MASK",)
FUNCTION = "apply_mask_rings"
def process_single_mask(self, mask: np.ndarray, strength: float, num_rings: int, max_ring_width: float, **kwargs) -> np.ndarray:
distance = distance_transform_edt(1 - mask)
max_distance = np.max(distance)
+3 -3
View File
@@ -19,8 +19,8 @@ class MovingShape:
"num_frames": ("INT", {"default": 30, "min": 1, "max": 120, "step": 1}),
"rgb": ("STRING", {"default": "(255,255,255)"}),
"shape": (["square", "circle", "triangle"],),
"shape_width_percent": ("FLOAT", {"default": 100, "min": 0, "max": 100, "step": 0.1}),
"shape_height_percent": ("FLOAT", {"default": 100, "min": 0, "max": 100, "step": 0.1}),
"shape_width_percent": ("FLOAT", {"default": 50, "min": 0, "max": 100, "step": 0.1}),
"shape_height_percent": ("FLOAT", {"default": 50, "min": 0, "max": 100, "step": 0.1}),
"shape_start_position_x": ("FLOAT", {"default": 0, "min": -100, "max": 100, "step": 0.1}),
"shape_start_position_y": ("FLOAT", {"default": 0, "min": -100, "max": 100, "step": 0.1}),
"shape_end_position_x": ("FLOAT", {"default": 0, "min": -100, "max": 100, "step": 0.1}),
@@ -247,7 +247,7 @@ class _mfc:
"threshold": ("INT", { "default": 0, "min": 0, "max": 127, "step": 1, }),
}
}
#TODO add image
RETURN_TYPES = ("MASK",)
FUNCTION = "execute"
CATEGORY = "/RyanOnTheInside/masks/"
+5
View File
@@ -0,0 +1,5 @@
def string_to_rgb(color_string):
if isinstance(color_string, tuple):
return color_string
color_values = color_string.strip('()').split(',')
return tuple(int(value.strip()) / 255.0 for value in color_values)
+1 -1
View File
@@ -3,7 +3,7 @@ name = "comfyui_ryanonyheinside"
description = "Custom nodes introducing particle simulations, optical flow, audio manipulation & reactivity, and temporal masks"
version = "1.2.0"
license = {file = "LICENSE"}
dependencies = ["opencv-python==4.10.0","scipy","pillow","librosa","pymunk==6.8.1","matplotlib","openunmix"]
dependencies = ["opencv-python==4.10.0","scipy","pillow","librosa","pymunk==6.8.1","matplotlib","openunmix","mido"]
[project.urls]
Repository = "https://github.com/ryanontheinside/ComfyUI_RyanOnTheInside"
+73 -372
View File
@@ -25,21 +25,23 @@ RyanOnTheInside node pack introduces the following:
</table>
*Examples showcasing various effects using particle emitters, vortices, and other node features*
<!--
## Table of Contents
- [Installation](#installation)
- [Requirements](#requirements)
- [Features](#features)
- [Particle System Masks](#particle-system-masks)
- [Audio Masks](#audio-masks)
- [Flex Masks](#flex-masks)
- [Audio and MIDI Masks](#audio-and-midi-masks)
- [Optical Flow Masks](#optical-flow-masks)
- [Temporal Masks](#temporal-masks)
- [Miscellaneous Nodes](#miscellaneous-nodes)
- [Contributing](#contributing)
- [License](#license)
- [Support](#support)
- [Support](#support) -->
## Installation
<details>
<summary><h2>Installation</h2></summary>
Install via the ComfyUI Manager by searching for RyanOnTheInside, or...
@@ -58,428 +60,127 @@ Install via the ComfyUI Manager by searching for RyanOnTheInside, or...
```
5. Restart ComfyUI if it's currently running and refresh your browser
## Requirements
### Requirements
See `requirements.txt` for a list of dependencies.
</details>
## Features
### Common Parameters for All Mask Nodes
All mask nodes in this system share the following parameters:
- `Masks`: Input mask or sequence of masks to be processed. (you can pass in a blank mask if you want)
- `Strength`: Overall strength of the effect (0.0 to 1.0). Higher values usually intensify the mask operation.
- `Invert`: When enabled, the node performs operations on the inverse of the input mask.
- `Subtract Original`: Amount of the original mask to subtract from the result (0.0 to 1.0). Useful for creating edge effects or isolating changes.
- `Grow with Blur`: Expands the mask edges using a blur effect (0.0 to 10.0). Higher values create softer, more expanded edges.
These parameters provide a consistent base for all mask operations, allowing for consistent control over the mask processing across various node types.
<details>
<summary><h3>Particle System Masks</h3></summary>
<summary><h3>🎆 Particle System Masks</h3></summary>
Create dynamic, fluid-like effects through particle simulation. This section includes nodes for particle emission, force fields, and complex particle behaviors.
Create mesmerizing, fluid-like effects through advanced particle simulation. Bring your masks to life with dynamic, interactive elements.
- Supports N particle emitters, each with independent settings including movement, particle (spread, speed, size, COLOR, etc)
- Supports N force fields (Gravity Well (repel and attract), and Vortex), each with independent settings
- Each simulation space has basic gravity and wind settings
- Boundary-respecting particle behavior, where the particles try to respect the boundary of a mask
- Static bodies for particle interactions
- Spring joint settings for more complex particle behaviors
- Multiple particle emitters with independent settings (spread, speed, size, color, etc.)
- Customizable force fields (Gravity Wells and Vortices) for complex particle interactions
- Adjustable global gravity and wind settings for each simulation space
- Boundary-respecting particles that interact with mask shapes
- Static bodies for intricate particle collisions and interactions
- Spring joint settings for creating interconnected particle systems
- Particle modulation over time (size, speed, color)
<img src="./assets/particle_examples.gif" width="400" alt="Particle System Demo showing various effects like object-aware particles, plume effects, and vortex interactions">
*Examples showcasing object-aware particles, initial plume effects, vortex interactions, gravity wells, and directional emitters.*
#### General Parameters for particle system
These settings apply to all particle system mask nodes, and generally affect the simulation space level.
- `particle_count`: Total number of particles in the system (1 to 10000)
- `particle_lifetime`: Lifetime of each particle in seconds (0.1 to 10.0)
- `wind_strength`: Strength of the wind effect (-100.0 to 100.0)
- `wind_direction`: Direction of the wind in degrees (0.0 to 360.0)
- `gravity`: Strength of gravity (-1000.0 to 1000.0)
- `start_frame`: Frame to start the effect (0 to 1000)
- `end_frame`: Frame to end the effect (0 to 1000)
- `respect_mask_boundary`: Whether particles should respect the mask boundary
- `emitters`: Particle emitter configurations (PARTICLE_EMITTER type)
- `vortices`: Optional vortex configurations (VORTEX type)
- `wells`: Optional gravity well configurations (GRAVITY_WELL type)
- `well_strength_multiplier`: Multiplier for gravity well strength (0.0 to 10.0)
</details>
#### Particle Emission Mask
This is the main node for particle simulations.
This node is particularly useful for creating complex, animated mask effects that can simulate natural phenomena like smoke, water, or energy fields...bowling balls, planets, whatever. The resulting masks can be used in various image processing and generation tasks within ComfyUI workflows.
<details>
<summary><h3>🎨 Flex Masks</h3></summary>
##### Parameters:
- `masks`: Input mask(s). Optionally interact with the particle system
- `emission_strength`: Strength of particle emission effect (0.0 to 1.0), basically opacity
- `draw_modifiers`: Visibility of vortices and gravity wells (0.0 to 1.0)
Create dynamic, feature-driven mask effects that adapt to various inputs such as time, audio, MIDI, or depth information.
##### Outputs:
- `MASK`: The resulting particle system binary mask
- `IMAGE`: A visualization of the particle system - useful for masking different colors for different particle systems
- Modulate mask operations based on extracted features
- Apply morphological operations (erode, dilate, open, close) with feature-based intensity
- Create warping effects (perlin, radial, swirl) modulated by features
- Perform geometric transformations (translate, rotate, scale) driven by feature values
- Combine masks using mathematical operations with feature-based strength
Features can be extracted from:
- Time-based patterns (smooth, accelerate, pulse, sawtooth, bounce)
- Audio characteristics (amplitude, energy, spectral centroid, onset detection, chroma)
- MIDI data (note velocity, pitch, control changes)
- Depth information (mean depth, variance, range, gradient, foreground/midground/background ratios)
#### ParticleEmitter Parameters
The Particle Emitter node...creates a particle emitter. They can be chained together to feed N to a particle based node.
- `emitter_x`: X-coordinate of the emitter (0.0 to 1.0)
- `emitter_y`: Y-coordinate of the emitter (0.0 to 1.0)
- `particle_direction`: Direction of particle emission in degrees
- `particle_spread`: Spread angle of particle emission in degrees
- `particle_speed`: Speed of emitted particles
- `particle_size`: Size of emitted particles
- `color`: Color of emitted particles (RGB tuple)
- `emission_rate`: Rate of particle emission
- `initial_plume`: Initial burst of particles (0.0 to 1.0)
- `emitter_movement`: (optional) Movement settings for the emitter
- `spring_joint_setting`: (optional) Spring joint configuration for particles
- `particle_modulation`: (optional) Modulation settings for particle properties over time
Example applications:
- Create masks that pulse with the beat of music
- Generate depth-aware mask effects that respond to scene geometry
- Produce complex animations driven by MIDI sequences
- Combine multiple features for intricate, multi-dimensional mask modulations
#### EmitterMovement Parameters
These settings are encapulated in a node that can optionally be fed to particle emitters.
Position Control:
- `emitter_y_frequency`: Rate of vertical oscillation (higher values = faster up-down movement)
- `emitter_y_amplitude`: Maximum vertical distance from center (larger values = taller movement)
- `emitter_x_frequency`: Rate of horizontal oscillation (higher values = faster left-right movement)
- `emitter_x_amplitude`: Maximum horizontal distance from center (larger values = wider movement)
Direction Control:
- `direction_frequency`: Rate of emission angle change (higher values = faster direction shifts)
- `direction_amplitude`: Maximum angle change in degrees (up to 180° in either direction)
These parameters work together to create complex, periodic movements for particle emitters.
By adjusting frequencies and amplitudes, you can achieve various patterns like circles,
figure-eights, or more chaotic motions. The direction parameters add extra dynamism by
altering the angle of particle emission over time.
#### Particle Modulation
These settings define how particle properties change over time:
##### Common Parameters for All Modulations:
- `start_frame`: Frame to start the modulation effect (0 to 1000)
- `end_frame`: Frame to end the modulation effect (0 to 1000)
- `effect_duration`: Duration of the modulation effect in frames (0 to 1000)
- `temporal_easing`: Easing function for the modulation effect ("ease_in_out", "linear", "bounce", "elastic", "none")
- `palindrome`: Whether to reverse the modulation effect after completion (True/False)
##### ParticleSizeModulation:
- `target_size`: Target size for particles at the end of the modulation (0.0 to 400.0)
##### ParticleSpeedModulation:
- `target_speed`: Target speed for particles at the end of the modulation (0.0 to 1000.0)
##### ParticleColorModulation:
- `target_color`: Target color for particles at the end of the modulation (RGB tuple)
These modulation settings can be chained together to create complex, dynamic particle behaviors over time.
#### SpringJointSetting Parameters
These settings define the behavior of spring joints attached to particles:
- `stiffness`: Stiffness of the spring (0.0 to 1000.0)
- `damping`: Damping factor of the spring (0.0 to 100.0)
- `rest_length`: Rest length of the spring (0.0 to 100.0)
- `max_distance`: Maximum distance the spring can stretch (0.0 to 500.0)
#### StaticBody Parameters
These settings define static bodies in the simulation that particles can interact with. It allows you to draw solid objects, like walls for instance. You can chain them together to add N to a given simulation:
- `shape_type`: Type of shape ("segment" or "polygon")
- `x1`, `y1`, `x2`, `y2`: Coordinates defining the shape
- `elasticity`: Bounciness of the static body (0.0 to 1.0)
- `friction`: Friction of the static body (0.0 to 1.0)
- `draw`: Whether to visualize the static body and how thick
- `color`: Color of the static body (RGB tuple)
#### GravityWell Parameters
These are optional inputs for a simulation space. They can be chained together to have N in a given simulation.
- `x`: X-coordinate of the gravity well (0.0 to 1.0)
- `y`: Y-coordinate of the gravity well (0.0 to 1.0)
- `strength`: Strength of the gravity well
- `radius`: Radius of effect for the gravity well
- `type`: Type of gravity well ('attract' or 'repel')
- `color`: Color of the gravity well visualization (RGB tuple)
- `draw`: Visibility of the gravity well (0.0 to 1.0)
#### Vortex Parameters
These are optional inputs for a simulation space. They can be chained together to have N in a given simulation.
- `x`: X-coordinate of the vortex center (0.0 to 1.0)
- `y`: Y-coordinate of the vortex center (0.0 to 1.0)
- `strength`: Strength of the vortex effect (0.0 to 1000.0)
- `radius`: Radius of effect for the vortex (10.0 to 500.0)
- `inward_factor`: Factor controlling how quickly particles are pulled towards the center (0.0 to 1.0)
- `movement_speed`: Speed of movement of the vortex object (0.0 to 10.0)
- `color`: Color of the vortex visualization (RGB tuple)
- `draw`: Visibility of the vortex (0.0 to 1.0)
These flexible mask nodes allow for complex, adaptive effects that respond dynamically to various inputs, enabling unprecedented levels of creative control and expressiveness in mask generation.
</details>
<details>
<summary><h3>Audio Masks</h3></summary>
<summary><h3>🎵 Audio and MIDI Masks</h3></summary>
Create dynamic, audio-reactive mask effects. This section covers nodes for audio-driven mask generation and manipulation.
Transform your masks with the power of sound and musical data. Create dynamic, audio-reactive effects that pulse and morph to the rhythm of your chosen audio or MIDI input.
- Create separate masks for vocals, bass, drums, other!
- Audio-driven morphological operations (erosion, dilation, opening, closing)
- Audio-reactive warping effects (perlin noise, radial, swirl)
- Audio-based mask transformations (translation, rotation, scaling)
- Mathematical operations between masks driven by audio features
- Supports various audio features for mask modulation
Audio Features:
- Separate audio into individual tracks (vocals, drums, bass, other instruments)
- Apply audio-driven effects to masks based on various audio features
- Create complex audio visualizations and mask modulations
- Utilize frequency filtering for targeted audio processing
- Generate audio-reactive animations and transformations
<img src="./assets/audio_mask_examples.gif" width="400" alt="Audio Mask Examples demonstrating morphing, warping, transformation, and mathematical operations">
MIDI Features:
- Extract features from MIDI files or real-time MIDI input
- Use note velocity, pitch, and control changes (expression, aftertouch, anything) to modulate mask properties
- Create rhythmic mask patterns based on MIDI note timing
- Apply complex mask transformations driven by MIDI sequences
*Examples of audio-driven mask effects: morphing, warping, transformation, and mathematical operations*
### Common Parameters for all AudioMask nodes
- `masks`: Input mask sequence to be processed
- `audio`: Input audio for driving the mask generation
- `video_frames`: Corresponding video frames
- `strength`: Overall strength of the effect (0.0 to 1.0)
- `audio_feature`: Audio feature to use for mask modulation (amplitude_envelope, rms_energy, spectral_centroid, onset_detection, chroma_features)
- `feature_threshold`: Threshold for audio feature detection (0.0 to 1.0)
- `frame_rate`: Frame rate of the video (0.1 to 120.0 fps)
### Audio Mask Nodes
#### AudioMaskMorph
Applies morphological operations to masks based on audio features:
- `morph_type`: Type of morphological operation ("erode", "dilate", "open", "close")
- `max_kernel_size`: Maximum size of the morphological kernel (3 to 21, odd numbers only)
- `max_iterations`: Maximum number of times to apply the operation (1 to 50)
#### AudioMaskWarp
Applies warping effects to masks driven by audio:
- `warp_type`: Type of warping effect ("perlin", "radial", "swirl")
- `frequency`: Frequency of the warping effect (0.01 to 1.0)
- `max_amplitude`: Maximum amplitude of the warping effect (0.1 to 500.0)
- `octaves`: Number of octaves for noise-based warps (1 to 8)
#### AudioMaskTransform
Applies geometric transformations to masks based on audio features:
- `transform_type`: Type of transformation ("translate", "rotate", "scale")
- `max_x_value`: Maximum X-axis transformation value (-1000.0 to 1000.0)
- `max_y_value`: Maximum Y-axis transformation value (-1000.0 to 1000.0)
#### AudioMaskMath
Performs mathematical operations between two masks, modulated by audio:
- `mask_b`: Second mask to combine with the input mask
- `combination_method`: Mathematical operation to apply ("add", "subtract", "multiply", "minimum", "maximum")
### Audio Processing and Visualization Nodes
#### AudioSeparator
Separates audio into different components:
- `audio`: Input audio to be processed
- `video_frames`: Corresponding video frames
- `frame_rate`: Frame rate of the video
Outputs: original audio, video frames, drums audio, vocals audio, bass audio, other audio
#### AudioFilter
Applies frequency filters to audio:
- `audio`: Input audio to be filtered
- `filters`: Frequency filters to be applied (FREQUENCY_FILTER type)
#### FrequencyFilterPreset
Creates preset filter chains for common audio processing tasks:
- `preset`: Preset to use (e.g., "isolate_kick_drum", "isolate_vocals", "remove_rumble")
- `previous_filter`: (Optional) Previous filter chain to append to
#### FrequencyFilterCustom
Creates custom frequency filters:
- `filter_type`: Type of filter ("lowpass", "highpass", "bandpass")
- `order`: Filter order (1 to 10)
- `cutoff`: Cutoff frequency (20 to 20000 Hz)
- `previous_filter`: (Optional) Previous filter chain to append to
#### AudioFeatureVisualizer
Visualizes various audio features:
- `audio`: Input audio to visualize
- `video_frames`: Corresponding video frames
- `visualization_type`: Type of visualization ("waveform", "spectrogram", "mfcc", "chroma", "tonnetz", "spectral_centroid")
- `frame_rate`: Frame rate of the video
These audio processing and visualization nodes enable complex audio analysis, filtering, and feature extraction, allowing for creative audio-driven effects and visualizations within the ComfyUI environment.
These audio and MIDI processing nodes enable analysis, filtering, and feature extraction, allowing for creative audio-driven and musically-responsive effects and visualizations within the ComfyUI environment.
</details>
<details>
<summary><h3>Optical Flow Masks</h3></summary>
<summary><h3>🌊 Optical Flow Masks</h3></summary>
Create motion-based effects using optical flow analysis. This section includes nodes for generating masks based on movement in video sequences.
Harness the power of motion to create stunning visual effects. Generate masks based on movement in video sequences for dynamic, flow-based animations.
- Multiple optical flow algorithms (Farneback, Lucas-Kanade, Pyramidal Lucas-Kanade)
- Particle simulations reactive to optical flow - work in progress
- Directional flow isolation and emphasis - in progress
- Advanced blending and modulation options
- Choose from multiple optical flow algorithms (Farneback, Lucas-Kanade, Pyramidal Lucas-Kanade)
- Create particle simulations reactive to optical flow (work in progress)
- Isolate and emphasize directional flow for targeted effects (in progress)
- Apply advanced blending and modulation options for seamless integration
<img src="./assets/opticalflow_examples.gif" width="400" alt="Optical Flow Demo showing motion-generated trails and effects">
*Motion generated trails using optical flow analysis*
### Optical Flow Mask Nodes
#### General optical mask settings
These settings apply across the different optical flow masks, and modulate the input mask based on optical flow, creating dynamic mask animations.
- `Images`: Input image sequence for optical flow calculation
- `Masks`: Input mask sequence to be processed
- `Strength`: Overall strength of the effect (0.0 to 1.0)
- `Flow Method`: Technique used for optical flow calculation
- Options: "Farneback", "LucasKanade", "PyramidalLK"
- `Flow Threshold`: Minimum flow magnitude to consider (0.0 to 1.0)
- `Magnitude Threshold`: Relative threshold for flow magnitude (0.0 to 1.0)
#### OpticalFlowMaskModulation
This is currently the main Optical Flow node. Use it to make motion based effects.
- `Modulation Strength`: Intensity of the modulation effect (0.0 to 5.0)
- `Blur Radius`: Smoothing applied to the flow magnitude (0 to 20 pixels)
- `Trail Length`: Number of frames for the trail effect (1 to 20)
- `Decay Factor`: Rate of trail decay over time (0.1 to 1.0)
- `Decay Style`: Method of trail decay
- Options: "fade" (opacity reduction), "thickness" (width reduction)
- `Max Thickness`: Maximum trail thickness for thickness-based decay (1 to 50 pixels)
#### Optical Flow Direction Mask - work in progress
This node will create masks based on the direction of optical flow, allowing for selective masking of moving areas.
<!--
- Inherits all parameters from Optical Flow Mask Base
- `Direction`: Flow direction to mask
- Options: "horizontal", "vertical", "radial_in", "radial_out", "clockwise", "counterclockwise"
- `Angle Threshold`: Tolerance for direction matching (0.0 to 180.0 degrees)
- `Blur Radius`: Smoothing applied to the resulting mask (0 to 20 pixels)
- `Invert`: Option to invert the output mask
-->
#### Optical Flow Particle System - work in progess
This node will generate a particle system animation driven by optical flow, creating dynamic visual effects.
<!--
- Inherits all parameters from Optical Flow Mask Base
- `Particle Count`: Number of particles in the system (100 to 10000)
- `Particle Size`: Diameter of each particle (1 to 50 pixels)
- `Particle Color`: Color of the particles (hex color code)
- `Particle Opacity`: Transparency of particles (0.0 to 1.0)
- `Flow Multiplier`: Influence of optical flow on particle movement (0.1 to 5.0)
- `Particle Lifetime`: Duration of each particle's existence (1 to 100 frames)
- `Initial Velocity`: Starting speed of newly emitted particles (0.1 to 5.0)
-->
</details>
<details>
<summary><h3>Temporal Masks</h3></summary>
<summary><h3>⏳ Temporal Masks</h3></summary>
Implement time-based transformations and animations on masks. This section covers nodes for creating dynamic mask effects over time.
Add the dimension of time to your mask effects. Create evolving, dynamic animations that transform your masks over the course of your video.
- Morphing and transformation effects (erosion, dilation, translation, rotation, scaling)
- Various mask combination methods
- Radial and pulsating effects
- Warp effects using Perlin noise, radial distortion, and swirling patterns
- Customizable easing functions and palindrome support
- Apply time-based morphing and transformation effects (erosion, dilation, translation, rotation, scaling)
- Utilize various mask combination methods for complex temporal interactions
- Create radial and pulsating effects for eye-catching animations
- Generate warp effects using Perlin noise, radial distortion, and swirling patterns
- Customize with various easing functions and palindrome support for smooth transitions
<img src="./assets/temporal_examples.gif" width="400" alt="Temporal Masks Examples showing various time-based effects">
*Examples of various combinations of temporal mask effects including morphing, transformations, math, and warping.*
- `Start Frame`: The frame number where the effect begins (0 to 1000).
- `End Frame`: The frame number where the effect ends (0 to 1000). If set to 0, processes until the last frame.
- `Effect Duration`: Number of frames over which the effect is applied (0 to 1000). If 0, uses (End Frame - Start Frame).
- `Temporal Easing`: Determines how the effect strength changes over time.
- Options: "ease_in_out", "linear", "bounce", "elastic", "none"
- `Palindrome`: When enabled, the effect plays forward then backward within the specified duration.
#### Mask Morph
Applies morphological operations to the mask, changing its shape over time.
- `Morph Type`: The type of morphological operation to apply.
- Options: "erode", "dilate", "open", "close"
- `Max Kernel Size`: Maximum size of the morphological kernel (3 to 21, odd numbers only).
- `Max Iterations`: Maximum number of times to apply the operation (1 to 50).
#### Mask Transform
Applies geometric transformations to the mask over time.
- `Transform Type`: The type of transformation to apply.
- Options: "translate", "rotate", "scale"
- `X Value`: Horizontal component of the transformation (-1000 to 1000).
- `Y Value`: Vertical component of the transformation (-1000 to 1000).
#### Mask Math
Combines two masks using various mathematical operations.
- `Mask B`: Second mask to combine with the input mask.
- `Combination Method`: Mathematical operation to apply.
- Options: "add", "subtract", "multiply", "minimum", "maximum"
#### Mask Rings
Creates concentric ring patterns based on the distance from the mask edges.
- `Num Rings`: Number of rings to generate (1 to 50).
- `Max Ring Width`: Maximum width of each ring as a fraction of the total distance (0.01 to 0.5).
#### Mask Warp
Applies various warping effects to the mask.
- `Warp Type`: The type of warping effect to apply.
- Options: "perlin", "radial", "swirl"
- `Frequency`: Frequency of the warping effect (0.01 to 1.0).
- `Amplitude`: Intensity of the warping effect (0.1 to 500.0).
- `Octaves`: Number of octaves for noise-based warps (1 to 8).
All these nodes inherit the common parameters from MaskBase and TemporalMaskBase, allowing for precise control over the timing and strength of the effects across multiple frames.
</details>
<details>
<summary><h3>Miscellaneous Nodes</h3></summary>
<summary><h3>🛠️ Miscellaneous Nodes</h3></summary>
Additional utility nodes for creating specific mask effects and animations.
Enhance your mask creation toolkit with additional utility nodes for specific effects and animations.
#### MovingShape
- Generate masks and images with customizable text
- Create moving shape masks with adjustable parameters (size, position, color, movement type)
- Combine multiple mask types for unique, hybrid effects
- Apply advanced blending modes between different mask types
- Create custom mask patterns using mathematical functions
Generate animated mask sequences featuring a moving shape with customizable parameters.
Parameters:
- `frame_width`: Width of each frame (1-3840 pixels)
- `frame_height`: Height of each frame (1-2160 pixels)
- `num_frames`: Number of frames in the sequence (1-120)
- `rgb`: Color of the shape in RGB format, e.g., "(255,255,255)"
- `shape`: Shape type ("square", "circle", or "triangle")
- `shape_width_percent`: Width of the shape as a percentage of frame width (0-100%)
- `shape_height_percent`: Height of the shape as a percentage of frame height (0-100%)
- `shape_start_position_x`: Starting X position of the shape (-100 to 100)
- `shape_start_position_y`: Starting Y position of the shape (-100 to 100)
- `shape_end_position_x`: Ending X position of the shape (-100 to 100)
- `shape_end_position_y`: Ending Y position of the shape (-100 to 100)
- `movement_type`: Type of movement ("linear", "ease_in_out", "bounce", or "elastic")
- `grow`: Growth factor of the shape during animation (0-100)
- `palindrome`: Whether to reverse the animation sequence (True/False)
- `delay`: Number of static frames at the start (0-60)
This node creates a mask sequence with a moving shape, allowing for various animations and transformations.
#### TextMaskNode
Generate mask and image sequences featuring customizable text.
Parameters:
- `width`: Width of the output image (1-8192 pixels)
- `height`: Height of the output image (1-8192 pixels)
- `text`: The text to be rendered
- `font`: Font to be used (selectable from system fonts)
- `font_size`: Size of the font (1-1000)
- `font_color`: Color of the text in RGB format, e.g., "(255,255,255)"
- `background_color`: Color of the background in RGB format, e.g., "(0,0,0)"
- `x_position`: Horizontal position of the text (0.0-1.0, where 0.5 is center)
- `y_position`: Vertical position of the text (0.0-1.0, where 0.5 is center)
- `rotation`: Rotation angle of the text (0-360 degrees)
- `max_width_ratio`: Maximum width of text as a ratio of image width (0.1-1.0)
- `batch_size`: Number of images to generate in the batch (1-10000)
This node creates both a mask and an image with the specified text, allowing for various text-based effects and animations. The text automatically wraps to fit within the specified maximum width.
These versatile nodes allow for endless creativity in mask generation and manipulation, enabling you to achieve precise control over your visual effects.
</details>
+2 -1
View File
@@ -4,4 +4,5 @@ pillow
librosa
pymunk==6.8.1
matplotlib
openunmix
openunmix
mido
File diff suppressed because one or more lines are too long
+277
View File
@@ -0,0 +1,277 @@
var a;if(!Array.prototype.map)Array.prototype.map=function(b,c){for(var d=this.length,f=new Array(d),g=0;g<d;g++)if(g in this)f[g]=b.call(c,this[g],g,this);return f};if(!Array.prototype.filter)Array.prototype.filter=function(b,c){for(var d=this.length,f=[],g=0;g<d;g++)if(g in this){var h=this[g];b.call(c,h,g,this)&&f.push(h)}return f};if(!Array.prototype.forEach)Array.prototype.forEach=function(b,c){for(var d=this.length>>>0,f=0;f<d;f++)f in this&&b.call(c,this[f],f,this)};
if(!Array.prototype.reduce)Array.prototype.reduce=function(b,c){var d=this.length;if(!d&&arguments.length==1)throw new Error("reduce: empty array, no initial value");var f=0;if(arguments.length<2)for(;;){if(f in this){c=this[f++];break}if(++f>=d)throw new Error("reduce: no values, no initial value");}for(;f<d;f++)if(f in this)c=b(c,this[f],f,this);return c};var pv={};pv.version="3.3.1";pv.identity=function(b){return b};pv.index=function(){return this.index};pv.child=function(){return this.childIndex};
pv.parent=function(){return this.parent.index};pv.extend=function(b){function c(){}c.prototype=b.prototype||b;return new c};
try{eval("pv.parse = function(x) x;")}catch(e){pv.parse=function(b){for(var c=new RegExp("function\\s*(\\b\\w+)?\\s*\\([^)]*\\)\\s*","mg"),d,f,g=0,h="";d=c.exec(b);){d=d.index+d[0].length;if(b.charAt(d)!="{"){h+=b.substring(g,d)+"{return ";g=d;for(var i=0;i>=0&&d<b.length;d++){var j=b.charAt(d);switch(j){case '"':case "'":for(;++d<b.length&&(f=b.charAt(d))!=j;)f=="\\"&&d++;break;case "[":case "(":i++;break;case "]":case ")":i--;break;case ";":case ",":i==0&&i--;break}}h+=pv.parse(b.substring(g,--d))+
";}";g=d}c.lastIndex=d}h+=b.substring(g);return h}}pv.css=function(b,c){return window.getComputedStyle?window.getComputedStyle(b,null).getPropertyValue(c):b.currentStyle[c]};pv.error=function(b){typeof console=="undefined"?alert(b):console.error(b)};pv.listen=function(b,c,d){d=pv.listener(d);return b.addEventListener?b.addEventListener(c,d,false):b.attachEvent("on"+c,d)};pv.listener=function(b){return b.$listener||(b.$listener=function(c){try{pv.event=c;return b.call(this,c)}finally{delete pv.event}})};
pv.ancestor=function(b,c){for(;c;){if(c==b)return true;c=c.parentNode}return false};pv.id=function(){var b=1;return function(){return b++}}();pv.functor=function(b){return typeof b=="function"?b:function(){return b}};pv.listen(window,"load",function(){for(pv.$={i:0,x:document.getElementsByTagName("script")};pv.$.i<pv.$.x.length;pv.$.i++){pv.$.s=pv.$.x[pv.$.i];if(pv.$.s.type=="text/javascript+protovis")try{window.eval(pv.parse(pv.$.s.text))}catch(b){pv.error(b)}}delete pv.$});pv.Format={};
pv.Format.re=function(b){return b.replace(/[\\\^\$\*\+\?\[\]\(\)\.\{\}]/g,"\\$&")};pv.Format.pad=function(b,c,d){c=c-String(d).length;return c<1?d:(new Array(c+1)).join(b)+d};
pv.Format.date=function(b){function c(f){return b.replace(/%[a-zA-Z0-9]/g,function(g){switch(g){case "%a":return["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][f.getDay()];case "%A":return["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"][f.getDay()];case "%h":case "%b":return["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"][f.getMonth()];case "%B":return["January","February","March","April","May","June","July","August","September","October","November","December"][f.getMonth()];
case "%c":return f.toLocaleString();case "%C":return d("0",2,Math.floor(f.getFullYear()/100)%100);case "%d":return d("0",2,f.getDate());case "%x":case "%D":return d("0",2,f.getMonth()+1)+"/"+d("0",2,f.getDate())+"/"+d("0",2,f.getFullYear()%100);case "%e":return d(" ",2,f.getDate());case "%H":return d("0",2,f.getHours());case "%I":return(g=f.getHours()%12)?d("0",2,g):12;case "%m":return d("0",2,f.getMonth()+1);case "%M":return d("0",2,f.getMinutes());case "%n":return"\n";case "%p":return f.getHours()<
12?"AM":"PM";case "%T":case "%X":case "%r":g=f.getHours()%12;return(g?d("0",2,g):12)+":"+d("0",2,f.getMinutes())+":"+d("0",2,f.getSeconds())+" "+(f.getHours()<12?"AM":"PM");case "%R":return d("0",2,f.getHours())+":"+d("0",2,f.getMinutes());case "%S":return d("0",2,f.getSeconds());case "%Q":return d("0",3,f.getMilliseconds());case "%t":return"\t";case "%u":return(g=f.getDay())?g:1;case "%w":return f.getDay();case "%y":return d("0",2,f.getFullYear()%100);case "%Y":return f.getFullYear();case "%%":return"%"}return g})}
var d=pv.Format.pad;c.format=c;c.parse=function(f){var g=1970,h=0,i=1,j=0,k=0,l=0,q=[function(){}],n=pv.Format.re(b).replace(/%[a-zA-Z0-9]/g,function(p){switch(p){case "%b":q.push(function(m){h={Jan:0,Feb:1,Mar:2,Apr:3,May:4,Jun:5,Jul:6,Aug:7,Sep:8,Oct:9,Nov:10,Dec:11}[m]});return"([A-Za-z]+)";case "%h":case "%B":q.push(function(m){h={January:0,February:1,March:2,April:3,May:4,June:5,July:6,August:7,September:8,October:9,November:10,December:11}[m]});return"([A-Za-z]+)";case "%e":case "%d":q.push(function(m){i=
m});return"([0-9]+)";case "%I":case "%H":q.push(function(m){j=m});return"([0-9]+)";case "%m":q.push(function(m){h=m-1});return"([0-9]+)";case "%M":q.push(function(m){k=m});return"([0-9]+)";case "%p":q.push(function(m){if(j==12){if(m=="am")j=0}else if(m=="pm")j=Number(j)+12});return"(am|pm)";case "%S":q.push(function(m){l=m});return"([0-9]+)";case "%y":q.push(function(m){m=Number(m);g=m+(0<=m&&m<69?2E3:m>=69&&m<100?1900:0)});return"([0-9]+)";case "%Y":q.push(function(m){g=m});return"([0-9]+)";case "%%":q.push(function(){});
return"%"}return p});(f=f.match(n))&&f.forEach(function(p,m){q[m](p)});return new Date(g,h,i,j,k,l)};return c};
pv.Format.time=function(b){function c(f){f=Number(f);switch(b){case "short":if(f>=31536E6)return(f/31536E6).toFixed(1)+" years";else if(f>=6048E5)return(f/6048E5).toFixed(1)+" weeks";else if(f>=864E5)return(f/864E5).toFixed(1)+" days";else if(f>=36E5)return(f/36E5).toFixed(1)+" hours";else if(f>=6E4)return(f/6E4).toFixed(1)+" minutes";return(f/1E3).toFixed(1)+" seconds";case "long":var g=[],h=f%36E5/6E4>>0;g.push(d("0",2,f%6E4/1E3>>0));if(f>=36E5){var i=f%864E5/36E5>>0;g.push(d("0",2,h));if(f>=864E5){g.push(d("0",
2,i));g.push(Math.floor(f/864E5).toFixed())}else g.push(i.toFixed())}else g.push(h.toFixed());return g.reverse().join(":")}}var d=pv.Format.pad;c.format=c;c.parse=function(f){switch(b){case "short":for(var g=/([0-9,.]+)\s*([a-z]+)/g,h,i=0;h=g.exec(f);){var j=parseFloat(h[0].replace(",","")),k=0;switch(h[2].toLowerCase()){case "year":case "years":k=31536E6;break;case "week":case "weeks":k=6048E5;break;case "day":case "days":k=864E5;break;case "hour":case "hours":k=36E5;break;case "minute":case "minutes":k=
6E4;break;case "second":case "seconds":k=1E3;break}i+=j*k}return i;case "long":h=f.replace(",","").split(":").reverse();i=0;if(h.length)i+=parseFloat(h[0])*1E3;if(h.length>1)i+=parseFloat(h[1])*6E4;if(h.length>2)i+=parseFloat(h[2])*36E5;if(h.length>3)i+=parseFloat(h[3])*864E5;return i}};return c};
pv.Format.number=function(){function b(r){if(Infinity>h)r=Math.round(r*i)/i;var s=String(Math.abs(r)).split("."),t=s[0];if(t.length>d)t=t.substring(t.length-d);if(l&&t.length<c)t=(new Array(c-t.length+1)).join(j)+t;if(t.length>3)t=t.replace(/\B(?=(?:\d{3})+(?!\d))/g,n);if(!l&&t.length<f)t=(new Array(f-t.length+1)).join(j)+t;s[0]=r<0?p+t+m:t;r=s[1]||"";if(r.length<g)s[1]=r+(new Array(g-r.length+1)).join(k);return s.join(q)}var c=0,d=Infinity,f=0,g=0,h=0,i=1,j="0",k="0",l=true,q=".",n=",",p="\u2212",
m="";b.format=b;b.parse=function(r){var s=pv.Format.re;r=String(r).replace(new RegExp("^("+s(j)+")*"),"").replace(new RegExp("("+s(k)+")*$"),"").split(q);s=r[0].replace(new RegExp(s(n),"g"),"");if(s.length>d)s=s.substring(s.length-d);r=r[1]?Number("0."+r[1]):0;if(Infinity>h)r=Math.round(r*i)/i;return Math.round(s)+r};b.integerDigits=function(r,s){if(arguments.length){c=Number(r);d=arguments.length>1?Number(s):c;f=c+Math.floor(c/3)*n.length;return this}return[c,d]};b.fractionDigits=function(r,s){if(arguments.length){g=
Number(r);h=arguments.length>1?Number(s):g;i=Math.pow(10,h);return this}return[g,h]};b.integerPad=function(r){if(arguments.length){j=String(r);l=/\d/.test(j);return this}return j};b.fractionPad=function(r){if(arguments.length){k=String(r);return this}return k};b.decimal=function(r){if(arguments.length){q=String(r);return this}return q};b.group=function(r){if(arguments.length){n=r?String(r):"";f=c+Math.floor(c/3)*n.length;return this}return n};b.negativeAffix=function(r,s){if(arguments.length){p=String(r||
"");m=String(s||"");return this}return[p,m]};return b};pv.map=function(b,c){var d={};return c?b.map(function(f,g){d.index=g;return c.call(d,f)}):b.slice()};pv.repeat=function(b,c){if(arguments.length==1)c=2;return pv.blend(pv.range(c).map(function(){return b}))};pv.cross=function(b,c){for(var d=[],f=0,g=b.length,h=c.length;f<g;f++)for(var i=0,j=b[f];i<h;i++)d.push([j,c[i]]);return d};pv.blend=function(b){return Array.prototype.concat.apply([],b)};
pv.transpose=function(b){var c=b.length,d=pv.max(b,function(i){return i.length});if(d>c){b.length=d;for(var f=c;f<d;f++)b[f]=new Array(c);for(f=0;f<c;f++)for(var g=f+1;g<d;g++){var h=b[f][g];b[f][g]=b[g][f];b[g][f]=h}}else{for(f=0;f<d;f++)b[f].length=c;for(f=0;f<c;f++)for(g=0;g<f;g++){h=b[f][g];b[f][g]=b[g][f];b[g][f]=h}}b.length=d;for(f=0;f<d;f++)b[f].length=c;return b};pv.normalize=function(b,c){b=pv.map(b,c);c=pv.sum(b);for(var d=0;d<b.length;d++)b[d]/=c;return b};
pv.permute=function(b,c,d){if(!d)d=pv.identity;var f=new Array(c.length),g={};c.forEach(function(h,i){g.index=h;f[i]=d.call(g,b[h])});return f};pv.numerate=function(b,c){if(!c)c=pv.identity;var d={},f={};b.forEach(function(g,h){f.index=h;d[c.call(f,g)]=h});return d};pv.uniq=function(b,c){if(!c)c=pv.identity;var d={},f=[],g={},h;b.forEach(function(i,j){g.index=j;h=c.call(g,i);h in d||(d[h]=f.push(h))});return f};pv.naturalOrder=function(b,c){return b<c?-1:b>c?1:0};
pv.reverseOrder=function(b,c){return c<b?-1:c>b?1:0};pv.search=function(b,c,d){if(!d)d=pv.identity;for(var f=0,g=b.length-1;f<=g;){var h=f+g>>1,i=d(b[h]);if(i<c)f=h+1;else if(i>c)g=h-1;else return h}return-f-1};pv.search.index=function(b,c,d){b=pv.search(b,c,d);return b<0?-b-1:b};
pv.range=function(b,c,d){if(arguments.length==1){c=b;b=0}if(d==undefined)d=1;if((c-b)/d==Infinity)throw new Error("range must be finite");var f=[],g=0,h;c-=(c-b)*1.0E-10;if(d<0)for(;(h=b+d*g++)>c;)f.push(h);else for(;(h=b+d*g++)<c;)f.push(h);return f};pv.random=function(b,c,d){if(arguments.length==1){c=b;b=0}if(d==undefined)d=1;return d?Math.floor(Math.random()*(c-b)/d)*d+b:Math.random()*(c-b)+b};
pv.sum=function(b,c){var d={};return b.reduce(c?function(f,g,h){d.index=h;return f+c.call(d,g)}:function(f,g){return f+g},0)};pv.max=function(b,c){if(c==pv.index)return b.length-1;return Math.max.apply(null,c?pv.map(b,c):b)};pv.max.index=function(b,c){if(!b.length)return-1;if(c==pv.index)return b.length-1;if(!c)c=pv.identity;for(var d=0,f=-Infinity,g={},h=0;h<b.length;h++){g.index=h;var i=c.call(g,b[h]);if(i>f){f=i;d=h}}return d};
pv.min=function(b,c){if(c==pv.index)return 0;return Math.min.apply(null,c?pv.map(b,c):b)};pv.min.index=function(b,c){if(!b.length)return-1;if(c==pv.index)return 0;if(!c)c=pv.identity;for(var d=0,f=Infinity,g={},h=0;h<b.length;h++){g.index=h;var i=c.call(g,b[h]);if(i<f){f=i;d=h}}return d};pv.mean=function(b,c){return pv.sum(b,c)/b.length};
pv.median=function(b,c){if(c==pv.index)return(b.length-1)/2;b=pv.map(b,c).sort(pv.naturalOrder);if(b.length%2)return b[Math.floor(b.length/2)];c=b.length/2;return(b[c-1]+b[c])/2};pv.variance=function(b,c){if(b.length<1)return NaN;if(b.length==1)return 0;var d=pv.mean(b,c),f=0,g={};if(!c)c=pv.identity;for(var h=0;h<b.length;h++){g.index=h;var i=c.call(g,b[h])-d;f+=i*i}return f};pv.deviation=function(b,c){return Math.sqrt(pv.variance(b,c)/(b.length-1))};pv.log=function(b,c){return Math.log(b)/Math.log(c)};
pv.logSymmetric=function(b,c){return b==0?0:b<0?-pv.log(-b,c):pv.log(b,c)};pv.logAdjusted=function(b,c){if(!isFinite(b))return b;var d=b<0;if(b<c)b+=(c-b)/c;return d?-pv.log(b,c):pv.log(b,c)};pv.logFloor=function(b,c){return b>0?Math.pow(c,Math.floor(pv.log(b,c))):-Math.pow(c,-Math.floor(-pv.log(-b,c)))};pv.logCeil=function(b,c){return b>0?Math.pow(c,Math.ceil(pv.log(b,c))):-Math.pow(c,-Math.ceil(-pv.log(-b,c)))};
(function(){var b=Math.PI/180,c=180/Math.PI;pv.radians=function(d){return b*d};pv.degrees=function(d){return c*d}})();pv.keys=function(b){var c=[];for(var d in b)c.push(d);return c};pv.entries=function(b){var c=[];for(var d in b)c.push({key:d,value:b[d]});return c};pv.values=function(b){var c=[];for(var d in b)c.push(b[d]);return c};pv.dict=function(b,c){for(var d={},f={},g=0;g<b.length;g++)if(g in b){var h=b[g];f.index=g;d[h]=c.call(f,h)}return d};pv.dom=function(b){return new pv.Dom(b)};
pv.Dom=function(b){this.$map=b};pv.Dom.prototype.$leaf=function(b){return typeof b!="object"};pv.Dom.prototype.leaf=function(b){if(arguments.length){this.$leaf=b;return this}return this.$leaf};pv.Dom.prototype.root=function(b){function c(g){var h=new pv.Dom.Node;for(var i in g){var j=g[i];h.appendChild(d(j)?new pv.Dom.Node(j):c(j)).nodeName=i}return h}var d=this.$leaf,f=c(this.$map);f.nodeName=b;return f};pv.Dom.prototype.nodes=function(){return this.root().nodes()};
pv.Dom.Node=function(b){this.nodeValue=b;this.childNodes=[]};a=pv.Dom.Node.prototype;a.parentNode=null;a.firstChild=null;a.lastChild=null;a.previousSibling=null;a.nextSibling=null;
a.removeChild=function(b){var c=this.childNodes.indexOf(b);if(c==-1)throw new Error("child not found");this.childNodes.splice(c,1);if(b.previousSibling)b.previousSibling.nextSibling=b.nextSibling;else this.firstChild=b.nextSibling;if(b.nextSibling)b.nextSibling.previousSibling=b.previousSibling;else this.lastChild=b.previousSibling;delete b.nextSibling;delete b.previousSibling;delete b.parentNode;return b};
a.appendChild=function(b){b.parentNode&&b.parentNode.removeChild(b);b.parentNode=this;if(b.previousSibling=this.lastChild)this.lastChild.nextSibling=b;else this.firstChild=b;this.lastChild=b;this.childNodes.push(b);return b};
a.insertBefore=function(b,c){if(!c)return this.appendChild(b);var d=this.childNodes.indexOf(c);if(d==-1)throw new Error("child not found");b.parentNode&&b.parentNode.removeChild(b);b.parentNode=this;b.nextSibling=c;if(b.previousSibling=c.previousSibling)c.previousSibling.nextSibling=b;else{if(c==this.lastChild)this.lastChild=b;this.firstChild=b}this.childNodes.splice(d,0,b);return b};
a.replaceChild=function(b,c){var d=this.childNodes.indexOf(c);if(d==-1)throw new Error("child not found");b.parentNode&&b.parentNode.removeChild(b);b.parentNode=this;b.nextSibling=c.nextSibling;if(b.previousSibling=c.previousSibling)c.previousSibling.nextSibling=b;else this.firstChild=b;if(c.nextSibling)c.nextSibling.previousSibling=b;else this.lastChild=b;this.childNodes[d]=b;return c};a.visitBefore=function(b){function c(d,f){b(d,f);for(d=d.firstChild;d;d=d.nextSibling)c(d,f+1)}c(this,0)};
a.visitAfter=function(b){function c(d,f){for(var g=d.firstChild;g;g=g.nextSibling)c(g,f+1);b(d,f)}c(this,0)};a.sort=function(b){if(this.firstChild){this.childNodes.sort(b);var c=this.firstChild=this.childNodes[0],d;delete c.previousSibling;for(var f=1;f<this.childNodes.length;f++){c.sort(b);d=this.childNodes[f];d.previousSibling=c;c=c.nextSibling=d}this.lastChild=c;delete c.nextSibling;c.sort(b)}return this};
a.reverse=function(){var b=[];this.visitAfter(function(c){for(;c.lastChild;)b.push(c.removeChild(c.lastChild));for(var d;d=b.pop();)c.insertBefore(d,c.firstChild)});return this};a.nodes=function(){function b(d){c.push(d);d.childNodes.forEach(b)}var c=[];b(this,c);return c};
a.toggle=function(b){if(b)return this.toggled?this.visitBefore(function(d){d.toggled&&d.toggle()}):this.visitAfter(function(d){d.toggled||d.toggle()});b=this;if(b.toggled){for(var c;c=b.toggled.pop();)b.appendChild(c);delete b.toggled}else if(b.lastChild)for(b.toggled=[];b.lastChild;)b.toggled.push(b.removeChild(b.lastChild))};pv.nodes=function(b){for(var c=new pv.Dom.Node,d=0;d<b.length;d++)c.appendChild(new pv.Dom.Node(b[d]));return c.nodes()};pv.tree=function(b){return new pv.Tree(b)};
pv.Tree=function(b){this.array=b};pv.Tree.prototype.keys=function(b){this.k=b;return this};pv.Tree.prototype.value=function(b){this.v=b;return this};pv.Tree.prototype.map=function(){for(var b={},c={},d=0;d<this.array.length;d++){c.index=d;for(var f=this.array[d],g=this.k.call(c,f),h=b,i=0;i<g.length-1;i++)h=h[g[i]]||(h[g[i]]={});h[g[i]]=this.v?this.v.call(c,f):f}return b};pv.nest=function(b){return new pv.Nest(b)};pv.Nest=function(b){this.array=b;this.keys=[]};a=pv.Nest.prototype;
a.key=function(b){this.keys.push(b);return this};a.sortKeys=function(b){this.keys[this.keys.length-1].order=b||pv.naturalOrder;return this};a.sortValues=function(b){this.order=b||pv.naturalOrder;return this};a.map=function(){for(var b={},c=[],d,f=0;f<this.array.length;f++){var g=this.array[f],h=b;for(d=0;d<this.keys.length-1;d++){var i=this.keys[d](g);h[i]||(h[i]={});h=h[i]}i=this.keys[d](g);if(!h[i]){d=[];c.push(d);h[i]=d}h[i].push(g)}if(this.order)for(d=0;d<c.length;d++)c[d].sort(this.order);return b};
a.entries=function(){function b(d){var f=[];for(var g in d){var h=d[g];f.push({key:g,values:h instanceof Array?h:b(h)})}return f}function c(d,f){var g=this.keys[f].order;g&&d.sort(function(i,j){return g(i.key,j.key)});if(++f<this.keys.length)for(var h=0;h<d.length;h++)c.call(this,d[h].values,f);return d}return c.call(this,b(this.map()),0)};a.rollup=function(b){function c(d){for(var f in d){var g=d[f];if(g instanceof Array)d[f]=b(g);else c(g)}return d}return c(this.map())};pv.flatten=function(b){return new pv.Flatten(b)};
pv.Flatten=function(b){this.map=b;this.keys=[]};pv.Flatten.prototype.key=function(b,c){this.keys.push({name:b,value:c});delete this.$leaf;return this};pv.Flatten.prototype.leaf=function(b){this.keys.length=0;this.$leaf=b;return this};
pv.Flatten.prototype.array=function(){function b(i,j){if(j<f.length-1)for(var k in i){d.push(k);b(i[k],j+1);d.pop()}else c.push(d.concat(i))}var c=[],d=[],f=this.keys,g=this.$leaf;if(g){function h(i,j){if(g(i))c.push({keys:d.slice(),value:i});else for(var k in i){d.push(k);h(i[k],j+1);d.pop()}}h(this.map,0);return c}b(this.map,0);return c.map(function(i){for(var j={},k=0;k<f.length;k++){var l=f[k],q=i[k];j[l.name]=l.value?l.value.call(null,q):q}return j})};
pv.vector=function(b,c){return new pv.Vector(b,c)};pv.Vector=function(b,c){this.x=b;this.y=c};a=pv.Vector.prototype;a.perp=function(){return new pv.Vector(-this.y,this.x)};a.norm=function(){var b=this.length();return this.times(b?1/b:1)};a.length=function(){return Math.sqrt(this.x*this.x+this.y*this.y)};a.times=function(b){return new pv.Vector(this.x*b,this.y*b)};a.plus=function(b,c){return arguments.length==1?new pv.Vector(this.x+b.x,this.y+b.y):new pv.Vector(this.x+b,this.y+c)};
a.minus=function(b,c){return arguments.length==1?new pv.Vector(this.x-b.x,this.y-b.y):new pv.Vector(this.x-b,this.y-c)};a.dot=function(b,c){return arguments.length==1?this.x*b.x+this.y*b.y:this.x*b+this.y*c};pv.Transform=function(){};pv.Transform.prototype={k:1,x:0,y:0};pv.Transform.identity=new pv.Transform;pv.Transform.prototype.translate=function(b,c){var d=new pv.Transform;d.k=this.k;d.x=this.k*b+this.x;d.y=this.k*c+this.y;return d};
pv.Transform.prototype.scale=function(b){var c=new pv.Transform;c.k=this.k*b;c.x=this.x;c.y=this.y;return c};pv.Transform.prototype.invert=function(){var b=new pv.Transform,c=1/this.k;b.k=c;b.x=-this.x*c;b.y=-this.y*c;return b};pv.Transform.prototype.times=function(b){var c=new pv.Transform;c.k=this.k*b.k;c.x=this.k*b.x+this.x;c.y=this.k*b.y+this.y;return c};pv.Scale=function(){};
pv.Scale.interpolator=function(b,c){if(typeof b=="number")return function(d){return d*(c-b)+b};b=pv.color(b).rgb();c=pv.color(c).rgb();return function(d){var f=b.a*(1-d)+c.a*d;if(f<1.0E-5)f=0;return b.a==0?pv.rgb(c.r,c.g,c.b,f):c.a==0?pv.rgb(b.r,b.g,b.b,f):pv.rgb(Math.round(b.r*(1-d)+c.r*d),Math.round(b.g*(1-d)+c.g*d),Math.round(b.b*(1-d)+c.b*d),f)}};
pv.Scale.quantitative=function(){function b(n){return new Date(n)}function c(n){var p=pv.search(d,n);if(p<0)p=-p-2;p=Math.max(0,Math.min(h.length-1,p));return h[p]((k(n)-f[p])/(f[p+1]-f[p]))}var d=[0,1],f=[0,1],g=[0,1],h=[pv.identity],i=Number,j=false,k=pv.identity,l=pv.identity,q=String;c.transform=function(n,p){k=function(m){return j?-n(-m):n(m)};l=function(m){return j?-p(-m):p(m)};f=d.map(k);return this};c.domain=function(n,p,m){if(arguments.length){var r;if(n instanceof Array){if(arguments.length<
2)p=pv.identity;if(arguments.length<3)m=p;r=n.length&&p(n[0]);d=n.length?[pv.min(n,p),pv.max(n,m)]:[]}else{r=n;d=Array.prototype.slice.call(arguments).map(Number)}if(d.length){if(d.length==1)d=[d[0],d[0]]}else d=[-Infinity,Infinity];j=(d[0]||d[d.length-1])<0;f=d.map(k);i=r instanceof Date?b:Number;return this}return d.map(i)};c.range=function(){if(arguments.length){g=Array.prototype.slice.call(arguments);if(g.length){if(g.length==1)g=[g[0],g[0]]}else g=[-Infinity,Infinity];h=[];for(var n=0;n<g.length-
1;n++)h.push(pv.Scale.interpolator(g[n],g[n+1]));return this}return g};c.invert=function(n){var p=pv.search(g,n);if(p<0)p=-p-2;p=Math.max(0,Math.min(h.length-1,p));return i(l(f[p]+(n-g[p])/(g[p+1]-g[p])*(f[p+1]-f[p])))};c.ticks=function(n){var p=d[0],m=d[d.length-1],r=m<p,s=r?m:p;m=r?p:m;var t=m-s;if(!t||!isFinite(t)){if(i==b)q=pv.Format.date("%x");return[i(s)]}if(i==b){function x(w,y){switch(y){case 31536E6:w.setMonth(0);case 2592E6:w.setDate(1);case 6048E5:y==6048E5&&w.setDate(w.getDate()-w.getDay());
case 864E5:w.setHours(0);case 36E5:w.setMinutes(0);case 6E4:w.setSeconds(0);case 1E3:w.setMilliseconds(0)}}var u,o,v=1;if(t>=94608E6){p=31536E6;u="%Y";o=function(w){w.setFullYear(w.getFullYear()+v)}}else if(t>=7776E6){p=2592E6;u="%m/%Y";o=function(w){w.setMonth(w.getMonth()+v)}}else if(t>=18144E5){p=6048E5;u="%m/%d";o=function(w){w.setDate(w.getDate()+7*v)}}else if(t>=2592E5){p=864E5;u="%m/%d";o=function(w){w.setDate(w.getDate()+v)}}else if(t>=108E5){p=36E5;u="%I:%M %p";o=function(w){w.setHours(w.getHours()+
v)}}else if(t>=18E4){p=6E4;u="%I:%M %p";o=function(w){w.setMinutes(w.getMinutes()+v)}}else if(t>=3E3){p=1E3;u="%I:%M:%S";o=function(w){w.setSeconds(w.getSeconds()+v)}}else{p=1;u="%S.%Qs";o=function(w){w.setTime(w.getTime()+v)}}q=pv.Format.date(u);s=new Date(s);u=[];x(s,p);t=t/p;if(t>10)switch(p){case 36E5:v=t>20?6:3;s.setHours(Math.floor(s.getHours()/v)*v);break;case 2592E6:v=3;s.setMonth(Math.floor(s.getMonth()/v)*v);break;case 6E4:v=t>30?15:t>15?10:5;s.setMinutes(Math.floor(s.getMinutes()/v)*v);
break;case 1E3:v=t>90?15:t>60?10:5;s.setSeconds(Math.floor(s.getSeconds()/v)*v);break;case 1:v=t>1E3?250:t>200?100:t>100?50:t>50?25:5;s.setMilliseconds(Math.floor(s.getMilliseconds()/v)*v);break;default:v=pv.logCeil(t/15,10);if(t/v<2)v/=5;else if(t/v<5)v/=2;s.setFullYear(Math.floor(s.getFullYear()/v)*v);break}for(;;){o(s);if(s>m)break;u.push(new Date(s))}return r?u.reverse():u}arguments.length||(n=10);v=pv.logFloor(t/n,10);p=n/(t/v);if(p<=0.15)v*=10;else if(p<=0.35)v*=5;else if(p<=0.75)v*=2;p=Math.ceil(s/
v)*v;m=Math.floor(m/v)*v;q=pv.Format.number().fractionDigits(Math.max(0,-Math.floor(pv.log(v,10)+0.01)));m=pv.range(p,m+v,v);return r?m.reverse():m};c.tickFormat=function(n){return q(n)};c.nice=function(){if(d.length!=2)return this;var n=d[0],p=d[d.length-1],m=p<n,r=m?p:n;n=m?n:p;p=n-r;if(!p||!isFinite(p))return this;p=Math.pow(10,Math.round(Math.log(p)/Math.log(10))-1);d=[Math.floor(r/p)*p,Math.ceil(n/p)*p];m&&d.reverse();f=d.map(k);return this};c.by=function(n){function p(){return c(n.apply(this,
arguments))}for(var m in c)p[m]=c[m];return p};c.domain.apply(c,arguments);return c};pv.Scale.linear=function(){var b=pv.Scale.quantitative();b.domain.apply(b,arguments);return b};
pv.Scale.log=function(){var b=pv.Scale.quantitative(1,10),c,d,f=function(h){return Math.log(h)/d},g=function(h){return Math.pow(c,h)};b.ticks=function(){var h=b.domain(),i=h[0]<0,j=Math.floor(i?-f(-h[0]):f(h[0])),k=Math.ceil(i?-f(-h[1]):f(h[1])),l=[];if(i)for(l.push(-g(-j));j++<k;)for(i=c-1;i>0;i--)l.push(-g(-j)*i);else{for(;j<k;j++)for(i=1;i<c;i++)l.push(g(j)*i);l.push(g(j))}for(j=0;l[j]<h[0];j++);for(k=l.length;l[k-1]>h[1];k--);return l.slice(j,k)};b.tickFormat=function(h){return h.toPrecision(1)};
b.nice=function(){var h=b.domain();return b.domain(pv.logFloor(h[0],c),pv.logCeil(h[1],c))};b.base=function(h){if(arguments.length){c=Number(h);d=Math.log(c);b.transform(f,g);return this}return c};b.domain.apply(b,arguments);return b.base(10)};pv.Scale.root=function(){var b=pv.Scale.quantitative();b.power=function(c){if(arguments.length){var d=Number(c),f=1/d;b.transform(function(g){return Math.pow(g,f)},function(g){return Math.pow(g,d)});return this}return d};b.domain.apply(b,arguments);return b.power(2)};
pv.Scale.ordinal=function(){function b(g){g in d||(d[g]=c.push(g)-1);return f[d[g]%f.length]}var c=[],d={},f=[];b.domain=function(g,h){if(arguments.length){g=g instanceof Array?arguments.length>1?pv.map(g,h):g:Array.prototype.slice.call(arguments);c=[];for(var i={},j=0;j<g.length;j++){var k=g[j];if(!(k in i)){i[k]=true;c.push(k)}}d=pv.numerate(c);return this}return c};b.range=function(g,h){if(arguments.length){f=g instanceof Array?arguments.length>1?pv.map(g,h):g:Array.prototype.slice.call(arguments);
if(typeof f[0]=="string")f=f.map(pv.color);return this}return f};b.split=function(g,h){var i=(h-g)/this.domain().length;f=pv.range(g+i/2,h,i);return this};b.splitFlush=function(g,h){var i=this.domain().length,j=(h-g)/(i-1);f=i==1?[(g+h)/2]:pv.range(g,h+j/2,j);return this};b.splitBanded=function(g,h,i){if(arguments.length<3)i=1;if(i<0){var j=this.domain().length;j=(h-g- -i*j)/(j+1);f=pv.range(g+j,h,j-i);f.band=-i}else{j=(h-g)/(this.domain().length+(1-i));f=pv.range(g+j*(1-i),h,j);f.band=j*i}return this};
b.by=function(g){function h(){return b(g.apply(this,arguments))}for(var i in b)h[i]=b[i];return h};b.domain.apply(b,arguments);return b};
pv.Scale.quantile=function(){function b(i){return h(Math.max(0,Math.min(d,pv.search.index(f,i)-1))/d)}var c=-1,d=-1,f=[],g=[],h=pv.Scale.linear();b.quantiles=function(i){if(arguments.length){c=Number(i);if(c<0){f=[g[0]].concat(g);d=g.length-1}else{f=[];f[0]=g[0];for(var j=1;j<=c;j++)f[j]=g[~~(j*(g.length-1)/c)];d=c-1}return this}return f};b.domain=function(i,j){if(arguments.length){g=i instanceof Array?pv.map(i,j):Array.prototype.slice.call(arguments);g.sort(pv.naturalOrder);b.quantiles(c);return this}return g};
b.range=function(){if(arguments.length){h.range.apply(h,arguments);return this}return h.range()};b.by=function(i){function j(){return b(i.apply(this,arguments))}for(var k in b)j[k]=b[k];return j};b.domain.apply(b,arguments);return b};
pv.histogram=function(b,c){var d=true;return{bins:function(f){var g=pv.map(b,c),h=[];arguments.length||(f=pv.Scale.linear(g).ticks());for(var i=0;i<f.length-1;i++){var j=h[i]=[];j.x=f[i];j.dx=f[i+1]-f[i];j.y=0}for(i=0;i<g.length;i++){j=pv.search.index(f,g[i])-1;j=h[Math.max(0,Math.min(h.length-1,j))];j.y++;j.push(b[i])}if(!d)for(i=0;i<h.length;i++)h[i].y/=g.length;return h},frequency:function(f){if(arguments.length){d=Boolean(f);return this}return d}}};
pv.color=function(b){if(b.rgb)return b.rgb();var c=/([a-z]+)\((.*)\)/i.exec(b);if(c){var d=c[2].split(","),f=1;switch(c[1]){case "hsla":case "rgba":f=parseFloat(d[3]);if(!f)return pv.Color.transparent;break}switch(c[1]){case "hsla":case "hsl":b=parseFloat(d[0]);var g=parseFloat(d[1])/100;d=parseFloat(d[2])/100;return(new pv.Color.Hsl(b,g,d,f)).rgb();case "rgba":case "rgb":function h(k){var l=parseFloat(k);return k[k.length-1]=="%"?Math.round(l*2.55):l}g=h(d[0]);var i=h(d[1]),j=h(d[2]);return pv.rgb(g,
i,j,f)}}if(f=pv.Color.names[b])return f;if(b.charAt(0)=="#"){if(b.length==4){g=b.charAt(1);g+=g;i=b.charAt(2);i+=i;j=b.charAt(3);j+=j}else if(b.length==7){g=b.substring(1,3);i=b.substring(3,5);j=b.substring(5,7)}return pv.rgb(parseInt(g,16),parseInt(i,16),parseInt(j,16),1)}return new pv.Color(b,1)};pv.Color=function(b,c){this.color=b;this.opacity=c};pv.Color.prototype.brighter=function(b){return this.rgb().brighter(b)};pv.Color.prototype.darker=function(b){return this.rgb().darker(b)};
pv.rgb=function(b,c,d,f){return new pv.Color.Rgb(b,c,d,arguments.length==4?f:1)};pv.Color.Rgb=function(b,c,d,f){pv.Color.call(this,f?"rgb("+b+","+c+","+d+")":"none",f);this.r=b;this.g=c;this.b=d;this.a=f};pv.Color.Rgb.prototype=pv.extend(pv.Color);a=pv.Color.Rgb.prototype;a.red=function(b){return pv.rgb(b,this.g,this.b,this.a)};a.green=function(b){return pv.rgb(this.r,b,this.b,this.a)};a.blue=function(b){return pv.rgb(this.r,this.g,b,this.a)};
a.alpha=function(b){return pv.rgb(this.r,this.g,this.b,b)};a.rgb=function(){return this};a.brighter=function(b){b=Math.pow(0.7,arguments.length?b:1);var c=this.r,d=this.g,f=this.b;if(!c&&!d&&!f)return pv.rgb(30,30,30,this.a);if(c&&c<30)c=30;if(d&&d<30)d=30;if(f&&f<30)f=30;return pv.rgb(Math.min(255,Math.floor(c/b)),Math.min(255,Math.floor(d/b)),Math.min(255,Math.floor(f/b)),this.a)};
a.darker=function(b){b=Math.pow(0.7,arguments.length?b:1);return pv.rgb(Math.max(0,Math.floor(b*this.r)),Math.max(0,Math.floor(b*this.g)),Math.max(0,Math.floor(b*this.b)),this.a)};pv.hsl=function(b,c,d,f){return new pv.Color.Hsl(b,c,d,arguments.length==4?f:1)};pv.Color.Hsl=function(b,c,d,f){pv.Color.call(this,"hsl("+b+","+c*100+"%,"+d*100+"%)",f);this.h=b;this.s=c;this.l=d;this.a=f};pv.Color.Hsl.prototype=pv.extend(pv.Color);a=pv.Color.Hsl.prototype;
a.hue=function(b){return pv.hsl(b,this.s,this.l,this.a)};a.saturation=function(b){return pv.hsl(this.h,b,this.l,this.a)};a.lightness=function(b){return pv.hsl(this.h,this.s,b,this.a)};a.alpha=function(b){return pv.hsl(this.h,this.s,this.l,b)};
a.rgb=function(){function b(j){if(j>360)j-=360;else if(j<0)j+=360;if(j<60)return i+(h-i)*j/60;if(j<180)return h;if(j<240)return i+(h-i)*(240-j)/60;return i}function c(j){return Math.round(b(j)*255)}var d=this.h,f=this.s,g=this.l;d%=360;if(d<0)d+=360;f=Math.max(0,Math.min(f,1));g=Math.max(0,Math.min(g,1));var h=g<=0.5?g*(1+f):g+f-g*f,i=2*g-h;return pv.rgb(c(d+120),c(d),c(d-120),this.a)};
pv.Color.names={aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgreen:"#006400",
darkgrey:"#a9a9a9",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkslategrey:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1e90ff",firebrick:"#b22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",
ghostwhite:"#f8f8ff",gold:"#ffd700",goldenrod:"#daa520",gray:"#808080",green:"#008000",greenyellow:"#adff2f",grey:"#808080",honeydew:"#f0fff0",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",lavender:"#e6e6fa",lavenderblush:"#fff0f5",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrodyellow:"#fafad2",lightgray:"#d3d3d3",lightgreen:"#90ee90",lightgrey:"#d3d3d3",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",
lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370db",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",
moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#db7093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",
seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",slategrey:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yellowgreen:"#9acd32",transparent:pv.Color.transparent=pv.rgb(0,0,0,0)};(function(){var b=pv.Color.names;for(var c in b)b[c]=pv.color(b[c])})();
pv.colors=function(){var b=pv.Scale.ordinal();b.range.apply(b,arguments);return b};pv.Colors={};pv.Colors.category10=function(){var b=pv.colors("#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd","#8c564b","#e377c2","#7f7f7f","#bcbd22","#17becf");b.domain.apply(b,arguments);return b};
pv.Colors.category20=function(){var b=pv.colors("#1f77b4","#aec7e8","#ff7f0e","#ffbb78","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5","#8c564b","#c49c94","#e377c2","#f7b6d2","#7f7f7f","#c7c7c7","#bcbd22","#dbdb8d","#17becf","#9edae5");b.domain.apply(b,arguments);return b};
pv.Colors.category19=function(){var b=pv.colors("#9c9ede","#7375b5","#4a5584","#cedb9c","#b5cf6b","#8ca252","#637939","#e7cb94","#e7ba52","#bd9e39","#8c6d31","#e7969c","#d6616b","#ad494a","#843c39","#de9ed6","#ce6dbd","#a55194","#7b4173");b.domain.apply(b,arguments);return b};pv.ramp=function(){var b=pv.Scale.linear();b.range.apply(b,arguments);return b};
pv.Scene=pv.SvgScene={svg:"http://www.w3.org/2000/svg",xmlns:"http://www.w3.org/2000/xmlns",xlink:"http://www.w3.org/1999/xlink",xhtml:"http://www.w3.org/1999/xhtml",scale:1,events:["DOMMouseScroll","mousewheel","mousedown","mouseup","mouseover","mouseout","mousemove","click","dblclick"],implicit:{svg:{"shape-rendering":"auto","pointer-events":"painted",x:0,y:0,dy:0,"text-anchor":"start",transform:"translate(0,0)",fill:"none","fill-opacity":1,stroke:"none","stroke-opacity":1,"stroke-width":1.5,"stroke-linejoin":"miter"},
css:{font:"10px sans-serif"}}};pv.SvgScene.updateAll=function(b){if(b.length&&b[0].reverse&&b.type!="line"&&b.type!="area"){for(var c=pv.extend(b),d=0,f=b.length-1;f>=0;d++,f--)c[d]=b[f];b=c}this.removeSiblings(this[b.type](b))};pv.SvgScene.create=function(b){return document.createElementNS(this.svg,b)};
pv.SvgScene.expect=function(b,c,d,f){if(b){if(b.tagName=="a")b=b.firstChild;if(b.tagName!=c){c=this.create(c);b.parentNode.replaceChild(c,b);b=c}}else b=this.create(c);for(var g in d){c=d[g];if(c==this.implicit.svg[g])c=null;c==null?b.removeAttribute(g):b.setAttribute(g,c)}for(g in f){c=f[g];if(c==this.implicit.css[g])c=null;if(c==null)b.style.removeProperty(g);else b.style[g]=c}return b};
pv.SvgScene.append=function(b,c,d){b.$scene={scenes:c,index:d};b=this.title(b,c[d]);b.parentNode||c.$g.appendChild(b);return b.nextSibling};pv.SvgScene.title=function(b,c){var d=b.parentNode;if(d&&d.tagName!="a")d=null;if(c.title){if(!d){d=this.create("a");b.parentNode&&b.parentNode.replaceChild(d,b);d.appendChild(b)}d.setAttributeNS(this.xlink,"title",c.title);return d}d&&d.parentNode.replaceChild(b,d);return b};
pv.SvgScene.dispatch=pv.listener(function(b){var c=b.target.$scene;if(c){var d=b.type;switch(d){case "DOMMouseScroll":d="mousewheel";b.wheel=-480*b.detail;break;case "mousewheel":b.wheel=(window.opera?12:1)*b.wheelDelta;break}pv.Mark.dispatch(d,c.scenes,c.index)&&b.preventDefault()}});pv.SvgScene.removeSiblings=function(b){for(;b;){var c=b.nextSibling;b.parentNode.removeChild(b);b=c}};pv.SvgScene.undefined=function(){};
pv.SvgScene.pathBasis=function(){function b(f,g,h,i,j){return{x:f[0]*g.left+f[1]*h.left+f[2]*i.left+f[3]*j.left,y:f[0]*g.top+f[1]*h.top+f[2]*i.top+f[3]*j.top}}var c=[[1/6,2/3,1/6,0],[0,2/3,1/3,0],[0,1/3,2/3,0],[0,1/6,2/3,1/6]],d=function(f,g,h,i){var j=b(c[1],f,g,h,i),k=b(c[2],f,g,h,i);f=b(c[3],f,g,h,i);return"C"+j.x+","+j.y+","+k.x+","+k.y+","+f.x+","+f.y};d.segment=function(f,g,h,i){var j=b(c[0],f,g,h,i),k=b(c[1],f,g,h,i),l=b(c[2],f,g,h,i);f=b(c[3],f,g,h,i);return"M"+j.x+","+j.y+"C"+k.x+","+k.y+
","+l.x+","+l.y+","+f.x+","+f.y};return d}();pv.SvgScene.curveBasis=function(b){if(b.length<=2)return"";var c="",d=b[0],f=d,g=d,h=b[1];c+=this.pathBasis(d,f,g,h);for(var i=2;i<b.length;i++){d=f;f=g;g=h;h=b[i];c+=this.pathBasis(d,f,g,h)}c+=this.pathBasis(f,g,h,h);c+=this.pathBasis(g,h,h,h);return c};
pv.SvgScene.curveBasisSegments=function(b){if(b.length<=2)return"";var c=[],d=b[0],f=d,g=d,h=b[1],i=this.pathBasis.segment(d,f,g,h);d=f;f=g;g=h;h=b[2];c.push(i+this.pathBasis(d,f,g,h));for(i=3;i<b.length;i++){d=f;f=g;g=h;h=b[i];c.push(this.pathBasis.segment(d,f,g,h))}c.push(this.pathBasis.segment(f,g,h,h)+this.pathBasis(g,h,h,h));return c};
pv.SvgScene.curveHermite=function(b,c){if(c.length<1||b.length!=c.length&&b.length!=c.length+2)return"";var d=b.length!=c.length,f="",g=b[0],h=b[1],i=c[0],j=i,k=1;if(d){f+="Q"+(h.left-i.x*2/3)+","+(h.top-i.y*2/3)+","+h.left+","+h.top;g=b[1];k=2}if(c.length>1){j=c[1];h=b[k];k++;f+="C"+(g.left+i.x)+","+(g.top+i.y)+","+(h.left-j.x)+","+(h.top-j.y)+","+h.left+","+h.top;for(g=2;g<c.length;g++,k++){h=b[k];j=c[g];f+="S"+(h.left-j.x)+","+(h.top-j.y)+","+h.left+","+h.top}}if(d){b=b[k];f+="Q"+(h.left+j.x*2/
3)+","+(h.top+j.y*2/3)+","+b.left+","+b.top}return f};
pv.SvgScene.curveHermiteSegments=function(b,c){if(c.length<1||b.length!=c.length&&b.length!=c.length+2)return[];var d=b.length!=c.length,f=[],g=b[0],h=g,i=c[0],j=i,k=1;if(d){h=b[1];f.push("M"+g.left+","+g.top+"Q"+(h.left-j.x*2/3)+","+(h.top-j.y*2/3)+","+h.left+","+h.top);k=2}for(var l=1;l<c.length;l++,k++){g=h;i=j;h=b[k];j=c[l];f.push("M"+g.left+","+g.top+"C"+(g.left+i.x)+","+(g.top+i.y)+","+(h.left-j.x)+","+(h.top-j.y)+","+h.left+","+h.top)}if(d){b=b[k];f.push("M"+h.left+","+h.top+"Q"+(h.left+j.x*
2/3)+","+(h.top+j.y*2/3)+","+b.left+","+b.top)}return f};pv.SvgScene.cardinalTangents=function(b,c){var d=[];c=(1-c)/2;for(var f=b[0],g=b[1],h=b[2],i=3;i<b.length;i++){d.push({x:c*(h.left-f.left),y:c*(h.top-f.top)});f=g;g=h;h=b[i]}d.push({x:c*(h.left-f.left),y:c*(h.top-f.top)});return d};pv.SvgScene.curveCardinal=function(b,c){if(b.length<=2)return"";return this.curveHermite(b,this.cardinalTangents(b,c))};
pv.SvgScene.curveCardinalSegments=function(b,c){if(b.length<=2)return"";return this.curveHermiteSegments(b,this.cardinalTangents(b,c))};
pv.SvgScene.monotoneTangents=function(b){var c=[],d=[],f=[],g=[],h=0;for(h=0;h<b.length-1;h++)d[h]=(b[h+1].top-b[h].top)/(b[h+1].left-b[h].left);f[0]=d[0];g[0]=b[1].left-b[0].left;for(h=1;h<b.length-1;h++){f[h]=(d[h-1]+d[h])/2;g[h]=(b[h+1].left-b[h-1].left)/2}f[h]=d[h-1];g[h]=b[h].left-b[h-1].left;for(h=0;h<b.length-1;h++)if(d[h]==0){f[h]=0;f[h+1]=0}for(h=0;h<b.length-1;h++)if(!(Math.abs(f[h])<1.0E-5||Math.abs(f[h+1])<1.0E-5)){var i=f[h]/d[h],j=f[h+1]/d[h],k=i*i+j*j;if(k>9){k=3/Math.sqrt(k);f[h]=
k*i*d[h];f[h+1]=k*j*d[h]}}for(h=0;h<b.length;h++){d=1+f[h]*f[h];c.push({x:g[h]/3/d,y:f[h]*g[h]/3/d})}return c};pv.SvgScene.curveMonotone=function(b){if(b.length<=2)return"";return this.curveHermite(b,this.monotoneTangents(b))};pv.SvgScene.curveMonotoneSegments=function(b){if(b.length<=2)return"";return this.curveHermiteSegments(b,this.monotoneTangents(b))};
pv.SvgScene.area=function(b){function c(n,p){for(var m=[],r=[],s=p;n<=s;n++,p--){var t=b[n],x=b[p];t=t.left+","+t.top;x=x.left+x.width+","+(x.top+x.height);if(n<s){var u=b[n+1],o=b[p-1];switch(g.interpolate){case "step-before":t+="V"+u.top;x+="H"+(o.left+o.width);break;case "step-after":t+="H"+u.left;x+="V"+(o.top+o.height);break}}m.push(t);r.push(x)}return m.concat(r).join("L")}function d(n,p){for(var m=[],r=[],s=p;n<=s;n++,p--){var t=b[p];m.push(b[n]);r.push({left:t.left+t.width,top:t.top+t.height})}if(g.interpolate==
"basis"){n=pv.SvgScene.curveBasis(m);p=pv.SvgScene.curveBasis(r)}else if(g.interpolate=="cardinal"){n=pv.SvgScene.curveCardinal(m,g.tension);p=pv.SvgScene.curveCardinal(r,g.tension)}else{n=pv.SvgScene.curveMonotone(m);p=pv.SvgScene.curveMonotone(r)}return m[0].left+","+m[0].top+n+"L"+r[0].left+","+r[0].top+p}var f=b.$g.firstChild;if(!b.length)return f;var g=b[0];if(g.segmented)return this.areaSegment(b);if(!g.visible)return f;var h=g.fillStyle,i=g.strokeStyle;if(!h.opacity&&!i.opacity)return f;for(var j=
[],k,l=0;l<b.length;l++){k=b[l];if(k.width||k.height){for(var q=l+1;q<b.length;q++){k=b[q];if(!k.width&&!k.height)break}l&&g.interpolate!="step-after"&&l--;q<b.length&&g.interpolate!="step-before"&&q++;j.push((q-l>2&&(g.interpolate=="basis"||g.interpolate=="cardinal"||g.interpolate=="monotone")?d:c)(l,q-1));l=q-1}}if(!j.length)return f;f=this.expect(f,"path",{"shape-rendering":g.antialias?null:"crispEdges","pointer-events":g.events,cursor:g.cursor,d:"M"+j.join("ZM")+"Z",fill:h.color,"fill-opacity":h.opacity||
null,stroke:i.color,"stroke-opacity":i.opacity||null,"stroke-width":i.opacity?g.lineWidth/this.scale:null});return this.append(f,b,0)};
pv.SvgScene.areaSegment=function(b){var c=b.$g.firstChild,d=b[0],f,g;if(d.interpolate=="basis"||d.interpolate=="cardinal"||d.interpolate=="monotone"){f=[];g=[];for(var h=0,i=b.length;h<i;h++){var j=b[i-h-1];f.push(b[h]);g.push({left:j.left+j.width,top:j.top+j.height})}if(d.interpolate=="basis"){f=this.curveBasisSegments(f);g=this.curveBasisSegments(g)}else if(d.interpolate=="cardinal"){f=this.curveCardinalSegments(f,d.tension);g=this.curveCardinalSegments(g,d.tension)}else{f=this.curveMonotoneSegments(f);
g=this.curveMonotoneSegments(g)}}h=0;for(i=b.length-1;h<i;h++){d=b[h];var k=b[h+1];if(d.visible&&k.visible){var l=d.fillStyle,q=d.strokeStyle;if(l.opacity||q.opacity){if(f){j=f[h];k="L"+g[i-h-1].substr(1);j=j+k+"Z"}else{var n=d;j=k;switch(d.interpolate){case "step-before":n=k;break;case "step-after":j=d;break}j="M"+d.left+","+n.top+"L"+k.left+","+j.top+"L"+(k.left+k.width)+","+(j.top+j.height)+"L"+(d.left+d.width)+","+(n.top+n.height)+"Z"}c=this.expect(c,"path",{"shape-rendering":d.antialias?null:
"crispEdges","pointer-events":d.events,cursor:d.cursor,d:j,fill:l.color,"fill-opacity":l.opacity||null,stroke:q.color,"stroke-opacity":q.opacity||null,"stroke-width":q.opacity?d.lineWidth/this.scale:null});c=this.append(c,b,h)}}}return c};
pv.SvgScene.bar=function(b){for(var c=b.$g.firstChild,d=0;d<b.length;d++){var f=b[d];if(f.visible){var g=f.fillStyle,h=f.strokeStyle;if(g.opacity||h.opacity){c=this.expect(c,"rect",{"shape-rendering":f.antialias?null:"crispEdges","pointer-events":f.events,cursor:f.cursor,x:f.left,y:f.top,width:Math.max(1.0E-10,f.width),height:Math.max(1.0E-10,f.height),fill:g.color,"fill-opacity":g.opacity||null,stroke:h.color,"stroke-opacity":h.opacity||null,"stroke-width":h.opacity?f.lineWidth/this.scale:null});
c=this.append(c,b,d)}}}return c};
pv.SvgScene.dot=function(b){for(var c=b.$g.firstChild,d=0;d<b.length;d++){var f=b[d];if(f.visible){var g=f.fillStyle,h=f.strokeStyle;if(g.opacity||h.opacity){var i=f.radius,j=null;switch(f.shape){case "cross":j="M"+-i+","+-i+"L"+i+","+i+"M"+i+","+-i+"L"+-i+","+i;break;case "triangle":j=i;var k=i*1.1547;j="M0,"+j+"L"+k+","+-j+" "+-k+","+-j+"Z";break;case "diamond":i*=Math.SQRT2;j="M0,"+-i+"L"+i+",0 0,"+i+" "+-i+",0Z";break;case "square":j="M"+-i+","+-i+"L"+i+","+-i+" "+i+","+i+" "+-i+","+i+"Z";break;
case "tick":j="M0,0L0,"+-f.size;break;case "bar":j="M0,"+f.size/2+"L0,"+-(f.size/2);break}g={"shape-rendering":f.antialias?null:"crispEdges","pointer-events":f.events,cursor:f.cursor,fill:g.color,"fill-opacity":g.opacity||null,stroke:h.color,"stroke-opacity":h.opacity||null,"stroke-width":h.opacity?f.lineWidth/this.scale:null};if(j){g.transform="translate("+f.left+","+f.top+")";if(f.angle)g.transform+=" rotate("+180*f.angle/Math.PI+")";g.d=j;c=this.expect(c,"path",g)}else{g.cx=f.left;g.cy=f.top;g.r=
i;c=this.expect(c,"circle",g)}c=this.append(c,b,d)}}}return c};
pv.SvgScene.image=function(b){for(var c=b.$g.firstChild,d=0;d<b.length;d++){var f=b[d];if(f.visible){c=this.fill(c,b,d);if(f.image){c=this.expect(c,"foreignObject",{cursor:f.cursor,x:f.left,y:f.top,width:f.width,height:f.height});var g=c.firstChild||c.appendChild(document.createElementNS(this.xhtml,"canvas"));g.$scene={scenes:b,index:d};g.style.width=f.width;g.style.height=f.height;g.width=f.imageWidth;g.height=f.imageHeight;g.getContext("2d").putImageData(f.image,0,0)}else{c=this.expect(c,"image",
{preserveAspectRatio:"none",cursor:f.cursor,x:f.left,y:f.top,width:f.width,height:f.height});c.setAttributeNS(this.xlink,"href",f.url)}c=this.append(c,b,d);c=this.stroke(c,b,d)}}return c};
pv.SvgScene.label=function(b){for(var c=b.$g.firstChild,d=0;d<b.length;d++){var f=b[d];if(f.visible){var g=f.textStyle;if(g.opacity&&f.text){var h=0,i=0,j=0,k="start";switch(f.textBaseline){case "middle":j=".35em";break;case "top":j=".71em";i=f.textMargin;break;case "bottom":i="-"+f.textMargin;break}switch(f.textAlign){case "right":k="end";h="-"+f.textMargin;break;case "center":k="middle";break;case "left":h=f.textMargin;break}c=this.expect(c,"text",{"pointer-events":f.events,cursor:f.cursor,x:h,
y:i,dy:j,transform:"translate("+f.left+","+f.top+")"+(f.textAngle?" rotate("+180*f.textAngle/Math.PI+")":"")+(this.scale!=1?" scale("+1/this.scale+")":""),fill:g.color,"fill-opacity":g.opacity||null,"text-anchor":k},{font:f.font,"text-shadow":f.textShadow,"text-decoration":f.textDecoration});if(c.firstChild)c.firstChild.nodeValue=f.text;else c.appendChild(document.createTextNode(f.text));c=this.append(c,b,d)}}}return c};
pv.SvgScene.line=function(b){var c=b.$g.firstChild;if(b.length<2)return c;var d=b[0];if(d.segmented)return this.lineSegment(b);if(!d.visible)return c;var f=d.fillStyle,g=d.strokeStyle;if(!f.opacity&&!g.opacity)return c;var h="M"+d.left+","+d.top;if(b.length>2&&(d.interpolate=="basis"||d.interpolate=="cardinal"||d.interpolate=="monotone"))switch(d.interpolate){case "basis":h+=this.curveBasis(b);break;case "cardinal":h+=this.curveCardinal(b,d.tension);break;case "monotone":h+=this.curveMonotone(b);
break}else for(var i=1;i<b.length;i++)h+=this.pathSegment(b[i-1],b[i]);c=this.expect(c,"path",{"shape-rendering":d.antialias?null:"crispEdges","pointer-events":d.events,cursor:d.cursor,d:h,fill:f.color,"fill-opacity":f.opacity||null,stroke:g.color,"stroke-opacity":g.opacity||null,"stroke-width":g.opacity?d.lineWidth/this.scale:null,"stroke-linejoin":d.lineJoin});return this.append(c,b,0)};
pv.SvgScene.lineSegment=function(b){var c=b.$g.firstChild,d=b[0],f;switch(d.interpolate){case "basis":f=this.curveBasisSegments(b);break;case "cardinal":f=this.curveCardinalSegments(b,d.tension);break;case "monotone":f=this.curveMonotoneSegments(b);break}d=0;for(var g=b.length-1;d<g;d++){var h=b[d],i=b[d+1];if(h.visible&&i.visible){var j=h.strokeStyle,k=pv.Color.transparent;if(j.opacity){if(h.interpolate=="linear"&&h.lineJoin=="miter"){k=j;j=pv.Color.transparent;i=this.pathJoin(b[d-1],h,i,b[d+2])}else i=
f?f[d]:"M"+h.left+","+h.top+this.pathSegment(h,i);c=this.expect(c,"path",{"shape-rendering":h.antialias?null:"crispEdges","pointer-events":h.events,cursor:h.cursor,d:i,fill:k.color,"fill-opacity":k.opacity||null,stroke:j.color,"stroke-opacity":j.opacity||null,"stroke-width":j.opacity?h.lineWidth/this.scale:null,"stroke-linejoin":h.lineJoin});c=this.append(c,b,d)}}}return c};
pv.SvgScene.pathSegment=function(b,c){var d=1;switch(b.interpolate){case "polar-reverse":d=0;case "polar":var f=c.left-b.left,g=c.top-b.top;b=1-b.eccentricity;f=Math.sqrt(f*f+g*g)/(2*b);if(b<=0||b>1)break;return"A"+f+","+f+" 0 0,"+d+" "+c.left+","+c.top;case "step-before":return"V"+c.top+"H"+c.left;case "step-after":return"H"+c.left+"V"+c.top}return"L"+c.left+","+c.top};pv.SvgScene.lineIntersect=function(b,c,d,f){return b.plus(c.times(d.minus(b).dot(f.perp())/c.dot(f.perp())))};
pv.SvgScene.pathJoin=function(b,c,d,f){var g=pv.vector(c.left,c.top);d=pv.vector(d.left,d.top);var h=d.minus(g),i=h.perp().norm(),j=i.times(c.lineWidth/(2*this.scale));c=g.plus(j);var k=d.plus(j),l=d.minus(j);j=g.minus(j);if(b&&b.visible){b=g.minus(b.left,b.top).perp().norm().plus(i);j=this.lineIntersect(g,b,j,h);c=this.lineIntersect(g,b,c,h)}if(f&&f.visible){f=pv.vector(f.left,f.top).minus(d).perp().norm().plus(i);l=this.lineIntersect(d,f,l,h);k=this.lineIntersect(d,f,k,h)}return"M"+c.x+","+c.y+
"L"+k.x+","+k.y+" "+l.x+","+l.y+" "+j.x+","+j.y};
pv.SvgScene.panel=function(b){for(var c=b.$g,d=c&&c.firstChild,f=0;f<b.length;f++){var g=b[f];if(g.visible){if(!b.parent){g.canvas.style.display="inline-block";if(c&&c.parentNode!=g.canvas)d=(c=g.canvas.firstChild)&&c.firstChild;if(!c){c=g.canvas.appendChild(this.create("svg"));c.setAttribute("font-size","10px");c.setAttribute("font-family","sans-serif");c.setAttribute("fill","none");c.setAttribute("stroke","none");c.setAttribute("stroke-width",1.5);for(var h=0;h<this.events.length;h++)c.addEventListener(this.events[h],
this.dispatch,false);d=c.firstChild}b.$g=c;c.setAttribute("width",g.width+g.left+g.right);c.setAttribute("height",g.height+g.top+g.bottom)}if(g.overflow=="hidden"){h=pv.id().toString(36);var i=this.expect(d,"g",{"clip-path":"url(#"+h+")"});i.parentNode||c.appendChild(i);b.$g=c=i;d=i.firstChild;d=this.expect(d,"clipPath",{id:h});h=d.firstChild||d.appendChild(this.create("rect"));h.setAttribute("x",g.left);h.setAttribute("y",g.top);h.setAttribute("width",g.width);h.setAttribute("height",g.height);d.parentNode||
c.appendChild(d);d=d.nextSibling}d=this.fill(d,b,f);var j=this.scale,k=g.transform,l=g.left+k.x,q=g.top+k.y;this.scale*=k.k;for(h=0;h<g.children.length;h++){g.children[h].$g=d=this.expect(d,"g",{transform:"translate("+l+","+q+")"+(k.k!=1?" scale("+k.k+")":"")});this.updateAll(g.children[h]);d.parentNode||c.appendChild(d);d=d.nextSibling}this.scale=j;d=this.stroke(d,b,f);if(g.overflow=="hidden"){b.$g=c=i.parentNode;d=i.nextSibling}}}return d};
pv.SvgScene.fill=function(b,c,d){var f=c[d],g=f.fillStyle;if(g.opacity||f.events=="all"){b=this.expect(b,"rect",{"shape-rendering":f.antialias?null:"crispEdges","pointer-events":f.events,cursor:f.cursor,x:f.left,y:f.top,width:f.width,height:f.height,fill:g.color,"fill-opacity":g.opacity,stroke:null});b=this.append(b,c,d)}return b};
pv.SvgScene.stroke=function(b,c,d){var f=c[d],g=f.strokeStyle;if(g.opacity||f.events=="all"){b=this.expect(b,"rect",{"shape-rendering":f.antialias?null:"crispEdges","pointer-events":f.events=="all"?"stroke":f.events,cursor:f.cursor,x:f.left,y:f.top,width:Math.max(1.0E-10,f.width),height:Math.max(1.0E-10,f.height),fill:null,stroke:g.color,"stroke-opacity":g.opacity,"stroke-width":f.lineWidth/this.scale});b=this.append(b,c,d)}return b};
pv.SvgScene.rule=function(b){for(var c=b.$g.firstChild,d=0;d<b.length;d++){var f=b[d];if(f.visible){var g=f.strokeStyle;if(g.opacity){c=this.expect(c,"line",{"shape-rendering":f.antialias?null:"crispEdges","pointer-events":f.events,cursor:f.cursor,x1:f.left,y1:f.top,x2:f.left+f.width,y2:f.top+f.height,stroke:g.color,"stroke-opacity":g.opacity,"stroke-width":f.lineWidth/this.scale});c=this.append(c,b,d)}}}return c};
pv.SvgScene.wedge=function(b){for(var c=b.$g.firstChild,d=0;d<b.length;d++){var f=b[d];if(f.visible){var g=f.fillStyle,h=f.strokeStyle;if(g.opacity||h.opacity){var i=f.innerRadius,j=f.outerRadius,k=Math.abs(f.angle);if(k>=2*Math.PI)i=i?"M0,"+j+"A"+j+","+j+" 0 1,1 0,"+-j+"A"+j+","+j+" 0 1,1 0,"+j+"M0,"+i+"A"+i+","+i+" 0 1,1 0,"+-i+"A"+i+","+i+" 0 1,1 0,"+i+"Z":"M0,"+j+"A"+j+","+j+" 0 1,1 0,"+-j+"A"+j+","+j+" 0 1,1 0,"+j+"Z";else{var l=Math.min(f.startAngle,f.endAngle),q=Math.max(f.startAngle,f.endAngle),
n=Math.cos(l),p=Math.cos(q);l=Math.sin(l);q=Math.sin(q);i=i?"M"+j*n+","+j*l+"A"+j+","+j+" 0 "+(k<Math.PI?"0":"1")+",1 "+j*p+","+j*q+"L"+i*p+","+i*q+"A"+i+","+i+" 0 "+(k<Math.PI?"0":"1")+",0 "+i*n+","+i*l+"Z":"M"+j*n+","+j*l+"A"+j+","+j+" 0 "+(k<Math.PI?"0":"1")+",1 "+j*p+","+j*q+"L0,0Z"}c=this.expect(c,"path",{"shape-rendering":f.antialias?null:"crispEdges","pointer-events":f.events,cursor:f.cursor,transform:"translate("+f.left+","+f.top+")",d:i,fill:g.color,"fill-rule":"evenodd","fill-opacity":g.opacity||
null,stroke:h.color,"stroke-opacity":h.opacity||null,"stroke-width":h.opacity?f.lineWidth/this.scale:null});c=this.append(c,b,d)}}}return c};pv.Mark=function(){this.$properties=[];this.$handlers={}};pv.Mark.prototype.properties={};pv.Mark.cast={};pv.Mark.prototype.property=function(b,c){if(!this.hasOwnProperty("properties"))this.properties=pv.extend(this.properties);this.properties[b]=true;pv.Mark.prototype.propertyMethod(b,false,pv.Mark.cast[b]=c);return this};
pv.Mark.prototype.propertyMethod=function(b,c,d){d||(d=pv.Mark.cast[b]);this[b]=function(f){if(c&&this.scene){var g=this.scene.defs;if(arguments.length){g[b]={id:f==null?0:pv.id(),value:f!=null&&d?d(f):f};return this}return g[b]?g[b].value:null}if(arguments.length){g=!c<<1|typeof f=="function";this.propertyValue(b,g&1&&d?function(){var h=f.apply(this,arguments);return h!=null?d(h):null}:f!=null&&d?d(f):f).type=g;return this}return this.instance()[b]}};
pv.Mark.prototype.propertyValue=function(b,c){var d=this.$properties;c={name:b,id:pv.id(),value:c};for(var f=0;f<d.length;f++)if(d[f].name==b){d.splice(f,1);break}d.push(c);return c};pv.Mark.prototype.property("data").property("visible",Boolean).property("left",Number).property("right",Number).property("top",Number).property("bottom",Number).property("cursor",String).property("title",String).property("reverse",Boolean).property("antialias",Boolean).property("events",String);a=pv.Mark.prototype;
a.childIndex=-1;a.index=-1;a.scale=1;a.defaults=(new pv.Mark).data(function(b){return[b]}).visible(true).antialias(true).events("painted");a.extend=function(b){this.proto=b;this.target=b.target;return this};a.add=function(b){return this.parent.add(b).extend(this)};a.def=function(b,c){this.propertyMethod(b,true);return this[b](arguments.length>1?c:null)};
a.anchor=function(b){b||(b="center");return(new pv.Anchor(this)).name(b).data(function(){return this.scene.target.map(function(c){return c.data})}).visible(function(){return this.scene.target[this.index].visible}).left(function(){var c=this.scene.target[this.index],d=c.width||0;switch(this.name()){case "bottom":case "top":case "center":return c.left+d/2;case "left":return null}return c.left+d}).top(function(){var c=this.scene.target[this.index],d=c.height||0;switch(this.name()){case "left":case "right":case "center":return c.top+
d/2;case "top":return null}return c.top+d}).right(function(){var c=this.scene.target[this.index];return this.name()=="left"?c.right+(c.width||0):null}).bottom(function(){var c=this.scene.target[this.index];return this.name()=="top"?c.bottom+(c.height||0):null}).textAlign(function(){switch(this.name()){case "bottom":case "top":case "center":return"center";case "right":return"right"}return"left"}).textBaseline(function(){switch(this.name()){case "right":case "left":case "center":return"middle";case "top":return"top"}return"bottom"})};
a.anchorTarget=function(){return this.target};a.margin=function(b){return this.left(b).right(b).top(b).bottom(b)};a.instance=function(b){var c=this.scene||this.parent.instance(-1).children[this.childIndex],d=!arguments.length||this.hasOwnProperty("index")?this.index:b;return c[d<0?c.length-1:d]};
a.instances=function(b){for(var c=this,d=[],f;!(f=c.scene);){b=b.parent;d.push({index:b.index,childIndex:c.childIndex});c=c.parent}for(;d.length;){b=d.pop();f=f[b.index].children[b.childIndex]}if(this.hasOwnProperty("index")){d=pv.extend(f[this.index]);d.right=d.top=d.left=d.bottom=0;return[d]}return f};a.first=function(){return this.scene[0]};a.last=function(){return this.scene[this.scene.length-1]};a.sibling=function(){return this.index==0?null:this.scene[this.index-1]};
a.cousin=function(){var b=this.parent;return(b=b&&b.sibling())&&b.children?b.children[this.childIndex][this.index]:null};
a.render=function(){function b(i,j,k){i.scale=k;if(j<g.length){f.unshift(null);if(i.hasOwnProperty("index"))c(i,j,k);else{for(var l=0,q=i.scene.length;l<q;l++){i.index=l;c(i,j,k)}delete i.index}f.shift()}else{i.build();pv.Scene.scale=k;pv.Scene.updateAll(i.scene)}delete i.scale}function c(i,j,k){var l=i.scene[i.index],q;if(l.visible){var n=g[j],p=i.children[n];for(q=0;q<n;q++)i.children[q].scene=l.children[q];f[0]=l.data;if(p.scene)b(p,j+1,k*l.transform.k);else{p.scene=l.children[n];b(p,j+1,k*l.transform.k);
delete p.scene}for(q=0;q<n;q++)delete i.children[q].scene}}var d=this.parent,f=pv.Mark.stack;if(d&&!this.root.scene)this.root.render();else{for(var g=[],h=this;h.parent;h=h.parent)g.unshift(h.childIndex);for(this.bind();d&&!d.hasOwnProperty("index");)d=d.parent;this.context(d?d.scene:undefined,d?d.index:-1,function(){b(this.root,0,1)})}};pv.Mark.stack=[];a=pv.Mark.prototype;
a.bind=function(){function b(j){do for(var k=j.$properties,l=k.length-1;l>=0;l--){var q=k[l];if(!(q.name in c)){c[q.name]=q;switch(q.name){case "data":f=q;break;case "visible":g=q;break;default:d[q.type].push(q);break}}}while(j=j.proto)}var c={},d=[[],[],[],[]],f,g;b(this);b(this.defaults);d[1].reverse();d[3].reverse();var h=this;do for(var i in h.properties)i in c||d[2].push(c[i]={name:i,type:2,value:null});while(h=h.proto);h=d[0].concat(d[1]);for(i=0;i<h.length;i++)this.propertyMethod(h[i].name,
true);this.binds={properties:c,data:f,defs:h,required:[g],optional:pv.blend(d)}};
a.build=function(){var b=this.scene,c=pv.Mark.stack;if(!b){b=this.scene=[];b.mark=this;b.type=this.type;b.childIndex=this.childIndex;if(this.parent){b.parent=this.parent.scene;b.parentIndex=this.parent.index}}if(this.target)b.target=this.target.instances(b);if(this.binds.defs.length){var d=b.defs;if(!d)b.defs=d={};for(var f=0;f<this.binds.defs.length;f++){var g=this.binds.defs[f],h=d[g.name];if(!h||g.id>h.id)d[g.name]={id:0,value:g.type&1?g.value.apply(this,c):g.value}}}d=this.binds.data;d=d.type&
1?d.value.apply(this,c):d.value;c.unshift(null);b.length=d.length;for(f=0;f<d.length;f++){pv.Mark.prototype.index=this.index=f;(g=b[f])||(b[f]=g={});g.data=c[0]=d[f];this.buildInstance(g)}pv.Mark.prototype.index=-1;delete this.index;c.shift();return this};a.buildProperties=function(b,c){for(var d=0,f=c.length;d<f;d++){var g=c[d],h=g.value;switch(g.type){case 0:case 1:h=this.scene.defs[g.name].value;break;case 3:h=h.apply(this,pv.Mark.stack);break}b[g.name]=h}};
a.buildInstance=function(b){this.buildProperties(b,this.binds.required);if(b.visible){this.buildProperties(b,this.binds.optional);this.buildImplied(b)}};
a.buildImplied=function(b){var c=b.left,d=b.right,f=b.top,g=b.bottom,h=this.properties,i=h.width?b.width:0,j=h.height?b.height:0,k=this.parent?this.parent.width():i+c+d;if(i==null)i=k-(d=d||0)-(c=c||0);else if(d==null)if(c==null)c=d=(k-i)/2;else d=k-i-(c=c||0);else if(c==null)c=k-i-d;k=this.parent?this.parent.height():j+f+g;if(j==null)j=k-(f=f||0)-(g=g||0);else if(g==null)g=f==null?(f=(k-j)/2):k-j-(f=f||0);else if(f==null)f=k-j-g;b.left=c;b.right=d;b.top=f;b.bottom=g;if(h.width)b.width=i;if(h.height)b.height=
j;if(h.textStyle&&!b.textStyle)b.textStyle=pv.Color.transparent;if(h.fillStyle&&!b.fillStyle)b.fillStyle=pv.Color.transparent;if(h.strokeStyle&&!b.strokeStyle)b.strokeStyle=pv.Color.transparent};
a.mouse=function(){var b=pv.event.pageX||0,c=pv.event.pageY||0,d=this.root.canvas();do{b-=d.offsetLeft;c-=d.offsetTop}while(d=d.offsetParent);d=pv.Transform.identity;var f=this.properties.transform?this:this.parent,g=[];do g.push(f);while(f=f.parent);for(;f=g.pop();)d=d.translate(f.left(),f.top()).times(f.transform());d=d.invert();return pv.vector(b*d.k+d.x,c*d.k+d.y)};a.event=function(b,c){this.$handlers[b]=pv.functor(c);return this};
a.context=function(b,c,d){function f(l,q){pv.Mark.scene=l;h.index=q;if(l){var n=l.mark,p=n,m=[];do{m.push(p);i.push(l[q].data);p.index=q;p.scene=l;q=l.parentIndex;l=l.parent}while(p=p.parent);l=m.length-1;for(q=1;l>0;l--){p=m[l];p.scale=q;q*=p.scene[p.index].transform.k}if(n.children){l=0;for(m=n.children.length;l<m;l++){p=n.children[l];p.scene=n.scene[n.index].children[l];p.scale=q}}}}function g(l){if(l){l=l.mark;var q;if(l.children)for(var n=0,p=l.children.length;n<p;n++){q=l.children[n];delete q.scene;
delete q.scale}q=l;do{i.pop();if(q.parent){delete q.scene;delete q.scale}delete q.index}while(q=q.parent)}}var h=pv.Mark.prototype,i=pv.Mark.stack,j=pv.Mark.scene,k=h.index;g(j,k);f(b,c);try{d.apply(this,i)}finally{g(b,c);f(j,k)}};pv.Mark.dispatch=function(b,c,d){var f=c.mark,g=c.parent,h=f.$handlers[b];if(!h)return g&&pv.Mark.dispatch(b,g,c.parentIndex);f.context(c,d,function(){(f=h.apply(f,pv.Mark.stack))&&f.render&&f.render()});return true};
pv.Anchor=function(b){pv.Mark.call(this);this.target=b;this.parent=b.parent};pv.Anchor.prototype=pv.extend(pv.Mark).property("name",String);pv.Anchor.prototype.extend=function(b){this.proto=b;return this};pv.Area=function(){pv.Mark.call(this)};
pv.Area.prototype=pv.extend(pv.Mark).property("width",Number).property("height",Number).property("lineWidth",Number).property("strokeStyle",pv.color).property("fillStyle",pv.color).property("segmented",Boolean).property("interpolate",String).property("tension",Number);pv.Area.prototype.type="area";pv.Area.prototype.defaults=(new pv.Area).extend(pv.Mark.prototype.defaults).lineWidth(1.5).fillStyle(pv.Colors.category20().by(pv.parent)).interpolate("linear").tension(0.7);
pv.Area.prototype.buildImplied=function(b){if(b.height==null)b.height=0;if(b.width==null)b.width=0;pv.Mark.prototype.buildImplied.call(this,b)};pv.Area.fixed={lineWidth:1,lineJoin:1,strokeStyle:1,fillStyle:1,segmented:1,interpolate:1,tension:1};
pv.Area.prototype.bind=function(){pv.Mark.prototype.bind.call(this);var b=this.binds,c=b.required;b=b.optional;for(var d=0,f=b.length;d<f;d++){var g=b[d];g.fixed=g.name in pv.Area.fixed;if(g.name=="segmented"){c.push(g);b.splice(d,1);d--;f--}}this.binds.$required=c;this.binds.$optional=b};
pv.Area.prototype.buildInstance=function(b){var c=this.binds;if(this.index){var d=c.fixed;if(!d){d=c.fixed=[];function f(i){return!i.fixed||(d.push(i),false)}c.required=c.required.filter(f);if(!this.scene[0].segmented)c.optional=c.optional.filter(f)}c=0;for(var g=d.length;c<g;c++){var h=d[c].name;b[h]=this.scene[0][h]}}else{c.required=c.$required;c.optional=c.$optional;c.fixed=null}pv.Mark.prototype.buildInstance.call(this,b)};
pv.Area.prototype.anchor=function(b){return pv.Mark.prototype.anchor.call(this,b).interpolate(function(){return this.scene.target[this.index].interpolate}).eccentricity(function(){return this.scene.target[this.index].eccentricity}).tension(function(){return this.scene.target[this.index].tension})};pv.Bar=function(){pv.Mark.call(this)};
pv.Bar.prototype=pv.extend(pv.Mark).property("width",Number).property("height",Number).property("lineWidth",Number).property("strokeStyle",pv.color).property("fillStyle",pv.color);pv.Bar.prototype.type="bar";pv.Bar.prototype.defaults=(new pv.Bar).extend(pv.Mark.prototype.defaults).lineWidth(1.5).fillStyle(pv.Colors.category20().by(pv.parent));pv.Dot=function(){pv.Mark.call(this)};
pv.Dot.prototype=pv.extend(pv.Mark).property("size",Number).property("radius",Number).property("shape",String).property("angle",Number).property("lineWidth",Number).property("strokeStyle",pv.color).property("fillStyle",pv.color);pv.Dot.prototype.type="dot";pv.Dot.prototype.defaults=(new pv.Dot).extend(pv.Mark.prototype.defaults).size(20).shape("circle").lineWidth(1.5).strokeStyle(pv.Colors.category10().by(pv.parent));
pv.Dot.prototype.anchor=function(b){return pv.Mark.prototype.anchor.call(this,b).left(function(){var c=this.scene.target[this.index];switch(this.name()){case "bottom":case "top":case "center":return c.left;case "left":return null}return c.left+c.radius}).right(function(){var c=this.scene.target[this.index];return this.name()=="left"?c.right+c.radius:null}).top(function(){var c=this.scene.target[this.index];switch(this.name()){case "left":case "right":case "center":return c.top;case "top":return null}return c.top+
c.radius}).bottom(function(){var c=this.scene.target[this.index];return this.name()=="top"?c.bottom+c.radius:null}).textAlign(function(){switch(this.name()){case "left":return"right";case "bottom":case "top":case "center":return"center"}return"left"}).textBaseline(function(){switch(this.name()){case "right":case "left":case "center":return"middle";case "bottom":return"top"}return"bottom"})};
pv.Dot.prototype.buildImplied=function(b){if(b.radius==null)b.radius=Math.sqrt(b.size);else if(b.size==null)b.size=b.radius*b.radius;pv.Mark.prototype.buildImplied.call(this,b)};pv.Label=function(){pv.Mark.call(this)};
pv.Label.prototype=pv.extend(pv.Mark).property("text",String).property("font",String).property("textAngle",Number).property("textStyle",pv.color).property("textAlign",String).property("textBaseline",String).property("textMargin",Number).property("textDecoration",String).property("textShadow",String);pv.Label.prototype.type="label";pv.Label.prototype.defaults=(new pv.Label).extend(pv.Mark.prototype.defaults).events("none").text(pv.identity).font("10px sans-serif").textAngle(0).textStyle("black").textAlign("left").textBaseline("bottom").textMargin(3);
pv.Line=function(){pv.Mark.call(this)};pv.Line.prototype=pv.extend(pv.Mark).property("lineWidth",Number).property("lineJoin",String).property("strokeStyle",pv.color).property("fillStyle",pv.color).property("segmented",Boolean).property("interpolate",String).property("eccentricity",Number).property("tension",Number);a=pv.Line.prototype;a.type="line";a.defaults=(new pv.Line).extend(pv.Mark.prototype.defaults).lineJoin("miter").lineWidth(1.5).strokeStyle(pv.Colors.category10().by(pv.parent)).interpolate("linear").eccentricity(0).tension(0.7);
a.bind=pv.Area.prototype.bind;a.buildInstance=pv.Area.prototype.buildInstance;a.anchor=function(b){return pv.Area.prototype.anchor.call(this,b).textAlign(function(){switch(this.name()){case "left":return"right";case "bottom":case "top":case "center":return"center";case "right":return"left"}}).textBaseline(function(){switch(this.name()){case "right":case "left":case "center":return"middle";case "top":return"bottom";case "bottom":return"top"}})};pv.Rule=function(){pv.Mark.call(this)};
pv.Rule.prototype=pv.extend(pv.Mark).property("width",Number).property("height",Number).property("lineWidth",Number).property("strokeStyle",pv.color);pv.Rule.prototype.type="rule";pv.Rule.prototype.defaults=(new pv.Rule).extend(pv.Mark.prototype.defaults).lineWidth(1).strokeStyle("black").antialias(false);pv.Rule.prototype.anchor=pv.Line.prototype.anchor;
pv.Rule.prototype.buildImplied=function(b){var c=b.left,d=b.right;if(b.width!=null||c==null&&d==null||d!=null&&c!=null)b.height=0;else b.width=0;pv.Mark.prototype.buildImplied.call(this,b)};pv.Panel=function(){pv.Bar.call(this);this.children=[];this.root=this;this.$dom=pv.$&&pv.$.s};pv.Panel.prototype=pv.extend(pv.Bar).property("transform").property("overflow",String).property("canvas",function(b){return typeof b=="string"?document.getElementById(b):b});a=pv.Panel.prototype;a.type="panel";
a.defaults=(new pv.Panel).extend(pv.Bar.prototype.defaults).fillStyle(null).overflow("visible");a.anchor=function(b){b=pv.Bar.prototype.anchor.call(this,b);b.parent=this;return b};a.add=function(b){b=new b;b.parent=this;b.root=this.root;b.childIndex=this.children.length;this.children.push(b);return b};a.bind=function(){pv.Mark.prototype.bind.call(this);for(var b=0;b<this.children.length;b++)this.children[b].bind()};
a.buildInstance=function(b){pv.Bar.prototype.buildInstance.call(this,b);if(b.visible){if(!b.children)b.children=[];var c=this.scale*b.transform.k,d,f=this.children.length;pv.Mark.prototype.index=-1;for(var g=0;g<f;g++){d=this.children[g];d.scene=b.children[g];d.scale=c;d.build()}for(g=0;g<f;g++){d=this.children[g];b.children[g]=d.scene;delete d.scene;delete d.scale}b.children.length=f}};
a.buildImplied=function(b){if(!this.parent){var c=b.canvas;if(c){if(c.$panel!=this)for(c.$panel=this;c.lastChild;)c.removeChild(c.lastChild);var d;if(b.width==null){d=parseFloat(pv.css(c,"width"));b.width=d-b.left-b.right}if(b.height==null){d=parseFloat(pv.css(c,"height"));b.height=d-b.top-b.bottom}}else{d=this.$canvas||(this.$canvas=[]);if(!(c=d[this.index])){c=d[this.index]=document.createElement("span");if(this.$dom)this.$dom.parentNode.insertBefore(c,this.$dom);else{for(d=document.body;d.lastChild&&
d.lastChild.tagName;)d=d.lastChild;if(d!=document.body)d=d.parentNode;d.appendChild(c)}}}b.canvas=c}if(!b.transform)b.transform=pv.Transform.identity;pv.Mark.prototype.buildImplied.call(this,b)};pv.Image=function(){pv.Bar.call(this)};pv.Image.prototype=pv.extend(pv.Bar).property("url",String).property("imageWidth",Number).property("imageHeight",Number);a=pv.Image.prototype;a.type="image";a.defaults=(new pv.Image).extend(pv.Bar.prototype.defaults).fillStyle(null);
a.image=function(b){this.$image=function(){var c=b.apply(this,arguments);return c==null?pv.Color.transparent:typeof c=="string"?pv.color(c):c};return this};a.bind=function(){pv.Bar.prototype.bind.call(this);var b=this.binds,c=this;do b.image=c.$image;while(!b.image&&(c=c.proto))};
a.buildImplied=function(b){pv.Bar.prototype.buildImplied.call(this,b);if(b.visible){if(b.imageWidth==null)b.imageWidth=b.width;if(b.imageHeight==null)b.imageHeight=b.height;if(b.url==null&&this.binds.image){var c=this.$canvas||(this.$canvas=document.createElement("canvas")),d=c.getContext("2d"),f=b.imageWidth,g=b.imageHeight,h=pv.Mark.stack;c.width=f;c.height=g;b=(b.image=d.createImageData(f,g)).data;h.unshift(null,null);for(d=c=0;c<g;c++){h[1]=c;for(var i=0;i<f;i++){h[0]=i;var j=this.binds.image.apply(this,
h);b[d++]=j.r;b[d++]=j.g;b[d++]=j.b;b[d++]=255*j.a}}h.splice(0,2)}}};pv.Wedge=function(){pv.Mark.call(this)};pv.Wedge.prototype=pv.extend(pv.Mark).property("startAngle",Number).property("endAngle",Number).property("angle",Number).property("innerRadius",Number).property("outerRadius",Number).property("lineWidth",Number).property("strokeStyle",pv.color).property("fillStyle",pv.color);a=pv.Wedge.prototype;a.type="wedge";
a.defaults=(new pv.Wedge).extend(pv.Mark.prototype.defaults).startAngle(function(){var b=this.sibling();return b?b.endAngle:-Math.PI/2}).innerRadius(0).lineWidth(1.5).strokeStyle(null).fillStyle(pv.Colors.category20().by(pv.index));a.midRadius=function(){return(this.innerRadius()+this.outerRadius())/2};a.midAngle=function(){return(this.startAngle()+this.endAngle())/2};
a.anchor=function(b){function c(g){return g.innerRadius||g.angle<2*Math.PI}function d(g){return(g.innerRadius+g.outerRadius)/2}function f(g){return(g.startAngle+g.endAngle)/2}return pv.Mark.prototype.anchor.call(this,b).left(function(){var g=this.scene.target[this.index];if(c(g))switch(this.name()){case "outer":return g.left+g.outerRadius*Math.cos(f(g));case "inner":return g.left+g.innerRadius*Math.cos(f(g));case "start":return g.left+d(g)*Math.cos(g.startAngle);case "center":return g.left+d(g)*Math.cos(f(g));
case "end":return g.left+d(g)*Math.cos(g.endAngle)}return g.left}).top(function(){var g=this.scene.target[this.index];if(c(g))switch(this.name()){case "outer":return g.top+g.outerRadius*Math.sin(f(g));case "inner":return g.top+g.innerRadius*Math.sin(f(g));case "start":return g.top+d(g)*Math.sin(g.startAngle);case "center":return g.top+d(g)*Math.sin(f(g));case "end":return g.top+d(g)*Math.sin(g.endAngle)}return g.top}).textAlign(function(){var g=this.scene.target[this.index];if(c(g))switch(this.name()){case "outer":return pv.Wedge.upright(f(g))?
"right":"left";case "inner":return pv.Wedge.upright(f(g))?"left":"right"}return"center"}).textBaseline(function(){var g=this.scene.target[this.index];if(c(g))switch(this.name()){case "start":return pv.Wedge.upright(g.startAngle)?"top":"bottom";case "end":return pv.Wedge.upright(g.endAngle)?"bottom":"top"}return"middle"}).textAngle(function(){var g=this.scene.target[this.index],h=0;if(c(g))switch(this.name()){case "center":case "inner":case "outer":h=f(g);break;case "start":h=g.startAngle;break;case "end":h=
g.endAngle;break}return pv.Wedge.upright(h)?h:h+Math.PI})};pv.Wedge.upright=function(b){b%=2*Math.PI;b=b<0?2*Math.PI+b:b;return b<Math.PI/2||b>=3*Math.PI/2};pv.Wedge.prototype.buildImplied=function(b){if(b.angle==null)b.angle=b.endAngle-b.startAngle;else if(b.endAngle==null)b.endAngle=b.startAngle+b.angle;pv.Mark.prototype.buildImplied.call(this,b)};pv.simulation=function(b){return new pv.Simulation(b)};pv.Simulation=function(b){for(var c=0;c<b.length;c++)this.particle(b[c])};a=pv.Simulation.prototype;
a.particle=function(b){b.next=this.particles;if(isNaN(b.px))b.px=b.x;if(isNaN(b.py))b.py=b.y;if(isNaN(b.fx))b.fx=0;if(isNaN(b.fy))b.fy=0;this.particles=b;return this};a.force=function(b){b.next=this.forces;this.forces=b;return this};a.constraint=function(b){b.next=this.constraints;this.constraints=b;return this};
a.stabilize=function(b){var c;arguments.length||(b=3);for(var d=0;d<b;d++){var f=new pv.Quadtree(this.particles);for(c=this.constraints;c;c=c.next)c.apply(this.particles,f)}for(c=this.particles;c;c=c.next){c.px=c.x;c.py=c.y}return this};
a.step=function(){var b;for(b=this.particles;b;b=b.next){var c=b.px,d=b.py;b.px=b.x;b.py=b.y;b.x+=b.vx=b.x-c+b.fx;b.y+=b.vy=b.y-d+b.fy}c=new pv.Quadtree(this.particles);for(b=this.constraints;b;b=b.next)b.apply(this.particles,c);for(b=this.particles;b;b=b.next)b.fx=b.fy=0;for(b=this.forces;b;b=b.next)b.apply(this.particles,c)};
pv.Quadtree=function(b){function c(l,q,n,p,m,r){if(!(isNaN(q.x)||isNaN(q.y)))if(l.leaf)if(l.p){if(!(Math.abs(l.p.x-q.x)+Math.abs(l.p.y-q.y)<0.01)){var s=l.p;l.p=null;d(l,s,n,p,m,r)}d(l,q,n,p,m,r)}else l.p=q;else d(l,q,n,p,m,r)}function d(l,q,n,p,m,r){var s=(n+m)*0.5,t=(p+r)*0.5,x=q.x>=s,u=q.y>=t;l.leaf=false;switch((u<<1)+x){case 0:l=l.c1||(l.c1=new pv.Quadtree.Node);break;case 1:l=l.c2||(l.c2=new pv.Quadtree.Node);break;case 2:l=l.c3||(l.c3=new pv.Quadtree.Node);break;case 3:l=l.c4||(l.c4=new pv.Quadtree.Node);
break}if(x)n=s;else m=s;if(u)p=t;else r=t;c(l,q,n,p,m,r)}var f,g=Number.POSITIVE_INFINITY,h=g,i=Number.NEGATIVE_INFINITY,j=i;for(f=b;f;f=f.next){if(f.x<g)g=f.x;if(f.y<h)h=f.y;if(f.x>i)i=f.x;if(f.y>j)j=f.y}f=i-g;var k=j-h;if(f>k)j=h+f;else i=g+k;this.xMin=g;this.yMin=h;this.xMax=i;this.yMax=j;this.root=new pv.Quadtree.Node;for(f=b;f;f=f.next)c(this.root,f,g,h,i,j)};pv.Quadtree.Node=function(){this.leaf=true;this.p=this.c4=this.c3=this.c2=this.c1=null};pv.Force={};
pv.Force.charge=function(b){function c(l){function q(m){c(m);l.cn+=m.cn;n+=m.cn*m.cx;p+=m.cn*m.cy}var n=0,p=0;l.cn=0;if(!l.leaf){l.c1&&q(l.c1);l.c2&&q(l.c2);l.c3&&q(l.c3);l.c4&&q(l.c4)}if(l.p){l.cn+=b;n+=b*l.p.x;p+=b*l.p.y}l.cx=n/l.cn;l.cy=p/l.cn}function d(l,q,n,p,m,r){var s=l.cx-q.x,t=l.cy-q.y,x=1/Math.sqrt(s*s+t*t);if(l.leaf&&l.p!=q||(m-n)*x<j){if(!(x<i)){if(x>g)x=g;l=l.cn*x*x*x;s=s*l;t=t*l;q.fx+=s;q.fy+=t}}else if(!l.leaf){var u=(n+m)*0.5,o=(p+r)*0.5;l.c1&&d(l.c1,q,n,p,u,o);l.c2&&d(l.c2,q,u,p,
m,o);l.c3&&d(l.c3,q,n,o,u,r);l.c4&&d(l.c4,q,u,o,m,r);if(!(x<i)){if(x>g)x=g;if(l.p&&l.p!=q){l=b*x*x*x;s=s*l;t=t*l;q.fx+=s;q.fy+=t}}}}var f=2,g=1/f,h=500,i=1/h,j=0.9,k={};arguments.length||(b=-40);k.constant=function(l){if(arguments.length){b=Number(l);return k}return b};k.domain=function(l,q){if(arguments.length){f=Number(l);g=1/f;h=Number(q);i=1/h;return k}return[f,h]};k.theta=function(l){if(arguments.length){j=Number(l);return k}return j};k.apply=function(l,q){c(q.root);for(l=l;l;l=l.next)d(q.root,
l,q.xMin,q.yMin,q.xMax,q.yMax)};return k};pv.Force.drag=function(b){var c={};arguments.length||(b=0.1);c.constant=function(d){if(arguments.length){b=d;return c}return b};c.apply=function(d){if(b)for(d=d;d;d=d.next){d.fx-=b*d.vx;d.fy-=b*d.vy}};return c};
pv.Force.spring=function(b){var c=0.1,d=20,f,g,h={};arguments.length||(b=0.1);h.links=function(i){if(arguments.length){f=i;g=i.map(function(j){return 1/Math.sqrt(Math.max(j.sourceNode.linkDegree,j.targetNode.linkDegree))});return h}return f};h.constant=function(i){if(arguments.length){b=Number(i);return h}return b};h.damping=function(i){if(arguments.length){c=Number(i);return h}return c};h.length=function(i){if(arguments.length){d=Number(i);return h}return d};h.apply=function(){for(var i=0;i<f.length;i++){var j=
f[i].sourceNode,k=f[i].targetNode,l=j.x-k.x,q=j.y-k.y,n=Math.sqrt(l*l+q*q),p=n?1/n:1;p=(b*g[i]*(n-d)+c*g[i]*(l*(j.vx-k.vx)+q*(j.vy-k.vy))*p)*p;l=-p*(n?l:0.01*(0.5-Math.random()));q=-p*(n?q:0.01*(0.5-Math.random()));j.fx+=l;j.fy+=q;k.fx-=l;k.fy-=q}};return h};pv.Constraint={};
pv.Constraint.collision=function(b){function c(l,q,n,p,m,r){if(!l.leaf){var s=(n+m)*0.5,t=(p+r)*0.5,x=t<j,u=s>g,o=s<i;if(t>h){l.c1&&u&&c(l.c1,q,n,p,s,t);l.c2&&o&&c(l.c2,q,s,p,m,t)}if(x){l.c3&&u&&c(l.c3,q,n,t,s,r);l.c4&&o&&c(l.c4,q,s,t,m,r)}}if(l.p&&l.p!=q){n=q.x-l.p.x;p=q.y-l.p.y;m=Math.sqrt(n*n+p*p);r=f+b(l.p);if(m<r){m=(m-r)/m*0.5;n*=m;p*=m;q.x-=n;q.y-=p;l.p.x+=n;l.p.y+=p}}}var d=1,f,g,h,i,j,k={};arguments.length||(f=10);k.repeat=function(l){if(arguments.length){d=Number(l);return k}return d};k.apply=
function(l,q){var n,p,m=-Infinity;for(n=l;n;n=n.next){p=b(n);if(p>m)m=p}for(var r=0;r<d;r++)for(n=l;n;n=n.next){p=(f=b(n))+m;g=n.x-p;i=n.x+p;h=n.y-p;j=n.y+p;c(q.root,n,q.xMin,q.yMin,q.xMax,q.yMax)}};return k};pv.Constraint.position=function(b){var c=1,d={};arguments.length||(b=function(f){return f.fix});d.alpha=function(f){if(arguments.length){c=Number(f);return d}return c};d.apply=function(f){for(f=f;f;f=f.next){var g=b(f);if(g){f.x+=(g.x-f.x)*c;f.y+=(g.y-f.y)*c;f.fx=f.fy=f.vx=f.vy=0}}};return d};
pv.Constraint.bound=function(){var b={},c,d;b.x=function(f,g){if(arguments.length){c={min:Math.min(f,g),max:Math.max(f,g)};return this}return c};b.y=function(f,g){if(arguments.length){d={min:Math.min(f,g),max:Math.max(f,g)};return this}return d};b.apply=function(f){if(c)for(var g=f;g;g=g.next)g.x=g.x<c.min?c.min:g.x>c.max?c.max:g.x;if(d)for(g=f;g;g=g.next)g.y=g.y<d.min?d.min:g.y>d.max?d.max:g.y};return b};pv.Layout=function(){pv.Panel.call(this)};pv.Layout.prototype=pv.extend(pv.Panel);
pv.Layout.prototype.property=function(b,c){if(!this.hasOwnProperty("properties"))this.properties=pv.extend(this.properties);this.properties[b]=true;this.propertyMethod(b,false,pv.Mark.cast[b]=c);return this};
pv.Layout.Network=function(){pv.Layout.call(this);var b=this;this.$id=pv.id();(this.node=(new pv.Mark).data(function(){return b.nodes()}).strokeStyle("#1f77b4").fillStyle("#fff").left(function(c){return c.x}).top(function(c){return c.y})).parent=this;this.link=(new pv.Mark).extend(this.node).data(function(c){return[c.sourceNode,c.targetNode]}).fillStyle(null).lineWidth(function(c,d){return d.linkValue*1.5}).strokeStyle("rgba(0,0,0,.2)");this.link.add=function(c){return b.add(pv.Panel).data(function(){return b.links()}).add(c).extend(this)};
(this.label=(new pv.Mark).extend(this.node).textMargin(7).textBaseline("middle").text(function(c){return c.nodeName||c.nodeValue}).textAngle(function(c){c=c.midAngle;return pv.Wedge.upright(c)?c:c+Math.PI}).textAlign(function(c){return pv.Wedge.upright(c.midAngle)?"left":"right"})).parent=this};
pv.Layout.Network.prototype=pv.extend(pv.Layout).property("nodes",function(b){return b.map(function(c,d){if(typeof c!="object")c={nodeValue:c};c.index=d;return c})}).property("links",function(b){return b.map(function(c){if(isNaN(c.linkValue))c.linkValue=isNaN(c.value)?1:c.value;return c})});pv.Layout.Network.prototype.reset=function(){this.$id=pv.id();return this};pv.Layout.Network.prototype.buildProperties=function(b,c){if((b.$id||0)<this.$id)pv.Layout.prototype.buildProperties.call(this,b,c)};
pv.Layout.Network.prototype.buildImplied=function(b){pv.Layout.prototype.buildImplied.call(this,b);if(b.$id>=this.$id)return true;b.$id=this.$id;b.nodes.forEach(function(c){c.linkDegree=0});b.links.forEach(function(c){var d=c.linkValue;(c.sourceNode||(c.sourceNode=b.nodes[c.source])).linkDegree+=d;(c.targetNode||(c.targetNode=b.nodes[c.target])).linkDegree+=d})};pv.Layout.Hierarchy=function(){pv.Layout.Network.call(this);this.link.strokeStyle("#ccc")};pv.Layout.Hierarchy.prototype=pv.extend(pv.Layout.Network);
pv.Layout.Hierarchy.prototype.buildImplied=function(b){if(!b.links)b.links=pv.Layout.Hierarchy.links.call(this);pv.Layout.Network.prototype.buildImplied.call(this,b)};pv.Layout.Hierarchy.links=function(){return this.nodes().filter(function(b){return b.parentNode}).map(function(b){return{sourceNode:b,targetNode:b.parentNode,linkValue:1}})};
pv.Layout.Hierarchy.NodeLink={buildImplied:function(b){function c(m){return m.parentNode?m.depth*(n-q)+q:0}function d(m){return m.parentNode?(m.breadth-0.25)*2*Math.PI:0}function f(m){switch(i){case "left":return m.depth*k;case "right":return k-m.depth*k;case "top":return m.breadth*k;case "bottom":return k-m.breadth*k;case "radial":return k/2+c(m)*Math.cos(m.midAngle)}}function g(m){switch(i){case "left":return m.breadth*l;case "right":return l-m.breadth*l;case "top":return m.depth*l;case "bottom":return l-
m.depth*l;case "radial":return l/2+c(m)*Math.sin(m.midAngle)}}var h=b.nodes,i=b.orient,j=/^(top|bottom)$/.test(i),k=b.width,l=b.height;if(i=="radial"){var q=b.innerRadius,n=b.outerRadius;if(q==null)q=0;if(n==null)n=Math.min(k,l)/2}for(b=0;b<h.length;b++){var p=h[b];p.midAngle=i=="radial"?d(p):j?Math.PI/2:0;p.x=f(p);p.y=g(p);if(p.firstChild)p.midAngle+=Math.PI}}};
pv.Layout.Hierarchy.Fill={constructor:function(){this.node.strokeStyle("#fff").fillStyle("#ccc").width(function(b){return b.dx}).height(function(b){return b.dy}).innerRadius(function(b){return b.innerRadius}).outerRadius(function(b){return b.outerRadius}).startAngle(function(b){return b.startAngle}).angle(function(b){return b.angle});this.label.textAlign("center").left(function(b){return b.x+b.dx/2}).top(function(b){return b.y+b.dy/2});delete this.link},buildImplied:function(b){function c(o,v){return(o+
v)/(1+v)}function d(o){switch(n){case "left":return c(o.minDepth,s)*m;case "right":return(1-c(o.maxDepth,s))*m;case "top":return o.minBreadth*m;case "bottom":return(1-o.maxBreadth)*m;case "radial":return m/2}}function f(o){switch(n){case "left":return o.minBreadth*r;case "right":return(1-o.maxBreadth)*r;case "top":return c(o.minDepth,s)*r;case "bottom":return(1-c(o.maxDepth,s))*r;case "radial":return r/2}}function g(o){switch(n){case "left":case "right":return(o.maxDepth-o.minDepth)/(1+s)*m;case "top":case "bottom":return(o.maxBreadth-
o.minBreadth)*m;case "radial":return o.parentNode?(o.innerRadius+o.outerRadius)*Math.cos(o.midAngle):0}}function h(o){switch(n){case "left":case "right":return(o.maxBreadth-o.minBreadth)*r;case "top":case "bottom":return(o.maxDepth-o.minDepth)/(1+s)*r;case "radial":return o.parentNode?(o.innerRadius+o.outerRadius)*Math.sin(o.midAngle):0}}function i(o){return Math.max(0,c(o.minDepth,s/2))*(x-t)+t}function j(o){return c(o.maxDepth,s/2)*(x-t)+t}function k(o){return(o.parentNode?o.minBreadth-0.25:0)*
2*Math.PI}function l(o){return(o.parentNode?o.maxBreadth-o.minBreadth:1)*2*Math.PI}var q=b.nodes,n=b.orient,p=/^(top|bottom)$/.test(n),m=b.width,r=b.height,s=-q[0].minDepth;if(n=="radial"){var t=b.innerRadius,x=b.outerRadius;if(t==null)t=0;if(t)s*=2;if(x==null)x=Math.min(m,r)/2}for(b=0;b<q.length;b++){var u=q[b];u.x=d(u);u.y=f(u);if(n=="radial"){u.innerRadius=i(u);u.outerRadius=j(u);u.startAngle=k(u);u.angle=l(u);u.midAngle=u.startAngle+u.angle/2}else u.midAngle=p?-Math.PI/2:0;u.dx=g(u);u.dy=h(u)}}};
pv.Layout.Grid=function(){pv.Layout.call(this);var b=this;(this.cell=(new pv.Mark).data(function(){return b.scene[b.index].$grid}).width(function(){return b.width()/b.cols()}).height(function(){return b.height()/b.rows()}).left(function(){return this.width()*(this.index%b.cols())}).top(function(){return this.height()*Math.floor(this.index/b.cols())})).parent=this};pv.Layout.Grid.prototype=pv.extend(pv.Layout).property("rows").property("cols");pv.Layout.Grid.prototype.defaults=(new pv.Layout.Grid).extend(pv.Layout.prototype.defaults).rows(1).cols(1);
pv.Layout.Grid.prototype.buildImplied=function(b){pv.Layout.prototype.buildImplied.call(this,b);var c=b.rows,d=b.cols;if(typeof d=="object")c=pv.transpose(d);if(typeof c=="object"){b.$grid=pv.blend(c);b.rows=c.length;b.cols=c[0]?c[0].length:0}else b.$grid=pv.repeat([b.data],c*d)};
pv.Layout.Stack=function(){function b(i){return function(){return f[i](this.parent.index,this.index)}}pv.Layout.call(this);var c=this,d=function(){return null},f={t:d,l:d,r:d,b:d,w:d,h:d},g,h=c.buildImplied;this.buildImplied=function(i){h.call(this,i);var j=i.layers,k=j.length,l,q=i.orient,n=/^(top|bottom)\b/.test(q),p=this.parent[n?"height":"width"](),m=[],r=[],s=[],t=pv.Mark.stack,x={parent:{parent:this}};t.unshift(null);g=[];for(var u=0;u<k;u++){s[u]=[];r[u]=[];x.parent.index=u;t[0]=j[u];g[u]=
this.$values.apply(x.parent,t);if(!u)l=g[u].length;t.unshift(null);for(var o=0;o<l;o++){t[0]=g[u][o];x.index=o;u||(m[o]=this.$x.apply(x,t));s[u][o]=this.$y.apply(x,t)}t.shift()}t.shift();switch(i.order){case "inside-out":var v=s.map(function(A){return pv.max.index(A)});x=pv.range(k).sort(function(A,D){return v[A]-v[D]});j=s.map(function(A){return pv.sum(A)});var w=t=0,y=[],z=[];for(u=0;u<k;u++){o=x[u];if(t<w){t+=j[o];y.push(o)}else{w+=j[o];z.push(o)}}j=z.reverse().concat(y);break;case "reverse":j=
pv.range(k-1,-1,-1);break;default:j=pv.range(k);break}switch(i.offset){case "silohouette":for(o=0;o<l;o++){for(u=x=0;u<k;u++)x+=s[u][o];r[j[0]][o]=(p-x)/2}break;case "wiggle":for(u=x=0;u<k;u++)x+=s[u][0];r[j[0]][0]=x=(p-x)/2;for(o=1;o<l;o++){t=p=0;w=m[o]-m[o-1];for(u=0;u<k;u++)p+=s[u][o];for(u=0;u<k;u++){y=(s[j[u]][o]-s[j[u]][o-1])/(2*w);for(i=0;i<u;i++)y+=(s[j[i]][o]-s[j[i]][o-1])/w;t+=y*s[j[u]][o]}r[j[0]][o]=x-=p?t/p*w:0}break;case "expand":for(o=0;o<l;o++){for(u=i=r[j[0]][o]=0;u<k;u++)i+=s[u][o];
if(i){i=p/i;for(u=0;u<k;u++)s[u][o]*=i}else{i=p/k;for(u=0;u<k;u++)s[u][o]=i}}break;default:for(o=0;o<l;o++)r[j[0]][o]=0;break}for(o=0;o<l;o++){x=r[j[0]][o];for(u=1;u<k;u++){x+=s[j[u-1]][o];r[j[u]][o]=x}}u=q.indexOf("-");k=n?"h":"w";n=u<0?n?"l":"b":q.charAt(u+1);q=q.charAt(0);for(var C in f)f[C]=d;f[n]=function(A,D){return m[D]};f[q]=function(A,D){return r[A][D]};f[k]=function(A,D){return s[A][D]}};this.layer=(new pv.Mark).data(function(){return g[this.parent.index]}).top(b("t")).left(b("l")).right(b("r")).bottom(b("b")).width(b("w")).height(b("h"));
this.layer.add=function(i){return c.add(pv.Panel).data(function(){return c.layers()}).add(i).extend(this)}};pv.Layout.Stack.prototype=pv.extend(pv.Layout).property("orient",String).property("offset",String).property("order",String).property("layers");a=pv.Layout.Stack.prototype;a.defaults=(new pv.Layout.Stack).extend(pv.Layout.prototype.defaults).orient("bottom-left").offset("zero").layers([[]]);a.$x=pv.Layout.Stack.prototype.$y=function(){return 0};a.x=function(b){this.$x=pv.functor(b);return this};
a.y=function(b){this.$y=pv.functor(b);return this};a.$values=pv.identity;a.values=function(b){this.$values=pv.functor(b);return this};
pv.Layout.Treemap=function(){pv.Layout.Hierarchy.call(this);this.node.strokeStyle("#fff").fillStyle("rgba(31, 119, 180, .25)").width(function(b){return b.dx}).height(function(b){return b.dy});this.label.visible(function(b){return!b.firstChild}).left(function(b){return b.x+b.dx/2}).top(function(b){return b.y+b.dy/2}).textAlign("center").textAngle(function(b){return b.dx>b.dy?0:-Math.PI/2});(this.leaf=(new pv.Mark).extend(this.node).fillStyle(null).strokeStyle(null).visible(function(b){return!b.firstChild})).parent=
this;delete this.link};pv.Layout.Treemap.prototype=pv.extend(pv.Layout.Hierarchy).property("round",Boolean).property("paddingLeft",Number).property("paddingRight",Number).property("paddingTop",Number).property("paddingBottom",Number).property("mode",String).property("order",String);a=pv.Layout.Treemap.prototype;a.defaults=(new pv.Layout.Treemap).extend(pv.Layout.Hierarchy.prototype.defaults).mode("squarify").order("ascending");a.padding=function(b){return this.paddingLeft(b).paddingRight(b).paddingTop(b).paddingBottom(b)};
a.$size=function(b){return Number(b.nodeValue)};a.size=function(b){this.$size=pv.functor(b);return this};
a.buildImplied=function(b){function c(r,s,t,x,u,o,v){for(var w=0,y=0;w<r.length;w++){var z=r[w];if(t){z.x=x+y;z.y=u;y+=z.dx=p(o*z.size/s);z.dy=v}else{z.x=x;z.y=u+y;z.dx=o;y+=z.dy=p(v*z.size/s)}}if(z)if(t)z.dx+=o-y;else z.dy+=v-y}function d(r,s){for(var t=-Infinity,x=Infinity,u=0,o=0;o<r.length;o++){var v=r[o].size;if(v<x)x=v;if(v>t)t=v;u+=v}u*=u;s*=s;return Math.max(s*t/u,u/(s*x))}function f(r,s){function t(A){var D=o==y,G=pv.sum(A,n),E=y?p(G/y):0;c(A,G,D,x,u,D?o:E,D?E:v);if(D){u+=E;v-=E}else{x+=
E;o-=E}y=Math.min(o,v);return D}var x=r.x+j,u=r.y+l,o=r.dx-j-k,v=r.dy-l-q;if(m!="squarify")c(r.childNodes,r.size,m=="slice"?true:m=="dice"?false:s&1,x,u,o,v);else{var w=[];s=Infinity;var y=Math.min(o,v),z=o*v/r.size;if(!(r.size<=0)){r.visitBefore(function(A){A.size*=z});for(r=r.childNodes.slice();r.length;){var C=r[r.length-1];if(C.size){w.push(C);z=d(w,y);if(z<=s){r.pop();s=z}else{w.pop();t(w);w.length=0;s=Infinity}}else r.pop()}if(t(w))for(s=0;s<w.length;s++)w[s].dy+=v;else for(s=0;s<w.length;s++)w[s].dx+=
o}}}if(!pv.Layout.Hierarchy.prototype.buildImplied.call(this,b)){var g=this,h=b.nodes[0],i=pv.Mark.stack,j=b.paddingLeft,k=b.paddingRight,l=b.paddingTop,q=b.paddingBottom,n=function(r){return r.size},p=b.round?Math.round:Number,m=b.mode;i.unshift(null);h.visitAfter(function(r,s){r.depth=s;r.x=r.y=r.dx=r.dy=0;r.size=r.firstChild?pv.sum(r.childNodes,function(t){return t.size}):g.$size.apply(g,(i[0]=r,i))});i.shift();switch(b.order){case "ascending":h.sort(function(r,s){return r.size-s.size});break;
case "descending":h.sort(function(r,s){return s.size-r.size});break;case "reverse":h.reverse();break}h.x=0;h.y=0;h.dx=b.width;h.dy=b.height;h.visitBefore(f)}};pv.Layout.Tree=function(){pv.Layout.Hierarchy.call(this)};pv.Layout.Tree.prototype=pv.extend(pv.Layout.Hierarchy).property("group",Number).property("breadth",Number).property("depth",Number).property("orient",String);pv.Layout.Tree.prototype.defaults=(new pv.Layout.Tree).extend(pv.Layout.Hierarchy.prototype.defaults).group(1).breadth(15).depth(60).orient("top");
pv.Layout.Tree.prototype.buildImplied=function(b){function c(o){var v,w,y;if(o.firstChild){v=o.firstChild;w=o.lastChild;for(var z=y=v;z;z=z.nextSibling){c(z);y=f(z,y)}j(o);w=0.5*(v.prelim+w.prelim);if(v=o.previousSibling){o.prelim=v.prelim+l(o.depth,true);o.mod=o.prelim-w}else o.prelim=w}else if(v=o.previousSibling)o.prelim=v.prelim+l(o.depth,true)}function d(o,v,w){o.breadth=o.prelim+v;v+=o.mod;for(o=o.firstChild;o;o=o.nextSibling)d(o,v,w)}function f(o,v){var w=o.previousSibling;if(w){var y=o,z=
o,C=w;w=o.parentNode.firstChild;var A=y.mod,D=z.mod,G=C.mod,E=w.mod;C=h(C);for(y=g(y);C&&y;){C=C;y=y;w=g(w);z=h(z);z.ancestor=o;var B=C.prelim+G-(y.prelim+A)+l(C.depth,false);if(B>0){i(k(C,o,v),o,B);A+=B;D+=B}G+=C.mod;A+=y.mod;E+=w.mod;D+=z.mod;C=h(C);y=g(y)}if(C&&!h(z)){z.thread=C;z.mod+=G-D}if(y&&!g(w)){w.thread=y;w.mod+=A-E;v=o}}return v}function g(o){return o.firstChild||o.thread}function h(o){return o.lastChild||o.thread}function i(o,v,w){var y=v.number-o.number;v.change-=w/y;v.shift+=w;o.change+=
w/y;v.prelim+=w;v.mod+=w}function j(o){var v=0,w=0;for(o=o.lastChild;o;o=o.previousSibling){o.prelim+=v;o.mod+=v;w+=o.change;v+=o.shift+w}}function k(o,v,w){return o.ancestor.parentNode==v.parentNode?o.ancestor:w}function l(o,v){return(v?1:t+1)/(m=="radial"?o:1)}function q(o){return m=="radial"?o.breadth/r:0}function n(o){switch(m){case "left":return o.depth;case "right":return x-o.depth;case "top":case "bottom":return o.breadth+x/2;case "radial":return x/2+o.depth*Math.cos(q(o))}}function p(o){switch(m){case "left":case "right":return o.breadth+
u/2;case "top":return o.depth;case "bottom":return u-o.depth;case "radial":return u/2+o.depth*Math.sin(q(o))}}if(!pv.Layout.Hierarchy.prototype.buildImplied.call(this,b)){var m=b.orient,r=b.depth,s=b.breadth,t=b.group,x=b.width,u=b.height;b=b.nodes[0];b.visitAfter(function(o,v){o.ancestor=o;o.prelim=0;o.mod=0;o.change=0;o.shift=0;o.number=o.previousSibling?o.previousSibling.number+1:0;o.depth=v});c(b);d(b,-b.prelim,0);b.visitAfter(function(o){o.breadth*=s;o.depth*=r;o.midAngle=q(o);o.x=n(o);o.y=p(o);
if(o.firstChild)o.midAngle+=Math.PI;delete o.breadth;delete o.depth;delete o.ancestor;delete o.prelim;delete o.mod;delete o.change;delete o.shift;delete o.number;delete o.thread})}};pv.Layout.Indent=function(){pv.Layout.Hierarchy.call(this);this.link.interpolate("step-after")};pv.Layout.Indent.prototype=pv.extend(pv.Layout.Hierarchy).property("depth",Number).property("breadth",Number);pv.Layout.Indent.prototype.defaults=(new pv.Layout.Indent).extend(pv.Layout.Hierarchy.prototype.defaults).depth(15).breadth(15);
pv.Layout.Indent.prototype.buildImplied=function(b){function c(i,j,k){i.x=g+k++*f;i.y=h+j++*d;i.midAngle=0;for(i=i.firstChild;i;i=i.nextSibling)j=c(i,j,k);return j}if(!pv.Layout.Hierarchy.prototype.buildImplied.call(this,b)){var d=b.breadth,f=b.depth,g=0,h=0;c(b.nodes[0],1,1)}};pv.Layout.Pack=function(){pv.Layout.Hierarchy.call(this);this.node.radius(function(b){return b.radius}).strokeStyle("rgb(31, 119, 180)").fillStyle("rgba(31, 119, 180, .25)");this.label.textAlign("center");delete this.link};
pv.Layout.Pack.prototype=pv.extend(pv.Layout.Hierarchy).property("spacing",Number).property("order",String);pv.Layout.Pack.prototype.defaults=(new pv.Layout.Pack).extend(pv.Layout.Hierarchy.prototype.defaults).spacing(1).order("ascending");pv.Layout.Pack.prototype.$radius=function(){return 1};pv.Layout.Pack.prototype.size=function(b){this.$radius=typeof b=="function"?function(){return Math.sqrt(b.apply(this,arguments))}:(b=Math.sqrt(b),function(){return b});return this};
pv.Layout.Pack.prototype.buildImplied=function(b){function c(n){var p=pv.Mark.stack;p.unshift(null);for(var m=0,r=n.length;m<r;m++){var s=n[m];if(!s.firstChild)s.radius=i.$radius.apply(i,(p[0]=s,p))}p.shift()}function d(n){var p=[];for(n=n.firstChild;n;n=n.nextSibling){if(n.firstChild)n.radius=d(n);n.n=n.p=n;p.push(n)}switch(b.order){case "ascending":p.sort(function(m,r){return m.radius-r.radius});break;case "descending":p.sort(function(m,r){return r.radius-m.radius});break;case "reverse":p.reverse();
break}return f(p)}function f(n){function p(B){t=Math.min(B.x-B.radius,t);x=Math.max(B.x+B.radius,x);u=Math.min(B.y-B.radius,u);o=Math.max(B.y+B.radius,o)}function m(B,F){var H=B.n;B.n=F;F.p=B;F.n=H;H.p=F}function r(B,F){B.n=F;F.p=B}function s(B,F){var H=F.x-B.x,I=F.y-B.y;B=B.radius+F.radius;return B*B-H*H-I*I>0.0010}var t=Infinity,x=-Infinity,u=Infinity,o=-Infinity,v,w,y,z,C;v=n[0];v.x=-v.radius;v.y=0;p(v);if(n.length>1){w=n[1];w.x=w.radius;w.y=0;p(w);if(n.length>2){y=n[2];g(v,w,y);p(y);m(v,y);v.p=
y;m(y,w);w=v.n;for(var A=3;A<n.length;A++){g(v,w,y=n[A]);var D=0,G=1,E=1;for(z=w.n;z!=w;z=z.n,G++)if(s(z,y)){D=1;break}if(D==1)for(C=v.p;C!=z.p;C=C.p,E++)if(s(C,y)){if(E<G){D=-1;z=C}break}if(D==0){m(v,y);w=y;p(y)}else if(D>0){r(v,z);w=z;A--}else if(D<0){r(z,w);v=z;A--}}}}v=(t+x)/2;w=(u+o)/2;for(A=y=0;A<n.length;A++){z=n[A];z.x-=v;z.y-=w;y=Math.max(y,z.radius+Math.sqrt(z.x*z.x+z.y*z.y))}return y+b.spacing}function g(n,p,m){var r=p.radius+m.radius,s=n.radius+m.radius,t=p.x-n.x;p=p.y-n.y;var x=Math.sqrt(t*
t+p*p),u=(s*s+x*x-r*r)/(2*s*x);r=Math.acos(u);u=u*s;s=Math.sin(r)*s;t/=x;p/=x;m.x=n.x+u*t+s*p;m.y=n.y+u*p-s*t}function h(n,p,m,r){for(var s=n.firstChild;s;s=s.nextSibling){s.x+=n.x;s.y+=n.y;h(s,p,m,r)}n.x=p+r*n.x;n.y=m+r*n.y;n.radius*=r}if(!pv.Layout.Hierarchy.prototype.buildImplied.call(this,b)){var i=this,j=b.nodes,k=j[0];c(j);k.x=0;k.y=0;k.radius=d(k);j=this.width();var l=this.height(),q=1/Math.max(2*k.radius/j,2*k.radius/l);h(k,j/2,l/2,q)}};
pv.Layout.Force=function(){pv.Layout.Network.call(this);this.link.lineWidth(function(b,c){return Math.sqrt(c.linkValue)*1.5});this.label.textAlign("center")};
pv.Layout.Force.prototype=pv.extend(pv.Layout.Network).property("bound",Boolean).property("iterations",Number).property("dragConstant",Number).property("chargeConstant",Number).property("chargeMinDistance",Number).property("chargeMaxDistance",Number).property("chargeTheta",Number).property("springConstant",Number).property("springDamping",Number).property("springLength",Number);pv.Layout.Force.prototype.defaults=(new pv.Layout.Force).extend(pv.Layout.Network.prototype.defaults).dragConstant(0.1).chargeConstant(-40).chargeMinDistance(2).chargeMaxDistance(500).chargeTheta(0.9).springConstant(0.1).springDamping(0.3).springLength(20);
pv.Layout.Force.prototype.buildImplied=function(b){function c(q){return q.fix?1:q.vx*q.vx+q.vy*q.vy}if(pv.Layout.Network.prototype.buildImplied.call(this,b)){if(b=b.$force){b.next=this.binds.$force;this.binds.$force=b}}else{for(var d=this,f=b.nodes,g=b.links,h=b.iterations,i=b.width,j=b.height,k=0,l;k<f.length;k++){l=f[k];if(isNaN(l.x))l.x=i/2+40*Math.random()-20;if(isNaN(l.y))l.y=j/2+40*Math.random()-20}l=pv.simulation(f);l.force(pv.Force.drag(b.dragConstant));l.force(pv.Force.charge(b.chargeConstant).domain(b.chargeMinDistance,
b.chargeMaxDistance).theta(b.chargeTheta));l.force(pv.Force.spring(b.springConstant).damping(b.springDamping).length(b.springLength).links(g));l.constraint(pv.Constraint.position());b.bound&&l.constraint(pv.Constraint.bound().x(6,i-6).y(6,j-6));if(h==null){l.step();l.step();b.$force=this.binds.$force={next:this.binds.$force,nodes:f,min:1.0E-4*(g.length+1),sim:l};if(!this.$timer)this.$timer=setInterval(function(){for(var q=false,n=d.binds.$force;n;n=n.next)if(pv.max(n.nodes,c)>n.min){n.sim.step();
q=true}q&&d.render()},42)}else for(k=0;k<h;k++)l.step()}};pv.Layout.Cluster=function(){pv.Layout.Hierarchy.call(this);var b,c=this.buildImplied;this.buildImplied=function(d){c.call(this,d);b=/^(top|bottom)$/.test(d.orient)?"step-before":/^(left|right)$/.test(d.orient)?"step-after":"linear"};this.link.interpolate(function(){return b})};
pv.Layout.Cluster.prototype=pv.extend(pv.Layout.Hierarchy).property("group",Number).property("orient",String).property("innerRadius",Number).property("outerRadius",Number);pv.Layout.Cluster.prototype.defaults=(new pv.Layout.Cluster).extend(pv.Layout.Hierarchy.prototype.defaults).group(0).orient("top");
pv.Layout.Cluster.prototype.buildImplied=function(b){if(!pv.Layout.Hierarchy.prototype.buildImplied.call(this,b)){var c=b.nodes[0],d=b.group,f,g,h=0,i=0.5-d/2,j=undefined;c.visitAfter(function(k){if(k.firstChild)k.depth=1+pv.max(k.childNodes,function(l){return l.depth});else{if(d&&j!=k.parentNode){j=k.parentNode;h+=d}h++;k.depth=0}});f=1/h;g=1/c.depth;j=undefined;c.visitAfter(function(k){if(k.firstChild)k.breadth=pv.mean(k.childNodes,function(l){return l.breadth});else{if(d&&j!=k.parentNode){j=k.parentNode;
i+=d}k.breadth=f*i++}k.depth=1-k.depth*g});c.visitAfter(function(k){k.minBreadth=k.firstChild?k.firstChild.minBreadth:k.breadth-f/2;k.maxBreadth=k.firstChild?k.lastChild.maxBreadth:k.breadth+f/2});c.visitBefore(function(k){k.minDepth=k.parentNode?k.parentNode.maxDepth:0;k.maxDepth=k.parentNode?k.depth+c.depth:k.minDepth+2*c.depth});c.minDepth=-g;pv.Layout.Hierarchy.NodeLink.buildImplied.call(this,b)}};pv.Layout.Cluster.Fill=function(){pv.Layout.Cluster.call(this);pv.Layout.Hierarchy.Fill.constructor.call(this)};
pv.Layout.Cluster.Fill.prototype=pv.extend(pv.Layout.Cluster);pv.Layout.Cluster.Fill.prototype.buildImplied=function(b){pv.Layout.Cluster.prototype.buildImplied.call(this,b)||pv.Layout.Hierarchy.Fill.buildImplied.call(this,b)};pv.Layout.Partition=function(){pv.Layout.Hierarchy.call(this)};pv.Layout.Partition.prototype=pv.extend(pv.Layout.Hierarchy).property("order",String).property("orient",String).property("innerRadius",Number).property("outerRadius",Number);
pv.Layout.Partition.prototype.defaults=(new pv.Layout.Partition).extend(pv.Layout.Hierarchy.prototype.defaults).orient("top");pv.Layout.Partition.prototype.$size=function(){return 1};pv.Layout.Partition.prototype.size=function(b){this.$size=b;return this};
pv.Layout.Partition.prototype.buildImplied=function(b){if(!pv.Layout.Hierarchy.prototype.buildImplied.call(this,b)){var c=this,d=b.nodes[0],f=pv.Mark.stack,g=0;f.unshift(null);d.visitAfter(function(i,j){if(j>g)g=j;i.size=i.firstChild?pv.sum(i.childNodes,function(k){return k.size}):c.$size.apply(c,(f[0]=i,f))});f.shift();switch(b.order){case "ascending":d.sort(function(i,j){return i.size-j.size});break;case "descending":d.sort(function(i,j){return j.size-i.size});break}var h=1/g;d.minBreadth=0;d.breadth=
0.5;d.maxBreadth=1;d.visitBefore(function(i){for(var j=i.minBreadth,k=i.maxBreadth-j,l=i.firstChild;l;l=l.nextSibling){l.minBreadth=j;l.maxBreadth=j+=l.size/i.size*k;l.breadth=(j+l.minBreadth)/2}});d.visitAfter(function(i,j){i.minDepth=(j-1)*h;i.maxDepth=i.depth=j*h});pv.Layout.Hierarchy.NodeLink.buildImplied.call(this,b)}};pv.Layout.Partition.Fill=function(){pv.Layout.Partition.call(this);pv.Layout.Hierarchy.Fill.constructor.call(this)};pv.Layout.Partition.Fill.prototype=pv.extend(pv.Layout.Partition);
pv.Layout.Partition.Fill.prototype.buildImplied=function(b){pv.Layout.Partition.prototype.buildImplied.call(this,b)||pv.Layout.Hierarchy.Fill.buildImplied.call(this,b)};pv.Layout.Arc=function(){pv.Layout.Network.call(this);var b,c,d,f=this.buildImplied;this.buildImplied=function(g){f.call(this,g);c=g.directed;b=g.orient=="radial"?"linear":"polar";d=g.orient=="right"||g.orient=="top"};this.link.data(function(g){var h=g.sourceNode;g=g.targetNode;return d!=(c||h.breadth<g.breadth)?[h,g]:[g,h]}).interpolate(function(){return b})};
pv.Layout.Arc.prototype=pv.extend(pv.Layout.Network).property("orient",String).property("directed",Boolean);pv.Layout.Arc.prototype.defaults=(new pv.Layout.Arc).extend(pv.Layout.Network.prototype.defaults).orient("bottom");pv.Layout.Arc.prototype.sort=function(b){this.$sort=b;return this};
pv.Layout.Arc.prototype.buildImplied=function(b){function c(m){switch(h){case "top":return-Math.PI/2;case "bottom":return Math.PI/2;case "left":return Math.PI;case "right":return 0;case "radial":return(m-0.25)*2*Math.PI}}function d(m){switch(h){case "top":case "bottom":return m*k;case "left":return 0;case "right":return k;case "radial":return k/2+q*Math.cos(c(m))}}function f(m){switch(h){case "top":return 0;case "bottom":return l;case "left":case "right":return m*l;case "radial":return l/2+q*Math.sin(c(m))}}
if(!pv.Layout.Network.prototype.buildImplied.call(this,b)){var g=b.nodes,h=b.orient,i=this.$sort,j=pv.range(g.length),k=b.width,l=b.height,q=Math.min(k,l)/2;i&&j.sort(function(m,r){return i(g[m],g[r])});for(b=0;b<g.length;b++){var n=g[j[b]],p=n.breadth=(b+0.5)/g.length;n.x=d(p);n.y=f(p);n.midAngle=c(p)}}};
pv.Layout.Horizon=function(){pv.Layout.call(this);var b=this,c,d,f,g,h,i,j=this.buildImplied;this.buildImplied=function(k){j.call(this,k);c=k.bands;d=k.mode;f=Math.round((d=="color"?0.5:1)*k.height);g=k.backgroundStyle;h=pv.ramp(g,k.negativeStyle).domain(0,c);i=pv.ramp(g,k.positiveStyle).domain(0,c)};c=(new pv.Panel).data(function(){return pv.range(c*2)}).overflow("hidden").height(function(){return f}).top(function(k){return d=="color"?(k&1)*f:0}).fillStyle(function(k){return k?null:g});this.band=
(new pv.Mark).top(function(k,l){return d=="mirror"&&l&1?(l+1>>1)*f:null}).bottom(function(k,l){return d=="mirror"?l&1?null:(l+1>>1)*-f:(l&1||-1)*(l+1>>1)*f}).fillStyle(function(k,l){return(l&1?h:i)((l>>1)+1)});this.band.add=function(k){return b.add(pv.Panel).extend(c).add(k).extend(this)}};pv.Layout.Horizon.prototype=pv.extend(pv.Layout).property("bands",Number).property("mode",String).property("backgroundStyle",pv.color).property("positiveStyle",pv.color).property("negativeStyle",pv.color);
pv.Layout.Horizon.prototype.defaults=(new pv.Layout.Horizon).extend(pv.Layout.prototype.defaults).bands(2).mode("offset").backgroundStyle("white").positiveStyle("#1f77b4").negativeStyle("#d62728");
pv.Layout.Rollup=function(){pv.Layout.Network.call(this);var b=this,c,d,f=b.buildImplied;this.buildImplied=function(g){f.call(this,g);c=g.$rollup.nodes;d=g.$rollup.links};this.node.data(function(){return c}).size(function(g){return g.nodes.length*20});this.link.interpolate("polar").eccentricity(0.8);this.link.add=function(g){return b.add(pv.Panel).data(function(){return d}).add(g).extend(this)}};pv.Layout.Rollup.prototype=pv.extend(pv.Layout.Network).property("directed",Boolean);
pv.Layout.Rollup.prototype.x=function(b){this.$x=pv.functor(b);return this};pv.Layout.Rollup.prototype.y=function(b){this.$y=pv.functor(b);return this};
pv.Layout.Rollup.prototype.buildImplied=function(b){function c(r){return i[r]+","+j[r]}if(!pv.Layout.Network.prototype.buildImplied.call(this,b)){var d=b.nodes,f=b.links,g=b.directed,h=d.length,i=[],j=[],k=0,l={},q={},n=pv.Mark.stack,p={parent:this};n.unshift(null);for(var m=0;m<h;m++){p.index=m;n[0]=d[m];i[m]=this.$x.apply(p,n);j[m]=this.$y.apply(p,n)}n.shift();for(m=0;m<d.length;m++){h=c(m);n=l[h];if(!n){n=l[h]=pv.extend(d[m]);n.index=k++;n.x=i[m];n.y=j[m];n.nodes=[]}n.nodes.push(d[m])}for(m=0;m<
f.length;m++){k=f[m].targetNode;d=l[c(f[m].sourceNode.index)];k=l[c(k.index)];h=!g&&d.index>k.index?k.index+","+d.index:d.index+","+k.index;(n=q[h])||(n=q[h]={sourceNode:d,targetNode:k,linkValue:0,links:[]});n.links.push(f[m]);n.linkValue+=f[m].linkValue}b.$rollup={nodes:pv.values(l),links:pv.values(q)}}};
pv.Layout.Matrix=function(){pv.Layout.Network.call(this);var b,c,d,f,g,h=this.buildImplied;this.buildImplied=function(i){h.call(this,i);b=i.nodes.length;c=i.width/b;d=i.height/b;f=i.$matrix.labels;g=i.$matrix.pairs};this.link.data(function(){return g}).left(function(){return c*(this.index%b)}).top(function(){return d*Math.floor(this.index/b)}).width(function(){return c}).height(function(){return d}).lineWidth(1.5).strokeStyle("#fff").fillStyle(function(i){return i.linkValue?"#555":"#eee"}).parent=
this;delete this.link.add;this.label.data(function(){return f}).left(function(){return this.index&1?c*((this.index>>1)+0.5):0}).top(function(){return this.index&1?0:d*((this.index>>1)+0.5)}).textMargin(4).textAlign(function(){return this.index&1?"left":"right"}).textAngle(function(){return this.index&1?-Math.PI/2:0});delete this.node};pv.Layout.Matrix.prototype=pv.extend(pv.Layout.Network).property("directed",Boolean);pv.Layout.Matrix.prototype.sort=function(b){this.$sort=b;return this};
pv.Layout.Matrix.prototype.buildImplied=function(b){if(!pv.Layout.Network.prototype.buildImplied.call(this,b)){var c=b.nodes,d=b.links,f=this.$sort,g=c.length,h=pv.range(g),i=[],j=[],k={};b.$matrix={labels:i,pairs:j};f&&h.sort(function(m,r){return f(c[m],c[r])});for(var l=0;l<g;l++)for(var q=0;q<g;q++){var n=h[l],p=h[q];j.push(k[n+"."+p]={row:l,col:q,sourceNode:c[n],targetNode:c[p],linkValue:0})}for(l=0;l<g;l++){n=h[l];i.push(c[n],c[n])}for(l=0;l<d.length;l++){i=d[l];g=i.sourceNode.index;h=i.targetNode.index;
i=i.linkValue;k[g+"."+h].linkValue+=i;b.directed||(k[h+"."+g].linkValue+=i)}}};
pv.Layout.Bullet=function(){pv.Layout.call(this);var b=this,c=b.buildImplied,d=b.x=pv.Scale.linear(),f,g,h,i,j;this.buildImplied=function(k){c.call(this,j=k);f=k.orient;g=/^left|right$/.test(f);h=pv.ramp("#bbb","#eee").domain(0,Math.max(1,j.ranges.length-1));i=pv.ramp("steelblue","lightsteelblue").domain(0,Math.max(1,j.measures.length-1))};(this.range=new pv.Mark).data(function(){return j.ranges}).reverse(true).left(function(){return f=="left"?0:null}).top(function(){return f=="top"?0:null}).right(function(){return f==
"right"?0:null}).bottom(function(){return f=="bottom"?0:null}).width(function(k){return g?d(k):null}).height(function(k){return g?null:d(k)}).fillStyle(function(){return h(this.index)}).antialias(false).parent=b;(this.measure=new pv.Mark).extend(this.range).data(function(){return j.measures}).left(function(){return f=="left"?0:g?null:this.parent.width()/3.25}).top(function(){return f=="top"?0:g?this.parent.height()/3.25:null}).right(function(){return f=="right"?0:g?null:this.parent.width()/3.25}).bottom(function(){return f==
"bottom"?0:g?this.parent.height()/3.25:null}).fillStyle(function(){return i(this.index)}).parent=b;(this.marker=new pv.Mark).data(function(){return j.markers}).left(function(k){return f=="left"?d(k):g?null:this.parent.width()/2}).top(function(k){return f=="top"?d(k):g?this.parent.height()/2:null}).right(function(k){return f=="right"?d(k):null}).bottom(function(k){return f=="bottom"?d(k):null}).strokeStyle("black").shape("bar").angle(function(){return g?0:Math.PI/2}).parent=b;(this.tick=new pv.Mark).data(function(){return d.ticks(7)}).left(function(k){return f==
"left"?d(k):null}).top(function(k){return f=="top"?d(k):null}).right(function(k){return f=="right"?d(k):g?null:-6}).bottom(function(k){return f=="bottom"?d(k):g?-8:null}).height(function(){return g?6:null}).width(function(){return g?null:6}).parent=b};pv.Layout.Bullet.prototype=pv.extend(pv.Layout).property("orient",String).property("ranges").property("markers").property("measures").property("maximum",Number);pv.Layout.Bullet.prototype.defaults=(new pv.Layout.Bullet).extend(pv.Layout.prototype.defaults).orient("left").ranges([]).markers([]).measures([]);
pv.Layout.Bullet.prototype.buildImplied=function(b){pv.Layout.prototype.buildImplied.call(this,b);var c=this.parent[/^left|right$/.test(b.orient)?"width":"height"]();b.maximum=b.maximum||pv.max([].concat(b.ranges,b.markers,b.measures));this.x.domain(0,b.maximum).range(0,c)};pv.Behavior={};
pv.Behavior.drag=function(){function b(k){g=this.index;f=this.scene;var l=this.mouse();i=((h=k).fix=pv.vector(k.x,k.y)).minus(l);j={x:this.parent.width()-(k.dx||0),y:this.parent.height()-(k.dy||0)};f.mark.context(f,g,function(){this.render()});pv.Mark.dispatch("dragstart",f,g)}function c(){if(f){f.mark.context(f,g,function(){var k=this.mouse();h.x=h.fix.x=Math.max(0,Math.min(i.x+k.x,j.x));h.y=h.fix.y=Math.max(0,Math.min(i.y+k.y,j.y));this.render()});pv.Mark.dispatch("drag",f,g)}}function d(){if(f){h.fix=
null;f.mark.context(f,g,function(){this.render()});pv.Mark.dispatch("dragend",f,g);f=null}}var f,g,h,i,j;pv.listen(window,"mousemove",c);pv.listen(window,"mouseup",d);return b};
pv.Behavior.point=function(b){function c(l,q){l=l[q];q={cost:Infinity};for(var n=0,p=l.visible&&l.children.length;n<p;n++){var m=l.children[n],r=m.mark,s;if(r.type=="panel"){r.scene=m;for(var t=0,x=m.length;t<x;t++){r.index=t;s=c(m,t);if(s.cost<q.cost)q=s}delete r.scene;delete r.index}else if(r.$handlers.point){r=r.mouse();t=0;for(x=m.length;t<x;t++){var u=m[t];s=r.x-u.left-(u.width||0)/2;u=r.y-u.top-(u.height||0)/2;var o=i*s*s+j*u*u;if(o<q.cost){q.distance=s*s+u*u;q.cost=o;q.scene=m;q.index=t}}}}return q}
function d(){var l=c(this.scene,this.index);if(l.cost==Infinity||l.distance>k)l=null;if(g){if(l&&g.scene==l.scene&&g.index==l.index)return;pv.Mark.dispatch("unpoint",g.scene,g.index)}if(g=l){pv.Mark.dispatch("point",l.scene,l.index);pv.listen(this.root.canvas(),"mouseout",f)}}function f(l){if(g&&!pv.ancestor(this,l.relatedTarget)){pv.Mark.dispatch("unpoint",g.scene,g.index);g=null}}var g,h=null,i=1,j=1,k=arguments.length?b*b:900;d.collapse=function(l){if(arguments.length){h=String(l);switch(h){case "y":i=
1;j=0;break;case "x":i=0;j=1;break;default:j=i=1;break}return d}return h};return d};
pv.Behavior.select=function(){function b(j){g=this.index;f=this.scene;i=this.mouse();h=j;h.x=i.x;h.y=i.y;h.dx=h.dy=0;pv.Mark.dispatch("selectstart",f,g)}function c(){if(f){f.mark.context(f,g,function(){var j=this.mouse();h.x=Math.max(0,Math.min(i.x,j.x));h.y=Math.max(0,Math.min(i.y,j.y));h.dx=Math.min(this.width(),Math.max(j.x,i.x))-h.x;h.dy=Math.min(this.height(),Math.max(j.y,i.y))-h.y;this.render()});pv.Mark.dispatch("select",f,g)}}function d(){if(f){pv.Mark.dispatch("selectend",f,g);f=null}}var f,
g,h,i;pv.listen(window,"mousemove",c);pv.listen(window,"mouseup",d);return b};
pv.Behavior.resize=function(b){function c(k){h=this.index;g=this.scene;j=this.mouse();i=k;switch(b){case "left":j.x=i.x+i.dx;break;case "right":j.x=i.x;break;case "top":j.y=i.y+i.dy;break;case "bottom":j.y=i.y;break}pv.Mark.dispatch("resizestart",g,h)}function d(){if(g){g.mark.context(g,h,function(){var k=this.mouse();i.x=Math.max(0,Math.min(j.x,k.x));i.y=Math.max(0,Math.min(j.y,k.y));i.dx=Math.min(this.parent.width(),Math.max(k.x,j.x))-i.x;i.dy=Math.min(this.parent.height(),Math.max(k.y,j.y))-i.y;
this.render()});pv.Mark.dispatch("resize",g,h)}}function f(){if(g){pv.Mark.dispatch("resizeend",g,h);g=null}}var g,h,i,j;pv.listen(window,"mousemove",d);pv.listen(window,"mouseup",f);return c};
pv.Behavior.pan=function(){function b(){g=this.index;f=this.scene;i=pv.vector(pv.event.pageX,pv.event.pageY);h=this.transform();j=1/(h.k*this.scale);if(k)k={x:(1-h.k)*this.width(),y:(1-h.k)*this.height()}}function c(){if(f){f.mark.context(f,g,function(){var l=h.translate((pv.event.pageX-i.x)*j,(pv.event.pageY-i.y)*j);if(k){l.x=Math.max(k.x,Math.min(0,l.x));l.y=Math.max(k.y,Math.min(0,l.y))}this.transform(l).render()});pv.Mark.dispatch("pan",f,g)}}function d(){f=null}var f,g,h,i,j,k;b.bound=function(l){if(arguments.length){k=
Boolean(l);return this}return Boolean(k)};pv.listen(window,"mousemove",c);pv.listen(window,"mouseup",d);return b};
pv.Behavior.zoom=function(b){function c(){var f=this.mouse(),g=pv.event.wheel*b;f=this.transform().translate(f.x,f.y).scale(g<0?1E3/(1E3-g):(1E3+g)/1E3).translate(-f.x,-f.y);if(d){f.k=Math.max(1,f.k);f.x=Math.max((1-f.k)*this.width(),Math.min(0,f.x));f.y=Math.max((1-f.k)*this.height(),Math.min(0,f.y))}this.transform(f).render();pv.Mark.dispatch("zoom",this.scene,this.index)}var d;arguments.length||(b=1/48);c.bound=function(f){if(arguments.length){d=Boolean(f);return this}return Boolean(d)};return c};
pv.Geo=function(){};
pv.Geo.projections={mercator:{project:function(b){return{x:b.lng/180,y:b.lat>85?1:b.lat<-85?-1:Math.log(Math.tan(Math.PI/4+pv.radians(b.lat)/2))/Math.PI}},invert:function(b){return{lng:b.x*180,lat:pv.degrees(2*Math.atan(Math.exp(b.y*Math.PI))-Math.PI/2)}}},"gall-peters":{project:function(b){return{x:b.lng/180,y:Math.sin(pv.radians(b.lat))}},invert:function(b){return{lng:b.x*180,lat:pv.degrees(Math.asin(b.y))}}},sinusoidal:{project:function(b){return{x:pv.radians(b.lng)*Math.cos(pv.radians(b.lat))/Math.PI,
y:b.lat/90}},invert:function(b){return{lng:pv.degrees(b.x*Math.PI/Math.cos(b.y*Math.PI/2)),lat:b.y*90}}},aitoff:{project:function(b){var c=pv.radians(b.lng);b=pv.radians(b.lat);var d=Math.acos(Math.cos(b)*Math.cos(c/2));return{x:2*(d?Math.cos(b)*Math.sin(c/2)*d/Math.sin(d):0)/Math.PI,y:2*(d?Math.sin(b)*d/Math.sin(d):0)/Math.PI}},invert:function(b){var c=b.y*Math.PI/2;return{lng:pv.degrees(b.x*Math.PI/2/Math.cos(c)),lat:pv.degrees(c)}}},hammer:{project:function(b){var c=pv.radians(b.lng);b=pv.radians(b.lat);
var d=Math.sqrt(1+Math.cos(b)*Math.cos(c/2));return{x:2*Math.SQRT2*Math.cos(b)*Math.sin(c/2)/d/3,y:Math.SQRT2*Math.sin(b)/d/1.5}},invert:function(b){var c=b.x*3;b=b.y*1.5;var d=Math.sqrt(1-c*c/16-b*b/4);return{lng:pv.degrees(2*Math.atan2(d*c,2*(2*d*d-1))),lat:pv.degrees(Math.asin(d*b))}}},identity:{project:function(b){return{x:b.lng/180,y:b.lat/90}},invert:function(b){return{lng:b.x*180,lat:b.y*90}}}};
pv.Geo.scale=function(b){function c(m){if(!n||m.lng!=n.lng||m.lat!=n.lat){n=m;m=d(m);p={x:k(m.x),y:l(m.y)}}return p}function d(m){return j.project({lng:m.lng-q.lng,lat:m.lat})}function f(m){m=j.invert(m);m.lng+=q.lng;return m}var g={x:0,y:0},h={x:1,y:1},i=[],j=pv.Geo.projections.identity,k=pv.Scale.linear(-1,1).range(0,1),l=pv.Scale.linear(-1,1).range(1,0),q={lng:0,lat:0},n,p;c.x=function(m){return c(m).x};c.y=function(m){return c(m).y};c.ticks={lng:function(m){var r;if(i.length>1){var s=pv.Scale.linear();
if(m==undefined)m=10;r=s.domain(i,function(t){return t.lat}).ticks(m);m=s.domain(i,function(t){return t.lng}).ticks(m)}else{r=pv.range(-80,81,10);m=pv.range(-180,181,10)}return m.map(function(t){return r.map(function(x){return{lat:x,lng:t}})})},lat:function(m){return pv.transpose(c.ticks.lng(m))}};c.invert=function(m){return f({x:k.invert(m.x),y:l.invert(m.y)})};c.domain=function(m,r){if(arguments.length){i=m instanceof Array?arguments.length>1?pv.map(m,r):m:Array.prototype.slice.call(arguments);
if(i.length>1){var s=i.map(function(x){return x.lng}),t=i.map(function(x){return x.lat});q={lng:(pv.max(s)+pv.min(s))/2,lat:(pv.max(t)+pv.min(t))/2};s=i.map(d);k.domain(s,function(x){return x.x});l.domain(s,function(x){return x.y})}else{q={lng:0,lat:0};k.domain(-1,1);l.domain(-1,1)}n=null;return this}return i};c.range=function(m,r){if(arguments.length){if(typeof m=="object"){g={x:Number(m.x),y:Number(m.y)};h={x:Number(r.x),y:Number(r.y)}}else{g={x:0,y:0};h={x:Number(m),y:Number(r)}}k.range(g.x,h.x);
l.range(h.y,g.y);n=null;return this}return[g,h]};c.projection=function(m){if(arguments.length){j=typeof m=="string"?pv.Geo.projections[m]||pv.Geo.projections.identity:m;return this.domain(i)}return m};c.by=function(m){function r(){return c(m.apply(this,arguments))}for(var s in c)r[s]=c[s];return r};arguments.length&&c.projection(b);return c};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+325
View File
@@ -0,0 +1,325 @@
import { app } from "../../../scripts/app.js";
// code based on mtb nodes by Mel Massadian https://github.com/melMass/comfy_mtb/
export const loadScript = (
FILE_URL,
async = true,
type = 'text/javascript',
) => {
return new Promise((resolve, reject) => {
try {
// Check if the script already exists
const existingScript = document.querySelector(`script[src="${FILE_URL}"]`)
if (existingScript) {
resolve({ status: true, message: 'Script already loaded' })
return
}
const scriptEle = document.createElement('script')
scriptEle.type = type
scriptEle.async = async
scriptEle.src = FILE_URL
scriptEle.addEventListener('load', (ev) => {
resolve({ status: true })
})
scriptEle.addEventListener('error', (ev) => {
reject({
status: false,
message: `Failed to load the script ${FILE_URL}`,
})
})
document.body.appendChild(scriptEle)
} catch (error) {
reject(error)
}
})
}
loadScript('/ryanontheinside_web_async/marked.min.js').catch((e) => {
console.log(e)
})
loadScript('/ryanontheinside_web_async/purify.min.js').catch((e) => {
console.log(e)
})
const categories = ["RyanOnTheInside"];
app.registerExtension({
name: "RyanOnTheInside.HelpPopup",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (app.ui.settings.getSettingValue("RyanOnTheInside.helpPopup") === false) {
return;
}
try {
categories.forEach(category => {
if (nodeData?.category?.startsWith(category)) {
addDocumentation(nodeData, nodeType);
}
else return
});
} catch (error) {
console.error("Error in registering RyanOnTheInside.HelpPopup", error);
}
},
});
const create_documentation_stylesheet = () => {
const tag = 'roti-documentation-stylesheet'
let styleTag = document.head.querySelector(tag)
if (!styleTag) {
styleTag = document.createElement('style')
styleTag.type = 'text/css'
styleTag.id = tag
styleTag.innerHTML = `
.roti-documentation-popup {
background: var(--comfy-menu-bg);
position: absolute;
color: var(--fg-color);
font: 12px monospace;
line-height: 1.5em;
padding: 10px;
border-radius: 10px;
border-style: solid;
border-width: medium;
border-color: var(--border-color);
z-index: 5;
overflow: hidden;
}
.content-wrapper {
overflow: auto;
max-height: 100%;
/* Scrollbar styling for Chrome */
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: var(--bg-color);
}
&::-webkit-scrollbar-thumb {
background-color: var(--fg-color);
border-radius: 6px;
border: 3px solid var(--bg-color);
}
/* Scrollbar styling for Firefox */
scrollbar-width: thin;
scrollbar-color: var(--fg-color) var(--bg-color);
a {
color: yellow;
}
a:visited {
color: orange;
}
a:hover {
color: red;
}
}
`
document.head.appendChild(styleTag)
}
}
/** Add documentation widget to the selected node */
export const addDocumentation = (
nodeData,
nodeType,
opts = { icon_size: 14, icon_margin: 4 },) => {
opts = opts || {}
const iconSize = opts.icon_size ? opts.icon_size : 14
const iconMargin = opts.icon_margin ? opts.icon_margin : 4
let docElement = null
let contentWrapper = null
//if no description in the node python code, don't do anything
if (!nodeData.description) {
return
}
const drawFg = nodeType.prototype.onDrawForeground
nodeType.prototype.onDrawForeground = function (ctx) {
const r = drawFg ? drawFg.apply(this, arguments) : undefined
if (this.flags.collapsed) return r
// icon position
const x = this.size[0] - iconSize - iconMargin
// create the popup
if (this.show_doc && docElement === null) {
docElement = document.createElement('div')
contentWrapper = document.createElement('div');
docElement.appendChild(contentWrapper);
create_documentation_stylesheet()
contentWrapper.classList.add('content-wrapper');
docElement.classList.add('roti-documentation-popup')
//parse the string from the python node code to html with marked, and sanitize the html with DOMPurify
contentWrapper.innerHTML = DOMPurify.sanitize(marked.parse(nodeData.description,))
// resize handle
const resizeHandle = document.createElement('div');
resizeHandle.style.width = '0';
resizeHandle.style.height = '0';
resizeHandle.style.position = 'absolute';
resizeHandle.style.bottom = '0';
resizeHandle.style.right = '0';
resizeHandle.style.cursor = 'se-resize';
// Add pseudo-elements to create a triangle shape
const borderColor = getComputedStyle(document.documentElement).getPropertyValue('--border-color').trim();
resizeHandle.style.borderTop = '10px solid transparent';
resizeHandle.style.borderLeft = '10px solid transparent';
resizeHandle.style.borderBottom = `10px solid ${borderColor}`;
resizeHandle.style.borderRight = `10px solid ${borderColor}`;
docElement.appendChild(resizeHandle)
let isResizing = false
let startX, startY, startWidth, startHeight
resizeHandle.addEventListener('mousedown', function (e) {
e.preventDefault();
e.stopPropagation();
isResizing = true;
startX = e.clientX;
startY = e.clientY;
startWidth = parseInt(document.defaultView.getComputedStyle(docElement).width, 10);
startHeight = parseInt(document.defaultView.getComputedStyle(docElement).height, 10);
},
{ signal: this.docCtrl.signal },
);
// close button
const closeButton = document.createElement('div');
closeButton.textContent = '❌';
closeButton.style.position = 'absolute';
closeButton.style.top = '0';
closeButton.style.right = '0';
closeButton.style.cursor = 'pointer';
closeButton.style.padding = '5px';
closeButton.style.color = 'red';
closeButton.style.fontSize = '12px';
docElement.appendChild(closeButton)
closeButton.addEventListener('mousedown', (e) => {
e.stopPropagation();
this.show_doc = !this.show_doc
docElement.parentNode.removeChild(docElement)
docElement = null
if (contentWrapper) {
contentWrapper.remove()
contentWrapper = null
}
},
{ signal: this.docCtrl.signal },
);
document.addEventListener('mousemove', function (e) {
if (!isResizing) return;
const scale = app.canvas.ds.scale;
const newWidth = startWidth + (e.clientX - startX) / scale;
const newHeight = startHeight + (e.clientY - startY) / scale;;
docElement.style.width = `${newWidth}px`;
docElement.style.height = `${newHeight}px`;
},
{ signal: this.docCtrl.signal },
);
document.addEventListener('mouseup', function () {
isResizing = false
},
{ signal: this.docCtrl.signal },
)
document.body.appendChild(docElement)
}
// close the popup
else if (!this.show_doc && docElement !== null) {
docElement.parentNode.removeChild(docElement)
docElement = null
}
// update position of the popup
if (this.show_doc && docElement !== null) {
const rect = ctx.canvas.getBoundingClientRect()
const scaleX = rect.width / ctx.canvas.width
const scaleY = rect.height / ctx.canvas.height
const transform = new DOMMatrix()
.scaleSelf(scaleX, scaleY)
.multiplySelf(ctx.getTransform())
.translateSelf(this.size[0] * scaleX * Math.max(1.0,window.devicePixelRatio) , 0)
.translateSelf(10, -32)
const scale = new DOMMatrix()
.scaleSelf(transform.a, transform.d);
const styleObject = {
transformOrigin: '0 0',
transform: scale,
left: `${transform.a + transform.e}px`,
top: `${transform.d + transform.f}px`,
};
Object.assign(docElement.style, styleObject);
}
ctx.save()
ctx.translate(x - 2, iconSize - 34)
ctx.scale(iconSize / 32, iconSize / 32)
ctx.strokeStyle = 'rgba(255,255,255,0.3)'
ctx.lineCap = 'round'
ctx.lineJoin = 'round'
ctx.lineWidth = 2.4
ctx.font = 'bold 36px monospace'
ctx.fillStyle = 'orange';
ctx.fillText('?', 0, 24)
ctx.restore()
return r
}
// handle clicking of the icon
const mouseDown = nodeType.prototype.onMouseDown
nodeType.prototype.onMouseDown = function (e, localPos, canvas) {
const r = mouseDown ? mouseDown.apply(this, arguments) : undefined
const iconX = this.size[0] - iconSize - iconMargin
const iconY = iconSize - 34
if (
localPos[0] > iconX &&
localPos[0] < iconX + iconSize &&
localPos[1] > iconY &&
localPos[1] < iconY + iconSize
) {
if (this.show_doc === undefined) {
this.show_doc = true
} else {
this.show_doc = !this.show_doc
}
if (this.show_doc) {
this.docCtrl = new AbortController()
} else {
this.docCtrl.abort()
}
return true;
}
return r;
}
const onRem = nodeType.prototype.onRemoved
nodeType.prototype.onRemoved = function () {
const r = onRem ? onRem.apply(this, []) : undefined
if (docElement) {
docElement.remove()
docElement = null
}
if (contentWrapper) {
contentWrapper.remove()
contentWrapper = null
}
return r
}
}
+416
View File
@@ -0,0 +1,416 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { drawPiano, handlePianoMouseDown } from "./piano.js";
app.registerExtension({
name: "org.ryanontheinside.midi_load_and_extract",
// async setup() {
// console.log("MIDI Extension: Setup called");
// },
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "MIDILoadAndExtract") {
console.log("MIDI Extension: Modifying MIDILoadAndExtract node");
// Add the disableWidget method to the nodeType prototype
nodeType.prototype.disableWidget = function(widget) {
if (widget) {
widget.disabled = true;
widget.visibleWidth = 0;
widget.onMouseDown = () => {};
widget.onMouseMove = () => {};
widget.onMouseUp = () => {};
}
};
nodeType.prototype.uploadMIDI = function() {
const input = document.createElement("input");
input.type = "file";
input.accept = ".mid,.midi";
input.onchange = (event) => {
const file = event.target.files[0];
if (file) {
const formData = new FormData();
formData.append("file", file);
fetch("/upload_midi", {
method: "POST",
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log("MIDI file uploaded successfully");
// Update the midi_file widget with the new file path
const midiFileWidget = this.widgets.find(w => w.name === "midi_file");
if (midiFileWidget) {
midiFileWidget.value = data.file_path;
// Trigger any necessary updates
if (midiFileWidget.callback) {
midiFileWidget.callback(data.file_path);
}
}
// You might want to call updateTrackSelection here if it's needed
this.updateTrackSelection();
this.saveState(); // Save state after successful upload
} else {
console.error("Failed to upload MIDI file:", data.error);
}
})
.catch(error => {
console.error("Error uploading MIDI file:", error);
});
}
};
input.click();
};
nodeType.prototype.onNodeCreated = function() {
console.log("MIDI Extension: onNodeCreated called for MIDILoadAndExtract");
this.properties = this.properties || {};
this.properties.selectedNotes = []; // Initialize with an empty array
this.pianoScroll = 0;
this.scrollLeftIndicator = null;
this.scrollRightIndicator = null;
this.availableNotes = new Set();
this.addWidget("button", "Upload MIDI", "upload", () => {
this.uploadMIDI();
});
this.addWidget("button", "Refresh", "refresh", () => {
this.refreshMIDIData();
});
const notesWidget = this.widgets.find(widget => widget.name === "notes");
if (notesWidget) {
// Disable the widget
this.disableWidget(notesWidget);
// Hide the widget
notesWidget.hidden = true;
}
// Add event listeners for midi_file and track_selection widgets
const midiFileWidget = this.widgets.find(w => w.name === "midi_file");
if (midiFileWidget) {
midiFileWidget.callback = (value) => {
console.log('MIDI file changed to:', value);
this.updateTrackSelection();
this.saveState(); // Save state when MIDI file changes
};
}
const trackSelectionWidget = this.widgets.find(w => w.name === "track_selection");
if (trackSelectionWidget) {
trackSelectionWidget.callback = () => this.onTrackSelectionChange(trackSelectionWidget.value);
}
// Increase the node's height and width
const extraHeight = 80; // Piano height + margins
const pianoWidth = 1020; // Total width of the piano
this.size[1] += extraHeight;
this.size[0] = Math.max(this.size[0], pianoWidth); // Set width to at least piano width plus margins
// Ensure the node recomputes its size
this.setDirtyCanvas(true, true);
// Sync the notes widget initially
this.syncNotesWidget();
// Load saved state
this.loadSavedState();
// Set up a flag to indicate initial load
this.isInitialLoad = true;
// Instead of calling updateTrackSelection here, we'll set up a callback
// that will be called when the graph is available
this.onGraphAvailable = () => {
console.log('Graph available, updating track selection');
this.updateTrackSelection();
};
};
nodeType.prototype.loadSavedState = function() {
console.log(`loadSavedState called for node ${this.id}`);
const savedState = localStorage.getItem(`MIDILoadAndExtract_${this.id}`);
if (savedState) {
this.loadedState = JSON.parse(savedState);
console.log('Loaded saved state:', this.loadedState);
const midiFileWidget = this.widgets.find(w => w.name === "midi_file");
if (midiFileWidget && this.loadedState.midiFile) {
midiFileWidget.value = this.loadedState.midiFile;
console.log(`Set midiFile to: ${midiFileWidget.value}`);
}
}
};
nodeType.prototype.updateTrackSelection = async function() {
console.log(`updateTrackSelection called, isInitialLoad: ${this.isInitialLoad}`);
if (this.isUpdatingTrackSelection) {
console.log('Track selection update already in progress, skipping');
return;
}
this.isUpdatingTrackSelection = true;
try {
const midiFile = this.getCurrentMidiFile();
console.log(`Current MIDI file: ${midiFile}`);
if (!midiFile) {
console.warn('No MIDI file selected');
return;
}
const response = await fetch('/get_track_notes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ midi_file: midiFile })
});
const data = await response.json();
if (data.error) {
console.error('Error fetching track notes:', data.error);
return;
}
console.log('Received track data:', data);
const trackSelectionWidget = this.widgets.find(w => w.name === "track_selection");
if (trackSelectionWidget) {
// Update the options
trackSelectionWidget.options.values = data.tracks;
}
// Update available notes
this.availableNotes = new Set(data.all_notes.split(',').map(Number));
console.log(`Updated available notes: ${Array.from(this.availableNotes)}`);
// Apply loaded state if this is the initial load
if (this.isInitialLoad && this.loadedState) {
this.applyLoadedState();
this.isInitialLoad = false;
}
this.setDirtyCanvas(true, true);
console.log('Track selection update completed');
} catch (error) {
console.error('Error updating track selection:', error);
this.availableNotes = new Set(); // Reset to empty Set in case of error
} finally {
this.isUpdatingTrackSelection = false;
}
};
nodeType.prototype.refreshMIDIData = async function() {
console.log("Refreshing MIDI data");
const midiFile = this.getCurrentMidiFile();
const trackSelection = this.widgets.find(w => w.name === "track_selection").value;
if (!midiFile) {
console.warn('No MIDI file selected');
return;
}
try {
const response = await fetch('/refresh_midi_data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ midi_file: midiFile, track_selection: trackSelection })
});
const data = await response.json();
if (data.error) {
console.error('Error refreshing MIDI data:', data.error);
return;
}
console.log('Received refreshed MIDI data:', data);
// Update available tracks
const trackSelectionWidget = this.widgets.find(w => w.name === "track_selection");
if (trackSelectionWidget) {
trackSelectionWidget.options.values = data.tracks;
}
// Update available notes
this.availableNotes = new Set(data.all_notes.split(',').map(Number));
console.log(`Updated available notes: ${Array.from(this.availableNotes)}`);
// Filter out selected notes that are no longer available
if (this.properties.selectedNotes) {
this.properties.selectedNotes = this.properties.selectedNotes.filter(note => this.availableNotes.has(note));
} else {
this.properties.selectedNotes = [];
}
this.syncNotesWidget();
// Trigger redraw
this.setDirtyCanvas(true, true);
} catch (error) {
console.error('Error refreshing MIDI data:', error);
}
};
nodeType.prototype.applyLoadedState = function() {
if (this.loadedState) {
console.log('Applying loaded state:', this.loadedState);
const trackSelectionWidget = this.widgets.find(w => w.name === "track_selection");
if (trackSelectionWidget && this.loadedState.trackSelection) {
if (trackSelectionWidget.options.values.includes(this.loadedState.trackSelection)) {
trackSelectionWidget.value = this.loadedState.trackSelection;
console.log(`Set track selection to loaded value: ${trackSelectionWidget.value}`);
} else {
console.log(`Loaded track selection ${this.loadedState.trackSelection} is no longer valid`);
}
}
if (this.loadedState.selectedNotes) {
this.properties.selectedNotes = this.loadedState.selectedNotes.filter(note => this.availableNotes.has(note));
console.log(`Applied selected notes: ${this.properties.selectedNotes}`);
}
this.syncNotesWidget();
delete this.loadedState;
this.saveState(); // Save the applied state
}
};
nodeType.prototype.saveState = function() {
const state = {
midiFile: this.getCurrentMidiFile(),
trackSelection: this.widgets.find(w => w.name === "track_selection")?.value,
selectedNotes: this.properties.selectedNotes
};
console.log("Saving state:", state);
localStorage.setItem(`MIDILoadAndExtract_${this.id}`, JSON.stringify(state));
};
nodeType.prototype.syncNotesWidget = function() {
const notesWidget = this.widgets.find(widget => widget.name === "notes");
if (notesWidget) {
const newValue = this.properties.selectedNotes.sort((a, b) => a - b).join(", ");
if (notesWidget.value !== newValue) {
notesWidget.value = newValue;
// Trigger a change event on the widget
if (notesWidget.callback) {
notesWidget.callback(newValue);
}
}
}
// Ensure the node updates its internal state
this.onPropertyChanged('selectedNotes');
// Only call saveState if it's available
if (typeof this.saveState === 'function') {
this.saveState();
}
};
nodeType.prototype.onPropertyChanged = function(property) {
if (property === 'selectedNotes') {
// Trigger any necessary updates or recomputation
this.setDirtyCanvas(true, true);
}
};
// Save the original method
nodeType.prototype._originalOnDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function(ctx) {
// Call the original onDrawForeground method if it exists
if (typeof nodeType.prototype._originalOnDrawForeground === 'function') {
nodeType.prototype._originalOnDrawForeground.call(this, ctx);
}
if (this.flags.collapsed) return;
// Ensure availableNotes exists before drawing
if (!this.availableNotes) {
console.warn('availableNotes is undefined, initializing as empty Set');
this.availableNotes = new Set();
}
// Draw widgets that are not hidden
for (let w of this.widgets) {
if (w.hidden) {
continue; // Skip drawing hidden widgets
}
if (w.draw) {
w.draw(ctx, this, this.pos);
}
}
drawPiano(ctx, this);
};
// Save the original onMouseDown method
nodeType.prototype._originalOnMouseDown = nodeType.prototype.onMouseDown;
nodeType.prototype.onMouseDown = function(event, pos, graphcanvas) {
// Call the original onMouseDown method if it exists
if (typeof nodeType.prototype._originalOnMouseDown === 'function') {
nodeType.prototype._originalOnMouseDown.call(this, event, pos, graphcanvas);
}
const [x, y] = pos;
return handlePianoMouseDown(this, x, y);
};
nodeType.prototype.getCurrentMidiFile = function() {
const midiFileWidget = this.widgets.find(w => w.name === "midi_file");
return midiFileWidget ? midiFileWidget.value : null;
};
nodeType.prototype.onTrackSelectionChange = async function(selectedTrack) {
try {
const midiFile = this.getCurrentMidiFile();
const response = await fetch('/get_track_notes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ midi_file: midiFile })
});
const data = await response.json();
if (data.error) {
console.error('Error fetching track notes:', data.error);
return;
}
const notesInTrack = selectedTrack === "all"
? data.all_notes
: data.track_notes[selectedTrack.split(':')[0]];
this.availableNotes = new Set(notesInTrack.split(',').map(Number));
// Filter out selected notes that are no longer available
if (this.properties.selectedNotes) {
this.properties.selectedNotes = this.properties.selectedNotes.filter(note => this.availableNotes.has(note));
} else {
this.properties.selectedNotes = [];
}
this.syncNotesWidget();
// Trigger redraw
this.setDirtyCanvas(true, true);
} catch (error) {
console.error('Error updating piano notes:', error);
}
};
// Add this at the end of the function
const onAdded = app.graph.onNodeAdded;
app.graph.onNodeAdded = function(node) {
if (onAdded) {
onAdded.call(this, node);
}
if (node.type === "MIDILoadAndExtract" && node.onGraphLoaded) {
node.onGraphLoaded();
}
};
}
},
});
+183
View File
@@ -0,0 +1,183 @@
export function drawPiano(ctx, node) {
const margin = 10;
const visibleWidth = node.size[0] - 2 * margin;
const totalWidth = 1000; // Fixed total width of the piano
const height = 60;
const y = node.size[1] - height - margin;
ctx.save();
ctx.beginPath();
ctx.rect(margin, y, visibleWidth, height);
ctx.clip();
ctx.fillStyle = "rgba(0, 0, 255, 0.1)";
ctx.fillRect(margin, y, totalWidth, height);
const whiteKeyWidth = totalWidth / 52;
const blackKeyWidth = whiteKeyWidth * 0.65;
const blackKeyHeight = height * 0.6;
let xPos = margin - (node.pianoScroll || 0);
// Draw white keys
for (let i = 0; i < 88; i++) {
const midiNote = i + 21;
const noteInOctave = (midiNote - 12) % 12;
const isBlack = [1, 3, 6, 8, 10].includes(noteInOctave);
const isAvailable = node.availableNotes.has(midiNote);
if (!isBlack) {
ctx.fillStyle = isAvailable ? "white" : "lightgray";
ctx.fillRect(xPos, y, whiteKeyWidth, height);
ctx.strokeStyle = "black";
ctx.strokeRect(xPos, y, whiteKeyWidth, height);
if (node.properties.selectedNotes && node.properties.selectedNotes.includes(midiNote)) {
ctx.fillStyle = "green";
ctx.fillRect(xPos + 2, y + height - 10, whiteKeyWidth - 4, 8);
}
xPos += whiteKeyWidth;
}
}
// Draw black keys
xPos = margin - (node.pianoScroll || 0);
for (let i = 0; i < 88; i++) {
const midiNote = i + 21;
const noteInOctave = (midiNote - 12) % 12;
const isBlack = [1, 3, 6, 8, 10].includes(noteInOctave);
const isAvailable = node.availableNotes.has(midiNote);
if (isBlack) {
ctx.fillStyle = isAvailable ? "black" : "darkgray";
ctx.fillRect(xPos - blackKeyWidth / 2, y, blackKeyWidth, blackKeyHeight);
if (node.properties.selectedNotes && node.properties.selectedNotes.includes(midiNote)) {
ctx.fillStyle = "green";
ctx.fillRect(xPos - blackKeyWidth / 2 + 2, y + blackKeyHeight - 10, blackKeyWidth - 4, 8);
}
} else {
xPos += whiteKeyWidth;
}
}
drawScrollIndicators(ctx, node, margin, y, visibleWidth, totalWidth, height);
ctx.restore();
}
export function drawScrollIndicators(ctx, node, margin, y, visibleWidth, totalWidth, height) {
if (node.pianoScroll > 0) {
const indicatorWidth = 20;
const indicatorHeight = height;
ctx.fillStyle = "black";
ctx.fillRect(margin, y, indicatorWidth, indicatorHeight);
ctx.fillStyle = "rgba(50, 50, 50, 0.95)";
ctx.beginPath();
ctx.moveTo(margin + 10, y + 15);
ctx.lineTo(margin + 5, y + indicatorHeight / 2);
ctx.lineTo(margin + 10, y + indicatorHeight - 15);
ctx.fill();
node.scrollLeftIndicator = { x: margin, y, width: indicatorWidth, height: indicatorHeight };
}
if (node.pianoScroll < totalWidth - visibleWidth) {
const indicatorWidth = 20;
const indicatorHeight = height;
ctx.fillStyle = "rgba(10, 10, 10, 1)";
ctx.fillRect(node.size[0] - margin - indicatorWidth, y, indicatorWidth, indicatorHeight);
ctx.fillStyle = "rgba(50, 50, 50, 0.95)";
ctx.beginPath();
ctx.moveTo(node.size[0] - margin - indicatorWidth + 10, y + 15);
ctx.lineTo(node.size[0] - margin - 5, y + indicatorHeight / 2);
ctx.lineTo(node.size[0] - margin - indicatorWidth + 10, y + indicatorHeight - 15);
ctx.fill();
node.scrollRightIndicator = { x: node.size[0] - margin - indicatorWidth, y, width: indicatorWidth, height: indicatorHeight };
}
}
export function handlePianoMouseDown(node, x, y) {
const margin = 10;
const visibleWidth = node.size[0] - 2 * margin;
const totalWidth = 1000;
const height = 60;
const pianoY = node.size[1] - height - margin;
if (node.scrollLeftIndicator && x >= node.scrollLeftIndicator.x && x <= node.scrollLeftIndicator.x + node.scrollLeftIndicator.width &&
y >= node.scrollLeftIndicator.y && y <= node.scrollLeftIndicator.y + node.scrollLeftIndicator.height) {
node.pianoScroll = Math.max(0, (node.pianoScroll || 0) - 20);
return true;
}
if (node.scrollRightIndicator && x >= node.scrollRightIndicator.x && x <= node.scrollRightIndicator.x + node.scrollRightIndicator.width &&
y >= node.scrollRightIndicator.y && y <= node.scrollRightIndicator.y + node.scrollRightIndicator.height) {
const maxScroll = totalWidth - visibleWidth;
node.pianoScroll = Math.min(maxScroll, (node.pianoScroll || 0) + 20);
return true;
}
if (y >= pianoY && y <= pianoY + height) {
const whiteKeyWidth = totalWidth / 52;
const blackKeyWidth = whiteKeyWidth * 0.65;
const blackKeyHeight = height * 0.6;
let xPos = margin - (node.pianoScroll || 0);
let clickedNote = null;
for (let i = 0; i < 88; i++) {
const midiNote = i + 21;
const noteInOctave = (midiNote - 12) % 12;
const isBlack = [1, 3, 6, 8, 10].includes(noteInOctave);
if (isBlack) {
if (x >= xPos - blackKeyWidth / 2 && x < xPos + blackKeyWidth / 2 && y <= pianoY + blackKeyHeight) {
clickedNote = midiNote;
break;
}
} else {
xPos += whiteKeyWidth;
}
}
if (clickedNote === null) {
xPos = margin - (node.pianoScroll || 0);
for (let i = 0; i < 88; i++) {
const midiNote = i + 21;
const noteInOctave = (midiNote - 12) % 12;
const isBlack = [1, 3, 6, 8, 10].includes(noteInOctave);
if (!isBlack) {
if (x >= xPos && x < xPos + whiteKeyWidth) {
clickedNote = midiNote;
break;
}
xPos += whiteKeyWidth;
}
}
}
if (clickedNote !== null && node.availableNotes.has(clickedNote)) {
if (!node.properties.selectedNotes) {
node.properties.selectedNotes = [];
}
if (node.properties.selectedNotes.includes(clickedNote)) {
node.properties.selectedNotes = node.properties.selectedNotes.filter(n => n !== clickedNote);
} else {
node.properties.selectedNotes.push(clickedNote);
}
// Use requestAnimationFrame to ensure the update happens in the next frame
requestAnimationFrame(() => {
node.syncNotesWidget();
node.setDirtyCanvas(true, true);
});
}
return true;
}
return false;
}