Skip to content

lustigb/visualsim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 

Repository files navigation

visualsim

https://editor.p5js.org/brian.r.lustig/full/eTsz1Zj8G

// Improved Visual Field to V1 Mapping Simulation

let stimulusX, stimulusY; let dragging = true; let showTrail = false; let trailVF = []; let trailV1 = []; let interocularDistance = 40; // pixels let maxTrailLength = 30; let simulationPaused = false; let showVisualField = true; let showCortex = true; let corticalMagnification = 30; // Default M scaling factor was 17.3 let corticalMagnificationSlider; let eccentricityFactor = 0.95; // Default eccentricity constant was 0.75 let eccentricityFactorSlider; let anisotropyFactor = 1.45; // Default anisotropy factor (1.0 = isotropic) let anisotropySlider; let showLabels = true; let showGridLines = true; let draggingDotIndex = -1;

function setup() { createCanvas(900, 600); stimulusX = width / 2; stimulusY = height / 4; textAlign(CENTER, CENTER); textSize(14); // Create sliders for adjusting parameters createControlPanel(); }

function createControlPanel() { // Cortical magnification factor slider corticalMagnificationSlider = createSlider(5, 30, corticalMagnification, 0.1); corticalMagnificationSlider.position(20, height - 40); corticalMagnificationSlider.style("width", "100px"); // Eccentricity factor slider

eccentricityFactorSlider = createSlider(0.1, 2, eccentricityFactor, 0.05); eccentricityFactorSlider.position(190, height - 40); eccentricityFactorSlider.style("width", "100px"); // Anisotropy factor slider (new)

anisotropySlider = createSlider(0.5, 2, anisotropyFactor, 0.05); anisotropySlider.position(190, height - 70); anisotropySlider.style("width", "100px"); }

function draw() { // Update parameters from sliders

corticalMagnification = corticalMagnificationSlider.value(); eccentricityFactor = eccentricityFactorSlider.value(); anisotropyFactor = anisotropySlider.value();

background(255); // === Visual Field Area (top half) ===

if (showVisualField) { drawVisualFieldBackground(); drawVisualFieldMask(); drawVisualFieldGrid(); } // === Visual Cortex Area (bottom half) ===

if (showCortex) { drawVisualCortex(); } // Draw control buttons

drawControlButtons(); // === Stimulus Dots (Four-Dot Plus Pattern - Independent Mapping) ===

let horizontalOffset = interocularDistance / 2; let verticalOffset = interocularDistance / 2; // Adjust as needed let stimuli = [ { x: stimulusX - horizontalOffset, y: stimulusY, color: [0, 200, 0], name: "Left Horizontal", }, // Green { x: stimulusX + horizontalOffset, y: stimulusY, color: [160, 0, 200], name: "Right Horizontal", }, // Purple { x: stimulusX, y: stimulusY - verticalOffset, color: [255, 100, 0], name: "Top Vertical", }, // Orange { x: stimulusX, y: stimulusY + verticalOffset, color: [0, 150, 255], name: "Bottom Vertical", }, // Cyan ];

if (showTrail && !simulationPaused && frameCount % 2 === 0) { trailVF.push(stimuli.map((s) => ({ x: s.x, y: s.y, color: s.color })));

if (trailVF.length > maxTrailLength) trailVF.shift();

}

let trailV1ThisFrame = []; // Process and draw each stimulus dot

for (let stimulus of stimuli) { let result = visualFieldToCortex(stimulus.x, stimulus.y);

trailV1ThisFrame.push({
  x: result.x,
  y: result.y,
  size: result.size,
  color: stimulus.color,
  proportion: result.proportion,
}); 

// Draw current dots in the visual field

if (showVisualField) {
  noStroke();

  fill(...stimulus.color);

  ellipse(stimulus.x, stimulus.y, 18, 18);
} 

// Draw transformed dots in the cortex

if (showCortex) {
  noStroke();
  let cortexX = result.x;
  let cortexY = result.y;
  let dotSizeCortex = result.size;
  let proportion = result.proportion;
  let color = stimulus.color;

  if (proportion < 0.45) {
    // Right VF (left cortex)
    fill(...color);
    ellipse(cortexX, cortexY, dotSizeCortex, dotSizeCortex);
    fill(...color, 255 * proportion * 2);
    ellipse(
      width * 0.15 + abs((stimulus.x - width / 2) * (1 - proportion)),
      cortexY,
      dotSizeCortex * proportion,
      dotSizeCortex
    );
  } else if (proportion > 0.55) {
    // Left VF (right cortex)

    fill(...color);
    ellipse(cortexX, cortexY, dotSizeCortex, dotSizeCortex);
    fill(...color, 255 * (1 - proportion) * 2);
    ellipse(
      width * 0.85 - abs((stimulus.x - width / 2) * proportion),
      cortexY,
      dotSizeCortex * (1 - proportion),
      dotSizeCortex
    );
  } else {
    // Transition zone

    fill(...color, 255 * (1 - proportion));
    ellipse(
      width * 0.85 - abs((stimulus.x - width / 2) * proportion),
      cortexY,
      dotSizeCortex * (1 - proportion),
      dotSizeCortex
    );

    fill(...color, 255 * proportion);
    ellipse(
      width * 0.15 + abs((stimulus.x - width / 2) * (1 - proportion)),
      cortexY,
      dotSizeCortex * proportion,
      dotSizeCortex
    );
  }
}

}

trailV1.push(trailV1ThisFrame); if (trailV1.length > maxTrailLength) trailV1.shift(); // === Trails === if (showTrail && !simulationPaused) { // Draw trails

drawTrails(); // Draw pixelwise meridian representation

if (showCortex && !simulationPaused) {
  drawPixelwiseMeridian();
}

} // === Labels and parameter values ===

if (showLabels) { drawLabels(); } // Draw grid lines if enabled

if (showGridLines) { // The grid drawing functions are already called at the beginning of draw() } }

function mousePressed() { // Check if any of the stimulus dots are clicked let horizontalOffset = interocularDistance / 2; let verticalOffset = interocularDistance / 2; let stimuliPositions = [ { x: stimulusX - horizontalOffset, y: stimulusY }, { x: stimulusX + horizontalOffset, y: stimulusY }, { x: stimulusX, y: stimulusY - verticalOffset }, { x: stimulusX, y: stimulusY + verticalOffset }, ];

for (let i = 0; i < stimuliPositions.length; i++) { let d = dist(mouseX, mouseY, stimuliPositions[i].x, stimuliPositions[i].y); console.log('mousepressed') if (d < 20 && mouseY < height / 2 && showVisualField) { dragging = true; draggingDotIndex = i; // Keep track of which dot is being dragged animationMode = false;

  return;
}

} // Button press logic

let buttonSize = 60; let buttonSpacing = 10; let buttonY = height - 30; let buttonHeight = 25; let startX = 350; // Trail toggle

if ( mouseX > startX && mouseX < startX + buttonSize && mouseY > buttonY && mouseY < buttonY + buttonHeight ) { showTrail = !showTrail;

return;

} // Visual field toggle

if ( mouseX > startX + (buttonSize + buttonSpacing) && mouseX < startX + 2 * (buttonSize + buttonSpacing) && mouseY > buttonY && mouseY < buttonY + buttonHeight ) { showVisualField = !showVisualField;

return;

} // Cortex toggle

if ( mouseX > startX + 2 * (buttonSize + buttonSpacing) && mouseX < startX + 3 * (buttonSize + buttonSpacing) && mouseY > buttonY && mouseY < buttonY + buttonHeight ) { showCortex = !showCortex;

return;

} // Labels toggle

if ( mouseX > startX + 3 * (buttonSize + buttonSpacing) && mouseX < startX + 4 * (buttonSize + buttonSpacing) && mouseY > buttonY && mouseY < buttonY + buttonHeight ) { showLabels = !showLabels;

return;

} }

function mouseDragged() { console.log('mousedragging') if ( dragging && draggingDotIndex !== -1 && mouseY < height / 2 && showVisualField ) { let horizontalOffset = interocularDistance / 2; let verticalOffset = interocularDistance / 2;

if (draggingDotIndex === 0) {
  // Left horizontal

  stimulusX = constrain(mouseX + horizontalOffset, 0, width);

  stimulusY = constrain(mouseY, 0, height / 2);
} else if (draggingDotIndex === 1) {
  // Right horizontal

  stimulusX = constrain(mouseX - horizontalOffset, 0, width);

  stimulusY = constrain(mouseY, 0, height / 2);
} else if (draggingDotIndex === 2) {
  // Top vertical

  stimulusX = constrain(mouseX, 0, width);

  stimulusY = constrain(mouseY + verticalOffset, 0, height / 2);
} else if (draggingDotIndex === 3) {
  // Bottom vertical

  stimulusX = constrain(mouseX, 0, width);

  stimulusY = constrain(mouseY - verticalOffset, 0, height / 2);
}

} else if ( dragging && draggingDotIndex === -1 && mouseY < height / 2 && showVisualField ) { stimulusX = constrain(mouseX, 0, width);

stimulusY = constrain(mouseY, 0, height / 2);

} }

function mouseReleased() { console.log('mousereleased') dragging = false;

draggingDotIndex = -1; // Reset the dragged dot index }

// Add a global variable to track which dot is being dragged

//let draggingDotIndex = -1;

function drawVisualFieldBackground() { fill(240); rect(0, 0, width, height / 2); fill(0);

text("Visual Field", width / 2, 20); // --- Vertical Meridian (blue) with thickness ---

push();

stroke(0, 0, 255, 100);

strokeWeight(6);

line(width / 2, 0, width / 2, height / 2); // Brighter center line

stroke(0, 0, 255);

strokeWeight(2);

line(width / 2, 0, width / 2, height / 2);

pop();

// --- Horizontal Meridian (red) with thickness --- push(); stroke(255, 0, 0, 100); strokeWeight(6); line(0, height / 4, width, height / 4);

// Brighter center line

stroke(255, 0, 0); strokeWeight(2); line(0, height / 4, width, height / 4); pop();

// Draw foveal region outlines

noFill(); stroke(0, 0, 0, 100); ellipse(width / 2, height / 4, 50, 50); ellipse(width / 2, height / 4, 100, 100); ellipse(width / 2, height / 4, 200, 200); // Left and right visual field labels

if (showLabels) { fill(0, 0, 0, 150);

text("Left Visual Field", width * 0.25, height * 0.125);
text("Right Visual Field", width * 0.75, height * 0.125);

} }

function drawVisualFieldMask() { let boundaryY = height / 2;

noStroke();

fill(240); // Match the visual field background color

rect(0, boundaryY, width, height - boundaryY); }

function drawVisualFieldGrid() { stroke(100, 100, 100, 40); strokeWeight(1); noFill(); let centerX = width / 2; let centerY = height / 4; let boundaryY = height / 2; let maxRadius = 300; // Maximum extent of the grid lines // Eccentricity circles for (let r = 50; r < maxRadius; r += 50) { ellipse(centerX, centerY, r * 2, r * 2); } // Angle lines - SIMPLIFIED

for (let angle = 0; angle < TWO_PI; angle += PI / 6) { let endX = centerX + cos(angle) * maxRadius;

let endY = centerY + sin(angle) * maxRadius;

line(centerX, centerY, endX, endY);

} }

function drawVisualCortex() { fill(220); rect(0, height / 2, width, height / 2); fill(0); text("Visual Cortex (retinotopic map)", width / 2, height / 2 + 20); // Midlines in cortex

stroke(0); strokeWeight(1); line(width / 2, height / 2, width / 2, height); // vertical line(0, height * 0.75, width, height * 0.75); // horizontal

// --- Transformed Vertical Meridian (blue curve) --- push(); stroke(0, 0, 255); strokeWeight(2); // Draw on left cortex (right VF) noFill(); beginShape();

for (let y = 0; y <= height / 2; y += 2) { let { x: cx, y: cy } = visualFieldToCortex(width / 2 + 0.5, y);

vertex(cx, cy);

}

endShape();

// Draw on right cortex (left VF) beginShape(); for (let y = 0; y <= height / 2; y += 2) { let { x: cx, y: cy } = visualFieldToCortex(width / 2 - 0.5, y);

vertex(cx, cy);

} endShape(); pop();

// --- Transformed Horizontal Meridian (red curve) ---

push();

stroke(255, 0, 0); strokeWeight(2); noFill(); beginShape(); for (let x = 0; x <= width; x += 2) { let { x: cx, y: cy } = visualFieldToCortex(x, height / 4);

vertex(cx, cy);

} endShape();

pop();

// Draw isoeccentricity contours in cortex noFill(); stroke(0, 0, 0, 80); drawTransformedCircle(width / 2, height / 4, 50); // 50px circle drawTransformedCircle(width / 2, height / 4, 100); // 100px circle drawTransformedCircle(width / 2, height / 4, 200); // 200px circle }

function drawTransformedCircle(centerX, centerY, radius) { // Draw based on the original X position relative to the visual field midline

beginShape();

for (let angle = 0; angle < TWO_PI; angle += 0.05) { let vfX = centerX + cos(angle) * radius; let vfY = centerY + sin(angle) * radius; let { x: cortexX, y: cortexY } = visualFieldToCortex(vfX, vfY); if (vfX > width / 2) { // Right visual field maps to left cortex

  if (cortexX < width / 2) {
    // Only draw on the left side of the cortex

    vertex(cortexX, cortexY);
  } else {
    endShape();

    beginShape(); // Restart shape to avoid connections
  }
} else if (vfX < width / 2) {
  // Left visual field maps to right cortex

  if (cortexX > width / 2) {
    // Only draw on the right side of the cortex

    vertex(cortexX, cortexY);
  } else {
    endShape();

    beginShape(); // Restart shape to avoid connections
  }
} else {
  // Exactly on the vertical meridian

  vertex(cortexX, cortexY);
}

}

endShape(); }

// Function to draw the vertical meridian with pixel-by-pixel transformation

function drawPixelwiseMeridian() { // Define the thickness of the meridian in pixels

let meridianWidth = 10; // Draw each pixel of the meridian

for (let y = 10; y < height / 2 - 10; y += 4) { for ( let x = width / 2 - meridianWidth / 2; x <= width / 2 + meridianWidth / 2; x += 2 ) {

  // Calculate distance from center of meridian

  let distFromCenter = abs(x - width / 2); // Calculate intensity based on distance (center is brightest)

  let intensity = map(distFromCenter, 0, meridianWidth / 2, 255, 100); // Transform this point to cortical space

  let result = visualFieldToCortex(x, y); // Draw the point in cortical space with appropriate color

  noStroke();

  fill(0, 0, 255, intensity); // Modulate size slightly with distance from center

  let pointSize = map(distFromCenter, 0, meridianWidth / 2, 4, 2);

  ellipse(result.x, result.y, pointSize, pointSize);
}

} }

function drawControlButtons() { let buttonSize = 60; let buttonSpacing = 10; let buttonY = height - 30; let buttonHeight = 25; let startX = 350; // Trail toggle button fill(showTrail ? "green" : "lightgray"); rect(startX, buttonY, buttonSize, buttonHeight); fill(0); textSize(12); text( showTrail ? "Trail" : "No Trail", startX + buttonSize / 2, buttonY + buttonHeight / 2 ); // Toggle visual field

fill(showVisualField ? "green" : "lightgray");

rect( startX + (buttonSize + buttonSpacing), buttonY, buttonSize, buttonHeight );

fill(0); text( "VF", startX + (buttonSize + buttonSpacing) + buttonSize / 2, buttonY + buttonHeight / 2 ); // Toggle cortex

fill(showCortex ? "green" : "lightgray"); rect( startX + 2 * (buttonSize + buttonSpacing), buttonY, buttonSize, buttonHeight );

fill(0); text( "V1", startX + 2 * (buttonSize + buttonSpacing) + buttonSize / 2, buttonY + buttonHeight / 2 ); // Labels toggle

fill(showLabels ? "green" : "lightgray"); rect( startX + 3 * (buttonSize + buttonSpacing), buttonY, buttonSize, buttonHeight ); fill(0); text( "Labels", startX + 3 * (buttonSize + buttonSpacing) + buttonSize / 2, buttonY + buttonHeight / 2 ); // Parameter labels

fill(0); textAlign(LEFT, CENTER); textSize(11); text("Magnification: " + corticalMagnification.toFixed(1), 25, height - 10); text("Eccentricity: " + eccentricityFactor.toFixed(2), 195, height - 10); text("Anisotropy: " + anisotropyFactor.toFixed(2), 195, height - 45); textAlign(CENTER, CENTER); textSize(14); }

function drawTrails() { // Draw visual field trails

if (showVisualField) { for (let i = 0; i < trailVF.length; i++) { let frame = trailVF[i]; let alpha = map(i, 0, trailVF.length, 10, 100); for (let pt of frame) { fill(pt.color[0], pt.color[1], pt.color[2], alpha); ellipse(pt.x, pt.y, 8, 8); } } } // Draw cortex trails

if (showCortex) { for (let i = 0; i < trailV1.length; i++) { let frame = trailV1[i];

  let alpha = map(i, 0, trailV1.length, 10, 100);

  for (let pt of frame) {
    fill(pt.color[0], pt.color[1], pt.color[2], alpha);

    ellipse(pt.x, pt.y, pt.size * 0.5, pt.size * 0.5); // Smaller trail dots
  }
}

} }

function drawLabels() { fill(0);

if (showCortex) { textSize(12); // Fix: Corrected hemisphere labels to match new orientation text("Left Visual Field → Right Cortex", width * 0.75, height / 2 + 40); text("Right Visual Field → Left Cortex", width * 0.25, height / 2 + 40); }

if (showVisualField) { textSize(12); text("Upper VF → Lower V1", width - 120, 30); text("Lower VF → Upper V1", width - 120, height / 2 - 30); } // Display current coordinates

fill(0);

textSize(12);

text( "Stimulus: (" + (stimulusX - width / 2).toFixed(0) + ", " + (stimulusY - height / 4).toFixed(0) + ")", width / 2, height / 4 + 30 ); // Eccentricity value

let dx = stimulusX - width / 2; let dy = stimulusY - height / 4; let eccentricity = sqrt(dx * dx + dy * dy);

text( "Eccentricity: " + eccentricity.toFixed(1) + " px", width / 2, height / 4 + 50 ); // Instructions - smaller and more discreet

fill(0, 0, 0, 150);

textAlign(LEFT, CENTER); textSize(11); text("Drag dots to move stimulus", 10, 15); textAlign(CENTER, CENTER); textSize(14); }

function visualFieldToCortex(x, y) { let dx = x - width / 2; let dy = y - height / 4; let eccentricity = sqrt(dx * dx + dy * dy) / 100; let M = corticalMagnification / (eccentricityFactor + eccentricity);

// Calculate polar coordinates let angle = atan2(dy, dx); // Apply anisotropy factor to horizontal vs vertical

let horizontalFactor = anisotropyFactor;

let verticalFactor = 1 / anisotropyFactor;

// Adjust dx and dy by anisotropy factors

let adjustedDx = dx * horizontalFactor;

let adjustedDy = dy * verticalFactor;

// Recalculate eccentricity with adjusted values for anisotropic magnification

let adjustedEccentricity = sqrt(adjustedDx * adjustedDx + adjustedDy * adjustedDy) / 100;

// Apply magnification factor to eccentricity

let warpedDistance = adjustedEccentricity * M * 6; // Increased scaling factor // Convert back to cartesian for display let warpedX = warpedDistance * cos(angle) * horizontalFactor;

let warpedY = warpedDistance * sin(angle) * verticalFactor; // Instead of discrete switching, calculate proportion based on x position // This creates a smooth transition across the vertical meridian

let proportion = smoothstep(dx, -10, 10); // -10 to 10 pixel smooth transition zone // Blend between left and right cortex positions let leftCortexX = width * 0.15 - abs(warpedX); // Changed the + to a - let rightCortexX = width * 0.85 + abs(warpedX); // Keeping this as is for now let cortexX;

if (dx > 0) { // Right visual field maps to the left cortex

cortexX = width * 0.15 + warpedX; // Using warpedX directly (with its sign)

} else if (dx < 0) { // Left visual field maps to the right cortex

cortexX = width * 0.85 + warpedX; // Trying subtraction here too

} else { // Along the vertical meridian

cortexX = width * 0.5;

}

// Fix: Upper visual field should map to lower cortex and vice versa // Invert the Y coordinate for proper vertical mapping

let cortexY = height * 0.75 - warpedY; // Size depends on magnification factor let dotSize = constrain(M * 2, 5, 80); return { x: cortexX, y: cortexY, size: dotSize, proportion: proportion }; }

// Helper function to create a smooth step between 0 and 1

function smoothstep(value, edge0, edge1) { // Clamp value between edges

let x = constrain((value - edge0) / (edge1 - edge0), 0, 1); // Apply smoothstep formula: 3x^2 - 2x^3

return x * x * (3 - 2 * x); }

// Linear interpolation helper

function mix(a, b, t) { return a * (1 - t) + b * t;

function touchStarted() { mousePressed(); // Use the same logic console.log('touchstart') return false; // Prevent scrolling

}

function touchMoved() { mouseDragged(); // Use your existing drag logic return false; }

function touchEnded() { event.preventDefault();
mouseReleased(); // Clean up dragging state return false; }

// function onDrag() { // preventDefault(); // Stops scrolling during touchmove // const pos = getEventPosition(); // // do your dragging logic here // }

}

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published