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 // }
}