From 5dfe546fd7dbdfdfd6950766b62e1b59f8cdb4f8 Mon Sep 17 00:00:00 2001 From: RyanOnTheInside <7623207+ryanontheinside@users.noreply.github.com> Date: Mon, 26 Aug 2024 10:32:13 -0400 Subject: [PATCH] Added flex, audio, midi, help system --- __init__.py | 293 ++++++- data/midi_files/sample_midi.mid | Bin 0 -> 1103 bytes data/sample_midi.mp4 | Bin 0 -> 284923 bytes node_configs.py | 718 ++++++++++++++++++ nodes/audio/audio_nodes.py | 321 ++++++++ nodes/audio/audio_utils.py | 188 +++++ nodes/audio/midi_nodes.py | 1 + nodes/audio/midi_utils.py | 1 + nodes/flex/feature_externals.py | 38 + nodes/flex/feature_extractors.py | 146 ++++ nodes/flex/feature_modulation.py | 173 +++++ nodes/flex/feature_pipe.py | 11 + nodes/flex/features.py | 418 ++++++++++ nodes/flex/midi_feature.py | 204 +++++ nodes/flex/midi_feature_extractor.py | 163 ++++ nodes/images/flex_images.py | 373 +++++++++ nodes/images/image_base.py | 278 +++++++ nodes/images/image_utility_nodes.py | 31 + nodes/images/image_utils.py | 198 +++++ nodes/masks/audio_masks.py | 62 -- nodes/masks/flex_masks.py | 89 +++ nodes/masks/mask_base.py | 111 ++- nodes/masks/mask_utils.py | 28 +- nodes/masks/particle_system_masks.py | 37 +- nodes/masks/structure.txt | 76 -- nodes/masks/temporal_masks.py | 6 + nodes/masks/utility_nodes.py | 6 +- nodes/node_utilities.py | 5 + pyproject.toml | 6 +- readme.md | 521 +++++-------- requirements.txt | 2 + ryanontheinside_web_async/marked.min.js | 6 + ryanontheinside_web_async/protovis.min.js | 277 +++++++ ryanontheinside_web_async/purify.min.js | 3 + .../svg-path-properties.min.js | 2 + web/js/help_popup.js | 325 ++++++++ web/js/midi_extensions.js | 416 ++++++++++ web/js/piano.js | 183 +++++ 38 files changed, 5162 insertions(+), 554 deletions(-) create mode 100644 data/midi_files/sample_midi.mid create mode 100644 data/sample_midi.mp4 create mode 100644 node_configs.py create mode 100644 nodes/audio/audio_nodes.py create mode 100644 nodes/audio/audio_utils.py create mode 100644 nodes/audio/midi_nodes.py create mode 100644 nodes/audio/midi_utils.py create mode 100644 nodes/flex/feature_externals.py create mode 100644 nodes/flex/feature_extractors.py create mode 100644 nodes/flex/feature_modulation.py create mode 100644 nodes/flex/feature_pipe.py create mode 100644 nodes/flex/features.py create mode 100644 nodes/flex/midi_feature.py create mode 100644 nodes/flex/midi_feature_extractor.py create mode 100644 nodes/images/flex_images.py create mode 100644 nodes/images/image_base.py create mode 100644 nodes/images/image_utility_nodes.py create mode 100644 nodes/images/image_utils.py delete mode 100644 nodes/masks/audio_masks.py create mode 100644 nodes/masks/flex_masks.py delete mode 100644 nodes/masks/structure.txt create mode 100644 nodes/node_utilities.py create mode 100644 ryanontheinside_web_async/marked.min.js create mode 100644 ryanontheinside_web_async/protovis.min.js create mode 100644 ryanontheinside_web_async/purify.min.js create mode 100644 ryanontheinside_web_async/svg-path-properties.min.js create mode 100644 web/js/help_popup.js create mode 100644 web/js/midi_extensions.js create mode 100644 web/js/piano.js diff --git a/__init__.py b/__init__.py index 253af2c..f0a193f 100644 --- a/__init__.py +++ b/__init__.py @@ -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" -} \ No newline at end of file + "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 diff --git a/data/midi_files/sample_midi.mid b/data/midi_files/sample_midi.mid new file mode 100644 index 0000000000000000000000000000000000000000..172176f9a97d023d2a7fe7566a97409567dcf6a2 GIT binary patch literal 1103 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�a!>{ioI_zl5oj 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
  • ${e}
  • \n`}checkbox(e){return"'}paragraph(e){return`

    ${e}

    \n`}table(e,t){return t&&(t=`${t}`),"\n\n"+e+"\n"+t+"
    \n"}tablerow(e){return`\n${e}\n`}tablecell(e,t){const n=t.header?"th":"td";return(t.align?`<${n} align="${t.align}">`:`<${n}>`)+e+`\n`}strong(e){return`${e}`}em(e){return`${e}`}codespan(e){return`${e}`}br(){return"
    "}del(e){return`${e}`}link(e,t,n){const s=g(e);if(null===s)return n;let r='
    ",r}image(e,t,n){const s=g(e);if(null===s)return n;let r=`${n}0&&"paragraph"===n.tokens[0].type?(n.tokens[0].text=e+" "+n.tokens[0].text,n.tokens[0].tokens&&n.tokens[0].tokens.length>0&&"text"===n.tokens[0].tokens[0].type&&(n.tokens[0].tokens[0].text=e+" "+n.tokens[0].tokens[0].text)):n.tokens.unshift({type:"text",text:e+" "}):o+=e+" "}o+=this.parse(n.tokens,i),l+=this.renderer.listitem(o,r,!!s)}n+=this.renderer.list(l,t,s);continue}case"html":{const e=r;n+=this.renderer.html(e.text,e.block);continue}case"paragraph":{const e=r;n+=this.renderer.paragraph(this.parseInline(e.tokens));continue}case"text":{let i=r,l=i.tokens?this.parseInline(i.tokens):i.text;for(;s+1{const r=e[s].flat(1/0);n=n.concat(this.walkTokens(r,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new se(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.renderer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new w(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new le;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.hooks[s],i=t[s];le.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return ne.lex(e,t??this.defaults)}parser(e,t){return ie.parse(e,t??this.defaults)}#e(e,t){return(n,s)=>{const r={...s},i={...this.defaults,...r};!0===this.defaults.async&&!1===r.async&&(i.silent||console.warn("marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored."),i.async=!0);const l=this.#t(!!i.silent,!!i.async);if(null==n)return l(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof n)return l(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));if(i.hooks&&(i.hooks.options=i),i.async)return Promise.resolve(i.hooks?i.hooks.preprocess(n):n).then((t=>e(t,i))).then((e=>i.hooks?i.hooks.processAllTokens(e):e)).then((e=>i.walkTokens?Promise.all(this.walkTokens(e,i.walkTokens)).then((()=>e)):e)).then((e=>t(e,i))).then((e=>i.hooks?i.hooks.postprocess(e):e)).catch(l);try{i.hooks&&(n=i.hooks.preprocess(n));let s=e(n,i);i.hooks&&(s=i.hooks.processAllTokens(s)),i.walkTokens&&this.walkTokens(s,i.walkTokens);let r=t(s,i);return i.hooks&&(r=i.hooks.postprocess(r)),r}catch(e){return l(e)}}}#t(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

    An error occurred:

    "+c(n.message+"",!0)+"
    ";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const ae=new oe;function ce(e,t){return ae.parse(e,t)}ce.options=ce.setOptions=function(e){return ae.setOptions(e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.getDefaults=t,ce.defaults=e.defaults,ce.use=function(...e){return ae.use(...e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.walkTokens=function(e,t){return ae.walkTokens(e,t)},ce.parseInline=ae.parseInline,ce.Parser=ie,ce.parser=ie.parse,ce.Renderer=se,ce.TextRenderer=re,ce.Lexer=ne,ce.lexer=ne.lex,ce.Tokenizer=w,ce.Hooks=le,ce.parse=ce;const he=ce.options,pe=ce.setOptions,ue=ce.use,ke=ce.walkTokens,ge=ce.parseInline,fe=ce,de=ie.parse,xe=ne.lex;e.Hooks=le,e.Lexer=ne,e.Marked=oe,e.Parser=ie,e.Renderer=se,e.TextRenderer=re,e.Tokenizer=w,e.getDefaults=t,e.lexer=xe,e.marked=ce,e.options=he,e.parse=fe,e.parseInline=ge,e.parser=de,e.setOptions=pe,e.use=ue,e.walkTokens=ke})); diff --git a/ryanontheinside_web_async/protovis.min.js b/ryanontheinside_web_async/protovis.min.js new file mode 100644 index 0000000..dfb8416 --- /dev/null +++ b/ryanontheinside_web_async/protovis.min.js @@ -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>>0,f=0;f=d)throw new Error("reduce: no values, no initial value");}for(;f=0&&d=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.length3)t=t.replace(/\B(?=(?:\d{3})+(?!\d))/g,n);if(!l&&t.lengthd)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;fc){b.length=d;for(var f=c;fc?1:0}; +pv.reverseOrder=function(b,c){return cb?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(ic)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++)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;h0?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=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=p0;i--)l.push(-g(-j)*i);else{for(;jh[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;j1?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;i360)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;i1){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;g9){k=3/Math.sqrt(k);f[h]= +k*i*d[h];f[h+1]=k*j*d[h]}}for(h=0;h2&&(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;h2&&(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;i1)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=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 "+(k1?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=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;ih.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;f0;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=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=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.xi)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)*xg)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(!(xg)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;ig,o=sh){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(mm)m=p}for(var r=0;rc.max?c.max:g.x;if(d)for(g=f;g;g=g.next)g.y=g.yd.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)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;bb.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;wt)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;s0){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;m0.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;A0){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;An.min){n.sim.step(); +q=true}q&&d.render()},42)}else for(k=0;kg)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>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;mk.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;lk)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}; diff --git a/ryanontheinside_web_async/purify.min.js b/ryanontheinside_web_async/purify.min.js new file mode 100644 index 0000000..c2f5164 --- /dev/null +++ b/ryanontheinside_web_async/purify.min.js @@ -0,0 +1,3 @@ +/*! @license DOMPurify 3.0.11 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.0.11/LICENSE */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:i,seal:a,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;i||(i=function(e){return e}),a||(a=function(e){return e}),c||(c=function(e,t,n){return e.apply(t,n)}),s||(s=function(e,t){return new e(...t)});const u=b(Array.prototype.forEach),m=b(Array.prototype.pop),p=b(Array.prototype.push),f=b(String.prototype.toLowerCase),d=b(String.prototype.toString),h=b(String.prototype.match),g=b(String.prototype.replace),T=b(String.prototype.indexOf),y=b(String.prototype.trim),E=b(Object.prototype.hasOwnProperty),A=b(RegExp.prototype.test),_=(N=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n1?n-1:0),r=1;r2&&void 0!==arguments[2]?arguments[2]:f;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function R(e){for(let t=0;t/gm),B=a(/\${[\w\W]*}/gm),W=a(/^data-[\-\w.\u00B7-\uFFFF]/),G=a(/^aria-[\-\w]+$/),Y=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),j=a(/^(?:\w+script|data):/i),X=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),q=a(/^html$/i),$=a(/^[a-z][.\w]*(-[.\w]+)+$/i);var K=Object.freeze({__proto__:null,MUSTACHE_EXPR:H,ERB_EXPR:z,TMPLIT_EXPR:B,DATA_ATTR:W,ARIA_ATTR:G,IS_ALLOWED_URI:Y,IS_SCRIPT_OR_DATA:j,ATTR_WHITESPACE:X,DOCTYPE_NAME:q,CUSTOM_ELEMENT:$});const V=function(){return"undefined"==typeof window?null:window},Z=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}};var J=function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:V();const o=e=>t(e);if(o.version="3.0.11",o.removed=[],!n||!n.document||9!==n.document.nodeType)return o.isSupported=!1,o;let{document:r}=n;const a=r,c=a.currentScript,{DocumentFragment:s,HTMLTemplateElement:N,Node:b,Element:R,NodeFilter:H,NamedNodeMap:z=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:B,DOMParser:W,trustedTypes:G}=n,j=R.prototype,X=L(j,"cloneNode"),$=L(j,"nextSibling"),J=L(j,"childNodes"),Q=L(j,"parentNode");if("function"==typeof N){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let ee,te="";const{implementation:ne,createNodeIterator:oe,createDocumentFragment:re,getElementsByTagName:ie}=r,{importNode:ae}=a;let le={};o.isSupported="function"==typeof e&&"function"==typeof Q&&ne&&void 0!==ne.createHTMLDocument;const{MUSTACHE_EXPR:ce,ERB_EXPR:se,TMPLIT_EXPR:ue,DATA_ATTR:me,ARIA_ATTR:pe,IS_SCRIPT_OR_DATA:fe,ATTR_WHITESPACE:de,CUSTOM_ELEMENT:he}=K;let{IS_ALLOWED_URI:ge}=K,Te=null;const ye=S({},[...D,...C,...O,...v,...M]);let Ee=null;const Ae=S({},[...I,...U,...P,...F]);let _e=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Ne=null,be=null,Se=!0,Re=!0,we=!1,Le=!0,De=!1,Ce=!0,Oe=!1,xe=!1,ve=!1,ke=!1,Me=!1,Ie=!1,Ue=!0,Pe=!1;const Fe="user-content-";let He=!0,ze=!1,Be={},We=null;const Ge=S({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ye=null;const je=S({},["audio","video","img","source","image","track"]);let Xe=null;const qe=S({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),$e="http://www.w3.org/1998/Math/MathML",Ke="http://www.w3.org/2000/svg",Ve="http://www.w3.org/1999/xhtml";let Ze=Ve,Je=!1,Qe=null;const et=S({},[$e,Ke,Ve],d);let tt=null;const nt=["application/xhtml+xml","text/html"],ot="text/html";let rt=null,it=null;const at=r.createElement("form"),lt=function(e){return e instanceof RegExp||e instanceof Function},ct=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!it||it!==e){if(e&&"object"==typeof e||(e={}),e=w(e),tt=-1===nt.indexOf(e.PARSER_MEDIA_TYPE)?ot:e.PARSER_MEDIA_TYPE,rt="application/xhtml+xml"===tt?d:f,Te=E(e,"ALLOWED_TAGS")?S({},e.ALLOWED_TAGS,rt):ye,Ee=E(e,"ALLOWED_ATTR")?S({},e.ALLOWED_ATTR,rt):Ae,Qe=E(e,"ALLOWED_NAMESPACES")?S({},e.ALLOWED_NAMESPACES,d):et,Xe=E(e,"ADD_URI_SAFE_ATTR")?S(w(qe),e.ADD_URI_SAFE_ATTR,rt):qe,Ye=E(e,"ADD_DATA_URI_TAGS")?S(w(je),e.ADD_DATA_URI_TAGS,rt):je,We=E(e,"FORBID_CONTENTS")?S({},e.FORBID_CONTENTS,rt):Ge,Ne=E(e,"FORBID_TAGS")?S({},e.FORBID_TAGS,rt):{},be=E(e,"FORBID_ATTR")?S({},e.FORBID_ATTR,rt):{},Be=!!E(e,"USE_PROFILES")&&e.USE_PROFILES,Se=!1!==e.ALLOW_ARIA_ATTR,Re=!1!==e.ALLOW_DATA_ATTR,we=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Le=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,De=e.SAFE_FOR_TEMPLATES||!1,Ce=!1!==e.SAFE_FOR_XML,Oe=e.WHOLE_DOCUMENT||!1,ke=e.RETURN_DOM||!1,Me=e.RETURN_DOM_FRAGMENT||!1,Ie=e.RETURN_TRUSTED_TYPE||!1,ve=e.FORCE_BODY||!1,Ue=!1!==e.SANITIZE_DOM,Pe=e.SANITIZE_NAMED_PROPS||!1,He=!1!==e.KEEP_CONTENT,ze=e.IN_PLACE||!1,ge=e.ALLOWED_URI_REGEXP||Y,Ze=e.NAMESPACE||Ve,_e=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&<(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(_e.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&<(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(_e.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(_e.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),De&&(Re=!1),Me&&(ke=!0),Be&&(Te=S({},M),Ee=[],!0===Be.html&&(S(Te,D),S(Ee,I)),!0===Be.svg&&(S(Te,C),S(Ee,U),S(Ee,F)),!0===Be.svgFilters&&(S(Te,O),S(Ee,U),S(Ee,F)),!0===Be.mathMl&&(S(Te,v),S(Ee,P),S(Ee,F))),e.ADD_TAGS&&(Te===ye&&(Te=w(Te)),S(Te,e.ADD_TAGS,rt)),e.ADD_ATTR&&(Ee===Ae&&(Ee=w(Ee)),S(Ee,e.ADD_ATTR,rt)),e.ADD_URI_SAFE_ATTR&&S(Xe,e.ADD_URI_SAFE_ATTR,rt),e.FORBID_CONTENTS&&(We===Ge&&(We=w(We)),S(We,e.FORBID_CONTENTS,rt)),He&&(Te["#text"]=!0),Oe&&S(Te,["html","head","body"]),Te.table&&(S(Te,["tbody"]),delete Ne.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw _('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw _('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');ee=e.TRUSTED_TYPES_POLICY,te=ee.createHTML("")}else void 0===ee&&(ee=Z(G,c)),null!==ee&&"string"==typeof te&&(te=ee.createHTML(""));i&&i(e),it=e}},st=S({},["mi","mo","mn","ms","mtext"]),ut=S({},["foreignobject","desc","title","annotation-xml"]),mt=S({},["title","style","font","a","script"]),pt=S({},[...C,...O,...x]),ft=S({},[...v,...k]),dt=function(e){let t=Q(e);t&&t.tagName||(t={namespaceURI:Ze,tagName:"template"});const n=f(e.tagName),o=f(t.tagName);return!!Qe[e.namespaceURI]&&(e.namespaceURI===Ke?t.namespaceURI===Ve?"svg"===n:t.namespaceURI===$e?"svg"===n&&("annotation-xml"===o||st[o]):Boolean(pt[n]):e.namespaceURI===$e?t.namespaceURI===Ve?"math"===n:t.namespaceURI===Ke?"math"===n&&ut[o]:Boolean(ft[n]):e.namespaceURI===Ve?!(t.namespaceURI===Ke&&!ut[o])&&(!(t.namespaceURI===$e&&!st[o])&&(!ft[n]&&(mt[n]||!pt[n]))):!("application/xhtml+xml"!==tt||!Qe[e.namespaceURI]))},ht=function(e){p(o.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){e.remove()}},gt=function(e,t){try{p(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){p(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Ee[e])if(ke||Me)try{ht(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},Tt=function(e){let t=null,n=null;if(ve)e=""+e;else{const t=h(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===tt&&Ze===Ve&&(e=''+e+"");const o=ee?ee.createHTML(e):e;if(Ze===Ve)try{t=(new W).parseFromString(o,tt)}catch(e){}if(!t||!t.documentElement){t=ne.createDocument(Ze,"template",null);try{t.documentElement.innerHTML=Je?te:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),Ze===Ve?ie.call(t,Oe?"html":"body")[0]:Oe?t.documentElement:i},yt=function(e){return oe.call(e.ownerDocument||e,e,H.SHOW_ELEMENT|H.SHOW_COMMENT|H.SHOW_TEXT|H.SHOW_PROCESSING_INSTRUCTION|H.SHOW_CDATA_SECTION,null)},Et=function(e){return e instanceof B&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof z)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},At=function(e){return"function"==typeof b&&e instanceof b},_t=function(e,t,n){le[e]&&u(le[e],(e=>{e.call(o,t,n,it)}))},Nt=function(e){let t=null;if(_t("beforeSanitizeElements",e,null),Et(e))return ht(e),!0;const n=rt(e.nodeName);if(_t("uponSanitizeElement",e,{tagName:n,allowedTags:Te}),e.hasChildNodes()&&!At(e.firstElementChild)&&A(/<[/\w]/g,e.innerHTML)&&A(/<[/\w]/g,e.textContent))return ht(e),!0;if(7===e.nodeType)return ht(e),!0;if(Ce&&8===e.nodeType&&A(/<[/\w]/g,e.data))return ht(e),!0;if(!Te[n]||Ne[n]){if(!Ne[n]&&St(n)){if(_e.tagNameCheck instanceof RegExp&&A(_e.tagNameCheck,n))return!1;if(_e.tagNameCheck instanceof Function&&_e.tagNameCheck(n))return!1}if(He&&!We[n]){const t=Q(e)||e.parentNode,n=J(e)||e.childNodes;if(n&&t){for(let o=n.length-1;o>=0;--o)t.insertBefore(X(n[o],!0),$(e))}}return ht(e),!0}return e instanceof R&&!dt(e)?(ht(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!A(/<\/no(script|embed|frames)/i,e.innerHTML)?(De&&3===e.nodeType&&(t=e.textContent,u([ce,se,ue],(e=>{t=g(t,e," ")})),e.textContent!==t&&(p(o.removed,{element:e.cloneNode()}),e.textContent=t)),_t("afterSanitizeElements",e,null),!1):(ht(e),!0)},bt=function(e,t,n){if(Ue&&("id"===t||"name"===t)&&(n in r||n in at))return!1;if(Re&&!be[t]&&A(me,t));else if(Se&&A(pe,t));else if(!Ee[t]||be[t]){if(!(St(e)&&(_e.tagNameCheck instanceof RegExp&&A(_e.tagNameCheck,e)||_e.tagNameCheck instanceof Function&&_e.tagNameCheck(e))&&(_e.attributeNameCheck instanceof RegExp&&A(_e.attributeNameCheck,t)||_e.attributeNameCheck instanceof Function&&_e.attributeNameCheck(t))||"is"===t&&_e.allowCustomizedBuiltInElements&&(_e.tagNameCheck instanceof RegExp&&A(_e.tagNameCheck,n)||_e.tagNameCheck instanceof Function&&_e.tagNameCheck(n))))return!1}else if(Xe[t]);else if(A(ge,g(n,de,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==T(n,"data:")||!Ye[e]){if(we&&!A(fe,g(n,de,"")));else if(n)return!1}else;return!0},St=function(e){return"annotation-xml"!==e&&h(e,he)},Rt=function(e){_t("beforeSanitizeAttributes",e,null);const{attributes:t}=e;if(!t)return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Ee};let r=t.length;for(;r--;){const i=t[r],{name:a,namespaceURI:l,value:c}=i,s=rt(a);let p="value"===a?c:y(c);if(n.attrName=s,n.attrValue=p,n.keepAttr=!0,n.forceKeepAttr=void 0,_t("uponSanitizeAttribute",e,n),p=n.attrValue,n.forceKeepAttr)continue;if(gt(a,e),!n.keepAttr)continue;if(!Le&&A(/\/>/i,p)){gt(a,e);continue}De&&u([ce,se,ue],(e=>{p=g(p,e," ")}));const f=rt(e.nodeName);if(bt(f,s,p)){if(!Pe||"id"!==s&&"name"!==s||(gt(a,e),p=Fe+p),ee&&"object"==typeof G&&"function"==typeof G.getAttributeType)if(l);else switch(G.getAttributeType(f,s)){case"TrustedHTML":p=ee.createHTML(p);break;case"TrustedScriptURL":p=ee.createScriptURL(p)}try{l?e.setAttributeNS(l,a,p):e.setAttribute(a,p),m(o.removed)}catch(e){}}}_t("afterSanitizeAttributes",e,null)},wt=function e(t){let n=null;const o=yt(t);for(_t("beforeSanitizeShadowDOM",t,null);n=o.nextNode();)_t("uponSanitizeShadowNode",n,null),Nt(n)||(n.content instanceof s&&e(n.content),Rt(n));_t("afterSanitizeShadowDOM",t,null)};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,i=null,l=null;if(Je=!e,Je&&(e="\x3c!--\x3e"),"string"!=typeof e&&!At(e)){if("function"!=typeof e.toString)throw _("toString is not a function");if("string"!=typeof(e=e.toString()))throw _("dirty is not a string, aborting")}if(!o.isSupported)return e;if(xe||ct(t),o.removed=[],"string"==typeof e&&(ze=!1),ze){if(e.nodeName){const t=rt(e.nodeName);if(!Te[t]||Ne[t])throw _("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof b)n=Tt("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),1===r.nodeType&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!ke&&!De&&!Oe&&-1===e.indexOf("<"))return ee&&Ie?ee.createHTML(e):e;if(n=Tt(e),!n)return ke?null:Ie?te:""}n&&ve&&ht(n.firstChild);const c=yt(ze?e:n);for(;i=c.nextNode();)Nt(i)||(i.content instanceof s&&wt(i.content),Rt(i));if(ze)return e;if(ke){if(Me)for(l=re.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(Ee.shadowroot||Ee.shadowrootmode)&&(l=ae.call(a,l,!0)),l}let m=Oe?n.outerHTML:n.innerHTML;return Oe&&Te["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&A(q,n.ownerDocument.doctype.name)&&(m="\n"+m),De&&u([ce,se,ue],(e=>{m=g(m,e," ")})),ee&&Ie?ee.createHTML(m):m},o.setConfig=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};ct(e),xe=!0},o.clearConfig=function(){it=null,xe=!1},o.isValidAttribute=function(e,t,n){it||ct({});const o=rt(e),r=rt(t);return bt(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&(le[e]=le[e]||[],p(le[e],t))},o.removeHook=function(e){if(le[e])return m(le[e])},o.removeHooks=function(e){le[e]&&(le[e]=[])},o.removeAllHooks=function(){le={}},o}();return J})); +//# sourceMappingURL=purify.min.js.map diff --git a/ryanontheinside_web_async/svg-path-properties.min.js b/ryanontheinside_web_async/svg-path-properties.min.js new file mode 100644 index 0000000..88d47e0 --- /dev/null +++ b/ryanontheinside_web_async/svg-path-properties.min.js @@ -0,0 +1,2 @@ +// http://geoexamples.com/path-properties/ v1.2.0 Copyright 2023 Roger Veciana i Rovira +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((t="undefined"!=typeof globalThis?globalThis:t||self).svgPathProperties={})}(this,(function(t){"use strict";function n(t,n){for(var e=0;et.length)&&(n=t.length);for(var e=0,i=new Array(n);eu.length&&(t=u.length);var n=f({x:u.x0,y:u.y0},u.rx,u.ry,u.xAxisRotate,u.LargeArcFlag,u.SweepFlag,{x:u.x1,y:u.y1},t/u.length);return{x:n.x,y:n.y}})),i(this,"getTangentAtLength",(function(t){t<0?t=0:t>u.length&&(t=u.length);var n,e=.05,i=u.getPointAtLength(t);t<0?t=0:t>u.length&&(t=u.length);var r=(n=t1&&(n=Math.sqrt(c)*n,e=Math.sqrt(c)*e);var f=(Math.pow(n,2)*Math.pow(e,2)-Math.pow(n,2)*Math.pow(l.y,2)-Math.pow(e,2)*Math.pow(l.x,2))/(Math.pow(n,2)*Math.pow(l.y,2)+Math.pow(e,2)*Math.pow(l.x,2));f=f<0?0:f;var y=(r!==h?1:-1)*Math.sqrt(f),v=y*(n*l.y/e),M=y*(-e*l.x/n),L={x:Math.cos(o)*v-Math.sin(o)*M+(t.x+s.x)/2,y:Math.sin(o)*v+Math.cos(o)*M+(t.y+s.y)/2},d={x:(l.x-v)/n,y:(l.y-M)/e},A=w({x:1,y:0},d),b=w(d,{x:(-l.x-v)/n,y:(-l.y-M)/e});!h&&b>0?b-=2*Math.PI:h&&b<0&&(b+=2*Math.PI);var P=A+(b%=2*Math.PI)*a,m=n*Math.cos(P),T=e*Math.sin(P);return{x:Math.cos(o)*m-Math.sin(o)*T+L.x,y:Math.sin(o)*m+Math.cos(o)*T+L.y,ellipticalArcStartAngle:A,ellipticalArcEndAngle:A+b,ellipticalArcAngle:P,ellipticalArcCenter:L,resultantRx:n,resultantRy:e}},y=function(t,n){t=t||500;for(var e,i=0,r=[],h=[],s=n(0),a=0;a0?Math.sqrt(l*l+c):0,y=u*u+c>0?Math.sqrt(u*u+c):0,p=u+Math.sqrt(u*u+c)!==0&&(l+f)/(u+y)!=0?c*Math.log(Math.abs((l+f)/(u+y))):0;return Math.sqrt(a)/2*(l*f-u*y+p)},_=function(t,n,e){return{x:2*(1-e)*(t[1]-t[0])+2*e*(t[2]-t[1]),y:2*(1-e)*(n[1]-n[0])+2*e*(n[2]-n[1])}};function S(t,n,e){var i=N(1,e,t),r=N(1,e,n),h=i*i+r*r;return Math.sqrt(h)}var N=function t(n,e,i){var r,h,s=i.length-1;if(0===s)return 0;if(0===n){h=0;for(var a=0;a<=s;a++)h+=A[s][a]*Math.pow(1-e,s-a)*Math.pow(e,a)*i[a];return h}r=new Array(s);for(var o=0;o.001;){var a=e(r+h),o=Math.abs(t-a)/n;if(o500)break}return r},j=e((function(t,n,e,r,h,s,a,o){var g=this;i(this,"a",void 0),i(this,"b",void 0),i(this,"c",void 0),i(this,"d",void 0),i(this,"length",void 0),i(this,"getArcLength",void 0),i(this,"getPoint",void 0),i(this,"getDerivative",void 0),i(this,"getTotalLength",(function(){return g.length})),i(this,"getPointAtLength",(function(t){var n=[g.a.x,g.b.x,g.c.x,g.d.x],e=[g.a.y,g.b.y,g.c.y,g.d.y],i=C(t,g.length,(function(t){return g.getArcLength(n,e,t)}));return g.getPoint(n,e,i)})),i(this,"getTangentAtLength",(function(t){var n=[g.a.x,g.b.x,g.c.x,g.d.x],e=[g.a.y,g.b.y,g.c.y,g.d.y],i=C(t,g.length,(function(t){return g.getArcLength(n,e,t)})),r=g.getDerivative(n,e,i),h=Math.sqrt(r.x*r.x+r.y*r.y);return h>0?{x:r.x/h,y:r.y/h}:{x:0,y:0}})),i(this,"getPropertiesAtLength",(function(t){var n,e=[g.a.x,g.b.x,g.c.x,g.d.x],i=[g.a.y,g.b.y,g.c.y,g.d.y],r=C(t,g.length,(function(t){return g.getArcLength(e,i,t)})),h=g.getDerivative(e,i,r),s=Math.sqrt(h.x*h.x+h.y*h.y);n=s>0?{x:h.x/s,y:h.y/s}:{x:0,y:0};var a=g.getPoint(e,i,r);return{x:a.x,y:a.y,tangentX:n.x,tangentY:n.y}})),i(this,"getC",(function(){return g.c})),i(this,"getD",(function(){return g.d})),this.a={x:t,y:n},this.b={x:e,y:r},this.c={x:h,y:s},void 0!==a&&void 0!==o?(this.getArcLength=m,this.getPoint=b,this.getDerivative=P,this.d={x:a,y:o}):(this.getArcLength=q,this.getPoint=T,this.getDerivative=_,this.d={x:0,y:0}),this.length=this.getArcLength([this.a.x,this.b.x,this.c.x,this.d.x],[this.a.y,this.b.y,this.c.y,this.d.y],1)})),O=e((function(t){var n=this;i(this,"length",0),i(this,"partial_lengths",[]),i(this,"functions",[]),i(this,"initial_point",null),i(this,"getPartAtLength",(function(t){t<0?t=0:t>n.length&&(t=n.length);for(var e=n.partial_lengths.length-1;n.partial_lengths[e]>=t&&e>0;)e--;return e++,{fraction:t-n.partial_lengths[e-1],i:e}})),i(this,"getTotalLength",(function(){return n.length})),i(this,"getPointAtLength",(function(t){var e=n.getPartAtLength(t),i=n.functions[e.i];if(i)return i.getPointAtLength(e.fraction);if(n.initial_point)return n.initial_point;throw new Error("Wrong function at this part.")})),i(this,"getTangentAtLength",(function(t){var e=n.getPartAtLength(t),i=n.functions[e.i];if(i)return i.getTangentAtLength(e.fraction);if(n.initial_point)return{x:0,y:0};throw new Error("Wrong function at this part.")})),i(this,"getPropertiesAtLength",(function(t){var e=n.getPartAtLength(t),i=n.functions[e.i];if(i)return i.getPropertiesAtLength(e.fraction);if(n.initial_point)return{x:n.initial_point.x,y:n.initial_point.y,tangentX:0,tangentY:0};throw new Error("Wrong function at this part.")})),i(this,"getParts",(function(){for(var t=[],e=0;e0?t:"M0,0").match(o);if(!n)throw new Error("No path elements found in string ".concat(t));return n.reduce((function(t,n){var e=n.charAt(0),i=e.toLowerCase(),h=u(n.substring(1));if("m"===i&&h.length>2&&(t.push([e].concat(r(h.splice(0,2)))),i="l",e="m"===e?"l":"L"),"a"===i.toLowerCase()&&(5===h.length||6===h.length)){var s=n.substring(1).trim().split(" ");h=[Number(s[0]),Number(s[1]),Number(s[2]),Number(s[3].charAt(0)),Number(s[3].charAt(1)),Number(s[3].substring(2)),Number(s[4])]}for(;h.length>=0;){if(h.length===a[i]){t.push([e].concat(r(h.splice(0,a[i]))));break}if(h.length0?(this.length+=e.getTotalLength(),this.functions.push(e),s=[h[y][5]+s[0],h[y][6]+s[1]]):this.functions.push(new l(s[0],s[0],s[1],s[1]));else if("S"===h[y][0]){if(y>0&&["C","c","S","s"].indexOf(h[y-1][0])>-1){if(e){var p=e.getC();e=new j(s[0],s[1],2*s[0]-p.x,2*s[1]-p.y,h[y][1],h[y][2],h[y][3],h[y][4])}}else e=new j(s[0],s[1],s[0],s[1],h[y][1],h[y][2],h[y][3],h[y][4]);e&&(this.length+=e.getTotalLength(),s=[h[y][3],h[y][4]],this.functions.push(e))}else if("s"===h[y][0]){if(y>0&&["C","c","S","s"].indexOf(h[y-1][0])>-1){if(e){var x=e.getC(),v=e.getD();e=new j(s[0],s[1],s[0]+v.x-x.x,s[1]+v.y-x.y,s[0]+h[y][1],s[1]+h[y][2],s[0]+h[y][3],s[1]+h[y][4])}}else e=new j(s[0],s[1],s[0],s[1],s[0]+h[y][1],s[1]+h[y][2],s[0]+h[y][3],s[1]+h[y][4]);e&&(this.length+=e.getTotalLength(),s=[h[y][3]+s[0],h[y][4]+s[1]],this.functions.push(e))}else if("Q"===h[y][0]){if(s[0]==h[y][1]&&s[1]==h[y][2]){var M=new l(h[y][1],h[y][3],h[y][2],h[y][4]);this.length+=M.getTotalLength(),this.functions.push(M)}else e=new j(s[0],s[1],h[y][1],h[y][2],h[y][3],h[y][4],void 0,void 0),this.length+=e.getTotalLength(),this.functions.push(e);s=[h[y][3],h[y][4]],g=[h[y][1],h[y][2]]}else if("q"===h[y][0]){if(0!=h[y][1]||0!=h[y][2])e=new j(s[0],s[1],s[0]+h[y][1],s[1]+h[y][2],s[0]+h[y][3],s[1]+h[y][4],void 0,void 0),this.length+=e.getTotalLength(),this.functions.push(e);else{var w=new l(s[0]+h[y][1],s[0]+h[y][3],s[1]+h[y][2],s[1]+h[y][4]);this.length+=w.getTotalLength(),this.functions.push(w)}g=[s[0]+h[y][1],s[1]+h[y][2]],s=[h[y][3]+s[0],h[y][4]+s[1]]}else if("T"===h[y][0]){if(y>0&&["Q","q","T","t"].indexOf(h[y-1][0])>-1)e=new j(s[0],s[1],2*s[0]-g[0],2*s[1]-g[1],h[y][1],h[y][2],void 0,void 0),this.functions.push(e),this.length+=e.getTotalLength();else{var L=new l(s[0],h[y][1],s[1],h[y][2]);this.functions.push(L),this.length+=L.getTotalLength()}g=[2*s[0]-g[0],2*s[1]-g[1]],s=[h[y][1],h[y][2]]}else if("t"===h[y][0]){if(y>0&&["Q","q","T","t"].indexOf(h[y-1][0])>-1)e=new j(s[0],s[1],2*s[0]-g[0],2*s[1]-g[1],s[0]+h[y][1],s[1]+h[y][2],void 0,void 0),this.length+=e.getTotalLength(),this.functions.push(e);else{var d=new l(s[0],s[0]+h[y][1],s[1],s[1]+h[y][2]);this.length+=d.getTotalLength(),this.functions.push(d)}g=[2*s[0]-g[0],2*s[1]-g[1]],s=[h[y][1]+s[0],h[y][2]+s[1]]}else if("A"===h[y][0]){var A=new c(s[0],s[1],h[y][1],h[y][2],h[y][3],1===h[y][4],1===h[y][5],h[y][6],h[y][7]);this.length+=A.getTotalLength(),s=[h[y][6],h[y][7]],this.functions.push(A)}else if("a"===h[y][0]){var b=new c(s[0],s[1],h[y][1],h[y][2],h[y][3],1===h[y][4],1===h[y][5],s[0]+h[y][6],s[1]+h[y][7]);this.length+=b.getTotalLength(),s=[s[0]+h[y][6],s[1]+h[y][7]],this.functions.push(b)}this.partial_lengths.push(this.length)}})),E=e((function(t){var n=this;if(i(this,"inst",void 0),i(this,"getTotalLength",(function(){return n.inst.getTotalLength()})),i(this,"getPointAtLength",(function(t){return n.inst.getPointAtLength(t)})),i(this,"getTangentAtLength",(function(t){return n.inst.getTangentAtLength(t)})),i(this,"getPropertiesAtLength",(function(t){return n.inst.getPropertiesAtLength(t)})),i(this,"getParts",(function(){return n.inst.getParts()})),this.inst=new O(t),!(this instanceof E))return new E(t)}));t.svgPathProperties=E})); diff --git a/web/js/help_popup.js b/web/js/help_popup.js new file mode 100644 index 0000000..ce9bc90 --- /dev/null +++ b/web/js/help_popup.js @@ -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 + } +} \ No newline at end of file diff --git a/web/js/midi_extensions.js b/web/js/midi_extensions.js new file mode 100644 index 0000000..4628a55 --- /dev/null +++ b/web/js/midi_extensions.js @@ -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(); + } + }; + } + }, +}); \ No newline at end of file diff --git a/web/js/piano.js b/web/js/piano.js new file mode 100644 index 0000000..812a0b5 --- /dev/null +++ b/web/js/piano.js @@ -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; +} \ No newline at end of file
  • mJ9Q(XEsiE%k@NJZdwkkGR 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 U3FZVgF55k|b=5TWm_0H4A9gGS$N&HU literal 0 HcmV?d00001 diff --git a/node_configs.py b/node_configs.py new file mode 100644 index 0000000..d36bfe8 --- /dev/null +++ b/node_configs.py @@ -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") +""" +}) diff --git a/nodes/audio/audio_nodes.py b/nodes/audio/audio_nodes.py new file mode 100644 index 0000000..e5204ca --- /dev/null +++ b/nodes/audio/audio_nodes.py @@ -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,) \ No newline at end of file diff --git a/nodes/audio/audio_utils.py b/nodes/audio/audio_utils.py new file mode 100644 index 0000000..afb32f8 --- /dev/null +++ b/nodes/audio/audio_utils.py @@ -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)]) \ No newline at end of file diff --git a/nodes/audio/midi_nodes.py b/nodes/audio/midi_nodes.py new file mode 100644 index 0000000..503fa1d --- /dev/null +++ b/nodes/audio/midi_nodes.py @@ -0,0 +1 @@ +#TODO \ No newline at end of file diff --git a/nodes/audio/midi_utils.py b/nodes/audio/midi_utils.py new file mode 100644 index 0000000..503fa1d --- /dev/null +++ b/nodes/audio/midi_utils.py @@ -0,0 +1 @@ +#TODO \ No newline at end of file diff --git a/nodes/flex/feature_externals.py b/nodes/flex/feature_externals.py new file mode 100644 index 0000000..93df7ca --- /dev/null +++ b/nodes/flex/feature_externals.py @@ -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,) diff --git a/nodes/flex/feature_extractors.py b/nodes/flex/feature_extractors.py new file mode 100644 index 0000000..963f390 --- /dev/null +++ b/nodes/flex/feature_extractors.py @@ -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) \ No newline at end of file diff --git a/nodes/flex/feature_modulation.py b/nodes/flex/feature_modulation.py new file mode 100644 index 0000000..73fc917 --- /dev/null +++ b/nodes/flex/feature_modulation.py @@ -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 \ No newline at end of file diff --git a/nodes/flex/feature_pipe.py b/nodes/flex/feature_pipe.py new file mode 100644 index 0000000..6e0bce2 --- /dev/null +++ b/nodes/flex/feature_pipe.py @@ -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 \ No newline at end of file diff --git a/nodes/flex/features.py b/nodes/flex/features.py new file mode 100644 index 0000000..92843a4 --- /dev/null +++ b/nodes/flex/features.py @@ -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)}") \ No newline at end of file diff --git a/nodes/flex/midi_feature.py b/nodes/flex/midi_feature.py new file mode 100644 index 0000000..a808089 --- /dev/null +++ b/nodes/flex/midi_feature.py @@ -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) \ No newline at end of file diff --git a/nodes/flex/midi_feature_extractor.py b/nodes/flex/midi_feature_extractor.py new file mode 100644 index 0000000..b065939 --- /dev/null +++ b/nodes/flex/midi_feature_extractor.py @@ -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) \ No newline at end of file diff --git a/nodes/images/flex_images.py b/nodes/images/flex_images.py new file mode 100644 index 0000000..c642b74 --- /dev/null +++ b/nodes/images/flex_images.py @@ -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) + diff --git a/nodes/images/image_base.py b/nodes/images/image_base.py new file mode 100644 index 0000000..f89eef1 --- /dev/null +++ b/nodes/images/image_base.py @@ -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 \ No newline at end of file diff --git a/nodes/images/image_utility_nodes.py b/nodes/images/image_utility_nodes.py new file mode 100644 index 0000000..0df560e --- /dev/null +++ b/nodes/images/image_utility_nodes.py @@ -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,) diff --git a/nodes/images/image_utils.py b/nodes/images/image_utils.py new file mode 100644 index 0000000..3e27719 --- /dev/null +++ b/nodes/images/image_utils.py @@ -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) \ No newline at end of file diff --git a/nodes/masks/audio_masks.py b/nodes/masks/audio_masks.py deleted file mode 100644 index f260866..0000000 --- a/nodes/masks/audio_masks.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/nodes/masks/flex_masks.py b/nodes/masks/flex_masks.py new file mode 100644 index 0000000..1d3b78f --- /dev/null +++ b/nodes/masks/flex_masks.py @@ -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),) \ No newline at end of file diff --git a/nodes/masks/mask_base.py b/nodes/masks/mask_base.py index 578c1c3..0d27f60 100644 --- a/nodes/masks/mask_base.py +++ b/nodes/masks/mask_base.py @@ -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 diff --git a/nodes/masks/mask_utils.py b/nodes/masks/mask_utils.py index 78d65c2..8489695 100644 --- a/nodes/masks/mask_utils.py +++ b/nodes/masks/mask_utils.py @@ -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) \ No newline at end of file diff --git a/nodes/masks/particle_system_masks.py b/nodes/masks/particle_system_masks.py index 88a3b69..78c4247 100644 --- a/nodes/masks/particle_system_masks.py +++ b/nodes/masks/particle_system_masks.py @@ -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 = { diff --git a/nodes/masks/structure.txt b/nodes/masks/structure.txt deleted file mode 100644 index 8813bdf..0000000 --- a/nodes/masks/structure.txt +++ /dev/null @@ -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 - -""" \ No newline at end of file diff --git a/nodes/masks/temporal_masks.py b/nodes/masks/temporal_masks.py index b12505a..7ab0544 100644 --- a/nodes/masks/temporal_masks.py +++ b/nodes/masks/temporal_masks.py @@ -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) diff --git a/nodes/masks/utility_nodes.py b/nodes/masks/utility_nodes.py index fc79d9e..08eba38 100644 --- a/nodes/masks/utility_nodes.py +++ b/nodes/masks/utility_nodes.py @@ -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/" diff --git a/nodes/node_utilities.py b/nodes/node_utilities.py new file mode 100644 index 0000000..cea0acf --- /dev/null +++ b/nodes/node_utilities.py @@ -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) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5e97362..c0509e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/readme.md b/readme.md index 9aab703..0453806 100644 --- a/readme.md +++ b/readme.md @@ -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 @@ -23,11 +26,193 @@ RyanOnTheInside node pack introduces the following (so far):
    -*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 + +
    +

    Flex Features

    + +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. + +
    + +
    +

    Particle Systems

    + +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. + +
    + +
    +

    Audio and MIDI Processing

    + +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. + +
    + +
    +

    Optical Flow

    + +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. + +
    + +
    +

    Temporal Effects

    + +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. + +
    ## 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 - - 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 - - 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. - -#### Optical Flow Particle System - work in progess -This node will generate a particle system animation driven by optical flow, creating dynamic visual effects. - - -## 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 - - 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. \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index cf04099..d5e8409 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,5 @@ pillow librosa pymunk==6.8.1 matplotlib +openunmix +mido \ No newline at end of file diff --git a/ryanontheinside_web_async/marked.min.js b/ryanontheinside_web_async/marked.min.js new file mode 100644 index 0000000..2e66c36 --- /dev/null +++ b/ryanontheinside_web_async/marked.min.js @@ -0,0 +1,6 @@ +/** + * marked v12.0.1 - a markdown parser + * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s=/[&<>"']/,r=new RegExp(s.source,"g"),i=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,l=new RegExp(i.source,"g"),o={"&":"&","<":"<",">":">",'"':""","'":"'"},a=e=>o[e];function c(e,t){if(t){if(s.test(e))return e.replace(r,a)}else if(i.test(e))return e.replace(l,a);return e}const h=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function p(e){return e.replace(h,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const u=/(^|[^\[])\^/g;function k(e,t){let n="string"==typeof e?e:e.source;t=t||"";const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(u,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}function g(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch(e){return null}return e}const f={exec:()=>null};function d(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:x(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t){const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return n.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=x(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){const e=x(t[0].replace(/^ *>[ \t]?/gm,""),"\n"),n=this.lexer.state.top;this.lexer.state.top=!0;const s=this.lexer.blockTokens(e);return this.lexer.state.top=n,{type:"blockquote",raw:t[0],tokens:s,text:e}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim();const s=n.length>1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=new RegExp(`^( {0,3}${n})((?:[\t ][^\\n]*)?(?:\\n|$))`);let l="",o="",a=!1;for(;e;){let n=!1;if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;l=t[0],e=e.substring(l.length);let s=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=0;this.options.pedantic?(h=2,o=s.trimStart()):(h=t[2].search(/[^ ]/),h=h>4?1:h,o=s.slice(h),h+=t[1].length);let p=!1;if(!s&&/^ *$/.test(c)&&(l+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),n=new RegExp(`^ {0,${Math.min(3,h-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),r=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:\`\`\`|~~~)`),i=new RegExp(`^ {0,${Math.min(3,h-1)}}#`);for(;e;){const a=e.split("\n",1)[0];if(c=a,this.options.pedantic&&(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),r.test(c))break;if(i.test(c))break;if(t.test(c))break;if(n.test(e))break;if(c.search(/[^ ]/)>=h||!c.trim())o+="\n"+c.slice(h);else{if(p)break;if(s.search(/[^ ]/)>=4)break;if(r.test(s))break;if(i.test(s))break;if(n.test(s))break;o+="\n"+c}p||c.trim()||(p=!0),l+=a+"\n",e=e.substring(a.length+1),s=c.slice(h)}}r.loose||(a?r.loose=!0:/\n *\n *$/.test(l)&&(a=!0));let u,k=null;this.options.gfm&&(k=/^\[[ xX]\] /.exec(o),k&&(u="[ ] "!==k[0],o=o.replace(/^\[[ xX]\] +/,""))),r.items.push({type:"list_item",raw:l,task:!!k,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=l}r.items[r.items.length-1].raw=l.trimEnd(),r.items[r.items.length-1].text=o.trimEnd(),r.raw=r.raw.trimEnd();for(let e=0;e"space"===e.type)),n=t.length>0&&t.some((e=>/\n.*\n/.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e$/,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:n,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(!t)return;if(!/[:|]/.test(t[2]))return;const n=d(t[1]),s=t[2].replace(/^\||\| *$/g,"").split("|"),r=t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[],i={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(const e of s)/^ *-+: *$/.test(e)?i.align.push("right"):/^ *:-+: *$/.test(e)?i.align.push("center"):/^ *:-+ *$/.test(e)?i.align.push("left"):i.align.push(null);for(const e of n)i.header.push({text:e,tokens:this.lexer.inline(e)});for(const e of r)i.rows.push(d(e,i.header.length).map((e=>({text:e,tokens:this.lexer.inline(e)}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:c(t[1])}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&/^
    /i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=x(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),/^$/.test(e)?n.slice(1):n.slice(1,-1)),b(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(/\s+/g," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return b(n,e,n[0],this.lexer)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(/[\p{L}\p{N}]/u))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const n=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return n&&s&&(e=e.substring(1,e.length-1)),e=c(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=c(t[1]),n="mailto:"+e):(e=c(t[1]),n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=c(t[0]),n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=c(t[0]),n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){let e;return e=this.lexer.state.inRawBlock?t[0]:c(t[0]),{type:"text",raw:t[0],text:e}}}}const m=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,y=/(?:[*+-]|\d{1,9}[.)])/,$=k(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,y).replace(/blockCode/g,/ {4}/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),z=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,T=/(?!\s*\])(?:\\.|[^\[\]\\])+/,R=k(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/).replace("label",T).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),_=k(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,y).getRegex(),A="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",S=/|$))/,I=k("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))","i").replace("comment",S).replace("tag",A).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),E=k(z).replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),q={blockquote:k(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",E).getRegex(),code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,def:R,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:m,html:I,lheading:$,list:_,newline:/^(?: *(?:\n|$))+/,paragraph:E,table:f,text:/^[^\n]+/},Z=k("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),L={...q,table:Z,paragraph:k(z).replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",Z).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex()},P={...q,html:k("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",S).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:f,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:k(z).replace("hr",m).replace("heading"," *#{1,6} *[^\n]").replace("lheading",$).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},Q=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,v=/^( {2,}|\\)\n(?!\s*$)/,B="\\p{P}\\p{S}",C=k(/^((?![*_])[\spunctuation])/,"u").replace(/punctuation/g,B).getRegex(),M=k(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,"u").replace(/punct/g,B).getRegex(),O=k("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])","gu").replace(/punct/g,B).getRegex(),D=k("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])","gu").replace(/punct/g,B).getRegex(),j=k(/\\([punct])/,"gu").replace(/punct/g,B).getRegex(),H=k(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),U=k(S).replace("(?:--\x3e|$)","--\x3e").getRegex(),X=k("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",U).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),F=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,N=k(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",F).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),G=k(/^!?\[(label)\]\[(ref)\]/).replace("label",F).replace("ref",T).getRegex(),J=k(/^!?\[(ref)\](?:\[\])?/).replace("ref",T).getRegex(),K={_backpedal:f,anyPunctuation:j,autolink:H,blockSkip:/\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g,br:v,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:f,emStrongLDelim:M,emStrongRDelimAst:O,emStrongRDelimUnd:D,escape:Q,link:N,nolink:J,punctuation:C,reflink:G,reflinkSearch:k("reflink|nolink(?!\\()","g").replace("reflink",G).replace("nolink",J).getRegex(),tag:X,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\t+" ".repeat(n.length)));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.space(e))e=e.substring(n.raw.length),1===n.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(n);else if(n=this.tokenizer.code(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?t.push(n):(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.fences(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.heading(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.hr(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.blockquote(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.list(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.html(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.def(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?this.tokens.links[n.tag]||(this.tokens.links[n.tag]={href:n.href,title:n.title}):(s.raw+="\n"+n.raw,s.text+="\n"+n.raw,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.table(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.lheading(e))e=e.substring(n.raw.length),t.push(n);else{if(r=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(this.state.top&&(n=this.tokenizer.paragraph(r)))s=t[t.length-1],i&&"paragraph"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n),i=r.length!==e.length,e=e.substring(n.raw.length);else if(n=this.tokenizer.text(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n,s,r,i,l,o,a=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(a));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(a));)a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.anyPunctuation.exec(a));)a=a.slice(0,i.index)+"++"+a.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;e;)if(l||(o=""),l=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.escape(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.tag(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.link(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.emStrong(e,a,o))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.codespan(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.br(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.del(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.autolink(e))e=e.substring(n.raw.length),t.push(n);else if(this.state.inLink||!(n=this.tokenizer.url(e))){if(r=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(n=this.tokenizer.inlineText(r))e=e.substring(n.raw.length),"_"!==n.raw.slice(-1)&&(o=n.raw.slice(-1)),l=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(n.raw.length),t.push(n);return t}}class se{options;constructor(t){this.options=t||e.defaults}code(e,t,n){const s=(t||"").match(/^\S*/)?.[0];return e=e.replace(/\n$/,"")+"\n",s?'
    '+(n?e:c(e,!0))+"
    \n":"
    "+(n?e:c(e,!0))+"
    \n"}blockquote(e){return`
    \n${e}
    \n`}html(e,t){return e}heading(e,t,n){return`${e}\n`}hr(){return"
    \n"}list(e,t,n){const s=t?"ol":"ul";return"<"+s+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"\n"}listitem(e,t,n){return`