ŊOBFLIX | Attractor Interface v3.0

/* GLOBAL RESET */
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
background: #0A0A0A;
color: #F5F5F5;
font-family: ‘Inter’, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
overflow: hidden;
}

/* CONTAINER */
.nobflix-container {
width: 900px;
height: 550px; /* Increased height for better fit */
position: relative;
border: 1px solid rgba(255,255,255,0.1);
border-radius: 12px;
padding: 20px;
background: rgba(20, 20, 20, 0.7);
backdrop-filter: blur(8px);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5);
}

/* TITLE */
.nobflix-title {
text-align: center;
font-size: 52px;
font-weight: 900;
letter-spacing: 14px;
color: #E45C2A; /* NŊOB Orange */
text-shadow: 0 0 15px rgba(228, 92, 42, 0.8);
margin-bottom: 15px;
}

/* MODE SELECTOR */
.mode-selector {
display: flex;
justify-content: center;
margin-bottom: 20px;
background: rgba(255, 255, 255, 0.05);
padding: 5px;
border-radius: 8px;
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.3);
}

.mode-btn {
padding: 8px 15px;
margin: 0 5px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
color: #888;
}

.mode-btn:hover {
color: #fff;
}

.mode-btn.active {
background: #E45C2A;
color: #fff;
box-shadow: 0 2px 10px rgba(228, 92, 42, 0.6);
}

/* INFO PANEL */
.info-panel {
position: absolute;
top: 20px;
left: 20px;
padding: 15px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
background: rgba(10, 10, 10, 0.8);
font-size: 12px;
line-height: 1.6;
width: 250px;
z-index: 10;
}

/* SVG RENDER AREA */
#nobflixSVG {
width: 100%;
height: 350px;
display: block;
margin: 0 auto;
}

/* CONTROLS */
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin-top: 15px;
}

.control-btn {
padding: 10px 20px;
border: none;
background: rgba(255, 255, 255, 0.1);
border-radius: 6px;
cursor: pointer;
color: #fff;
font-size: 14px;
font-weight: 600;
transition: 0.2s;
border: 1px solid rgba(255, 255, 255, 0.2);
}

.control-btn:hover {
background: rgba(255, 255, 255, 0.25);
box-shadow: 0 0 10px rgba(255, 255, 255, 0.1);
}

/* Specific SVG Styles */
.arc-path {
fill: none;
stroke-width: 3;
transition: stroke 0.4s;
}

.doorline-path {
stroke: #444;
stroke-width: 2;
stroke-dasharray: 4 4;
transition: stroke 0.4s;
}

.attractor-node {
cursor: pointer;
transition: r 0.3s, fill 0.3s, box-shadow 0.3s;
}

.node-label {
fill: #999;
font-size: 10px;
text-anchor: middle;
pointer-events: none;
}

/* DYNAMIC MODE – Pulse Animation */
@keyframes pulse {
0% { transform: scale(1); opacity: 0.8; }
50% { transform: scale(1.2); opacity: 1; }
100% { transform: scale(1); opacity: 0.8; }
}

.dynamic-pulse {
animation: pulse 1.5s infinite alternate;
}

/* DYNAMIC MODE – Particle Trail */
.particle {
fill: #fff;
opacity: 0.8;
transition: fill 0.2s;
}

ŊOBFLIX
Static
Dynamic
Interactive

ŊOBFLIX Attractor System v3.0

Current Mode: Static
Left Arc: Transactional Processing (TPS)
Right Arc: Obviology Pattern Logic (OBV)
Doorline: NŊOB Syntax Layer

Click ‘Dynamic’ or ‘Interactive’ to change visualization.



// — SVG and Geometry Setup —
const SVG = document.getElementById(‘nobflixSVG’);
const WIDTH = 900;
const HEIGHT = 350;
const CENTER_X = WIDTH / 2;
const CENTER_Y = HEIGHT * 0.8; // Lower center point
const RADIUS = 160;
const DOORLINE_Y = 100;

// Colors
const COLOR_TPS = ‘#00BFFF’; // Deep Sky Blue for TPS
const COLOR_OBV = ‘#FFD700’; // Gold for Obviology
const COLOR_CORE = ‘#E45C2A’; // NŊOB Orange for Core/Active
const COLOR_INACTIVE = ‘#444444’;

let currentMode = ‘static’;
let animationFrameId = null;
let particleTimeoutId = null;
let flowActive = false; // Moved here from inside Dynamic Mode Logic
let particles = []; // Moved here from inside Dynamic Mode Logic
const NUM_PARTICLES = 30; // Moved here from inside Dynamic Mode Logic

