Added flex, audio, midi, help system
This commit is contained in:
+259
-34
@@ -1,24 +1,92 @@
|
||||
from .node_configs import CombinedMeta
|
||||
from collections import OrderedDict
|
||||
|
||||
#allows for central management and inheritance of class variables for help documentation
|
||||
class RyanOnTheInside(metaclass=CombinedMeta):
|
||||
@classmethod
|
||||
def get_description(cls):
|
||||
|
||||
display_name = NODE_DISPLAY_NAME_MAPPINGS.get(cls.__name__, cls.__name__)
|
||||
footer = "For more information, visit [RyanOnTheInside GitHub](https://github.com/ryanontheinside)."
|
||||
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"
|
||||
|
||||
additional_info = OrderedDict()
|
||||
for c in cls.mro()[::-1]:
|
||||
if hasattr(c, 'ADDITIONAL_INFO'):
|
||||
info = c.ADDITIONAL_INFO.strip()
|
||||
|
||||
additional_info[c.__name__] = info
|
||||
|
||||
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,
|
||||
MaskMath,
|
||||
MaskRings,
|
||||
MaskWarp,
|
||||
#, MaskBlur, MaskThreshold, MaskTransform
|
||||
)
|
||||
from .nodes.masks.audio_masks import AudioControlledMaskMorph
|
||||
|
||||
from .nodes.audio.audio_nodes import (
|
||||
AudioSeparator,
|
||||
DownloadOpenUnmixModel,
|
||||
AudioFeatureVisualizer,
|
||||
FrequencyFilterCustom,
|
||||
FrequencyFilterPreset,
|
||||
AudioFilter,
|
||||
|
||||
)
|
||||
|
||||
from .nodes.flex.feature_extractors import(
|
||||
|
||||
AudioFeatureExtractor,
|
||||
TimeFeatureNode,
|
||||
DepthFeatureNode,
|
||||
ColorFeatureNode,
|
||||
BrightnessFeatureNode,
|
||||
MotionFeatureNode,
|
||||
)
|
||||
|
||||
from .nodes.flex.midi_feature_extractor import(
|
||||
MIDILoadAndExtract,
|
||||
)
|
||||
|
||||
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 (
|
||||
ParticleEmissionMask,
|
||||
#PulsatingParticleSystemMask,
|
||||
Vortex,
|
||||
GravityWell,
|
||||
ParticleEmitter,
|
||||
@@ -30,9 +98,56 @@ from .nodes.masks.particle_system_masks import (
|
||||
ParticleSpeedModulation,
|
||||
)
|
||||
|
||||
from .nodes.masks.utility_nodes import _mfc, TextMaskNode, MovingShape
|
||||
|
||||
|
||||
from .nodes.masks.utility_nodes import (
|
||||
_mfc,
|
||||
TextMaskNode,
|
||||
MovingShape,
|
||||
)
|
||||
|
||||
###images
|
||||
|
||||
from .nodes.images.image_utility_nodes import (
|
||||
DyeImage,
|
||||
)
|
||||
from .nodes.images.flex_images import (
|
||||
FlexImageEdgeDetect,
|
||||
FlexImagePosterize,
|
||||
FlexImageKaleidoscope,
|
||||
FlexImageBloom,
|
||||
FlexImageChromaticAberration,
|
||||
FlexImageGlitch,
|
||||
FlexImagePixelate,
|
||||
FlexImageColorGrade,
|
||||
FlexImageTiltShift,
|
||||
)
|
||||
|
||||
from .nodes.flex.feature_externals import (
|
||||
FeatureToWeightsStrategy,
|
||||
)
|
||||
|
||||
from .nodes.flex.feature_modulation import (
|
||||
FeatureMixer
|
||||
)
|
||||
|
||||
|
||||
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 = {
|
||||
|
||||
###temporal
|
||||
"MaskMorph": MaskMorph,
|
||||
"MaskTransform":MaskTransform,
|
||||
"MaskMath":MaskMath,
|
||||
@@ -40,13 +155,13 @@ NODE_CLASS_MAPPINGS = {
|
||||
"MaskWarp":MaskWarp,
|
||||
|
||||
|
||||
#optical flow
|
||||
"OpticalFlowMaskModulation": OpticalFlowMaskModulation,
|
||||
"OpticalFlowParticleSystem":OpticalFlowParticleSystem,
|
||||
#"OpticalFlowDirectionMask":OpticalFlowDirectionMask,
|
||||
"OpticalFlowDirectionMask":OpticalFlowDirectionMask,
|
||||
|
||||
|
||||
#particle simulation
|
||||
"ParticleEmissionMask":ParticleEmissionMask,
|
||||
#"PulsatingParticleSystemMask":PulsatingParticleSystemMask,
|
||||
"Vortex":Vortex,
|
||||
"GravityWell":GravityWell,
|
||||
"EmitterMovement":EmitterMovement,
|
||||
@@ -56,39 +171,149 @@ NODE_CLASS_MAPPINGS = {
|
||||
"ParticleColorModulation":ParticleColorModulation,
|
||||
"ParticleSizeModulation":ParticleSizeModulation,
|
||||
"ParticleSpeedModulation":ParticleSpeedModulation,
|
||||
# "AudioControlledMaskMorph": AudioControlledMaskMorph,
|
||||
|
||||
|
||||
#flex masks
|
||||
"FlexMaskMorph": FlexMaskMorph,
|
||||
"FlexMaskWarp": FlexMaskWarp,
|
||||
"FlexMaskTransform": FlexMaskTransform,
|
||||
"FlexMaskMath": FlexMaskMath,
|
||||
|
||||
#audio
|
||||
"AudioSeparator": AudioSeparator,
|
||||
"DownloadOpenUnmixModel":DownloadOpenUnmixModel,
|
||||
"AudioFeatureVisualizer":AudioFeatureVisualizer,
|
||||
"FrequencyFilterCustom": FrequencyFilterCustom,
|
||||
"FrequencyFilterPreset": FrequencyFilterPreset,
|
||||
"AudioFilter":AudioFilter,
|
||||
|
||||
#features
|
||||
"AudioFeatureExtractor":AudioFeatureExtractor,
|
||||
"MIDILoadAndExtract":MIDILoadAndExtract,
|
||||
"TimeFeatureNode":TimeFeatureNode,
|
||||
"DepthFeatureNode": DepthFeatureNode,
|
||||
"ColorFeatureNode":ColorFeatureNode,
|
||||
"BrightnessFeatureNode":BrightnessFeatureNode,
|
||||
"MotionFeatureNode":MotionFeatureNode,
|
||||
"FeatureToWeightsStrategy": FeatureToWeightsStrategy,
|
||||
"FeatureMixer":FeatureMixer,
|
||||
|
||||
'FlexImageEdgeDetect':FlexImageEdgeDetect,
|
||||
"FlexImagePosterize":FlexImagePosterize,
|
||||
"FlexImageKaleidoscope":FlexImageKaleidoscope,
|
||||
"FlexImageBloom":FlexImageBloom,
|
||||
"FlexImageChromaticAberration":FlexImageChromaticAberration,
|
||||
"FlexImageGlitch":FlexImageGlitch,
|
||||
"FlexImagePixelate":FlexImagePixelate,
|
||||
"FlexImageColorGrade":FlexImageColorGrade,
|
||||
"FlexImageTiltShift":FlexImageTiltShift,
|
||||
|
||||
|
||||
#garb
|
||||
"DyeImage": DyeImage,
|
||||
"MovingShape": MovingShape,
|
||||
"_mfc":_mfc,
|
||||
"TextMaskNode":TextMaskNode,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
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":"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",
|
||||
|
||||
# "PulsatingParticleSystemMask":"Pulsating Particle System Mask | RyanOnTheInside",
|
||||
"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",
|
||||
|
||||
# "AudioControlledMaskMorph": "Audio Controlled Mask Morph | RyanOnTheInside",
|
||||
"AudioMaskMorph":"Audio Mask Morph",
|
||||
"AudioMaskWarp": "Audio Mask Warp",
|
||||
"AudioMaskTransform":"Audio Mask Transform",
|
||||
"AudioMaskMath": "Audio Mask Math",
|
||||
"AudioSeparator": "Audio Separator",
|
||||
|
||||
"AudioFeatureVisualizer": "Audio Feature Visualizer" ,
|
||||
"Frequency Filter Custom": "Frequency Filter Custom",
|
||||
"Frequency Filter Preset": "Frequency Filter Preset",
|
||||
"AudioFilter": "Audio Filter",
|
||||
|
||||
"MovingShape": "Moving Shape | RyanOnTheInside",
|
||||
"TextMaskNode":"Text Mask Node | RyanOnTheInside"
|
||||
}
|
||||
"MIDILoadAndExtract": "MIDI Load & Feature Extract",
|
||||
"AudioFeatureExtractor": "Audio Feature & Extractor",
|
||||
"TimeFeatureNode": "Time Feature",
|
||||
"DepthFeatureNode":"Depth Feature",
|
||||
"BrightnessFeatureNode":"Brightness Feature",
|
||||
"MotionFeatureNode":"Motion Feature",
|
||||
|
||||
"MovingShape": "Moving Shape",
|
||||
"TextMaskNode":"Text Mask Node",
|
||||
|
||||
|
||||
"DyeImage" : "Dye Image",
|
||||
# "FlexImageAdjustment":"Flex Image Adjustment",
|
||||
# "FlexImageFilter":"Flex Image Filter",
|
||||
# "FlexImageBlend":"Flex Image Blend",
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
import re
|
||||
|
||||
suffix = " | RyanOnTheInside"
|
||||
|
||||
for node_name in NODE_CLASS_MAPPINGS.keys():
|
||||
if node_name not in NODE_DISPLAY_NAME_MAPPINGS:
|
||||
# Convert camelCase or snake_case to Title Case
|
||||
display_name = ' '.join(word.capitalize() for word in re.findall(r'[A-Z]?[a-z]+|[A-Z]+(?=[A-Z][a-z]|\d|\W|$)|\d+', node_name))
|
||||
else:
|
||||
display_name = NODE_DISPLAY_NAME_MAPPINGS[node_name]
|
||||
|
||||
# Add the suffix if it's not already present
|
||||
if not display_name.endswith(suffix):
|
||||
display_name += suffix
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS[node_name] = display_name
|
||||
|
||||
|
||||
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.
|
||||
#
|
||||
# Again credit to KJNodes and MTB nodes
|
||||
|
||||
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.
+718
@@ -0,0 +1,718 @@
|
||||
#NOTE: this abstraction allows for both the documentation to be centrally managed and inherited
|
||||
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
|
||||
|
||||
def add_node_config(node_name, config):
|
||||
NODE_CONFIGS[node_name] = config
|
||||
|
||||
NODE_CONFIGS = {}
|
||||
|
||||
add_node_config("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.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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)
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("OpticalFlowDirectionMask", {
|
||||
"TOP_DESCRIPTION": "***WORK IN PROGRESS***"
|
||||
})
|
||||
|
||||
add_node_config("OpticalFlowParticleSystem", {
|
||||
"TOP_DESCRIPTION": "***WORK IN PROGRESS***"
|
||||
})
|
||||
|
||||
add_node_config("MaskTransform", {
|
||||
"TOP_DESCRIPTION": "Applies geometric transformations to the mask over time.",
|
||||
"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.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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"
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("MaskWarp", {
|
||||
"TOP_DESCRIPTION": "Applies various warping effects to the mask, creating distortions and movement.",
|
||||
"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.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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."
|
||||
})
|
||||
|
||||
add_node_config("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)
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("AudioFeatureVisualizer", {
|
||||
"TOP_DESCRIPTION": "***WORK IN PROGESS*** 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
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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)
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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)
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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)
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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)
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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)
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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)
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("ParticleColorModulation", {
|
||||
"TOP_DESCRIPTION": "Modulates particle color over time.",
|
||||
"ADDITIONAL_INFO": """
|
||||
- `target_color`: Target color for particles at the end of the modulation (RGB tuple)
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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)
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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.
|
||||
|
||||
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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"
|
||||
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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"
|
||||
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("ColorFeatureNode", {
|
||||
"TOP_DESCRIPTION": "Extracts color-related features from video frames for mask modulation.",
|
||||
"ADDITIONAL_INFO": """
|
||||
- `feature_type`: Type of color feature to extract
|
||||
- Options: "dominant_color" (most prevalent color), "color_variance" (variation in colors), "saturation" (color intensity), "red_ratio" (proportion of red), "green_ratio" (proportion of green), "blue_ratio" (proportion of blue)
|
||||
|
||||
Analyzes the input video frames to extract the specified color-related feature. This feature can be used to modulate masks based on color information in the scene, creating effects that respond to color changes over time.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("BrightnessFeatureNode", {
|
||||
"TOP_DESCRIPTION": "Extracts brightness-related features from video frames for mask modulation.",
|
||||
"ADDITIONAL_INFO": """
|
||||
- `feature_type`: Type of brightness feature to extract
|
||||
- Options: "mean_brightness" (average brightness), "brightness_variance" (variation in brightness), "dark_ratio" (proportion of dark areas), "mid_ratio" (proportion of mid-tone areas), "bright_ratio" (proportion of bright areas)
|
||||
|
||||
Analyzes the input video frames to extract the specified brightness-related feature. This feature can be used to modulate masks based on lighting changes in the scene, allowing for effects that respond to overall brightness or specific tonal ranges.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("MotionFeatureNode", {
|
||||
"TOP_DESCRIPTION": "Extracts motion-related features from video frames for mask modulation.",
|
||||
"ADDITIONAL_INFO": """
|
||||
- `feature_type`: Type of motion feature to extract
|
||||
- Options: "mean_motion" (average motion), "max_motion" (peak motion), "motion_direction" (overall direction), "horizontal_motion" (left-right movement), "vertical_motion" (up-down movement), "motion_complexity" (intricacy of motion)
|
||||
- `flow_method`: Technique used for optical flow calculation
|
||||
- Options: "Farneback" (dense flow), "LucasKanade" (sparse flow), "PyramidalLK" (multi-scale sparse flow)
|
||||
- `flow_threshold`: Minimum motion magnitude to consider (0.0 to 10.0). Higher values ignore subtle movements.
|
||||
- `magnitude_threshold`: Relative threshold for motion magnitude (0.0 to 1.0). Higher values focus on areas of stronger motion.
|
||||
|
||||
Analyzes the input video frames to extract the specified motion-related feature using optical flow techniques. This feature can be used to modulate masks based on movement in the scene, creating effects that respond to motion intensity, direction, or complexity.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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"
|
||||
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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.
|
||||
|
||||
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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)
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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.
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("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)
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("_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
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("DownloadOpenUnmixModel", {
|
||||
"TOP_DESCRIPTION": "Downloads and loads Open Unmix models for audio classification",
|
||||
"ADDITIONAL_INFO": """
|
||||
-umxl (default) trained on private stems dataset of compressed stems. Note, that the weights are only licensed for non-commercial use (CC BY-NC-SA 4.0).
|
||||
|
||||
-umxhq trained on MUSDB18-HQ which comprises the same tracks as in MUSDB18 but un-compressed which yield in a full bandwidth of 22050 Hz.
|
||||
|
||||
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("FeatureMixer", {
|
||||
"TOP_DESCRIPTION": "Advanced feature modulation node for fine-tuning and shaping feature values.",
|
||||
"ADDITIONAL_INFO": """
|
||||
- `feature`: Input feature to be processed (FEATURE type)
|
||||
- `base_gain`: Overall amplification of the feature values (0.0 to 10.0). Higher values increase the overall intensity.
|
||||
- `floor`: Minimum value for the processed feature (0.0 to 1.0). Prevents values from going below this threshold.
|
||||
- `ceiling`: Maximum value for the processed feature (0.0 to 10.0). Caps values at this upper limit.
|
||||
- `peak_sharpness`: Sharpness of peaks in the feature curve (0.1 to 10.0). Higher values create more pronounced peaks.
|
||||
- `valley_sharpness`: Sharpness of valleys in the feature curve (0.1 to 10.0). Higher values create deeper valleys.
|
||||
- `attack`: Speed at which the envelope follower responds to increasing values (0.01 to 1.0). Lower values create slower attack.
|
||||
- `release`: Speed at which the envelope follower responds to decreasing values (0.01 to 1.0). Lower values create slower release.
|
||||
- `smoothing`: Amount of smoothing applied to the final curve (0.0 to 1.0). Higher values create smoother transitions.
|
||||
|
||||
This node provides extensive control over feature modulation, allowing for complex shaping of feature values over time. It combines multiple processing stages including gain, waveshaping, envelope following, and smoothing to create highly customized feature curves for mask modulation.
|
||||
|
||||
Outputs:
|
||||
- Processed FEATURE
|
||||
- Visualization of the processed feature (IMAGE type)
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("FeatureToWeightsStrategy", {
|
||||
"TOP_DESCRIPTION": "Converts a FEATURE input into a WEIGHTS_STRATEGY for use with IPAdapter nodes.",
|
||||
"ADDITIONAL_INFO": """
|
||||
- `feature`: Input feature to be converted (FEATURE type)
|
||||
|
||||
This node takes a FEATURE input and converts it into a WEIGHTS_STRATEGY that can be used with IPAdapter nodes. It creates a custom weights strategy based on the feature values for each frame.
|
||||
This node is particularly useful for creating dynamic, feature-driven animations with IPAdapter, where the strength of the adaptation can vary over time based on extracted features from audio, video, or other sources.
|
||||
|
||||
"""
|
||||
})
|
||||
|
||||
NODE_CONFIGS["FlexImageBase"] = {
|
||||
"BASE_DESCRIPTION": """
|
||||
|
||||
## Common Parameters
|
||||
- `images`: Input image sequence (IMAGE type)
|
||||
- `feature`: Feature used to modulate the effect (FEATURE type)
|
||||
- `feature_pipe`: Feature pipe containing frame information (FEATURE_PIPE type)
|
||||
- `strength`: Overall strength of the effect (0.0 to 1.0)
|
||||
- `feature_threshold`: Minimum feature value to apply the effect (0.0 to 1.0)
|
||||
- `modulate_param`: Parameter to be modulated by the feature
|
||||
- `modulation_mode`: How the feature modulates the parameter ("relative" or "absolute")
|
||||
"""
|
||||
}
|
||||
|
||||
add_node_config("FlexImagePosterize", {
|
||||
"TOP_DESCRIPTION": "Applies a posterization effect to the image, reducing the number of colors.",
|
||||
"ADDITIONAL_INFO": """
|
||||
- `max_levels`: Maximum number of color levels per channel (2 to 256)
|
||||
- `dither_strength`: Intensity of dithering effect (0.0 to 1.0)
|
||||
- `channel_separation`: Degree of separation between color channels (0.0 to 1.0)
|
||||
- `gamma`: Gamma correction applied before posterization (0.1 to 2.2)
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("FlexImageKaleidoscope", {
|
||||
"TOP_DESCRIPTION": "Creates a kaleidoscope effect by mirroring and rotating segments of the image.",
|
||||
"ADDITIONAL_INFO": """
|
||||
- `segments`: Number of mirror segments (2 to 32)
|
||||
- `center_x`: X-coordinate of the effect center (0.0 to 1.0)
|
||||
- `center_y`: Y-coordinate of the effect center (0.0 to 1.0)
|
||||
- `zoom`: Zoom factor for the effect (0.1 to 2.0)
|
||||
- `rotation`: Rotation angle of the effect (0.0 to 360.0 degrees)
|
||||
- `precession`: Rate of rotation change over time (-1.0 to 1.0)
|
||||
- `speed`: Speed of the effect animation (0.1 to 5.0)
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("FlexImageColorGrade", {
|
||||
"TOP_DESCRIPTION": "Applies color grading to the image using a Look-Up Table (LUT).",
|
||||
"ADDITIONAL_INFO": """
|
||||
- `intensity`: Strength of the color grading effect (0.0 to 1.0)
|
||||
- `mix`: Blend factor between original and graded image (0.0 to 1.0)
|
||||
- `lut_file`: Path to the LUT file (optional)
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("FlexImageGlitch", {
|
||||
"TOP_DESCRIPTION": "Creates a glitch effect by applying horizontal shifts and color channel separation.",
|
||||
"ADDITIONAL_INFO": """
|
||||
- `shift_amount`: Magnitude of horizontal shift (0.0 to 1.0)
|
||||
- `scan_lines`: Number of scan lines to add (0 to 100)
|
||||
- `color_shift`: Amount of color channel separation (0.0 to 1.0)
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("FlexImageChromaticAberration", {
|
||||
"TOP_DESCRIPTION": "Simulates chromatic aberration by shifting color channels.",
|
||||
"ADDITIONAL_INFO": """
|
||||
- `shift_amount`: Magnitude of color channel shift (0.0 to 0.1)
|
||||
- `angle`: Angle of the shift effect (0.0 to 360.0 degrees)
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("FlexImagePixelate", {
|
||||
"TOP_DESCRIPTION": "Applies a pixelation effect to the image.",
|
||||
"ADDITIONAL_INFO": """
|
||||
- `pixel_size`: Size of each pixelated block (1 to 100 pixels)
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("FlexImageBloom", {
|
||||
"TOP_DESCRIPTION": "Adds a bloom effect to bright areas of the image.",
|
||||
"ADDITIONAL_INFO": """
|
||||
- `threshold`: Brightness threshold for the bloom effect (0.0 to 1.0)
|
||||
- `blur_amount`: Amount of blur applied to the bloom (0.0 to 50.0)
|
||||
- `intensity`: Strength of the bloom effect (0.0 to 1.0)
|
||||
"""
|
||||
})
|
||||
|
||||
add_node_config("FlexImageTiltShift", {
|
||||
"TOP_DESCRIPTION": "Creates a tilt-shift effect, simulating a shallow depth of field.",
|
||||
"ADDITIONAL_INFO": """
|
||||
- `blur_amount`: Strength of the blur effect (0.0 to 50.0)
|
||||
- `focus_position_x`: X-coordinate of the focus center (0.0 to 1.0)
|
||||
- `focus_position_y`: Y-coordinate of the focus center (0.0 to 1.0)
|
||||
- `focus_width`: Width of the focus area (0.0 to 1.0)
|
||||
- `focus_height`: Height of the focus area (0.0 to 1.0)
|
||||
- `focus_shape`: Shape of the focus area ("rectangle" or "ellipse")
|
||||
"""
|
||||
})
|
||||
@@ -0,0 +1,321 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
from scipy.ndimage import zoom
|
||||
from .audio_utils import AudioVisualizer
|
||||
from scipy import signal
|
||||
import comfy.model_management as mm
|
||||
import folder_paths
|
||||
import os
|
||||
from ... import RyanOnTheInside
|
||||
from ..flex.feature_pipe import FeaturePipe
|
||||
from ... import RyanOnTheInside
|
||||
|
||||
|
||||
|
||||
class AudioNodeBase(RyanOnTheInside):
|
||||
CATEGORY= "RyanOnTheInside/Audio"
|
||||
|
||||
class DownloadOpenUnmixModel(AudioNodeBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"model_name": (["umxl", "umxhq"], {"default": "umxl"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("OPEN_UNMIX_MODEL",)
|
||||
FUNCTION = "download_and_load_model"
|
||||
CATEGORY = "RyanOnTheInside/Audio"
|
||||
|
||||
def download_and_load_model(self, model_name):
|
||||
device = mm.get_torch_device()
|
||||
download_path = os.path.join(folder_paths.models_dir, "openunmix")
|
||||
os.makedirs(download_path, exist_ok=True)
|
||||
|
||||
model_file = f"{model_name}.pth"
|
||||
model_path = os.path.join(download_path, model_file)
|
||||
|
||||
if not os.path.exists(model_path):
|
||||
print(f"Downloading {model_name} model...")
|
||||
separator = torch.hub.load('sigsep/open-unmix-pytorch', model_name, device='cpu')
|
||||
torch.save(separator.state_dict(), model_path)
|
||||
print(f"Model saved to: {model_path}")
|
||||
else:
|
||||
print(f"Loading model from: {model_path}")
|
||||
separator = torch.hub.load('sigsep/open-unmix-pytorch', model_name, device='cpu')
|
||||
separator.load_state_dict(torch.load(model_path, map_location='cpu'))
|
||||
|
||||
separator = separator.to(device)
|
||||
separator.eval()
|
||||
|
||||
return (separator,)
|
||||
|
||||
class AudioSeparator(AudioNodeBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"model": ("OPEN_UNMIX_MODEL",),
|
||||
"audio": ("AUDIO",),
|
||||
"video_frames": ("IMAGE",),
|
||||
"frame_rate": ("FLOAT", {"default": 30, "min": 0.1, "max": 120, "step": 0.1}),
|
||||
}
|
||||
}
|
||||
|
||||
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 process_audio(self, model, audio, video_frames, frame_rate):
|
||||
waveform = audio['waveform']
|
||||
sample_rate = audio['sample_rate']
|
||||
|
||||
num_frames, height, width, _ = video_frames.shape
|
||||
|
||||
if waveform.dim() == 3:
|
||||
waveform = waveform.squeeze(0)
|
||||
if waveform.dim() == 1:
|
||||
waveform = waveform.unsqueeze(0) # Add channel dimension if mono
|
||||
if waveform.shape[0] != 2:
|
||||
waveform = waveform.repeat(2, 1) # Duplicate mono to stereo if necessary
|
||||
|
||||
waveform = waveform.unsqueeze(0)
|
||||
|
||||
# Determine the device
|
||||
device = next(model.parameters()).device
|
||||
waveform = waveform.to(device)
|
||||
|
||||
estimates = model(waveform)
|
||||
|
||||
# Create isolated audio objects for each target
|
||||
isolated_audio = {}
|
||||
target_indices = {'drums': 1, 'vocals': 0, 'bass': 2, 'other': 3} # Corrected indices
|
||||
for target, index in target_indices.items():
|
||||
target_waveform = estimates[:, index, :, :] # Shape: (1, 2, num_samples)
|
||||
|
||||
isolated_audio[target] = {
|
||||
'waveform': target_waveform.cpu(), # Move back to CPU
|
||||
'sample_rate': sample_rate,
|
||||
'frame_rate': frame_rate
|
||||
}
|
||||
|
||||
# Create FeaturePipe
|
||||
feature_pipe = FeaturePipe(frame_rate, video_frames)
|
||||
|
||||
return (
|
||||
audio,
|
||||
isolated_audio['drums'],
|
||||
isolated_audio['vocals'],
|
||||
isolated_audio['bass'],
|
||||
isolated_audio['other'],
|
||||
feature_pipe,
|
||||
)
|
||||
|
||||
class AudioFilter(AudioNodeBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"audio": ("AUDIO",),
|
||||
"filters": ("FREQUENCY_FILTER",)
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
sample_rate = audio['sample_rate']
|
||||
|
||||
filtered_channels = []
|
||||
for channel in audio_np:
|
||||
filtered_channel = channel
|
||||
for filter_params in filters:
|
||||
filtered_channel = self.apply_single_filter(filtered_channel, filter_params, sample_rate)
|
||||
filtered_channels.append(filtered_channel)
|
||||
|
||||
filtered_audio = np.stack(filtered_channels)
|
||||
|
||||
max_val = np.max(np.abs(filtered_audio))
|
||||
if max_val > 1.0:
|
||||
filtered_audio = filtered_audio / max_val
|
||||
|
||||
filtered_tensor = torch.from_numpy(filtered_audio).unsqueeze(0).float()
|
||||
|
||||
return ({"waveform": filtered_tensor, "sample_rate": sample_rate},)
|
||||
|
||||
def apply_single_filter(self, audio, filter_params, sample_rate):
|
||||
filter_type = filter_params['type']
|
||||
order = filter_params['order']
|
||||
cutoff = filter_params['cutoff']
|
||||
|
||||
if filter_type == 'lowpass':
|
||||
b, a = signal.butter(order, cutoff, btype='low', fs=sample_rate)
|
||||
elif filter_type == 'highpass':
|
||||
b, a = signal.butter(order, cutoff, btype='high', fs=sample_rate)
|
||||
elif filter_type == 'bandpass':
|
||||
low = cutoff - 50
|
||||
high = cutoff + 50
|
||||
b, a = signal.butter(order, [low, high], btype='band', fs=sample_rate)
|
||||
else:
|
||||
raise ValueError(f"Unsupported filter type: {filter_type}")
|
||||
|
||||
return signal.lfilter(b, a, audio)
|
||||
|
||||
class FrequencyFilterPreset(AudioNodeBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"preset": ([
|
||||
"isolate_kick_drum",
|
||||
"isolate_snare_drum",
|
||||
"isolate_hi_hat",
|
||||
"isolate_bass",
|
||||
"isolate_vocals",
|
||||
"remove_rumble",
|
||||
"brighten_mix",
|
||||
"remove_hiss"
|
||||
],),
|
||||
},
|
||||
"optional": {
|
||||
"previous_filter": ("FREQUENCY_FILTER",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("FREQUENCY_FILTER",)
|
||||
FUNCTION = "create_preset_filter_chain"
|
||||
|
||||
def create_preset_filter_chain(self, preset, previous_filter=None):
|
||||
new_filters = self.get_preset_filters(preset)
|
||||
|
||||
if previous_filter:
|
||||
if isinstance(previous_filter, list):
|
||||
return (previous_filter + new_filters,)
|
||||
else:
|
||||
return ([previous_filter] + new_filters,)
|
||||
else:
|
||||
return (new_filters,)
|
||||
|
||||
def get_preset_filters(self, preset):
|
||||
if preset == "isolate_kick_drum":
|
||||
return [
|
||||
{"type": "highpass", "order": 2, "cutoff": 30},
|
||||
{"type": "lowpass", "order": 4, "cutoff": 200}
|
||||
]
|
||||
elif preset == "isolate_snare_drum":
|
||||
return [
|
||||
{"type": "bandpass", "order": 4, "cutoff": 200},
|
||||
{"type": "bandpass", "order": 4, "cutoff": 5000}
|
||||
]
|
||||
elif preset == "isolate_hi_hat":
|
||||
return [
|
||||
{"type": "highpass", "order": 4, "cutoff": 8000}
|
||||
]
|
||||
elif preset == "isolate_bass":
|
||||
return [
|
||||
{"type": "lowpass", "order": 4, "cutoff": 250}
|
||||
]
|
||||
elif preset == "isolate_vocals":
|
||||
return [
|
||||
{"type": "bandpass", "order": 2, "cutoff": 300},
|
||||
{"type": "bandpass", "order": 2, "cutoff": 4000}
|
||||
]
|
||||
elif preset == "remove_rumble":
|
||||
return [
|
||||
{"type": "highpass", "order": 4, "cutoff": 40}
|
||||
]
|
||||
elif preset == "brighten_mix":
|
||||
return [
|
||||
{"type": "highpass", "order": 2, "cutoff": 6000}
|
||||
]
|
||||
elif preset == "remove_hiss":
|
||||
return [
|
||||
{"type": "lowpass", "order": 6, "cutoff": 7500}
|
||||
]
|
||||
else:
|
||||
raise ValueError(f"Unknown preset: {preset}")
|
||||
|
||||
class FrequencyFilterCustom(AudioNodeBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"filter_type": (["lowpass", "highpass", "bandpass"],),
|
||||
"order": ("INT", {"default": 4, "min": 1, "max": 10, "step": 1}),
|
||||
"cutoff": ("FLOAT", {"default": 1000, "min": 20, "max": 20000, "step": 1}),
|
||||
},
|
||||
"optional": {
|
||||
"previous_filter": ("FREQUENCY_FILTER",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("FREQUENCY_FILTER",)
|
||||
FUNCTION = "create_filter"
|
||||
|
||||
def create_filter(self, filter_type, order, cutoff, previous_filter=None):
|
||||
filter_params = {
|
||||
"type": filter_type,
|
||||
"order": order,
|
||||
"cutoff": cutoff,
|
||||
}
|
||||
|
||||
if previous_filter:
|
||||
if isinstance(previous_filter, list):
|
||||
previous_filter.append(filter_params)
|
||||
return (previous_filter,)
|
||||
else:
|
||||
return ([previous_filter, filter_params],)
|
||||
else:
|
||||
return ([filter_params],)
|
||||
|
||||
class AudioFeatureVisualizer(AudioNodeBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"audio": ("AUDIO",),
|
||||
"video_frames": ("IMAGE",),
|
||||
"visualization_type": ([
|
||||
"waveform",
|
||||
"spectrogram",
|
||||
"mfcc",
|
||||
"chroma",
|
||||
"tonnetz",
|
||||
"spectral_centroid"
|
||||
],),
|
||||
"frame_rate": ("FLOAT", {"default": 30, "min": 0.1, "max": 120, "step": 0.1}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "visualize_audio_feature"
|
||||
CATEGORY = "audio"
|
||||
|
||||
def visualize_audio_feature(self, audio, video_frames, visualization_type, frame_rate):
|
||||
num_frames, height, width, _ = video_frames.shape
|
||||
|
||||
visualizer = AudioVisualizer(audio, num_frames, height, width, frame_rate)
|
||||
|
||||
if visualization_type == "waveform":
|
||||
mask = visualizer.create_waveform()
|
||||
elif visualization_type == "spectrogram":
|
||||
mask = visualizer.create_spectrogram()
|
||||
elif visualization_type == "mfcc":
|
||||
mask = visualizer.create_mfcc()
|
||||
elif visualization_type == "chroma":
|
||||
mask = visualizer.create_chroma()
|
||||
elif visualization_type == "tonnetz":
|
||||
mask = visualizer.create_tonnetz()
|
||||
elif visualization_type == "spectral_centroid":
|
||||
mask = visualizer.create_spectral_centroid()
|
||||
else:
|
||||
raise ValueError(f"Unsupported visualization type: {visualization_type}")
|
||||
|
||||
|
||||
return (mask,)
|
||||
@@ -0,0 +1,188 @@
|
||||
import numpy as np
|
||||
import torch
|
||||
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
|
||||
self.sample_rate = audio['sample_rate']
|
||||
self.num_frames = num_frames
|
||||
self.height = height
|
||||
self.width = width
|
||||
self.frame_rate = frame_rate
|
||||
|
||||
self.audio_duration = len(self.audio) / self.sample_rate
|
||||
self.frame_duration = 1 / self.frame_rate if self.frame_rate > 0 else self.audio_duration / self.num_frames
|
||||
|
||||
def _normalize(self, data):
|
||||
return (data - data.min()) / (data.max() - data.min())
|
||||
|
||||
def _enhance_contrast(self, data, power=0.3):
|
||||
return np.power(data, power)
|
||||
|
||||
def _resize(self, data, new_width, new_height):
|
||||
return cv2.resize(data, (new_width, new_height), interpolation=cv2.INTER_LINEAR)
|
||||
|
||||
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]
|
||||
|
||||
class AudioVisualizer(BaseAudioProcessor):
|
||||
def create_spectrogram(self):
|
||||
|
||||
frames = []
|
||||
for i in range(self.num_frames):
|
||||
audio_frame = self._get_audio_frame(i)
|
||||
|
||||
n_fft = min(2048, len(audio_frame))
|
||||
S = librosa.stft(audio_frame, n_fft=n_fft)
|
||||
S_db = librosa.amplitude_to_db(np.abs(S), ref=np.max)
|
||||
|
||||
S_db_normalized = self._normalize(S_db)
|
||||
S_db_enhanced = self._enhance_contrast(S_db_normalized)
|
||||
|
||||
S_db_resized = self._resize(S_db_enhanced, self.width, self.height)
|
||||
frames.append(S_db_resized)
|
||||
|
||||
frames = np.stack(frames, axis=0)
|
||||
|
||||
return torch.from_numpy(frames).float()
|
||||
|
||||
def create_waveform(self):
|
||||
|
||||
frames = []
|
||||
for i in range(self.num_frames):
|
||||
audio_frame = self._get_audio_frame(i)
|
||||
|
||||
plt.figure(figsize=(self.width / 100, self.height / 100), dpi=100)
|
||||
plt.plot(audio_frame)
|
||||
plt.axis('off')
|
||||
plt.tight_layout(pad=0)
|
||||
|
||||
plt.gcf().canvas.draw()
|
||||
frame = np.frombuffer(plt.gcf().canvas.tostring_rgb(), dtype=np.uint8)
|
||||
frame = frame.reshape(self.height, self.width, 3)
|
||||
frames.append(frame)
|
||||
plt.close()
|
||||
|
||||
frames = np.stack(frames, axis=0)
|
||||
|
||||
return torch.from_numpy(frames).float() / 255.0
|
||||
|
||||
def create_mfcc(self):
|
||||
|
||||
frames = []
|
||||
for i in range(self.num_frames):
|
||||
audio_frame = self._get_audio_frame(i)
|
||||
|
||||
mfccs = librosa.feature.mfcc(y=audio_frame, sr=self.sample_rate, n_mfcc=self.height)
|
||||
mfccs_normalized = self._normalize(mfccs)
|
||||
mfccs_enhanced = self._enhance_contrast(mfccs_normalized)
|
||||
|
||||
mfccs_resized = self._resize(mfccs_enhanced, self.width, self.height)
|
||||
frames.append(mfccs_resized)
|
||||
|
||||
frames = np.stack(frames, axis=0)
|
||||
|
||||
return torch.from_numpy(frames).float()
|
||||
|
||||
def create_chroma(self):
|
||||
|
||||
frames = []
|
||||
for i in range(self.num_frames):
|
||||
audio_frame = self._get_audio_frame(i)
|
||||
|
||||
chroma = librosa.feature.chroma_stft(y=audio_frame, sr=self.sample_rate)
|
||||
chroma_normalized = self._normalize(chroma)
|
||||
chroma_enhanced = self._enhance_contrast(chroma_normalized)
|
||||
|
||||
chroma_resized = self._resize(chroma_enhanced, self.width, self.height)
|
||||
frames.append(chroma_resized)
|
||||
|
||||
frames = np.stack(frames, axis=0)
|
||||
|
||||
return torch.from_numpy(frames).float()
|
||||
|
||||
def create_tonnetz(self):
|
||||
|
||||
frames = []
|
||||
for i in range(self.num_frames):
|
||||
audio_frame = self._get_audio_frame(i)
|
||||
|
||||
tonnetz = librosa.feature.tonnetz(y=audio_frame, sr=self.sample_rate)
|
||||
tonnetz_normalized = self._normalize(tonnetz)
|
||||
tonnetz_enhanced = self._enhance_contrast(tonnetz_normalized)
|
||||
|
||||
tonnetz_resized = self._resize(tonnetz_enhanced, self.width, self.height)
|
||||
frames.append(tonnetz_resized)
|
||||
|
||||
frames = np.stack(frames, axis=0)
|
||||
|
||||
return torch.from_numpy(frames).float()
|
||||
|
||||
def create_spectral_centroid(self):
|
||||
|
||||
frames = []
|
||||
for i in range(self.num_frames):
|
||||
audio_frame = self._get_audio_frame(i)
|
||||
|
||||
centroid = librosa.feature.spectral_centroid(y=audio_frame, sr=self.sample_rate)
|
||||
centroid_normalized = self._normalize(centroid)
|
||||
centroid_enhanced = self._enhance_contrast(centroid_normalized)
|
||||
|
||||
centroid_resized = self._resize(centroid_enhanced, self.width, self.height)
|
||||
frames.append(centroid_resized)
|
||||
|
||||
frames = np.stack(frames, axis=0)
|
||||
|
||||
return torch.from_numpy(frames).float()
|
||||
|
||||
class AudioFeatureExtractor(BaseAudioProcessor):
|
||||
def __init__(self, audio, num_frames, frame_rate, feature_type='amplitude_envelope'):
|
||||
super().__init__(audio, num_frames, None, None, frame_rate)
|
||||
self.feature_type = feature_type
|
||||
|
||||
def extract(self):
|
||||
if self.feature_type == 'amplitude_envelope':
|
||||
return self._amplitude_envelope()
|
||||
elif self.feature_type == 'rms_energy':
|
||||
return self._rms_energy()
|
||||
elif self.feature_type == 'spectral_centroid':
|
||||
return self._spectral_centroid()
|
||||
elif self.feature_type == 'onset_detection':
|
||||
return self._onset_detection()
|
||||
elif self.feature_type == 'chroma_features':
|
||||
return self._chroma_features()
|
||||
else:
|
||||
raise ValueError("Unsupported feature type")
|
||||
|
||||
#TODO HANDLE NO MASKS
|
||||
#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)])
|
||||
|
||||
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)])
|
||||
@@ -0,0 +1 @@
|
||||
#TODO
|
||||
@@ -0,0 +1 @@
|
||||
#TODO
|
||||
@@ -0,0 +1,38 @@
|
||||
from ... import RyanOnTheInside
|
||||
|
||||
class FlexExternalModulator(RyanOnTheInside):
|
||||
CATEGORY = "RyanOnTheInside/FlexExternalMod"
|
||||
|
||||
class FeatureToWeightsStrategy(FlexExternalModulator):
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"feature": ("FEATURE",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("WEIGHTS_STRATEGY",)
|
||||
RETURN_NAMES = ("WEIGHTS_STRATEGY",)
|
||||
FUNCTION = "convert"
|
||||
|
||||
def convert(self, feature):
|
||||
frames = feature.frame_count
|
||||
values = [feature.get_value_at_frame(i) for i in range(frames)]
|
||||
|
||||
weights_str = ", ".join(map(lambda x: f"{x:.8f}", values))
|
||||
|
||||
weights_strategy = {
|
||||
"weights": weights_str,
|
||||
"timing": "custom",
|
||||
"frames": frames,
|
||||
"start_frame": 0,
|
||||
"end_frame": frames,
|
||||
"add_starting_frames": 0,
|
||||
"add_ending_frames": 0,
|
||||
"method": "full batch",
|
||||
"frame_count": frames,
|
||||
}
|
||||
|
||||
|
||||
return (weights_strategy,)
|
||||
@@ -0,0 +1,146 @@
|
||||
from .feature_pipe import FeaturePipe
|
||||
from ... import RyanOnTheInside
|
||||
from .features import AudioFeature, TimeFeature, DepthFeature, ColorFeature, BrightnessFeature, MotionFeature
|
||||
|
||||
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)
|
||||
feature.extract()
|
||||
return (feature, feature_pipe)
|
||||
|
||||
|
||||
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)
|
||||
time_feature.extract()
|
||||
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)
|
||||
depth_feature.extract()
|
||||
return (depth_feature, feature_pipe)
|
||||
|
||||
class ColorFeatureNode(FirstFeature):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
**super().INPUT_TYPES(),
|
||||
"required": {
|
||||
**super().INPUT_TYPES()["required"],
|
||||
"feature_type": (["dominant_color", "color_variance", "saturation", "red_ratio", "green_ratio", "blue_ratio"],),
|
||||
}
|
||||
}
|
||||
|
||||
def create_feature(self, video_frames, frame_rate, feature_type):
|
||||
feature_pipe = FeaturePipe(frame_rate, video_frames)
|
||||
color_feature = ColorFeature("color_feature", feature_pipe.frame_rate, feature_pipe.frame_count, video_frames, feature_type)
|
||||
color_feature.extract()
|
||||
return (color_feature, feature_pipe)
|
||||
|
||||
class BrightnessFeatureNode(FirstFeature):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
**super().INPUT_TYPES(),
|
||||
"required": {
|
||||
**super().INPUT_TYPES()["required"],
|
||||
"feature_type": (["mean_brightness", "brightness_variance", "dark_ratio", "mid_ratio", "bright_ratio"],),
|
||||
}
|
||||
}
|
||||
|
||||
def create_feature(self, video_frames, frame_rate, feature_type):
|
||||
feature_pipe = FeaturePipe(frame_rate, video_frames)
|
||||
brightness_feature = BrightnessFeature("brightness_feature", feature_pipe.frame_rate, feature_pipe.frame_count, video_frames, feature_type)
|
||||
brightness_feature.extract()
|
||||
return (brightness_feature, feature_pipe)
|
||||
|
||||
class MotionFeatureNode(FirstFeature):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
**super().INPUT_TYPES(),
|
||||
"required": {
|
||||
**super().INPUT_TYPES()["required"],
|
||||
"feature_type": (["mean_motion", "max_motion", "motion_direction", "horizontal_motion", "vertical_motion", "motion_complexity"],),
|
||||
"flow_method": (["Farneback", "LucasKanade", "PyramidalLK"],),
|
||||
"flow_threshold": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.1}),
|
||||
"magnitude_threshold": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("FEATURE", "FEATURE_PIPE")
|
||||
FUNCTION = "create_feature"
|
||||
|
||||
def create_feature(self, video_frames, frame_rate, feature_type, flow_method, flow_threshold, magnitude_threshold):
|
||||
feature_pipe = FeaturePipe(frame_rate, video_frames)
|
||||
motion_feature = MotionFeature(
|
||||
"motion_feature",
|
||||
feature_pipe.frame_rate,
|
||||
feature_pipe.frame_count,
|
||||
video_frames,
|
||||
feature_type,
|
||||
flow_method,
|
||||
flow_threshold,
|
||||
magnitude_threshold
|
||||
)
|
||||
motion_feature.extract()
|
||||
return (motion_feature, feature_pipe)
|
||||
@@ -0,0 +1,173 @@
|
||||
from ... import RyanOnTheInside
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import torch
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
|
||||
class FeatureModulationBase(RyanOnTheInside):
|
||||
CATEGORY = "RyanOnTheInside/FlexFeatures"
|
||||
FUNCTION = "modulate"
|
||||
|
||||
def visualize(self, feature, width=1920, height=1080):
|
||||
values = [feature.get_value_at_frame(i) for i in range(feature.frame_count)]
|
||||
frames = len(values)
|
||||
|
||||
plt.figure(figsize=(width/100, height/100), dpi=100)
|
||||
plt.style.use('dark_background')
|
||||
|
||||
plt.plot(values, color='dodgerblue', linewidth=2)
|
||||
|
||||
plt.xlabel('Frame', color='white', fontsize=14)
|
||||
plt.ylabel('Value', color='white', fontsize=14)
|
||||
|
||||
plt.grid(True, linestyle='--', alpha=0.3, color='gray')
|
||||
|
||||
plt.tick_params(axis='both', colors='white', labelsize=12)
|
||||
|
||||
max_ticks = 10
|
||||
step = max(1, frames // max_ticks)
|
||||
x_ticks = range(0, frames, step)
|
||||
plt.xticks(x_ticks, [str(x) for x in x_ticks])
|
||||
|
||||
y_min, y_max = min(values), max(values)
|
||||
y_range = y_max - y_min
|
||||
plt.ylim(y_min - 0.05*y_range, y_max + 0.05*y_range)
|
||||
|
||||
plt.gca().spines['top'].set_visible(False)
|
||||
plt.gca().spines['right'].set_visible(False)
|
||||
plt.gca().spines['bottom'].set_color('white')
|
||||
plt.gca().spines['left'].set_color('white')
|
||||
|
||||
plt.title(f'Feature: {feature.name}', color='white', fontsize=16)
|
||||
|
||||
plt.tight_layout(pad=0.5)
|
||||
|
||||
buf = BytesIO()
|
||||
plt.savefig(buf, format='png', facecolor='black', edgecolor='none')
|
||||
buf.seek(0)
|
||||
|
||||
img = Image.open(buf)
|
||||
img_array = np.array(img)
|
||||
img_tensor = torch.from_numpy(img_array).float() / 255.0
|
||||
if img_tensor.dim() == 3:
|
||||
img_tensor = img_tensor.unsqueeze(0)
|
||||
|
||||
plt.close()
|
||||
buf.close()
|
||||
|
||||
return img_tensor
|
||||
|
||||
class FeatureMixer(FeatureModulationBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"feature": ("FEATURE",),
|
||||
"base_gain": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
|
||||
"floor": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"ceiling": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
|
||||
"peak_sharpness": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1}),
|
||||
"valley_sharpness": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1}),
|
||||
"attack": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 1.0, "step": 0.01}),
|
||||
"release": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 1.0, "step": 0.01}),
|
||||
"smoothing": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"feature_threshold": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"rise_detection_threshold": ("FLOAT", {"default": 0.3, "min": 0.0, "max": 1.0, "step": 0.05}),
|
||||
"rise_smoothing_factor": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 2.0, "step": 0.05}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("FEATURE", "IMAGE")
|
||||
RETURN_NAMES = ("FEATURE", "FEATURE_VISUALIZATION")
|
||||
FUNCTION = "modulate"
|
||||
|
||||
def modulate(self, feature, base_gain, floor, ceiling, peak_sharpness, valley_sharpness, attack, release, smoothing, feature_threshold, rise_detection_threshold, rise_smoothing_factor):
|
||||
values = [feature.get_value_at_frame(i) for i in range(feature.frame_count)]
|
||||
|
||||
# feature threshold
|
||||
values = [v if v >= feature_threshold else 0 for v in values]
|
||||
|
||||
# 0-1 range
|
||||
min_val, max_val = min(values), max(values)
|
||||
if min_val == max_val:
|
||||
normalized = [0 for _ in values] # All values are the same, normalize to 0
|
||||
else:
|
||||
normalized = [(v - min_val) / (max_val - min_val) for v in values]
|
||||
|
||||
# multiply gain
|
||||
gained = [v * base_gain for v in normalized]
|
||||
|
||||
|
||||
def waveshape(v):
|
||||
return v**peak_sharpness if v > 0.5 else 1 - (1-v)**valley_sharpness
|
||||
waveshaped = [waveshape(v) for v in gained]
|
||||
|
||||
|
||||
def apply_envelope(values, attack, release):
|
||||
envelope = []
|
||||
current = values[0]
|
||||
for v in values:
|
||||
if v > current:
|
||||
current += (v - current) * attack
|
||||
else:
|
||||
current += (v - current) * release
|
||||
envelope.append(current)
|
||||
return envelope
|
||||
enveloped = apply_envelope(waveshaped, attack, release)
|
||||
|
||||
|
||||
def smooth_values(values, smoothing_factor):
|
||||
smoothed = values.copy()
|
||||
for i in range(1, len(values)):
|
||||
smoothed[i] = smoothed[i-1] + (values[i] - smoothed[i-1]) * (1 - smoothing_factor)
|
||||
return smoothed
|
||||
smoothed = smooth_values(enveloped, smoothing)
|
||||
|
||||
# try to do look ahead
|
||||
adjusted = self.apply_rise_time_adjustment(smoothed, rise_detection_threshold, rise_smoothing_factor)
|
||||
|
||||
# chop
|
||||
final_values = [max(floor, min(ceiling, v)) for v in adjusted]
|
||||
|
||||
#we create a new feature with our values and override extract method
|
||||
# NOTE: tested with several feature type, but not all
|
||||
class ProcessedFeature(type(feature)):
|
||||
def __init__(self, original_feature, processed_values):
|
||||
self.__dict__.update(original_feature.__dict__)
|
||||
self.name = f"Processed_{original_feature.name}"
|
||||
self.frame_rate = original_feature.frame_rate
|
||||
self.frame_count = original_feature.frame_count
|
||||
self.data = processed_values
|
||||
|
||||
def extract(self):
|
||||
return self
|
||||
|
||||
def get_value_at_frame(self, frame_index):
|
||||
return self.data[frame_index]
|
||||
|
||||
processed_feature = ProcessedFeature(feature, final_values)
|
||||
return (processed_feature, self.visualize(processed_feature))
|
||||
|
||||
def apply_rise_time_adjustment(self, values, rise_detection_threshold, rise_smoothing_factor):
|
||||
if all(v == 0 for v in values):
|
||||
return values
|
||||
|
||||
adjusted = values.copy()
|
||||
window_size = 5
|
||||
|
||||
for i in range(len(values) - window_size):
|
||||
if values[i + window_size] - values[i] > rise_detection_threshold:
|
||||
|
||||
start_index = i
|
||||
end_index = min(len(values), i + 2*window_size)
|
||||
peak_index = start_index + np.argmax(values[start_index:end_index])
|
||||
peak_value = values[peak_index]
|
||||
|
||||
|
||||
for j in range(start_index, peak_index):
|
||||
progress = (j - start_index) / (peak_index - start_index)
|
||||
smoothed_value = values[start_index] + (peak_value - values[start_index]) * (progress ** (1/rise_smoothing_factor))
|
||||
adjusted[j] = max(adjusted[j], smoothed_value) # Take the max to prevent lowering existing higher values
|
||||
|
||||
return adjusted
|
||||
@@ -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
|
||||
@@ -0,0 +1,418 @@
|
||||
from abc import ABC, abstractmethod
|
||||
import numpy as np
|
||||
import librosa
|
||||
import torch
|
||||
import cv2
|
||||
from ..masks.mask_utils import calculate_optical_flow
|
||||
|
||||
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
|
||||
self.features = None
|
||||
self.inverted = False
|
||||
|
||||
@abstractmethod
|
||||
def extract(self):
|
||||
pass
|
||||
|
||||
def get_value_at_frame(self, frame_index):
|
||||
if self.data is not None:
|
||||
return self.data[frame_index]
|
||||
elif self.features is not None and hasattr(self, 'feature_name'):
|
||||
return self.features[self.feature_name][frame_index]
|
||||
else:
|
||||
raise ValueError("No data or features available")
|
||||
|
||||
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
|
||||
|
||||
def invert(self):
|
||||
if self.data is not None:
|
||||
self.data = 1 - self.data
|
||||
if self.features is not None:
|
||||
for key in self.features:
|
||||
if isinstance(self.features[key], list):
|
||||
self.features[key] = [1 - value for value in self.features[key]]
|
||||
elif isinstance(self.features[key], np.ndarray):
|
||||
self.features[key] = 1 - self.features[key]
|
||||
elif isinstance(self.features[key], torch.Tensor):
|
||||
self.features[key] = 1 - self.features[key]
|
||||
self.inverted = not self.inverted
|
||||
return self
|
||||
|
||||
class TimeFeature(BaseFeature):
|
||||
def __init__(self, name, frame_rate, frame_count, effect_type='smooth', speed=1.0, offset=0.0):
|
||||
self.effect_type = effect_type
|
||||
self.speed = speed
|
||||
self.offset = offset
|
||||
super().__init__(name, "time", frame_rate, frame_count)
|
||||
|
||||
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()
|
||||
|
||||
class AudioFeature(BaseFeature):
|
||||
def __init__(self, name, audio, num_frames, frame_rate, feature_type='amplitude_envelope'):
|
||||
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
|
||||
super().__init__(name, "audio", frame_rate, num_frames)
|
||||
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_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'):
|
||||
self.depth_maps = depth_maps
|
||||
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"
|
||||
]
|
||||
super().__init__(name, "depth", frame_rate, frame_count)
|
||||
|
||||
def extract(self):
|
||||
self.features = {self.feature_name: []}
|
||||
|
||||
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)
|
||||
|
||||
if self.feature_name == 'mean_depth':
|
||||
self.features[self.feature_name].append(torch.mean(normalized_depth).item())
|
||||
elif self.feature_name == 'depth_variance':
|
||||
self.features[self.feature_name].append(torch.var(normalized_depth).item())
|
||||
elif self.feature_name == 'depth_range':
|
||||
self.features[self.feature_name].append((depth_max - depth_min).item())
|
||||
elif self.feature_name == 'gradient_magnitude':
|
||||
grad_y, grad_x = torch.gradient(normalized_depth)
|
||||
gradient_magnitude = torch.sqrt(grad_x**2 + grad_y**2)
|
||||
self.features[self.feature_name].append(torch.mean(gradient_magnitude).item())
|
||||
elif self.feature_name in ['foreground_ratio', 'midground_ratio', 'background_ratio']:
|
||||
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
|
||||
if self.feature_name == 'foreground_ratio':
|
||||
self.features[self.feature_name].append(foreground)
|
||||
elif self.feature_name == 'midground_ratio':
|
||||
self.features[self.feature_name].append(midground)
|
||||
elif self.feature_name == 'background_ratio':
|
||||
self.features[self.feature_name].append(background)
|
||||
|
||||
feature_tensor = torch.tensor(self.features[self.feature_name])
|
||||
feature_min = torch.min(feature_tensor)
|
||||
feature_max = torch.max(feature_tensor)
|
||||
if feature_max > feature_min:
|
||||
self.features[self.feature_name] = ((feature_tensor - feature_min) / (feature_max - feature_min)).tolist()
|
||||
else:
|
||||
self.features[self.feature_name] = torch.zeros_like(feature_tensor).tolist()
|
||||
|
||||
return self
|
||||
|
||||
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)}")
|
||||
|
||||
class ColorFeature(BaseFeature):
|
||||
def __init__(self, name, frame_rate, frame_count, images, feature_name='dominant_color'):
|
||||
if images.dim() != 4 or images.shape[-1] != 3:
|
||||
raise ValueError(f"Expected images in BHWC format, but got shape {images.shape}")
|
||||
self.images = images
|
||||
self.feature_name = feature_name
|
||||
self.available_features = [
|
||||
"dominant_color", "color_variance", "saturation",
|
||||
"red_ratio", "green_ratio", "blue_ratio"
|
||||
]
|
||||
super().__init__(name, "color", frame_rate, frame_count)
|
||||
|
||||
def extract(self):
|
||||
self.features = {self.feature_name: []}
|
||||
|
||||
images = self.images.float() / 255.0 if self.images.dtype == torch.uint8 else self.images
|
||||
|
||||
if self.feature_name in ['red_ratio', 'green_ratio', 'blue_ratio']:
|
||||
color_sums = torch.sum(images, dim=(1, 2))
|
||||
totals = torch.sum(color_sums, dim=1, keepdim=True)
|
||||
ratios = color_sums / totals
|
||||
|
||||
if self.feature_name == 'red_ratio':
|
||||
self.features[self.feature_name] = ratios[:, 0].tolist()
|
||||
elif self.feature_name == 'green_ratio':
|
||||
self.features[self.feature_name] = ratios[:, 1].tolist()
|
||||
elif self.feature_name == 'blue_ratio':
|
||||
self.features[self.feature_name] = ratios[:, 2].tolist()
|
||||
else:
|
||||
for image in images:
|
||||
self._extract_features(image)
|
||||
|
||||
self._normalize_features()
|
||||
print("ColorFeature extraction completed")
|
||||
return self
|
||||
|
||||
def _extract_features(self, image):
|
||||
if self.feature_name == 'dominant_color':
|
||||
self.features[self.feature_name].append(self._get_dominant_color(image))
|
||||
elif self.feature_name == 'color_variance':
|
||||
self.features[self.feature_name].append(torch.var(image.reshape(-1, 3), dim=0).mean().item())
|
||||
elif self.feature_name == 'saturation':
|
||||
self.features[self.feature_name].append(self._calculate_saturation(image).item())
|
||||
|
||||
def _get_dominant_color(self, image):
|
||||
print("Calculating dominant color")
|
||||
quantized = (image * 31).long().reshape(-1, 3)
|
||||
unique, counts = np.unique(quantized.cpu().numpy(), axis=0, return_counts=True)
|
||||
dominant = unique[np.argmax(counts)]
|
||||
return np.mean(dominant / 31)
|
||||
|
||||
def _calculate_saturation(self, image):
|
||||
print("Calculating saturation")
|
||||
max_val, _ = torch.max(image, dim=-1)
|
||||
min_val, _ = torch.min(image, dim=-1)
|
||||
return torch.mean(max_val - min_val)
|
||||
|
||||
def _normalize_features(self):
|
||||
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()
|
||||
|
||||
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)}")
|
||||
|
||||
class BrightnessFeature(BaseFeature):
|
||||
def __init__(self, name, frame_rate, frame_count, images, feature_name='mean_brightness'):
|
||||
self.images = images
|
||||
self.feature_name = feature_name
|
||||
self.available_features = [
|
||||
"mean_brightness", "brightness_variance", "brightness_histogram",
|
||||
"dark_ratio", "mid_ratio", "bright_ratio"
|
||||
]
|
||||
super().__init__(name, "brightness", frame_rate, frame_count)
|
||||
|
||||
def extract(self):
|
||||
self.features = {self.feature_name: []}
|
||||
|
||||
images = self.images.float() / 255.0 if self.images.dtype == torch.uint8 else self.images
|
||||
|
||||
for image in images:
|
||||
self._extract_features(image)
|
||||
|
||||
self._normalize_features()
|
||||
return self
|
||||
|
||||
def _extract_features(self, image):
|
||||
grayscale = 0.2989 * image[..., 0] + 0.5870 * image[..., 1] + 0.1140 * image[..., 2]
|
||||
|
||||
if self.feature_name == 'mean_brightness':
|
||||
self.features[self.feature_name].append(torch.mean(grayscale).item())
|
||||
elif self.feature_name == 'brightness_variance':
|
||||
self.features[self.feature_name].append(torch.var(grayscale).item())
|
||||
elif self.feature_name == 'brightness_histogram':
|
||||
histogram = torch.histc(grayscale, bins=10, min=0, max=1)
|
||||
self.features[self.feature_name].append(histogram.tolist())
|
||||
else:
|
||||
total_pixels = grayscale.numel()
|
||||
if self.feature_name == 'dark_ratio':
|
||||
self.features[self.feature_name].append((torch.sum(grayscale < 0.3) / total_pixels).item())
|
||||
elif self.feature_name == 'mid_ratio':
|
||||
self.features[self.feature_name].append((torch.sum((grayscale >= 0.3) & (grayscale < 0.7)) / total_pixels).item())
|
||||
elif self.feature_name == 'bright_ratio':
|
||||
self.features[self.feature_name].append((torch.sum(grayscale >= 0.7) / total_pixels).item())
|
||||
|
||||
def _normalize_features(self):
|
||||
for key in self.features:
|
||||
if key != 'brightness_histogram':
|
||||
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()
|
||||
|
||||
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)}")
|
||||
|
||||
class MotionFeature(BaseFeature):
|
||||
def __init__(self, name, frame_rate, frame_count, images, feature_name='mean_motion', flow_method='Farneback', flow_threshold=0.0, magnitude_threshold=0.0):
|
||||
self.images = images
|
||||
self.feature_name = feature_name
|
||||
self.flow_method = flow_method
|
||||
self.flow_threshold = flow_threshold
|
||||
self.magnitude_threshold = magnitude_threshold
|
||||
self.available_features = [
|
||||
"mean_motion", "max_motion", "motion_direction",
|
||||
"horizontal_motion", "vertical_motion", "motion_complexity"
|
||||
]
|
||||
super().__init__(name, "motion", frame_rate, frame_count)
|
||||
|
||||
def extract(self):
|
||||
print("Starting MotionFeature extraction")
|
||||
self.features = {self.feature_name: []}
|
||||
|
||||
images_np = (self.images.cpu().numpy() * 255).astype(np.uint8)
|
||||
|
||||
for i in range(len(images_np) - 1):
|
||||
print(f"Processing frames {i+1} and {i+2}")
|
||||
frame1 = images_np[i]
|
||||
frame2 = images_np[i+1]
|
||||
|
||||
flow = calculate_optical_flow(frame1, frame2, self.flow_method)
|
||||
self._extract_features(flow)
|
||||
|
||||
self._pad_last_frame()
|
||||
self._normalize_features()
|
||||
print("MotionFeature extraction completed")
|
||||
return self
|
||||
|
||||
def _extract_features(self, flow):
|
||||
flow_magnitude = np.sqrt(flow[..., 0]**2 + flow[..., 1]**2)
|
||||
|
||||
if self.flow_threshold > 0:
|
||||
flow_magnitude[flow_magnitude < self.flow_threshold] = 0
|
||||
if self.magnitude_threshold > 0:
|
||||
flow_magnitude[flow_magnitude < self.magnitude_threshold * np.max(flow_magnitude)] = 0
|
||||
|
||||
if self.feature_name == 'mean_motion':
|
||||
self.features[self.feature_name].append(np.mean(flow_magnitude))
|
||||
elif self.feature_name == 'max_motion':
|
||||
self.features[self.feature_name].append(np.max(flow_magnitude))
|
||||
elif self.feature_name == 'motion_direction':
|
||||
self.features[self.feature_name].append(np.mean(np.arctan2(flow[..., 1], flow[..., 0])))
|
||||
elif self.feature_name == 'horizontal_motion':
|
||||
self.features[self.feature_name].append(np.mean(np.abs(flow[..., 0])))
|
||||
elif self.feature_name == 'vertical_motion':
|
||||
self.features[self.feature_name].append(np.mean(np.abs(flow[..., 1])))
|
||||
elif self.feature_name == 'motion_complexity':
|
||||
self.features[self.feature_name].append(np.std(flow_magnitude))
|
||||
|
||||
def _pad_last_frame(self):
|
||||
for feature in self.features:
|
||||
if feature != "motion_direction":
|
||||
self.features[feature].append(self.features[feature][-1])
|
||||
else:
|
||||
self.features[feature].append(0)
|
||||
|
||||
def _normalize_features(self):
|
||||
for key in self.features:
|
||||
if key != "motion_direction":
|
||||
feature_array = np.array(self.features[key])
|
||||
feature_min = np.min(feature_array)
|
||||
feature_max = np.max(feature_array)
|
||||
if feature_max > feature_min:
|
||||
self.features[key] = ((feature_array - feature_min) / (feature_max - feature_min)).tolist()
|
||||
else:
|
||||
self.features[key] = np.zeros_like(feature_array).tolist()
|
||||
else:
|
||||
self.features[key] = (np.array(self.features[key]) / (2 * np.pi)).tolist()
|
||||
|
||||
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)}")
|
||||
@@ -0,0 +1,204 @@
|
||||
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):
|
||||
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 = []
|
||||
super().__init__(name, "midi", frame_rate, frame_count)
|
||||
|
||||
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 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)
|
||||
@@ -0,0 +1,163 @@
|
||||
import mido
|
||||
import os
|
||||
from .feature_pipe import FeaturePipe
|
||||
import folder_paths
|
||||
from server import PromptServer
|
||||
from aiohttp import web
|
||||
import shutil
|
||||
from .midi_feature import MIDIFeature
|
||||
from .feature_extractors import FeatureExtractorBase
|
||||
|
||||
class MIDILoadAndExtract(FeatureExtractorBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"midi_file": (folder_paths.get_filename_list("midi_files"),),
|
||||
"track_selection": (["all"],),
|
||||
"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
|
||||
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,373 @@
|
||||
import cv2
|
||||
import torch
|
||||
import numpy as np
|
||||
from .image_base import FlexImageBase
|
||||
from scipy.ndimage import gaussian_filter
|
||||
import torch.nn.functional as F
|
||||
|
||||
class FlexImageEdgeDetect(FlexImageBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
**super().INPUT_TYPES(),
|
||||
"required": {
|
||||
**super().INPUT_TYPES()["required"],
|
||||
"low_threshold": ("FLOAT", {"default": 100, "min": 0, "max": 255, "step": 1}),
|
||||
"high_threshold": ("FLOAT", {"default": 200, "min": 0, "max": 255, "step": 1}),
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_modifiable_params(cls):
|
||||
return ["low_threshold", "high_threshold"]
|
||||
|
||||
def apply_effect_internal(self, image: np.ndarray, low_threshold: float, high_threshold: float, **kwargs) -> np.ndarray:
|
||||
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
|
||||
|
||||
edges = cv2.Canny(np.uint8(gray * 255), low_threshold, high_threshold)
|
||||
|
||||
# Convert back to RGB
|
||||
edges_rgb = cv2.cvtColor(edges, cv2.COLOR_GRAY2RGB)
|
||||
|
||||
return edges_rgb.astype(float) / 255.0
|
||||
|
||||
class FlexImagePosterize(FlexImageBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
**super().INPUT_TYPES(),
|
||||
"required": {
|
||||
**super().INPUT_TYPES()["required"],
|
||||
"max_levels": ("INT", {"default": 8, "min": 2, "max": 256, "step": 1}),
|
||||
"dither_strength": ("FLOAT", {"default": 0.3, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"channel_separation": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"gamma": ("FLOAT", {"default": 1.2, "min": 0.1, "max": 2.2, "step": 0.1}),
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_modifiable_params(cls):
|
||||
return ["max_levels", "dither_strength", "channel_separation", "gamma"]
|
||||
|
||||
def apply_effect_internal(self, image: np.ndarray, max_levels: int, dither_strength: float,
|
||||
channel_separation: float, gamma: float, **kwargs) -> np.ndarray:
|
||||
# Apply gamma correction
|
||||
image_gamma = np.power(image, gamma)
|
||||
|
||||
# Convert image to uint8
|
||||
image_uint8 = (image_gamma * 255).astype(np.uint8)
|
||||
|
||||
posterized = np.zeros_like(image_uint8)
|
||||
|
||||
for c in range(3): # RGB channels
|
||||
# Calculate levels for each channel
|
||||
channel_levels = int(np.clip(2 + (max_levels - 2) * (1 + channel_separation * (c - 1)), 2, max_levels))
|
||||
|
||||
# Posterize
|
||||
div = 256 // channel_levels
|
||||
posterized[:,:,c] = (image_uint8[:,:,c] // div) * div
|
||||
|
||||
if dither_strength > 0:
|
||||
# Apply Floyd-Steinberg dithering with adjustable strength
|
||||
error = (image_uint8.astype(np.float32) - posterized) * dither_strength
|
||||
h, w = image.shape[:2]
|
||||
for c in range(3): # RGB channels
|
||||
for i in range(h - 1):
|
||||
for j in range(w - 1):
|
||||
old_pixel = posterized[i, j, c]
|
||||
new_pixel = np.clip(old_pixel + error[i, j, c], 0, 255)
|
||||
quant_error = old_pixel - new_pixel
|
||||
posterized[i, j, c] = new_pixel
|
||||
error[i, j+1, c] += quant_error * 7 / 16
|
||||
error[i+1, j-1, c] += quant_error * 3 / 16
|
||||
error[i+1, j, c] += quant_error * 5 / 16
|
||||
error[i+1, j+1, c] += quant_error * 1 / 16
|
||||
|
||||
# Convert back to float32 and apply gamma correction
|
||||
return np.power(posterized.astype(np.float32) / 255.0, 1/gamma)
|
||||
|
||||
class FlexImageKaleidoscope(FlexImageBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
**super().INPUT_TYPES(),
|
||||
"required": {
|
||||
**super().INPUT_TYPES()["required"],
|
||||
"segments": ("INT", {"default": 8, "min": 2, "max": 32, "step": 1}),
|
||||
"center_x": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"center_y": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"zoom": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 2.0, "step": 0.1}),
|
||||
"rotation": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 360.0, "step": 1.0}),
|
||||
"precession": ("FLOAT", {"default": 0.0, "min": -1.0, "max": 1.0, "step": 0.01}),
|
||||
"speed": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 5.0, "step": 0.1}),
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_modifiable_params(cls):
|
||||
return ["segments", "zoom", "rotation", "precession", "speed"]
|
||||
|
||||
def apply_effect_internal(self, image: np.ndarray, segments: int, center_x: float, center_y: float,
|
||||
zoom: float, rotation: float, precession: float, speed: float, **kwargs) -> np.ndarray:
|
||||
h, w = image.shape[:2]
|
||||
center = (int(w * center_x), int(h * center_y))
|
||||
|
||||
# Ensure segments is an integer
|
||||
segments = max(2, int(segments))
|
||||
|
||||
# Create the kaleidoscope effect
|
||||
segment_angle = 360 / segments
|
||||
result = np.zeros_like(image)
|
||||
|
||||
for i in range(segments):
|
||||
angle = i * segment_angle + rotation * speed
|
||||
# Apply precession effect
|
||||
precession_factor = 1 + precession * (i / segments) * speed
|
||||
matrix = cv2.getRotationMatrix2D(center, angle, zoom * precession_factor)
|
||||
rotated = cv2.warpAffine(image, matrix, (w, h), borderMode=cv2.BORDER_REFLECT)
|
||||
|
||||
mask = np.zeros((h, w), dtype=np.float32)
|
||||
pts = np.array([center, (0, 0), (w, 0), (w, h), (0, h)], dtype=np.int32)
|
||||
cv2.fillPoly(mask, [pts], 1.0)
|
||||
|
||||
segment_mask = cv2.warpAffine(mask, matrix, (w, h))
|
||||
result = np.maximum(result, rotated * segment_mask[:,:,None])
|
||||
|
||||
# Ensure the result is not all black
|
||||
if np.max(result) == 0:
|
||||
print("Warning: Kaleidoscope effect resulted in a black image")
|
||||
return image # Return original image if result is all black
|
||||
|
||||
return result.clip(0, 1)
|
||||
|
||||
class FlexImageColorGrade(FlexImageBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
**super().INPUT_TYPES(),
|
||||
"required": {
|
||||
**super().INPUT_TYPES()["required"],
|
||||
"intensity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"mix": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
},
|
||||
"optional": {
|
||||
"lut_file": ("STRING", {"default": None}),
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_modifiable_params(cls):
|
||||
return ["intensity", "mix"]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.lut_cache = {}
|
||||
|
||||
def load_lut(self, lut_file):
|
||||
if lut_file is None:
|
||||
return None
|
||||
if lut_file not in self.lut_cache:
|
||||
lut = cv2.imread(lut_file, cv2.IMREAD_UNCHANGED)
|
||||
if lut is None:
|
||||
print(f"Warning: Failed to load LUT file: {lut_file}")
|
||||
return None
|
||||
self.lut_cache[lut_file] = lut
|
||||
return self.lut_cache[lut_file]
|
||||
|
||||
def apply_effect_internal(self, image: np.ndarray, intensity: float, mix: float,
|
||||
lut_file: str = None, **kwargs) -> np.ndarray:
|
||||
# Load the LUT
|
||||
lut = self.load_lut(lut_file)
|
||||
|
||||
# Apply color grading if LUT is available
|
||||
if lut is not None:
|
||||
graded = cv2.LUT(image, lut)
|
||||
|
||||
# Apply intensity
|
||||
graded = cv2.addWeighted(image, 1 - intensity, graded, intensity, 0)
|
||||
|
||||
# Mix with original
|
||||
result = cv2.addWeighted(image, 1 - mix, graded, mix, 0)
|
||||
else:
|
||||
result = image # If no LUT is available, return the original image
|
||||
|
||||
return np.clip(result, 0, 1)
|
||||
|
||||
class FlexImageGlitch(FlexImageBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
base_inputs = super().INPUT_TYPES()
|
||||
base_inputs["required"].update({
|
||||
"shift_amount": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"scan_lines": ("INT", {"default": 10, "min": 0, "max": 100, "step": 1}),
|
||||
"color_shift": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
})
|
||||
return base_inputs
|
||||
|
||||
@classmethod
|
||||
def get_modifiable_params(cls):
|
||||
return ["shift_amount", "scan_lines", "color_shift"]
|
||||
|
||||
def apply_effect_internal(self, image: np.ndarray, shift_amount: float, scan_lines: int, color_shift: float, **kwargs) -> np.ndarray:
|
||||
h, w = image.shape[:2]
|
||||
|
||||
# Apply horizontal shift
|
||||
shift = int(w * shift_amount)
|
||||
result = np.roll(image, shift, axis=1)
|
||||
|
||||
# Add scan lines
|
||||
if scan_lines > 0:
|
||||
scan_line_mask = np.zeros((h, w))
|
||||
scan_line_mask[::scan_lines] = 1
|
||||
result = result * (1 - scan_line_mask)[:,:,np.newaxis] + scan_line_mask[:,:,np.newaxis]
|
||||
|
||||
# Apply color channel shift
|
||||
if color_shift > 0:
|
||||
color_shift_amount = int(w * color_shift)
|
||||
result[:,:,0] = np.roll(result[:,:,0], color_shift_amount, axis=1)
|
||||
result[:,:,2] = np.roll(result[:,:,2], -color_shift_amount, axis=1)
|
||||
|
||||
return np.clip(result, 0, 1)
|
||||
|
||||
class FlexImageChromaticAberration(FlexImageBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
**super().INPUT_TYPES(),
|
||||
"required": {
|
||||
**super().INPUT_TYPES()["required"],
|
||||
"shift_amount": ("FLOAT", {"default": 0.01, "min": 0.0, "max": 0.1, "step": 0.001}),
|
||||
"angle": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 360.0, "step": 1.0}),
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_modifiable_params(cls):
|
||||
return ["shift_amount", "angle"]
|
||||
|
||||
def apply_effect_internal(self, image: np.ndarray, shift_amount: float, angle: float, **kwargs) -> np.ndarray:
|
||||
h, w = image.shape[:2]
|
||||
|
||||
# Calculate shift vectors
|
||||
dx = int(w * shift_amount * np.cos(np.radians(angle)))
|
||||
dy = int(h * shift_amount * np.sin(np.radians(angle)))
|
||||
|
||||
# Shift red and blue channels
|
||||
result = np.zeros_like(image)
|
||||
result[:,:,0] = np.roll(image[:,:,0], (dy, dx), (0, 1)) # Red channel
|
||||
result[:,:,1] = image[:,:,1] # Green channel (no shift)
|
||||
result[:,:,2] = np.roll(image[:,:,2], (-dy, -dx), (0, 1)) # Blue channel
|
||||
|
||||
return np.clip(result, 0, 1)
|
||||
|
||||
|
||||
class FlexImagePixelate(FlexImageBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
**super().INPUT_TYPES(),
|
||||
"required": {
|
||||
**super().INPUT_TYPES()["required"],
|
||||
"pixel_size": ("INT", {"default": 10, "min": 1, "max": 100, "step": 1}),
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_modifiable_params(cls):
|
||||
return ["pixel_size"]
|
||||
|
||||
def apply_effect_internal(self, image: np.ndarray, pixel_size: int, **kwargs) -> np.ndarray:
|
||||
h, w = image.shape[:2]
|
||||
|
||||
# Ensure pixel_size is an integer and greater than 0
|
||||
pixel_size = max(1, int(pixel_size))
|
||||
|
||||
# Calculate new dimensions
|
||||
new_h, new_w = h // pixel_size, w // pixel_size
|
||||
|
||||
# Ensure new dimensions are at least 1x1
|
||||
new_h, new_w = max(1, new_h), max(1, new_w)
|
||||
|
||||
# Resize down
|
||||
small = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
|
||||
|
||||
# Resize up
|
||||
result = cv2.resize(small, (w, h), interpolation=cv2.INTER_NEAREST)
|
||||
|
||||
return result
|
||||
|
||||
class FlexImageBloom(FlexImageBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
**super().INPUT_TYPES(),
|
||||
"required": {
|
||||
**super().INPUT_TYPES()["required"],
|
||||
"threshold": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"blur_amount": ("FLOAT", {"default": 10.0, "min": 0.0, "max": 50.0, "step": 0.1}),
|
||||
"intensity": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_modifiable_params(cls):
|
||||
return ["threshold", "blur_amount", "intensity"]
|
||||
|
||||
def apply_effect_internal(self, image: np.ndarray, threshold: float, blur_amount: float,
|
||||
intensity: float, **kwargs) -> np.ndarray:
|
||||
# Extract bright areas
|
||||
bright_areas = np.maximum(image - threshold, 0) / (1 - threshold)
|
||||
|
||||
# Apply gaussian blur
|
||||
blurred = gaussian_filter(bright_areas, sigma=blur_amount)
|
||||
|
||||
# Combine with original image
|
||||
result = image + blurred * intensity
|
||||
|
||||
return np.clip(result, 0, 1)
|
||||
|
||||
class FlexImageTiltShift(FlexImageBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
**super().INPUT_TYPES(),
|
||||
"required": {
|
||||
**super().INPUT_TYPES()["required"],
|
||||
"blur_amount": ("FLOAT", {"default": 10.0, "min": 0.0, "max": 50.0, "step": 0.1}),
|
||||
"focus_position_x": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"focus_position_y": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"focus_width": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"focus_height": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"focus_shape": (["rectangle", "ellipse"], {"default": "rectangle"}),
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_modifiable_params(cls):
|
||||
return ["blur_amount", "focus_position_x", "focus_position_y", "focus_width", "focus_height"]
|
||||
|
||||
def apply_effect_internal(self, image: np.ndarray, blur_amount: float, focus_position_x: float,
|
||||
focus_position_y: float, focus_width: float, focus_height: float,
|
||||
focus_shape: str, **kwargs) -> np.ndarray:
|
||||
h, w = image.shape[:2]
|
||||
mask = np.zeros((h, w), dtype=np.float32)
|
||||
center_x, center_y = int(w * focus_position_x), int(h * focus_position_y)
|
||||
width, height = int(w * focus_width), int(h * focus_height)
|
||||
|
||||
if focus_shape == "rectangle":
|
||||
cv2.rectangle(mask,
|
||||
(center_x - width//2, center_y - height//2),
|
||||
(center_x + width//2, center_y + height//2),
|
||||
1, -1)
|
||||
else: # ellipse
|
||||
cv2.ellipse(mask,
|
||||
(center_x, center_y),
|
||||
(width//2, height//2),
|
||||
0, 0, 360, 1, -1)
|
||||
|
||||
mask = cv2.GaussianBlur(mask, (0, 0), sigmaX=min(width, height) / 6)
|
||||
blurred = cv2.GaussianBlur(image, (0, 0), sigmaX=blur_amount)
|
||||
result = image * mask[:,:,np.newaxis] + blurred * (1 - mask[:,:,np.newaxis])
|
||||
|
||||
return np.clip(result, 0, 1)
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
# import torch
|
||||
# import numpy as np
|
||||
# from abc import ABC, abstractmethod
|
||||
# from tqdm import tqdm
|
||||
# from comfy.utils import ProgressBar
|
||||
# from .image_utils import preserve_original_colors, apply_blur
|
||||
# from ... import RyanOnTheInside
|
||||
|
||||
# class ImageBase(RyanOnTheInside, ABC):
|
||||
# @classmethod
|
||||
# def INPUT_TYPES(cls):
|
||||
# return {
|
||||
# "required": {
|
||||
# "images": ("IMAGE",),
|
||||
# "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
# "blend_mode": (["normal", "multiply", "screen", "overlay", "soft_light"],),
|
||||
# "preserve_colors": ("BOOLEAN", {"default": False}),
|
||||
# "apply_to_alpha": ("BOOLEAN", {"default": False}),
|
||||
# }
|
||||
# }
|
||||
|
||||
# CATEGORY = "RyanOnTheInside/Images"
|
||||
|
||||
# def __init__(self):
|
||||
# self.pre_processors = []
|
||||
# self.post_processors = []
|
||||
# self.progress_bar = None
|
||||
# self.tqdm_bar = None
|
||||
# self.current_progress = 0
|
||||
# self.total_steps = 0
|
||||
|
||||
# def add_pre_processor(self, func):
|
||||
# self.pre_processors.append(func)
|
||||
# return self
|
||||
|
||||
# def add_post_processor(self, func):
|
||||
# self.post_processors.append(func)
|
||||
# return self
|
||||
|
||||
# def pre_process(self, image):
|
||||
# for processor in self.pre_processors:
|
||||
# image = processor(image)
|
||||
# return image
|
||||
|
||||
# def post_process(self, image):
|
||||
# for processor in self.post_processors:
|
||||
# image = processor(image)
|
||||
# return image
|
||||
|
||||
# def start_progress(self, total_steps, desc="Processing"):
|
||||
# self.progress_bar = ProgressBar(total_steps)
|
||||
# self.tqdm_bar = tqdm(total=total_steps, desc=desc, leave=False)
|
||||
# self.current_progress = 0
|
||||
# self.total_steps = total_steps
|
||||
|
||||
# def update_progress(self, step=1):
|
||||
# self.current_progress += step
|
||||
# if self.progress_bar:
|
||||
# self.progress_bar.update(step)
|
||||
# if self.tqdm_bar:
|
||||
# self.tqdm_bar.update(step)
|
||||
|
||||
# def end_progress(self):
|
||||
# if self.tqdm_bar:
|
||||
# self.tqdm_bar.close()
|
||||
# self.progress_bar = None
|
||||
# self.tqdm_bar = None
|
||||
# self.current_progress = 0
|
||||
# self.total_steps = 0
|
||||
|
||||
# @abstractmethod
|
||||
# def process_image(self, image: np.ndarray, strength: float, **kwargs) -> np.ndarray:
|
||||
# """
|
||||
# Process a single image. This method must be implemented by child classes.
|
||||
# """
|
||||
# pass
|
||||
|
||||
# def apply_image_operation(self, processed_images: torch.Tensor, original_images: torch.Tensor, strength: float, blend_mode: str, preserve_colors: bool, apply_to_alpha: bool, **kwargs) -> torch.Tensor:
|
||||
# processed_images_np = processed_images.cpu().numpy() if isinstance(processed_images, torch.Tensor) else processed_images
|
||||
# original_images_np = original_images.cpu().numpy() if isinstance(original_images, torch.Tensor) else original_images
|
||||
# num_frames = processed_images_np.shape[0]
|
||||
|
||||
# self.start_progress(num_frames, desc="Applying image operation")
|
||||
|
||||
# result = []
|
||||
# for processed_image, original_image in zip(processed_images_np, original_images_np):
|
||||
# # Pre-processing
|
||||
# processed_image = self.pre_process(processed_image)
|
||||
|
||||
# if preserve_colors:
|
||||
# processed_image = preserve_original_colors(original_image, processed_image)
|
||||
|
||||
# if not apply_to_alpha and original_image.shape[-1] == 4:
|
||||
# processed_image[..., -1] = original_image[..., -1]
|
||||
|
||||
# # Post-processing
|
||||
# processed_image = self.post_process(processed_image)
|
||||
|
||||
# # Ensure the final image is clipped between 0 and 1
|
||||
# processed_image = np.clip(processed_image, 0, 1)
|
||||
|
||||
# result.append(processed_image)
|
||||
# self.update_progress()
|
||||
|
||||
# self.end_progress()
|
||||
|
||||
# return torch.from_numpy(np.stack(result)).float()
|
||||
|
||||
# @abstractmethod
|
||||
# def main_function(self, *args, **kwargs) -> torch.Tensor:
|
||||
# """
|
||||
# Main entry point for the node. This method must be implemented by child classes.
|
||||
# It should call apply_image_operation with the appropriate arguments.
|
||||
# """
|
||||
# pass
|
||||
|
||||
|
||||
|
||||
# import torch
|
||||
# import numpy as np
|
||||
# from abc import abstractmethod
|
||||
# from .image_base import ImageBase
|
||||
|
||||
# class FlexImageBase(ImageBase):
|
||||
|
||||
# BLEND_MODES = ["multiply", "add", "subtract", "override"]
|
||||
# @classmethod
|
||||
# def INPUT_TYPES(cls):
|
||||
# return {
|
||||
# "required": {
|
||||
# **super().INPUT_TYPES()["required"],
|
||||
# "feature": ("FEATURE",),
|
||||
# "feature_pipe": ("FEATURE_PIPE",),
|
||||
# "feature_threshold": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
# "feature_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
|
||||
# "feature_blend_mode": (cls.BLEND_MODES,),
|
||||
# }
|
||||
# }
|
||||
|
||||
# CATEGORY = "RyanOnTheInside/FlexImages"
|
||||
# RETURN_TYPES = ("IMAGE",)
|
||||
# FUNCTION = "main_function"
|
||||
|
||||
# @abstractmethod
|
||||
# def process_image(self, image: np.ndarray, feature_value: float, strength: float, **kwargs) -> np.ndarray:
|
||||
# pass
|
||||
|
||||
# def apply_image_operation(self, images, feature, feature_pipe, strength, blend_mode, preserve_colors, apply_to_alpha, feature_threshold, feature_strength, feature_blend_mode, **kwargs):
|
||||
# num_frames = feature_pipe.frame_count
|
||||
# original_images = images.clone()
|
||||
|
||||
# self.start_progress(num_frames, desc="Applying flex image operation")
|
||||
|
||||
# result = []
|
||||
# for i in range(num_frames):
|
||||
# image = images[i].numpy()
|
||||
# feature_value = feature.get_value_at_frame(i)
|
||||
|
||||
# if feature_value >= feature_threshold:
|
||||
# # Apply the effect with the feature value directly controlling the intensity
|
||||
# processed_image = self.process_image(image, feature_value * feature_strength, strength, **kwargs)
|
||||
# else:
|
||||
# processed_image = image
|
||||
|
||||
# result.append(processed_image)
|
||||
# self.update_progress()
|
||||
|
||||
# self.end_progress()
|
||||
|
||||
# processed_images = torch.from_numpy(np.stack(result)).float()
|
||||
# return super().apply_image_operation(processed_images, original_images, strength, blend_mode, preserve_colors, apply_to_alpha, **kwargs)
|
||||
|
||||
# @abstractmethod
|
||||
# def main_function(self, images, feature, feature_pipe, strength, blend_mode, preserve_colors, apply_to_alpha, feature_threshold, feature_strength, feature_blend_mode, **kwargs):
|
||||
# pass
|
||||
|
||||
|
||||
|
||||
import torch
|
||||
import numpy as np
|
||||
from abc import ABC, abstractmethod
|
||||
from tqdm import tqdm
|
||||
from comfy.utils import ProgressBar
|
||||
from ... import RyanOnTheInside
|
||||
|
||||
class FlexImageBase(RyanOnTheInside, ABC):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"feature": ("FEATURE",),
|
||||
"feature_pipe": ("FEATURE_PIPE",),
|
||||
"strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"feature_threshold": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"modulate_param": (cls.get_modifiable_params(), {"default": cls.get_modifiable_params()[0]}),
|
||||
"modulation_mode": (["relative", "absolute"], {"default": "relative"}),
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = "RyanOnTheInside/ImageEffects"
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "apply_effect"
|
||||
|
||||
def __init__(self):
|
||||
self.progress_bar = None
|
||||
|
||||
def start_progress(self, total_steps, desc="Processing"):
|
||||
self.progress_bar = ProgressBar(total_steps)
|
||||
|
||||
def update_progress(self):
|
||||
if self.progress_bar:
|
||||
self.progress_bar.update(1)
|
||||
|
||||
def end_progress(self):
|
||||
self.progress_bar = None
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def get_modifiable_params(cls):
|
||||
"""Return a list of parameter names that can be modulated."""
|
||||
return []
|
||||
|
||||
def modulate_param(self, param_name, param_value, feature_value, strength, mode):
|
||||
if mode == "relative":
|
||||
return param_value * (1 + (feature_value - 0.5) * strength)
|
||||
else: # absolute
|
||||
return param_value * feature_value * strength
|
||||
|
||||
def apply_effect(self, images, feature, feature_pipe, strength, feature_threshold, **kwargs):
|
||||
num_frames = feature_pipe.frame_count
|
||||
images_np = images.cpu().numpy()
|
||||
|
||||
self.start_progress(num_frames, desc=f"Applying {self.__class__.__name__}")
|
||||
|
||||
result = []
|
||||
for i in range(num_frames):
|
||||
image = images_np[i]
|
||||
feature_value = feature.get_value_at_frame(i)
|
||||
|
||||
if feature_value >= feature_threshold:
|
||||
processed_image = self.process_image(image, feature_value, strength, **kwargs)
|
||||
else:
|
||||
processed_image = image
|
||||
|
||||
result.append(processed_image)
|
||||
self.update_progress()
|
||||
|
||||
self.end_progress()
|
||||
|
||||
# Convert the list of numpy arrays to a single numpy array
|
||||
result_np = np.stack(result)
|
||||
|
||||
# Convert the numpy array to a PyTorch tensor and ensure it's in BHWC format
|
||||
result_tensor = torch.from_numpy(result_np).float()
|
||||
|
||||
# If the tensor is not in BHWC format, transpose it
|
||||
if result_tensor.shape[1] == 3: # If it's in BCHW format
|
||||
result_tensor = result_tensor.permute(0, 2, 3, 1)
|
||||
|
||||
return (result_tensor,)
|
||||
|
||||
def process_image(self, image: np.ndarray, feature_value: float, strength: float,
|
||||
modulate_param: str, modulation_mode: str, **kwargs) -> np.ndarray:
|
||||
# Modulate the selected parameter
|
||||
for param_name in self.get_modifiable_params():
|
||||
if param_name in kwargs:
|
||||
if param_name == modulate_param:
|
||||
kwargs[param_name] = self.modulate_param(param_name, kwargs[param_name],
|
||||
feature_value, strength, modulation_mode)
|
||||
|
||||
# Call the child class's implementation
|
||||
return self.apply_effect_internal(image, **kwargs)
|
||||
|
||||
@abstractmethod
|
||||
def apply_effect_internal(self, image: np.ndarray, **kwargs) -> np.ndarray:
|
||||
"""Apply the effect to the image. To be implemented by child classes."""
|
||||
pass
|
||||
@@ -0,0 +1,31 @@
|
||||
import torch
|
||||
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,)
|
||||
@@ -0,0 +1,198 @@
|
||||
import numpy as np
|
||||
import cv2
|
||||
|
||||
def apply_blend_mode(base_image: np.ndarray, blend_image: np.ndarray, mode: str, opacity: float) -> np.ndarray:
|
||||
"""
|
||||
Apply various blend modes to combine two images.
|
||||
|
||||
:param base_image: The bottom layer image
|
||||
:param blend_image: The top layer image to blend
|
||||
:param mode: The blending mode to use
|
||||
:param opacity: The opacity of the blend (0.0 to 1.0)
|
||||
:return: The blended image
|
||||
"""
|
||||
if mode == "normal":
|
||||
blended = base_image * (1 - opacity) + blend_image * opacity
|
||||
elif mode == "multiply":
|
||||
blended = base_image * blend_image
|
||||
elif mode == "screen":
|
||||
blended = 1 - (1 - base_image) * (1 - blend_image)
|
||||
elif mode == "overlay":
|
||||
mask = base_image >= 0.5
|
||||
blended = np.where(mask, 1 - 2 * (1 - base_image) * (1 - blend_image), 2 * base_image * blend_image)
|
||||
elif mode == "soft_light":
|
||||
mask = blend_image <= 0.5
|
||||
blended = np.where(mask,
|
||||
base_image - (1 - 2 * blend_image) * base_image * (1 - base_image),
|
||||
base_image + (2 * blend_image - 1) * (np.sqrt(base_image) - base_image))
|
||||
else:
|
||||
raise ValueError(f"Unsupported blend mode: {mode}")
|
||||
|
||||
return base_image * (1 - opacity) + blended * opacity
|
||||
|
||||
def preserve_original_colors(original_image: np.ndarray, processed_image: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Preserve the colors of the original image while keeping the luminance of the processed image.
|
||||
|
||||
:param original_image: The original image
|
||||
:param processed_image: The processed image whose luminance we want to keep
|
||||
:return: An image with original colors and processed luminance
|
||||
"""
|
||||
def rgb_to_hsl(rgb):
|
||||
r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2]
|
||||
max_rgb = np.max(rgb, axis=-1)
|
||||
min_rgb = np.min(rgb, axis=-1)
|
||||
diff = max_rgb - min_rgb
|
||||
|
||||
l = (max_rgb + min_rgb) / 2
|
||||
|
||||
s = np.zeros_like(l)
|
||||
mask = diff != 0
|
||||
s[mask] = diff[mask] / (1 - np.abs(2 * l[mask] - 1))
|
||||
|
||||
h = np.zeros_like(l)
|
||||
mask_r = (max_rgb == r) & (diff != 0)
|
||||
h[mask_r] = ((g[mask_r] - b[mask_r]) / diff[mask_r]) % 6
|
||||
mask_g = (max_rgb == g) & (diff != 0)
|
||||
h[mask_g] = ((b[mask_g] - r[mask_g]) / diff[mask_g]) + 2
|
||||
mask_b = (max_rgb == b) & (diff != 0)
|
||||
h[mask_b] = ((r[mask_b] - g[mask_b]) / diff[mask_b]) + 4
|
||||
h /= 6
|
||||
|
||||
return np.stack([h, s, l], axis=-1)
|
||||
|
||||
def hsl_to_rgb(hsl):
|
||||
h, s, l = hsl[..., 0], hsl[..., 1], hsl[..., 2]
|
||||
c = (1 - np.abs(2 * l - 1)) * s
|
||||
x = c * (1 - np.abs((h * 6) % 2 - 1))
|
||||
m = l - c / 2
|
||||
|
||||
rgb = np.zeros_like(hsl)
|
||||
mask = (h < 1/6)
|
||||
rgb[mask] = [c[mask], x[mask], 0]
|
||||
mask = (1/6 <= h) & (h < 2/6)
|
||||
rgb[mask] = [x[mask], c[mask], 0]
|
||||
mask = (2/6 <= h) & (h < 3/6)
|
||||
rgb[mask] = [0, c[mask], x[mask]]
|
||||
mask = (3/6 <= h) & (h < 4/6)
|
||||
rgb[mask] = [0, x[mask], c[mask]]
|
||||
mask = (4/6 <= h) & (h < 5/6)
|
||||
rgb[mask] = [x[mask], 0, c[mask]]
|
||||
mask = (5/6 <= h)
|
||||
rgb[mask] = [c[mask], 0, x[mask]]
|
||||
|
||||
return rgb + m[..., np.newaxis]
|
||||
|
||||
original_hsl = rgb_to_hsl(original_image[..., :3])
|
||||
processed_hsl = rgb_to_hsl(processed_image[..., :3])
|
||||
|
||||
result_hsl = np.copy(original_hsl)
|
||||
result_hsl[..., 2] = processed_hsl[..., 2] # Use luminance from processed image
|
||||
|
||||
result_rgb = hsl_to_rgb(result_hsl)
|
||||
|
||||
if original_image.shape[-1] == 4: # If there's an alpha channel
|
||||
result = np.dstack((result_rgb, original_image[..., 3]))
|
||||
else:
|
||||
result = result_rgb
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
def normalize_array(arr: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Normalize an array to the range [0, 1].
|
||||
|
||||
:param arr: Input array
|
||||
:return: Normalized array
|
||||
"""
|
||||
min_val = np.min(arr)
|
||||
max_val = np.max(arr)
|
||||
if max_val > min_val:
|
||||
return (arr - min_val) / (max_val - min_val)
|
||||
return arr
|
||||
|
||||
def apply_blur(image: np.ndarray, intensity: float, kernel_size: int, sigma: float = 0) -> np.ndarray:
|
||||
blurred = cv2.GaussianBlur(image, (kernel_size, kernel_size), sigma)
|
||||
return blurred
|
||||
|
||||
def apply_sharpen(image: np.ndarray, intensity: float, kernel_size: int, sigma: float = 1.0) -> np.ndarray:
|
||||
blurred = cv2.GaussianBlur(image, (kernel_size, kernel_size), sigma)
|
||||
sharpened = cv2.addWeighted(image, 1 + intensity, blurred, -intensity, 0)
|
||||
return np.clip(sharpened, 0, 1)
|
||||
|
||||
def apply_edge_detect(image: np.ndarray, intensity: float, kernel_size: int) -> np.ndarray:
|
||||
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
|
||||
edges = cv2.Canny(np.uint8(gray * 255), 100, 200)
|
||||
edges = edges.astype(float) / 255.0
|
||||
return np.clip(image * (1 - intensity) + np.dstack([edges] * 3) * intensity, 0, 1)
|
||||
|
||||
def apply_emboss(image: np.ndarray, intensity: float, kernel_size: int) -> np.ndarray:
|
||||
kernel = np.array([[-1,-1,0], [-1,0,1], [0,1,1]])
|
||||
if kernel_size > 3:
|
||||
kernel = cv2.resize(kernel, (kernel_size, kernel_size), interpolation=cv2.INTER_LINEAR)
|
||||
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
|
||||
embossed = cv2.filter2D(gray, -1, kernel * intensity)
|
||||
embossed = (embossed - np.min(embossed)) / (np.max(embossed) - np.min(embossed))
|
||||
return np.clip(image * (1 - intensity) + np.dstack([embossed] * 3) * intensity, 0, 1)
|
||||
|
||||
def apply_posterize(image: np.ndarray, intensity: float, levels: int, dither: bool = False) -> np.ndarray:
|
||||
effective_levels = int(np.clip(levels * (1 - intensity) + 2 * intensity, 2, levels))
|
||||
quantized = np.round(image * (effective_levels - 1)) / (effective_levels - 1)
|
||||
if dither:
|
||||
error = image - quantized
|
||||
quantized += np.random.uniform(-0.5/effective_levels, 0.5/effective_levels, image.shape)
|
||||
return np.clip(quantized, 0, 1)
|
||||
|
||||
def apply_brightness(image: np.ndarray, intensity: float, midpoint: float = 0.5, preserve_luminosity: bool = False) -> np.ndarray:
|
||||
adjusted = image + intensity
|
||||
|
||||
if preserve_luminosity:
|
||||
original_luminosity = np.mean(image)
|
||||
adjusted_luminosity = np.mean(adjusted)
|
||||
adjusted *= original_luminosity / adjusted_luminosity
|
||||
|
||||
return np.clip(adjusted, 0, 1)
|
||||
|
||||
def apply_contrast(image: np.ndarray, intensity: float, midpoint: float = 0.5, preserve_luminosity: bool = False) -> np.ndarray:
|
||||
factor = 1 + intensity
|
||||
adjusted = (image - midpoint) * factor + midpoint
|
||||
|
||||
if preserve_luminosity:
|
||||
original_luminosity = np.mean(image)
|
||||
adjusted_luminosity = np.mean(adjusted)
|
||||
adjusted *= original_luminosity / adjusted_luminosity
|
||||
|
||||
return np.clip(adjusted, 0, 1)
|
||||
|
||||
def apply_saturation(image: np.ndarray, intensity: float, preserve_luminosity: bool = True) -> np.ndarray:
|
||||
hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)
|
||||
hsv[..., 1] = np.clip(hsv[..., 1] * (1 + intensity), 0, 1)
|
||||
adjusted = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)
|
||||
|
||||
if preserve_luminosity:
|
||||
original_luminosity = np.mean(image)
|
||||
adjusted_luminosity = np.mean(adjusted)
|
||||
adjusted *= original_luminosity / adjusted_luminosity
|
||||
|
||||
return np.clip(adjusted, 0, 1)
|
||||
|
||||
def apply_hue_shift(image: np.ndarray, intensity: float, preserve_luminosity: bool = True, hue_method: str = "circular") -> np.ndarray:
|
||||
hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)
|
||||
|
||||
if hue_method == "linear":
|
||||
hsv[..., 0] = np.clip(hsv[..., 0] + intensity, 0, 1)
|
||||
elif hue_method == "circular":
|
||||
hsv[..., 0] = (hsv[..., 0] + intensity) % 1.0
|
||||
else:
|
||||
raise ValueError(f"Unsupported hue shift method: {hue_method}")
|
||||
|
||||
adjusted = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)
|
||||
|
||||
if preserve_luminosity:
|
||||
original_luminosity = np.mean(image)
|
||||
adjusted_luminosity = np.mean(adjusted)
|
||||
adjusted *= original_luminosity / adjusted_luminosity
|
||||
|
||||
return np.clip(adjusted, 0, 1)
|
||||
@@ -1,62 +0,0 @@
|
||||
import numpy as np
|
||||
from .mask_base import MaskBase
|
||||
from .mask_utils import normalize_array, morph_mask
|
||||
|
||||
class AudioControlledMaskBase(MaskBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
**super().INPUT_TYPES(),
|
||||
"required": {
|
||||
**super().INPUT_TYPES()["required"],
|
||||
"audio": ("AUDIO",),
|
||||
"bass_freq_range": ("STRING", {"default": "20,150"}),
|
||||
"reactivity_curve": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 5.0, "step": 0.1}),
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = "/Masks/Audio"
|
||||
|
||||
def apply_mask_operation(self, masks, audio, bass_freq_range, reactivity_curve, **kwargs):
|
||||
audio_np = audio['waveform'].cpu().numpy().squeeze()
|
||||
sample_rate = audio['sample_rate']
|
||||
|
||||
if audio_np.ndim > 1:
|
||||
audio_np = np.mean(audio_np, axis=0)
|
||||
|
||||
num_frames = len(masks)
|
||||
samples_per_frame = len(audio_np) // num_frames
|
||||
|
||||
frame_energy = np.array([
|
||||
np.sqrt(np.mean(audio_np[i*samples_per_frame:(i+1)*samples_per_frame]**2))
|
||||
for i in range(num_frames)
|
||||
])
|
||||
|
||||
energy_min, energy_max = np.percentile(frame_energy, [5, 95])
|
||||
normalized_energy = np.clip((frame_energy - energy_min) / (energy_max - energy_min), 0, 1)
|
||||
|
||||
smoothed_energy = np.convolve(normalized_energy, np.ones(5)/5, mode='same')
|
||||
enhanced_energy = np.tanh(3 * (smoothed_energy - smoothed_energy.mean()))
|
||||
enhanced_energy = normalize_array(enhanced_energy)
|
||||
|
||||
enhanced_energy = np.power(enhanced_energy, reactivity_curve)
|
||||
enhanced_energy = normalize_array(enhanced_energy)
|
||||
|
||||
return super().apply_mask_operation(masks, enhanced_energy, **kwargs)
|
||||
|
||||
class AudioControlledMaskMorph(AudioControlledMaskBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
**super().INPUT_TYPES(),
|
||||
"required": {
|
||||
**super().INPUT_TYPES()["required"],
|
||||
"morph_type": (["dilate", "erode", "open", "close"],),
|
||||
"kernel_size": ("INT", {"default": 5, "min": 3, "max": 21, "step": 2}),
|
||||
"growth_size": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1}),
|
||||
}
|
||||
}
|
||||
|
||||
def process_mask(self, mask, strength, morph_type, kernel_size, growth_size, **kwargs):
|
||||
iterations = max(1, int(growth_size * strength * 5))
|
||||
return morph_mask(mask, morph_type, kernel_size, iterations)
|
||||
@@ -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),)
|
||||
+91
-20
@@ -18,9 +18,11 @@ import random
|
||||
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 = []
|
||||
@@ -150,6 +153,8 @@ class TemporalMaskBase(MaskBase, ABC):
|
||||
"palindrome": ("BOOLEAN", {"default": False}),
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY="RyanOnTheInside/TemporalMasks"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -221,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}),
|
||||
@@ -235,7 +241,7 @@ class ParticleSystemMaskBase(MaskBase, ABC):
|
||||
|
||||
RETURN_TYPES = ("MASK", "IMAGE")
|
||||
FUNCTION = "main_function"
|
||||
CATEGORY = "/RyanOnTheInside/masks/"
|
||||
CATEGORY="RyanOnTheInside/ParticleSystemMasks"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -273,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 = []
|
||||
@@ -301,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']))
|
||||
|
||||
@@ -573,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:
|
||||
@@ -635,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
|
||||
|
||||
@@ -651,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
|
||||
@@ -700,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
|
||||
@@ -784,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']
|
||||
@@ -794,10 +806,22 @@ 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):
|
||||
@@ -812,7 +836,7 @@ class OpticalFlowMaskBase(MaskBase, ABC):
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = "/RyanOnTheInside/masks/"
|
||||
CATEGORY="RyanOnTheInside/OpticalFlowMasks"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -860,5 +884,52 @@ class OpticalFlowMaskBase(MaskBase, ABC):
|
||||
processed_masks = np.stack(result)
|
||||
return self.apply_mask_operation(processed_masks, masks, strength, **kwargs)
|
||||
|
||||
#TODO check if input mask is blank and just skip, then check children
|
||||
class FlexMaskBase(MaskBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
**super().INPUT_TYPES()["required"],
|
||||
"feature": ("FEATURE",),
|
||||
"feature_pipe": ("FEATURE_PIPE",),
|
||||
"feature_threshold": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = "RyanOnTheInside/AudioMasks"
|
||||
RETURN_TYPES = ("MASK",)
|
||||
FUNCTION = "main_function"
|
||||
|
||||
@abstractmethod
|
||||
def process_mask(self, mask: np.ndarray, feature_value: float, strength: float, **kwargs) -> np.ndarray:
|
||||
pass
|
||||
|
||||
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):
|
||||
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:
|
||||
processed_mask = mask
|
||||
|
||||
result.append(processed_mask)
|
||||
self.update_progress()
|
||||
|
||||
self.end_progress()
|
||||
|
||||
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
@@ -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)
|
||||
@@ -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
|
||||
@@ -22,8 +22,8 @@ class ParticleEmissionMask(ParticleSystemMaskBase):
|
||||
|
||||
RETURN_TYPES = ("MASK","IMAGE")
|
||||
FUNCTION = "main_function"
|
||||
CATEGORY = "Masks/Particle Systems"
|
||||
|
||||
|
||||
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)
|
||||
@@ -57,7 +57,10 @@ 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 {
|
||||
@@ -78,7 +81,6 @@ class Vortex:
|
||||
|
||||
RETURN_TYPES = ("VORTEX",)
|
||||
FUNCTION = "create_vortex"
|
||||
CATEGORY = "RyanOnTheInside/ParticleSystems"
|
||||
|
||||
def create_vortex(self, x, y, strength, radius, inward_factor, movement_speed, color, draw, previous_vortex=None):
|
||||
vortex = {
|
||||
@@ -99,7 +101,7 @@ class Vortex:
|
||||
|
||||
return (vortex_list,)
|
||||
|
||||
class GravityWell:
|
||||
class GravityWell(ParticleSystemModulatorBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -119,7 +121,6 @@ class GravityWell:
|
||||
|
||||
RETURN_TYPES = ("GRAVITY_WELL",)
|
||||
FUNCTION = "create_gravity_well"
|
||||
CATEGORY = "RyanOnTheInside/ParticleSystems"
|
||||
|
||||
def create_gravity_well(self, x, y, strength, radius, type, color, draw, previous_well=None):
|
||||
well = {
|
||||
@@ -139,7 +140,7 @@ class GravityWell:
|
||||
|
||||
return (well_list,)
|
||||
|
||||
class ParticleEmitter:
|
||||
class ParticleEmitter(ParticleSystemModulatorBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -153,6 +154,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,11 +167,11 @@ class ParticleEmitter:
|
||||
|
||||
RETURN_TYPES = ("PARTICLE_EMITTER",)
|
||||
FUNCTION = "create_emitter"
|
||||
CATEGORY = "RyanOnTheInside/ParticleSystems"
|
||||
|
||||
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,
|
||||
@@ -179,6 +182,8 @@ class ParticleEmitter:
|
||||
"emission_rate": emission_rate,
|
||||
"color": color,
|
||||
"initial_plume": initial_plume,
|
||||
"start_frame": start_frame,
|
||||
"end_frame": end_frame,
|
||||
}
|
||||
|
||||
if emitter_movement:
|
||||
@@ -197,7 +202,7 @@ class ParticleEmitter:
|
||||
|
||||
return (emitter_list,)
|
||||
|
||||
class SpringJointSetting:
|
||||
class SpringJointSetting(ParticleSystemModulatorBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -211,7 +216,6 @@ class SpringJointSetting:
|
||||
|
||||
RETURN_TYPES = ("SPRING_JOINT_SETTING",)
|
||||
FUNCTION = "create_setting"
|
||||
CATEGORY = "RyanOnTheInside/ParticleSystems"
|
||||
|
||||
def create_setting(self, stiffness, damping, rest_length, max_distance):
|
||||
return ({
|
||||
@@ -221,7 +225,7 @@ class SpringJointSetting:
|
||||
"max_distance": max_distance,
|
||||
},)
|
||||
|
||||
class EmitterMovement:
|
||||
class EmitterMovement(ParticleSystemModulatorBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -237,7 +241,6 @@ class EmitterMovement:
|
||||
|
||||
RETURN_TYPES = ("EMITTER_MOVEMENT",)
|
||||
FUNCTION = "create_movement"
|
||||
CATEGORY = "RyanOnTheInside/ParticleSystems"
|
||||
|
||||
def create_movement(self, emitter_x_frequency, emitter_y_frequency, direction_frequency,
|
||||
emitter_x_amplitude, emitter_y_amplitude, direction_amplitude):
|
||||
@@ -251,7 +254,7 @@ class EmitterMovement:
|
||||
}
|
||||
return (movement,)
|
||||
|
||||
class StaticBody:
|
||||
class StaticBody(ParticleSystemModulatorBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -274,7 +277,6 @@ class StaticBody:
|
||||
|
||||
RETURN_TYPES = ("STATIC_BODY",)
|
||||
FUNCTION = "create_static_body"
|
||||
CATEGORY = "RyanOnTheInside/ParticleSystems"
|
||||
|
||||
def create_static_body(self, shape_type, x1, y1, x2, y2, elasticity, friction, draw, color, previous_body=None):
|
||||
body = {
|
||||
@@ -297,7 +299,7 @@ class StaticBody:
|
||||
|
||||
return (body_list,)
|
||||
|
||||
class ParticleModulationBase:
|
||||
class ParticleModulationBase(ParticleSystemModulatorBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -312,8 +314,7 @@ class ParticleModulationBase:
|
||||
"previous_modulation": ("PARTICLE_MODULATION",),
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = "RyanOnTheInside/ParticleSystems"
|
||||
|
||||
|
||||
def create_modulation(self, start_frame, end_frame, effect_duration, temporal_easing, palindrome, previous_modulation=None):
|
||||
modulation = {
|
||||
|
||||
@@ -1,76 +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
|
||||
|
||||
"""
|
||||
@@ -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)
|
||||
@@ -77,6 +81,7 @@ class MaskMath(TemporalMaskBase):
|
||||
mask_b_np = mask_b.cpu().numpy() if isinstance(mask_b, torch.Tensor) else mask_b
|
||||
return super().main_function(masks, strength, mask_b=mask_b_np, combination_method=combination_method, **kwargs)
|
||||
|
||||
#TODO CONFIRM THAT NOTHING HAPPENS WITH EMPTY MASK
|
||||
class MaskRings(TemporalMaskBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -92,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)
|
||||
|
||||
@@ -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/"
|
||||
|
||||
@@ -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)
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
[project]
|
||||
name = "comfyui_ryanonyheinside"
|
||||
description = "Custom nodes introducing particle simulations, optical flow, and temporal masks"
|
||||
version = "1.2.0"
|
||||
description = "Custom nodes introducing particle simulations, optical flow, audio manipulation & reactivity, and temporal masks"
|
||||
version = "1.3.0"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = ["opencv-python==4.10.0","scipy","pillow","librosa","pymunk==6.8.1","matplotlib"]
|
||||
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"
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# ComfyUI RyanOnTheInside Node Pack
|
||||
|
||||
## Overview
|
||||
RyanOnTheInside node pack introduces the following (so far):
|
||||
- Particle Systems
|
||||
- Optical Flow
|
||||
- Temporal Masks
|
||||
RyanOnTheInside node pack introduces dynamic features to ComfyUI:
|
||||
|
||||
- 🎆 **Particle Systems**: Create mesmerizing, fluid-like effects
|
||||
- 🎵 **Audio & MIDI Processing**: Separate instruments and create audio-reactive visuals
|
||||
- 🎛️ **Flex Features**: Dynamic control over IPAdapters, Masks, and Images
|
||||
- 🌊 **Optical Flow**: Generate masks based on motion in videos
|
||||
- ⏳ **Temporal Effects**: Apply time-based transformations to masks and images
|
||||
|
||||
<table style="border-collapse: collapse; border: none;">
|
||||
<tr>
|
||||
@@ -23,11 +26,193 @@ RyanOnTheInside node pack introduces the following (so far):
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
*Some examples using particle emitters and vortices*
|
||||
*Examples showcasing various effects using particle emitters, vortices, and other node features*
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
Getting started with the RyanOnTheInside node pack is easy:
|
||||
|
||||
1. Install the node pack as described in the Installation section.
|
||||
2. Open ComfyUI and look for nodes prefixed with "RyanOnTheInside" in the node browser.
|
||||
3. Check out the example workflows on Civitai to see how different features can be used.
|
||||
|
||||
### Example Workflows on Civitai
|
||||
|
||||
For a range of example workflows showcasing the capabilities of this node pack, visit my Civitai profile:
|
||||
|
||||
[RyanOnTheInside Civitai Profile](https://civitai.com/user/RyanOnTheInside)
|
||||
|
||||
|
||||
## 🚀 Key Features
|
||||
|
||||
### 🎛️ Flex Features
|
||||
Dynamic control over various aspects of your workflow:
|
||||
- Modulate IPAdapters, Masks, and Images based on extracted features
|
||||
- Features include: Audio, MIDI, Motion, Depth, Color, Time, and more
|
||||
- Create adaptive, responsive effects that evolve with your input data
|
||||
|
||||
### 🎆 Particle Systems
|
||||
- Multiple particle emitters with customizable settings
|
||||
- Force fields (Gravity Wells and Vortices) for complex interactions
|
||||
- Boundary-respecting particles and static body interactions
|
||||
- Time-based particle modulation (size, speed, color)
|
||||
|
||||
### 🎵 Audio and MIDI Processing
|
||||
- Separate audio into individual instrument tracks
|
||||
- Extract features from audio and MIDI for visual effects
|
||||
- Create audio-reactive animations and transformations
|
||||
|
||||
### 🌊 Optical Flow
|
||||
- Generate masks based on movement in video sequences
|
||||
- Multiple optical flow algorithms available
|
||||
- Create motion-reactive particle simulations
|
||||
|
||||
### ⏳ Temporal Effects
|
||||
- Apply time-based transformations to masks and images
|
||||
- Create evolving animations with various effects (erosion, dilation, warping)
|
||||
- Customize with easing functions and palindrome support
|
||||
|
||||
## 📚 Detailed Documentation
|
||||
|
||||
<details>
|
||||
<summary><h3>Flex Features</h3></summary>
|
||||
|
||||
The Flex Features system allows for dynamic control over various aspects of your workflow by extracting and utilizing different types of features:
|
||||
|
||||
#### Audio Features
|
||||
- **Amplitude Envelope**: Tracks the overall volume changes in the audio
|
||||
- **RMS Energy**: Measures the average energy of the audio signal
|
||||
- **Spectral Centroid**: Indicates the "center of mass" of the spectrum
|
||||
- **Onset Detection**: Detects the beginning of musical notes or events
|
||||
- **Chroma Features**: Represents the tonal content of the audio
|
||||
|
||||
#### MIDI Features
|
||||
- **Velocity**: Intensity of MIDI note presses
|
||||
- **Pitch**: Musical note values
|
||||
- **Note On/Off**: Timing of note starts and ends
|
||||
- **Duration**: Length of individual notes
|
||||
- **Density**: Number of active notes over time
|
||||
- **Pitchbend**: Pitch modulation data
|
||||
- **Aftertouch**: Pressure applied after initial note press
|
||||
- **Various CC (Control Change) data**: Modulation, expression, sustain, etc.
|
||||
|
||||
#### Motion Features
|
||||
- **Mean Motion**: Average movement across the frame
|
||||
- **Max Motion**: Largest movement detected
|
||||
- **Motion Direction**: Overall direction of movement
|
||||
- **Horizontal/Vertical Motion**: Movement along specific axes
|
||||
- **Motion Complexity**: Variation in movement across the frame
|
||||
|
||||
#### Depth Features
|
||||
- **Mean Depth**: Average depth in the scene
|
||||
- **Depth Variance**: Variation in depth values
|
||||
- **Depth Range**: Difference between nearest and farthest points
|
||||
- **Gradient Magnitude**: Rate of depth change
|
||||
- **Foreground/Midground/Background Ratios**: Proportion of scene at different depths
|
||||
|
||||
#### Color Features
|
||||
- **Dominant Color**: Most prevalent color in the image
|
||||
- **Color Variance**: Spread of colors used
|
||||
- **Saturation**: Intensity of colors
|
||||
- **RGB Ratios**: Proportion of red, green, and blue in the image
|
||||
|
||||
#### Brightness Features
|
||||
- **Mean Brightness**: Overall lightness of the image
|
||||
- **Brightness Variance**: Spread of light and dark areas
|
||||
- **Brightness Histogram**: Distribution of brightness levels
|
||||
- **Dark/Mid/Bright Ratios**: Proportion of image at different brightness levels
|
||||
|
||||
#### Time Features
|
||||
- **Smooth**: Linear progression over time
|
||||
- **Accelerate**: Increasing rate of change
|
||||
- **Pulse**: Periodic oscillation
|
||||
- **Sawtooth**: Rapid rise followed by sudden drop
|
||||
- **Bounce**: Emulates a bouncing motion
|
||||
|
||||
These features can be used to control IPAdapters, Masks, and Images, creating dynamic and responsive effects that adapt to the input data.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Particle Systems</h3></summary>
|
||||
|
||||
Create mesmerizing, fluid-like effects through advanced particle simulation:
|
||||
|
||||
- **Multiple Emitters**: Create complex particle flows with independent settings
|
||||
- Customize spread, speed, size, color, and more for each emitter
|
||||
- **Force Fields**: Add depth to your simulations
|
||||
- Gravity Wells: Attract or repel particles
|
||||
- Vortices: Create swirling, tornado-like effects
|
||||
- **Global Settings**: Fine-tune the overall simulation
|
||||
- Adjust gravity and wind for the entire particle space
|
||||
- **Boundary Interactions**: Particles respect mask shapes and edges
|
||||
- **Static Bodies**: Add obstacles and surfaces for particles to interact with
|
||||
- **Spring Joints**: Create interconnected particle systems
|
||||
- **Time-based Modulation**: Evolve particle properties over time
|
||||
- Adjust size, speed, and color dynamically
|
||||
|
||||
These features allow for the creation of complex, dynamic particle effects that can be used to generate masks, animate elements, or create stunning visual effects.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Audio and MIDI Processing</h3></summary>
|
||||
|
||||
Transform your visuals with the power of sound and musical data:
|
||||
|
||||
#### Audio Processing
|
||||
- **Track Separation**: Isolate vocals, drums, bass, and other instruments
|
||||
- **Feature Extraction**: Analyze audio for amplitude, frequency, and tonal content
|
||||
- **Frequency Filtering**: Target specific frequency ranges for processing
|
||||
- **Visualizations**: Create complex audio-reactive visual effects
|
||||
|
||||
#### MIDI Processing
|
||||
- **Feature Extraction**: Utilize note velocity, pitch, timing, and control data
|
||||
- **Real-time Input**: Process live MIDI data for interactive visuals
|
||||
- **Sequencing**: Create rhythmic visual patterns based on MIDI sequences
|
||||
- **Control Mapping**: Use MIDI controllers to adjust visual parameters
|
||||
|
||||
These audio and MIDI processing capabilities enable the creation of music-driven animations, visualizations, and effects that respond dynamically to sound input.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Optical Flow</h3></summary>
|
||||
|
||||
Harness the power of motion to create stunning visual effects:
|
||||
|
||||
- **Multiple Algorithms**: Choose from various optical flow calculation methods
|
||||
- Farneback: Dense optical flow estimation
|
||||
- Lucas-Kanade: Sparse feature tracking
|
||||
- Pyramidal Lucas-Kanade: Multi-scale feature tracking for larger motions
|
||||
- **Motion-based Masking**: Generate masks that highlight areas of movement
|
||||
- **Flow Visualization**: Create visual representations of motion in video
|
||||
- **Particle Interaction**: Use optical flow data to influence particle systems
|
||||
- **Directional Effects**: Apply effects based on the direction of detected motion
|
||||
|
||||
Optical flow analysis allows for the creation of dynamic, motion-responsive effects that can be used for masking, animation, or as input for other visual processes.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>Temporal Effects</h3></summary>
|
||||
|
||||
Add the dimension of time to your mask and image effects:
|
||||
|
||||
- **Morphological Operations**: Apply time-varying erosion, dilation, opening, and closing
|
||||
- **Geometric Transformations**: Animate translation, rotation, and scaling over time
|
||||
- **Mask Combinations**: Blend multiple masks with time-based operations
|
||||
- **Warping Effects**: Create dynamic distortions using Perlin noise, radial, or swirl patterns
|
||||
- **Easing Functions**: Customize the rate of change for smooth animations
|
||||
- **Palindrome Mode**: Create seamless back-and-forth animations
|
||||
|
||||
These temporal effects enable the creation of evolving, dynamic animations that transform masks and images over the course of your video or animation sequence.
|
||||
|
||||
</details>
|
||||
|
||||
## Installation
|
||||
|
||||
Install via the ComfyUI Manager, or...
|
||||
Install via the ComfyUI Manager by searching for RyanOnTheInside, or...
|
||||
|
||||
1. Navigate to your ComfyUI's `custom_nodes` directory
|
||||
2. Clone the repository:
|
||||
@@ -44,330 +229,10 @@ Install via the ComfyUI Manager, or...
|
||||
```
|
||||
5. Restart ComfyUI if it's currently running and refresh your browser
|
||||
|
||||
|
||||
## Requirements
|
||||
### Requirements
|
||||
|
||||
See `requirements.txt` for a list of dependencies.
|
||||
|
||||
## 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 fine-tuned control over the mask processing across various node types. There are currently three categories of masks...
|
||||
|
||||
## Particle System Masks
|
||||
|
||||
Create dynamic, fluid-like effects through particle simulation:
|
||||
- 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
|
||||
|
||||
<img src="./assets/particle_examples.gif" width="400" alt="Particle System Demo">
|
||||
|
||||
*A few 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)
|
||||
|
||||
#### 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.
|
||||
|
||||
##### 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)
|
||||
|
||||
##### Outputs:
|
||||
- `MASK`: The resulting particle system binary mask
|
||||
- `IMAGE`: A visualization of the particle system - useful for masking different colors for different particle systems
|
||||
|
||||
|
||||
#### 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
|
||||
|
||||
|
||||
#### 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)
|
||||
|
||||
## Optical Flow Masks
|
||||
|
||||
Motion-based effects:
|
||||
|
||||
- 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
|
||||
|
||||
<img src="./assets/opticalflow_examples.gif" width="400" alt="Particle System Demo">
|
||||
|
||||
*Motion generated trails*
|
||||
|
||||
### 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)
|
||||
-->
|
||||
|
||||
## Temporal Masks
|
||||
|
||||
Implement time-based transformations and animations:
|
||||
|
||||
- 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
|
||||
|
||||
<img src="./assets/temporal_examples.gif" width="400" alt="Temporal Masks Examples">
|
||||
|
||||
*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.
|
||||
|
||||
|
||||
### Miscellaneous Nodes
|
||||
|
||||
#### MovingShape
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Both to the code and EXAMPLE WORKFLOWS!!! If you'd like to contribute:
|
||||
@@ -388,4 +253,4 @@ For more details, see the [full license text](https://creativecommons.org/licens
|
||||
|
||||
## Support
|
||||
|
||||
For issues, questions, or suggestions, please open an issue on the GitHub repository.
|
||||
For issues, questions, or suggestions, please open an issue on the GitHub repository.
|
||||
@@ -4,3 +4,5 @@ pillow
|
||||
librosa
|
||||
pymunk==6.8.1
|
||||
matplotlib
|
||||
openunmix
|
||||
mido
|
||||
+6
File diff suppressed because one or more lines are too long
+277
@@ -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};
|
||||
+3
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user