Spaces:
Running
Running
| """ | |
| Signal Attractor Node - Generates a 2D chaotic pattern from two signals | |
| Place this file in the 'nodes' folder | |
| """ | |
| import numpy as np | |
| from PyQt6 import QtGui | |
| import cv2 | |
| import sys | |
| import os | |
| # --- This is the new, correct block --- | |
| import __main__ | |
| BaseNode = __main__.BaseNode | |
| PA_INSTANCE = getattr(__main__, "PA_INSTANCE", None) | |
| # ------------------------------------ | |
| class SignalAttractorNode(BaseNode): | |
| NODE_CATEGORY = "Transform" | |
| NODE_COLOR = QtGui.QColor(180, 80, 180) # Attractor Purple | |
| def __init__(self, width=128, height=128, param_c=1.0, param_d=0.7): | |
| super().__init__() | |
| self.node_title = "Signal Attractor" | |
| self.inputs = { | |
| 'signal_a': 'signal', | |
| 'signal_b': 'signal' | |
| } | |
| self.outputs = {'image': 'image', 'x_out': 'signal', 'y_out': 'signal'} | |
| self.w, self.h = int(width), int(height) | |
| # Attractor state | |
| self.x, self.y = 0.1, 0.1 | |
| # Parameters (a & b are controlled by input, c & d are configurable) | |
| self.param_c = float(param_c) | |
| self.param_d = float(param_d) | |
| # For visualization | |
| self.points = np.zeros((self.h, self.w), dtype=np.float32) | |
| self.img = np.zeros((self.h, self.w), dtype=np.float32) | |
| def step(self): | |
| # Get signals, map from [-1, 1] to [-2, 2] | |
| param_a = (self.get_blended_input('signal_a', 'sum') or 0.0) * 2.0 | |
| param_b = (self.get_blended_input('signal_b', 'sum') or 0.0) * 2.0 | |
| # Iterate the attractor equations 500 times per frame | |
| for _ in range(500): | |
| # Clifford Attractor equations | |
| x_new = np.sin(param_a * self.y) + self.param_c * np.cos(param_a * self.x) | |
| y_new = np.sin(param_b * self.x) + self.param_d * np.cos(param_b * self.y) | |
| self.x, self.y = x_new, y_new | |
| # Scale from [-2, 2] range to image coordinates | |
| px = int((self.x + 2.0) / 4.0 * self.w) | |
| py = int((self.y + 2.0) / 4.0 * self.h) | |
| if 0 <= px < self.w and 0 <= py < self.h: | |
| self.points[py, px] += 0.1 # Add energy | |
| # Apply decay to the image so it fades | |
| self.points *= 0.97 | |
| self.points = np.clip(self.points, 0, 1.0) | |
| # Blur for a "glowing" effect | |
| self.img = cv2.GaussianBlur(self.points, (3, 3), 0) | |
| def get_output(self, port_name): | |
| if port_name == 'image': | |
| return self.img | |
| elif port_name == 'x_out': | |
| return self.x / 2.0 # Normalize to [-1, 1] | |
| elif port_name == 'y_out': | |
| return self.y / 2.0 # Normalize to [-1, 1] | |
| return None | |
| def get_display_image(self): | |
| img_u8 = (np.clip(self.img, 0, 1) * 255).astype(np.uint8) | |
| img_u8 = np.ascontiguousarray(img_u8) | |
| return QtGui.QImage(img_u8.data, self.w, self.h, self.w, QtGui.QImage.Format.Format_Grayscale8) | |
| def get_config_options(self): | |
| return [ | |
| ("Param C", "param_c", self.param_c, None), | |
| ("Param D", "param_d", self.param_d, None), | |
| ("Width", "w", self.w, None), | |
| ("Height", "h", self.h, None), | |
| ] |