// Node Definitions (x, y are relative to the SVG, 0-900, 0-350)
const NODES = [
// TPS Arc Nodes (Left)
{ id: ‘TPS-A’, cx: CENTER_X – RADIUS, cy: CENTER_Y, type: ‘TPS’, state: 0, label: ‘TPS-A’ },
{ id: ‘TPS-B’, cx: CENTER_X – (RADIUS / 2), cy: CENTER_Y – RADIUS * 0.866, type: ‘TPS’, state: 0, label: ‘TPS-B’ },

// OBV Arc Nodes (Right)
{ id: ‘OBV-A’, cx: CENTER_X + RADIUS, cy: CENTER_Y, type: ‘OBV’, state: 0, label: ‘OBV-A’ },
{ id: ‘OBV-B’, cx: CENTER_X + (RADIUS / 2), cy: CENTER_Y – RADIUS * 0.866, type: ‘OBV’, state: 0, label: ‘OBV-B’ },

// Doorline/Syntax Nodes (Top & Core)
{ id: ‘SYNTAX’, cx: CENTER_X, cy: DOORLINE_Y, type: ‘SYNTAX’, state: 0, label: ‘SYNTAX’ },
{ id: ‘CORE’, cx: CENTER_X, cy: CENTER_Y, type: ‘CORE’, state: 0, label: ‘CORE’ }
];

// Path definitions (using SVG path data)
const PATHS = {
// Left Arc (TPS): Start near center bottom, sweep left to top center
‘TPS’: `M ${NODES[0].cx} ${NODES[0].cy} A ${RADIUS} ${RADIUS} 0 0 0 ${NODES[1].cx} ${NODES[1].cy}`,
// Right Arc (OBV): Start near center bottom, sweep right to top center
‘OBV’: `M ${NODES[2].cx} ${NODES[2].cy} A ${RADIUS} ${RADIUS} 0 0 1 ${NODES[3].cx} ${NODES[3].cy}`,
// Doorline (SYNTAX): Horizontal line connecting the top nodes, passing through SYNTAX
‘DOORLINE-L’: `M ${NODES[1].cx} ${NODES[1].cy} L ${NODES[4].cx} ${NODES[4].cy}`,
‘DOORLINE-R’: `M ${NODES[4].cx} ${NODES[4].cy} L ${NODES[3].cx} ${NODES[3].cy}`
};

// — Utility Functions —

