Created
January 29, 2021 21:59
-
-
Save GEJ1/f0922c0c58fd70876eac1053ea4f5155 to your computer and use it in GitHub Desktop.
virtualchin
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* plugin for jsPsych based in Qisheng Li 11/2019. /// https://github.com/QishengLi/virtual_chinrest | |
Modified by Gustavo Juantorena 08/2020 // https://github.com/GEJ1 | |
Contributions from Peter J. Kohler: https://github.com/pjkohler | |
*/ | |
jsPsych.plugins['virtual-chinrest'] = (function() { | |
var plugin = {}; | |
plugin.info = { | |
name: "virtual-chinrest", | |
parameters: { | |
resize_units: { | |
type: jsPsych.plugins.parameterType.STRING, | |
default: "none", | |
description: 'What units to resize to? ["none"/"cm"/"inch"/"deg"]. If "none", no resize will be done.' | |
}, | |
pixels_per_unit: { | |
type: jsPsych.plugins.parameterType.INT, | |
pretty_name: 'Pixels per unit', | |
default: 100, | |
description: 'After the scaling factor is applied, this many pixels will equal one unit of measurement.' | |
}, | |
blindspot_reps: { | |
type: jsPsych.plugins.parameterType.INT, | |
pretty_name: 'Blindspot measurement repetitions', | |
default: 5, | |
description: 'How many times to measure the blindspot location? If 0, blindspot will not detected and viewing distance not computed.' | |
}, | |
prompt_card: { | |
type: jsPsych.plugins.parameterType.STRING, | |
default: '<b> Let’s find out how big your monitor is! </b>'+ | |
'<p>Please use any credit card that you have available.<br>' + | |
'It can also be a grocery store membership card,<br>'+ | |
'your drivers license or anything else of the same format.<br>'+ | |
'<b>Place your card flat onto the screen, and adjust the slider below to match its size.</b></p>'+ | |
'<p>If you do not have access to a real card <br>'+ | |
'you can use a ruler to measure the image width to 3.37 inches or 85.6 mm.<br>', | |
description: 'The content displayed below the resizable caard and above the button.' | |
}, | |
button_label_card: { | |
type: jsPsych.plugins.parameterType.STRING, | |
default: 'Click here when you are done!', | |
description: 'Label to display on the button to complete calibration.', | |
}, | |
prompt_blindspot: { | |
type: jsPsych.plugins.parameterType.STRING, | |
default: '<b>Now, let’s quickly test how far away you are sitting.</b>'+ | |
'<p>You might know that vision tests at a doctor’s practice often involve chinrests.<br>'+ | |
'The doctor basically asks you to sit away from a screen in a specific distance.<br>'+ | |
'We do this here with a “virtual chinrest”.</p><br>'+ | |
'<b>Instructions</b>'+ | |
'<div style="text-align: left">'+ | |
'<ol><li>Put your finger on <b>space bar</b> on the keyboard.</li>'+ | |
'<li>Close your right eye. <em>(Tips: it might be easier to cover your right eye by hand!)</em></li>'+ | |
'<li>Using your left eye, focus on the black square.</li>'+ | |
'<li>Click the button below to start the animation of the red ball. The <b style="color: red">red ball </b>'+ | |
'will disappear as it moves from right to left. Press the “Space” key as soon as the ball disappears from your eye sight.</li>'+ | |
'</div><br>' | |
}, | |
card_path: { | |
type: jsPsych.plugins.parameterType.STRING, | |
default: "img/card.png" | |
}, | |
item_height: { | |
type: jsPsych.plugins.parameterType.INT, | |
pretty_name: 'Item height', | |
default: 1, | |
description: 'The height of the item to be measured.' | |
}, | |
item_width: { | |
type: jsPsych.plugins.parameterType.INT, | |
pretty_name: 'Item width', | |
default: 1, | |
description: 'The width of the item to be measured.' | |
}, | |
starting_size: { | |
type: jsPsych.plugins.parameterType.INT, | |
pretty_name: 'Starting size', | |
default: 100, | |
description: 'The initial size of the card, in pixels, along the largest dimension.' | |
} | |
} | |
} | |
// Get screen size | |
var w = window.innerWidth; | |
var h = window.innerHeight; | |
const screen_size_px = [] | |
screen_size_px.push(w) | |
screen_size_px.push('x') | |
screen_size_px.push(h) | |
let trial_data = { | |
"card_width_mm": 85.60, //card dimension: 85.60 × 53.98 mm (3.370 × 2.125 in) | |
}; | |
let config_data = { | |
"ball_pos": [], | |
"slider_clck": false | |
} | |
plugin.trial = function(display_element, trial) { | |
try { | |
if ( !( trial.blindspot_reps > 0 ) && ( (trial.resize_units == "deg" ) || (trial.resize_units == "degrees" ) ) ) { | |
throw Error("Blindspot repetitions set to 0, so resizing to degrees of visual angle is not possible!") | |
} else { | |
const start_time = performance.now(); | |
if ( trial.blindspot_reps > 0 ) { | |
button_str = '<button id=blind_spot_button class="btn btn-primary">' | |
} else { | |
button_str = '<button id=proceed class="btn btn-primary">' | |
} | |
// variables to determine div size | |
var aspect_ratio = trial.item_width / trial.item_height; | |
if(trial.item_width >= trial.item_height){ | |
var start_div_width = trial.starting_size; | |
var start_div_height = Math.round(trial.starting_size / aspect_ratio); | |
} else { | |
var start_div_height = trial.starting_size; | |
var start_div_width = Math.round(trial.starting_size * aspect_ratio); | |
} | |
let pagesize_content = '<div id="page-size"><br><br>' | |
pagesize_content += '<div id="jspsych-resize-div" style="border: 2px solid steelblue; height: '+start_div_height+'px; width:'+start_div_width+'px; margin: 7px auto; background-color: lightsteelblue; position: relative;">'; | |
pagesize_content += '<div id="jspsych-resize-handle" style="cursor: nwse-resize; background-color: steelblue; width: 10px; height: 10px; border: 2px solid lightsteelblue; position: absolute; bottom: 0; right: 0;"></div>'; | |
pagesize_content += '</div> </div>' | |
if (trial.prompt_card !== null){ | |
pagesize_content += trial.prompt_card; | |
} | |
pagesize_content += '<br><br>' + button_str + trial.button_label_card + '</button>'; | |
// '<div id="page-size"><br><br>'+ | |
// trial.prompt_card + | |
// '<div id="container">'+ | |
// '<div id="slider"></div><br>'+ | |
// '<img id="card" src="' + trial.card_path + '" style="width: 50%">'+ | |
// '<br><br>' + button_str + 'Click here when you are done!</button>'+ | |
// '</div>'+ | |
// '</div>' | |
blindspot_content = | |
'<div id="blind-spot-screen" style="visibility: hidden">' + | |
trial.prompt_blindspot + | |
'<p>Please do it <b>' + trial.blindspot_reps + '</b> times. Keep your right eye closed and hit the “Space” key fast!</p><br>' + | |
'<div id="svgDiv" style="width:1000px;height:200px;"></div>'+ | |
'<button class="btn btn-primary" id="start_ball">Start</button>'+ | |
'<button class="btn btn-primary" id="proceed" style="display:none">Proceed</button><br>'+ | |
'<b>Hit space <div id="click" style="display:inline; color: red">' + trial.blindspot_reps + '</div> more times!<b><br>'+ | |
'<div id="info" style="visibility:hidden">'+ | |
'<b id="info-h">Estimated viewing distance (cm): </b>'+ | |
'</div>'+ | |
'</div>' | |
//render | |
display_element.innerHTML = | |
'<div id="content" style="width: 900px; margin: 0 auto;">'+ | |
pagesize_content + | |
blindspot_content + | |
'</div>' | |
// add card image as background of the resize div | |
document.getElementById("jspsych-resize-div").style.backgroundImage = "url('img/card.png')"; | |
document.getElementById("jspsych-resize-div").style.backgroundSize = "100% auto"; | |
document.getElementById("jspsych-resize-div").style.backgroundRepeat = "no-repeat"; | |
// We want to do this everytime but in some cases we don't want to scale (or at least scale 1:1) | |
// !!!!!!!!!!!!!! | |
document.getElementById("blind_spot_button").addEventListener('click', function() { | |
configureBlindSpot(); | |
document.getElementById("blind_spot_button").style.display = 'none' | |
// end_trial(); // I think that we dont need to finish the trial here everytime, in some cases we need to show the blindspot task | |
}); | |
// All the logic for cursor drag of the card and resize of the resize-div | |
var dragging = false; | |
var origin_x, origin_y; | |
var cx, cy; | |
var mousedownevent = function(e){ | |
e.preventDefault(); | |
dragging = true; | |
origin_x = e.pageX; | |
origin_y = e.pageY; | |
cx = parseInt(scale_div.style.width); | |
cy = parseInt(scale_div.style.height); | |
} | |
display_element.querySelector('#jspsych-resize-handle').addEventListener('mousedown', mousedownevent); | |
var mouseupevent = function(e){ | |
dragging = false; | |
} | |
document.addEventListener('mouseup', mouseupevent); | |
var scale_div = display_element.querySelector('#jspsych-resize-div'); | |
var resizeevent = function(e){ | |
if(dragging){ | |
var dx = (e.pageX - origin_x); | |
var dy = (e.pageY - origin_y); | |
if(Math.abs(dx) >= Math.abs(dy)){ | |
scale_div.style.width = Math.round(Math.max(20, cx+dx*2)) + "px"; | |
scale_div.style.height = Math.round(Math.max(20, cx+dx*2) / aspect_ratio ) + "px"; | |
} else { | |
scale_div.style.height = Math.round(Math.max(20, cy+dy*2)) + "px"; | |
scale_div.style.width = Math.round(aspect_ratio * Math.max(20, cy+dy*2)) + "px"; | |
} | |
} | |
} | |
document.addEventListener('mousemove', resizeevent); | |
function scale() { | |
final_width_px = scale_div.offsetWidth; | |
var pixels_unit_screen = final_width_px / trial.item_width; | |
scale_factor = pixels_unit_screen / trial.pixels_per_unit; | |
// scale_factor = px2unit_scr / trial.pixels_per_unit; | |
document.getElementById("jspsych-content").style.transform = "scale(" + scale_factor + ")"; | |
// pixels have been scaled, so pixels per degree, pixels per mm and pixels per card_width needs to be updated | |
trial_data.px2deg = trial_data.px2deg / scale_factor | |
trial_data.px2mm = trial_data.px2mm / scale_factor | |
trial_data.card_width_px = trial_data.card_width_px / scale_factor | |
trial_data.scale_factor = scale_factor | |
//final_height_px = scale_div.offsetHeight; | |
// var pixels_unit_screen = final_width_px / trial.item_width; | |
// document.getElementById("jspsych-content").style.transform = "scale(" + scale_factor + ")"; | |
}; | |
//Event listeners for buttons | |
if ( trial.blindspot_reps > 0 ) { | |
display_element.querySelector('#blind_spot_button').addEventListener('click', function(){ | |
configureBlindSpot() | |
}) | |
display_element.querySelector('#start_ball').addEventListener('click', function(){ | |
animateBall() | |
}) | |
} else { | |
// run the two relevant functions to get card_width_mm and px2mm | |
distanceSetup.px2mm(getCardWidth()) | |
} | |
display_element.querySelector('#proceed').addEventListener('click', function(){ | |
// finish trial | |
trial_data.rt = performance.now() - start_time; | |
display_element.innerHTML = ''; | |
trial_data.card_width_deg = 2*(Math.atan((trial_data["card_width_mm"]/2)/trial_data["view_dist_mm"])) * 180/Math.PI | |
trial_data.px2deg = trial_data["card_width_px"] / trial_data.card_width_deg // size of card in pixels divided by size of card in degrees of visual angle | |
let px2unit_scr = 0 | |
switch (trial.resize_units) { | |
case "cm": | |
case "centimeters": | |
px2unit_scr = trial_data["px2mm"]*10 // pixels per centimeter | |
break; | |
case "inch": | |
case "inches": | |
px2unit_scr = trial_data["px2mm"]*25.4 // pixels per inch | |
break; | |
case "deg": | |
case "degrees": | |
px2unit_scr = trial_data["px2deg"] // pixels per degree of visual angle | |
break; | |
} | |
if (px2unit_scr > 0) { | |
// scale the window | |
scale(); | |
} | |
if ( trial.blindspot_reps > 0 ) { | |
trial_data.win_width_deg = window.innerWidth/trial_data.px2deg | |
trial_data.win_height_deg = window.innerHeight/trial_data.px2deg | |
} else { | |
// delete degree related properties | |
delete trial_data.px2deg | |
delete trial_data.card_width_deg | |
} | |
jsPsych.finishTrial(trial_data); | |
jsPsych.pluginAPI.cancelAllKeyboardResponses(); | |
}) | |
} | |
} catch (e) { | |
console.error(e) | |
} | |
}; | |
(function ( distanceSetup, $ ) { // jQuery short-hand for $(document).ready(function() { ... }); | |
distanceSetup.round = function(value, decimals) { | |
return Number(Math.round(value+'e'+decimals)+'e-'+decimals); | |
}; | |
distanceSetup.px2mm = function(cardImageWidth) { | |
const cardWidth = 85.6; //card dimension: 85.60 × 53.98 mm (3.370 × 2.125 in) | |
var px2mm = cardImageWidth/cardWidth; | |
trial_data["px2mm"] = distanceSetup.round(px2mm, 2); | |
return px2mm; | |
}; | |
}( window.distanceSetup = window.distanceSetup || {}, jQuery)) | |
function getCardWidth() { | |
var card_width_px = $('#card').width(); | |
trial_data["card_width_px"] = distanceSetup.round(card_width_px,2); | |
return card_width_px | |
} | |
function configureBlindSpot() { | |
drawBall(); | |
$('#page-size').remove(); | |
$('#blind-spot-screen').css({'visibility':'visible'}); | |
// $(document).on('keydown', recordPosition); | |
$(document).on('keydown', recordPosition); | |
} | |
$( function() { | |
$( "#slider" ).slider({value:"50"}); | |
} ); | |
$(document).ready(function() { | |
$( "#slider" ).on("slide", function (event, ui) { | |
var cardWidth = ui.value + "%"; | |
$("#card").css({"width":cardWidth}); | |
}); | |
$('#slider').on('slidechange', function(event, ui){ | |
config_data["slider_clck"] = true; | |
}); | |
}); | |
//============================= | |
//Ball Animation | |
function drawBall(pos=180){ | |
// pos: define where the fixation square should be. | |
var mySVG = SVG("svgDiv"); | |
const cardWidthPx = getCardWidth() | |
const rectX = distanceSetup.px2mm(cardWidthPx)*pos; | |
const ballX = rectX*0.6 // define where the ball is | |
var ball = mySVG.circle(30).move(ballX, 50).fill("#f00"); | |
window.ball = ball; | |
var square = mySVG.rect(30, 30).move(Math.min(rectX - 50, 950), 50); //square position | |
config_data["square_pos"] = distanceSetup.round(square.cx(),2); | |
config_data['rectX'] = rectX | |
config_data['ballX'] = ballX | |
}; | |
function animateBall(){ | |
ball.animate(7000).during( | |
function(pos){ | |
moveX = - pos*config_data['ballX']; | |
window.moveX = moveX; | |
moveY = 0; | |
ball.attr({transform:"translate("+moveX+","+moveY+")"}); | |
} | |
).loop(true, false). | |
after(function(){ | |
animateBall(); | |
}); | |
//disable the button after clicked once. | |
$("#start_ball").attr("disabled", true); | |
$('#start_ball').css("display", "none"); | |
}; | |
function recordPosition(event, angle=13.5) { | |
// angle: define horizontal blind spot entry point position in degrees. | |
if (event.keyCode == '32') { //Press "Space" | |
config_data["ball_pos"].push(distanceSetup.round((ball.cx() + moveX),2)); | |
var sum = config_data["ball_pos"].reduce((a, b) => a + b, 0); | |
var ballPosLen = config_data["ball_pos"].length; | |
config_data["avg_ball_pos"] = distanceSetup.round(sum/ballPosLen, 2); | |
var ball_sqr_distance = (config_data["square_pos"]-config_data["avg_ball_pos"])/trial_data["px2mm"]; | |
var viewDistance = ball_sqr_distance/Math.radians(angle) | |
trial_data["view_dist_mm"] = distanceSetup.round(viewDistance, 2); | |
//counter and stop | |
var counter = Number($('#click').text()); | |
counter = counter - 1; | |
$('#click').text(Math.max(counter, 0)); | |
if (counter <= 0) { | |
ball.stop(); | |
// Disable space key | |
$('html').bind('keydown', function(e) | |
{ | |
if (e.keyCode == 32) {return false;} | |
}); | |
// Display data | |
$('#info').css("visibility", "visible"); | |
$('#info-h').append(trial_data["view_dist_mm"]/10) | |
$('#proceed').css("display", "inline"); | |
return | |
} | |
ball.stop(); | |
animateBall(); | |
} | |
}; | |
//helper function for radians | |
// Converts from degrees to radians. | |
Math.radians = function(degrees) { | |
return degrees * Math.PI / 180; | |
}; | |
// *************Implementing resize as jspsych-resize plugin***************** | |
// var aspect_ratio = trial.item_width / trial.item_height; | |
// // variables to determine div size | |
// if(trial.item_width >= trial.item_height){ | |
// var start_div_width = trial.starting_size; | |
// var start_div_height = Math.round(trial.starting_size / aspect_ratio); | |
// } else { | |
// var start_div_height = trial.starting_size; | |
// var start_div_width = Math.round(trial.starting_size * aspect_ratio); | |
// } | |
// create html for display | |
// var html = '<div id="page-size"><br><br>' | |
// html += '<div id="jspsych-resize-div" style="border: 2px solid steelblue; height: '+start_div_height+'px; width:'+start_div_width+'px; margin: 7px auto; background-color: lightsteelblue; position: relative;">'; | |
// html += '<div id="jspsych-resize-handle" style="cursor: nwse-resize; background-color: steelblue; width: 10px; height: 10px; border: 2px solid lightsteelblue; position: absolute; bottom: 0; right: 0;"></div>'; | |
// html += '</div> </div>' | |
// if (trial.prompt_card !== null){ | |
// html += trial.prompt_card; | |
// } | |
// html += '<a class="jspsych-btn" id="jspsych-resize-btn">'+trial.button_label_card+'</a>'; | |
// render | |
// display_element.innerHTML = html; | |
// // add card image as background of the div | |
// document.getElementById("jspsych-resize-div").style.backgroundImage = "url('img/card.png')"; | |
// document.getElementById("jspsych-resize-div").style.backgroundSize = "100% auto"; | |
// listens for the click | |
// document.getElementById("jspsych-resize-btn").addEventListener('click', function() { | |
// scale(); | |
// // end_trial(); // I think that we dont need to finish the trial here everytime, in some cases we need to show the blindspot task | |
// }); | |
// var dragging = false; | |
// var origin_x, origin_y; | |
// var cx, cy; | |
// var mousedownevent = function(e){ | |
// e.preventDefault(); | |
// dragging = true; | |
// origin_x = e.pageX; | |
// origin_y = e.pageY; | |
// cx = parseInt(scale_div.style.width); | |
// cy = parseInt(scale_div.style.height); | |
// } | |
// display_element.querySelector('#jspsych-resize-handle').addEventListener('mousedown', mousedownevent); | |
// var mouseupevent = function(e){ | |
// dragging = false; | |
// } | |
// document.addEventListener('mouseup', mouseupevent); | |
// var scale_div = display_element.querySelector('#jspsych-resize-div'); | |
// var resizeevent = function(e){ | |
// if(dragging){ | |
// var dx = (e.pageX - origin_x); | |
// var dy = (e.pageY - origin_y); | |
// if(Math.abs(dx) >= Math.abs(dy)){ | |
// scale_div.style.width = Math.round(Math.max(20, cx+dx*2)) + "px"; | |
// scale_div.style.height = Math.round(Math.max(20, cx+dx*2) / aspect_ratio ) + "px"; | |
// } else { | |
// scale_div.style.height = Math.round(Math.max(20, cy+dy*2)) + "px"; | |
// scale_div.style.width = Math.round(aspect_ratio * Math.max(20, cy+dy*2)) + "px"; | |
// } | |
// } | |
// } | |
// document.addEventListener('mousemove', resizeevent); | |
// function scale() { | |
// final_width_px = scale_div.offsetWidth; | |
// //final_height_px = scale_div.offsetHeight; | |
// var pixels_unit_screen = final_width_px / trial.item_width; | |
// scale_factor = pixels_unit_screen / trial.pixels_per_unit; | |
// document.getElementById("jspsych-content").style.transform = "scale(" + scale_factor + ")"; | |
// }; | |
return plugin; | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment