/** * jspsych-webgazer-validate * Josh de Leeuw **/ jsPsych.plugins["webgazer-validate"] = (function() { var plugin = {}; plugin.info = { name: 'webgazer-validate', description: '', parameters: { validation_points: { type: jsPsych.plugins.parameterType.INT, default: [[10,10], [10,50], [10,90], [50,10], [50,50], [50,90], [90,10], [90,50], [90,90]] }, validation_point_coordinates: { type: jsPsych.plugins.parameterType.STRING, default: 'percent' // options: 'percent', 'center-offset-pixels' }, roi_radius: { type: jsPsych.plugins.parameterType.INT, default: 200 }, randomize_validation_order: { type: jsPsych.plugins.parameterType.BOOL, default: false }, time_to_saccade: { type: jsPsych.plugins.parameterType.INT, default: 1000 }, validation_duration: { type: jsPsych.plugins.parameterType.INT, default: 2000 }, point_size:{ type: jsPsych.plugins.parameterType.INT, default: 20 }, show_validation_data: { type: jsPsych.plugins.parameterType.BOOL, default: false } } } plugin.trial = function(display_element, trial) { var trial_data = {} trial_data.raw_gaze = []; trial_data.percent_in_roi = []; trial_data.average_offset = []; trial_data.validation_points = null; var html = `
` display_element.innerHTML = html; var wg_container = display_element.querySelector('#webgazer-validate-container'); var points_completed = -1; var val_points = null; var start = performance.now(); validate(); function validate(){ if(trial.randomize_validation_order){ val_points = jsPsych.randomization.shuffle(trial.validation_points); } else { val_points = trial.validation_points; } trial_data.validation_points = val_points; points_completed = -1; //jsPsych.extensions['webgazer'].resume(); jsPsych.extensions.webgazer.startSampleInterval(); //jsPsych.extensions.webgazer.showPredictions(); next_validation_point(); } function next_validation_point(){ points_completed++; if(points_completed == val_points.length){ validation_done(); } else { var pt = val_points[points_completed]; validation_display(pt); } } function validation_display(pt){ var pt_html = drawValidationPoint(pt[0], pt[1]); wg_container.innerHTML = pt_html; var pt_dom = wg_container.querySelector('.validation-point'); var br = pt_dom.getBoundingClientRect(); var x = br.left + br.width / 2; var y = br.top + br.height / 2; var pt_start_val = performance.now() + trial.time_to_saccade; var pt_finish = pt_start_val + trial.validation_duration; var pt_data = []; var cancelGazeUpdate = jsPsych.extensions['webgazer'].onGazeUpdate(function(prediction){ if(performance.now() > pt_start_val){ pt_data.push({x: prediction.x, y: prediction.y, dx: prediction.x - x, dy: prediction.y - y, t: Math.round(prediction.t-start)}); } }); requestAnimationFrame(function watch_dot(){ if(performance.now() < pt_finish){ requestAnimationFrame(watch_dot); } else { trial_data.raw_gaze.push(pt_data); cancelGazeUpdate(); next_validation_point(); } }); } function drawValidationPoint(x,y){ if(trial.validation_point_coordinates == 'percent'){ return drawValidationPoint_PercentMode(x,y); } if(trial.validation_point_coordinates == 'center-offset-pixels'){ return drawValidationPoint_CenterOffsetMode(x,y); } } function drawValidationPoint_PercentMode(x,y){ return `
` } function drawValidationPoint_CenterOffsetMode(x,y){ return `
` } function drawCircle(target_x, target_y, dx, dy, r){ if(trial.validation_point_coordinates == 'percent'){ return drawCircle_PercentMode(target_x, target_y, dx, dy, r); } if(trial.validation_point_coordinates == 'center-offset-pixels'){ return drawCircle_CenterOffsetMode(target_x, target_y, dx, dy, r); } } function drawCircle_PercentMode(target_x, target_y, dx, dy, r){ var html = `
` return html; } function drawCircle_CenterOffsetMode(target_x, target_y, dx, dy, r){ var html = `
` return html; } function drawRawDataPoint(target_x, target_y, dx, dy, ){ if(trial.validation_point_coordinates == 'percent'){ return drawRawDataPoint_PercentMode(target_x, target_y, dx, dy); } if(trial.validation_point_coordinates == 'center-offset-pixels'){ return drawRawDataPoint_CenterOffsetMode(target_x, target_y, dx, dy); } } function drawRawDataPoint_PercentMode(target_x, target_y, dx, dy){ var color = Math.sqrt(dx*dx + dy*dy) <= trial.roi_radius ? '#afa' : '#faa'; return `
` } function drawRawDataPoint_CenterOffsetMode(target_x, target_y, dx, dy){ var color = Math.sqrt(dx*dx + dy*dy) <= trial.roi_radius ? '#afa' : '#faa'; return `
` } function median(arr){ var mid = Math.floor(arr.length/2); var sorted_arr = arr.sort((a,b) => a-b); if(arr.length % 2 == 0){ return sorted_arr[mid-1] + sorted_arr[mid] / 2; } else { return sorted_arr[mid]; } } function calculateGazeCentroid(gazeData){ var x_diff_m = gazeData.reduce(function(accumulator, currentValue, index){ accumulator += currentValue.dx; if(index == gazeData.length-1){ return accumulator / gazeData.length; } else { return accumulator; } }, 0); var y_diff_m = gazeData.reduce(function(accumulator, currentValue, index){ accumulator += currentValue.dy; if(index == gazeData.length-1){ return accumulator / gazeData.length; } else { return accumulator; } }, 0); var median_distance = median(gazeData.map(function(x){ return(Math.sqrt(Math.pow(x.dx-x_diff_m,2) + Math.pow(x.dy-y_diff_m,2)))})); return { x: x_diff_m, y: y_diff_m, r: median_distance } } function calculatePercentInROI(gazeData){ var distances = gazeData.map(function(p){ return(Math.sqrt(Math.pow(p.dx,2) + Math.pow(p.dy,2))) }); var sum_in_roi = distances.reduce(function(accumulator, currentValue){ if(currentValue <= trial.roi_radius){ accumulator++; } return accumulator; }, 0); var percent = sum_in_roi / gazeData.length * 100; return percent; } function calculateSampleRate(gazeData){ var mean_diff = []; for(var i=0; i 1){ var t_diff = []; for(var j=1; j 0){ return 1000 / (mean_diff.reduce(function(a,b) { return(a+b) }, 0) / mean_diff.length); } else { return null; } } function validation_done(){ trial_data.samples_per_sec = calculateSampleRate(trial_data.raw_gaze).toFixed(2); for(var i=0; i