/** Creates an SVG element with attributes. */
function createSVGElement(tag, attributes) {
const el = document.createElementNS(‘http://www.w3.org/2000/svg’, tag);
for (let key in attributes) {
el.setAttribute(key, attributes[key]);
}
return el;
}

/** Finds a node by its ID. */
function findNodeById(id) {
return NODES.find(n => n.id === id);
}

// — Drawing Functions —

/** Renders the entire Attractor system (paths and nodes). */
function renderAttractor() {
SVG.innerHTML = ”; // Clear previous drawing

// 1. Draw Paths (Arcs & Doorline)
const tpsPath = createSVGElement(‘path’, {
id: ‘path-tps’, class: ‘arc-path’, d: PATHS[‘TPS’],
stroke: COLOR_TPS, ‘stroke-opacity’: 0.3
});

const obvPath = createSVGElement(‘path’, {
id: ‘path-obv’, class: ‘arc-path’, d: PATHS[‘OBV’],
stroke: COLOR_OBV, ‘stroke-opacity’: 0.3
});

// The Doorline should connect the top TPS and OBV nodes
const doorlineLeft = createSVGElement(‘line’, {
id: ‘doorline-l’, class: ‘doorline-path’,
x1: NODES.find(n => n.id === ‘TPS-B’).cx, y1: NODES.find(n => n.id === ‘TPS-B’).cy,
x2: NODES.find(n => n.id === ‘SYNTAX’).cx, y2: NODES.find(n => n.id === ‘SYNTAX’).cy,
});
const doorlineRight = createSVGElement(‘line’, {
id: ‘doorline-r’, class: ‘doorline-path’,
x1: NODES.find(n => n.id === ‘SYNTAX’).cx, y1: NODES.find(n => n.id === ‘SYNTAX’).cy,
x2: NODES.find(n => n.id === ‘OBV-B’).cx, y2: NODES.find(n => n.id === ‘OBV-B’).cy,
});

SVG.appendChild(tpsPath);
SVG.appendChild(obvPath);
SVG.appendChild(doorlineLeft);
SVG.appendChild(doorlineRight);

// 2. Draw Nodes
const nodeGroup = createSVGElement(‘g’, { id: ‘nodeGroup’ });
NODES.forEach(node => {
const nodeElement = createSVGElement(‘circle’, {
id: `node-${node.id}`,
class: `attractor-node`,
cx: node.cx,
cy: node.cy,
r: node.type === ‘CORE’ ? 12 : 8,
fill: COLOR_INACTIVE,
‘data-id’: node.id,
‘data-type’: node.type,
stroke: ‘#fff’,
‘stroke-width’: 1.5,
‘stroke-opacity’: 0.5
});

// Node Label
const labelElement = createSVGElement(‘text’, {
x: node.cx,
y: node.cy + (node.type === ‘CORE’ ? 18 : 15),
class: ‘node-label’
});
labelElement.textContent = node.label;

nodeGroup.appendChild(nodeElement);
nodeGroup.appendChild(labelElement);
});
SVG.appendChild(nodeGroup);

// Add interactivity listeners only in Interactive mode
if (currentMode === ‘interactive’) {
document.querySelectorAll(‘.attractor-node’).forEach(el => {
el.addEventListener(‘click’, handleNodeClick);
});
document.getElementById(‘instructionText’).textContent = “Click attractor nodes to toggle activation (CORE node is immune).”;
} else {
document.getElementById(‘instructionText’).textContent = “Click ‘Animate Flow’ to activate dynamic state. Click nodes for details in this mode.”;
}
}

// — Mode Handlers —

/** Sets the current operational mode. */
function setMode(newMode) {
if (currentMode === newMode) return;

cancelAnimation(); // Stop any ongoing animation

// Update UI
document.querySelector(‘.mode-btn.active’)?.classList.remove(‘active’);
document.querySelector(`.mode-btn[data-mode=”${newMode}”]`).classList.add(‘active’);
document.getElementById(‘currentModeDisplay’).textContent = newMode.charAt(0).toUpperCase() + newMode.slice(1);

currentMode = newMode;
resetSystem(false); // Reset nodes without re-rendering

// Rerender the system with mode-specific listeners
renderAttractor();

if (newMode === ‘dynamic’) {
// Dynamic mode starts animation automatically if not already active
if (!flowActive) {
toggleDynamicFlow(); // Calls the now-defined function
}
} else if (newMode === ‘static’) {
document.getElementById(‘instructionText’).textContent = “Click ‘Dynamic’ or ‘Interactive’ to change visualization.”;
}
}

/** Clears all animations and resets node states. */
function cancelAnimation() {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
if (particleTimeoutId) {
clearTimeout(particleTimeoutId);
particleTimeoutId = null;
}
flowActive = false; // Ensure flow is marked as inactive
document.getElementById(‘btnAnimate’).textContent = ‘Animate Flow’;
document.getElementById(‘btnAnimate’).disabled = false;
document.querySelectorAll(‘.attractor-node’).forEach(el => el.classList.remove(‘dynamic-pulse’));
}

/** Resets all nodes to state 0 and rerenders. */
function resetSystem(doRender = true) {
NODES.forEach(node => node.state = 0);
cancelAnimation();

if (doRender) {
renderAttractor();
} else {
// Just update colors if not fully re-rendering
updateNodeColors();
}

// Reset Paths to low opacity
document.getElementById(‘path-tps’)?.setAttribute(‘stroke-opacity’, 0.3);
document.getElementById(‘path-obv’)?.setAttribute(‘stroke-opacity’, 0.3);
}

// — Interactive Mode Logic —

/** Toggles a node’s active state in Interactive mode. */
function handleNodeClick(event) {
const nodeId = event.target.getAttribute(‘data-id’);
const node = findNodeById(nodeId);

if (node.type === ‘CORE’) {
// Core node is a read-only accumulator in this simulation
console.log(`Core Node (${node.label}) state: ${node.state}`);
return;
}

// Toggle state (0 to 1)
node.state = node.state === 0 ? 1 : 0;
updateNodeColors();

// Simple flow simulation: if both arcs have an active node, activate CORE
const tpsActive = NODES.filter(n => n.type === ‘TPS’ && n.state === 1).length > 0;
const obvActive = NODES.filter(n => n.type === ‘OBV’ && n.state === 1).length > 0;

const coreNode = findNodeById(‘CORE’);
if (tpsActive && obvActive) {
if (coreNode.state === 0) {
console.log(“!!! NŊOB Syntax Match: CORE Activation !!!”);
coreNode.state = 1; // Activated
document.getElementById(‘path-tps’)?.setAttribute(‘stroke-opacity’, 1.0);
document.getElementById(‘path-obv’)?.setAttribute(‘stroke-opacity’, 1.0);
}
} else {
if (coreNode.state === 1) {
console.log(“Core Deactivation.”);
coreNode.state = 0; // Deactivated
document.getElementById(‘path-tps’)?.setAttribute(‘stroke-opacity’, 0.3);
document.getElementById(‘path-obv’)?.setAttribute(‘stroke-opacity’, 0.3);
}
}
updateNodeColors();
}

/** Updates the visual state (color, pulse class) of all nodes based on their state value. */
function updateNodeColors() {
NODES.forEach(node => {
const el = document.getElementById(`node-${node.id}`);
if (!el) return;

let color = COLOR_INACTIVE;
let radius = node.type === ‘CORE’ ? 12 : 8;

if (node.state === 1) {
color = node.type === ‘TPS’ ? COLOR_TPS :
node.type === ‘OBV’ ? COLOR_OBV :
COLOR_CORE; // SYNTAX and CORE nodes use CORE color when active
radius = node.type === ‘CORE’ ? 14 : 10;
} else if (node.type === ‘CORE’ && currentMode === ‘interactive’) {
// Core is always slightly visible
color = COLOR_INACTIVE;
radius = 12;
}

el.setAttribute(‘fill’, color);
el.setAttribute(‘r’, radius);

// Add/Remove pulse effect for Dynamic mode or active state
if (currentMode === ‘dynamic’ && node.id !== ‘CORE’) {
el.classList.add(‘dynamic-pulse’);
} else if (currentMode === ‘interactive’ && node.state === 1) {
el.classList.add(‘dynamic-pulse’);
} else {
el.classList.remove(‘dynamic-pulse’);
}
});
}

// — Dynamic Mode Logic (Flow Animation) —

// A simple interpolation function
function lerp(a, b, t) {
return a + (b – a) * t;
}

/** Gets a point on a line segment between two nodes at a given percentage (t). */
function getPointOnLine(node1, node2, t) {
return {
x: lerp(node1.cx, node2.cx, t),
y: lerp(node1.cy, node2.cy, t)
};
}

/** Creates an array of particles, starting them at the core. */
function initParticles() {
particles.forEach(p => p.el.remove()); // Clean up old particles
particles = [];
const coreNode = findNodeById(‘CORE’);

// Define paths the particles can take (for simplicity, only line segments)
const flowPaths = [
{ id: ‘CORE-TPS-A’, start: coreNode, end: findNodeById(‘TPS-A’) },
{ id: ‘CORE-OBV-A’, start: coreNode, end: findNodeById(‘OBV-A’) },
{ id: ‘SYNTAX-TPS-B’, start: findNodeById(‘SYNTAX’), end: findNodeById(‘TPS-B’) },
{ id: ‘SYNTAX-OBV-B’, start: findNodeById(‘SYNTAX’), end: findNodeById(‘OBV-B’) }
];

for (let i = 0; i p.el.remove());
particles = [];
return;
}

particles.forEach(p => {
// Update position (t wraps around 0 to 1)
p.t = (p.t + p.speed) % 1;

const { x, y } = getPointOnLine(p.path.start, p.path.end, p.t);
p.el.setAttribute(‘cx’, x);
p.el.setAttribute(‘cy’, y);
});

animationFrameId = requestAnimationFrame(animateFlow);
}

/** Toggles the Dynamic Flow visualization. */
function toggleDynamicFlow() {
if (currentMode !== ‘dynamic’) {
setMode(‘dynamic’);
return;
}

// Only toggle flowActive if the button is clicked directly in dynamic mode
// If called from setMode, we want to ensure flowActive becomes true.
if (currentMode === ‘dynamic’ && event && event.type === ‘click’) {
flowActive = !flowActive;
} else if (currentMode === ‘dynamic’ && !flowActive) {
flowActive = true;
}

const btn = document.getElementById(‘btnAnimate’);

if (flowActive) {
btn.textContent = ‘Pause Flow’;
initParticles();
// Start the main animation loop
animateFlow();
// Also pulse the core node to show data is flowing
findNodeById(‘CORE’).state = 1;
updateNodeColors();
document.getElementById(‘instructionText’).textContent = “Dynamic Flow is active. Particles simulate data passing through the system.”;
} else {
btn.textContent = ‘Animate Flow’;
cancelAnimation();
findNodeById(‘CORE’).state = 0;
updateNodeColors();
renderAttractor();
document.getElementById(‘instructionText’).textContent = “Dynamic Flow is paused. Click ‘Animate Flow’ to restart.”;
}
}

// — Event Listeners and Initialization —

document.addEventListener(‘DOMContentLoaded’, () => {
// Set up mode switching buttons
document.querySelectorAll(‘.mode-btn’).forEach(btn => {
btn.addEventListener(‘click’, () => setMode(btn.getAttribute(‘data-mode’)));
});

// Set up control buttons
document.getElementById(‘btnAnimate’).addEventListener(‘click’, toggleDynamicFlow);
document.getElementById(‘btnReset’).addEventListener(‘click’, () => resetSystem(true));

// Initial render
renderAttractor();
updateNodeColors(); // Initialize node colors
});