/************************************************************* * test.js * * Sets up the experiment and contains all the logic for the * progression of the experiment. The main javascript file that * powers this experiment. * * Author: Yuechen Zhao * Last Modified: Dianna Hu - July 23, 2013 * * © Copyright 2013 Intelligent and Interactive Systems * Group, Harvard University. * For questions about this file and permission to use * the code, contact us at info@labinthewild.org *************************************************************/ // variable used to determine whether using a touch device later in the code var withTouch = false; // variable used to determine where the common resources are var commonResourcesLoc = "../common/"; // experiment object holds all the information necessary for the experiment, records data var expment = { /* Trials will hold all the information for each trial. */ trials: { practice: [], experiment: [] }, // results will hold all of the results that get submitted at the end results: { practice: [], experiment: [] }, trialCounts: { practice: { changeDetectionSameLocation: 2, changeDetectionExpand: 2, changeDetectionShrink: 2, changeDetectionRandom: 2, focalDetection: 2 }, experiment: { changeDetectionSameLocation: 8, changeDetectionExpand: 8, changeDetectionShrink: 8, changeDetectionRandom: 8, focalDetection: 6 } } // trialCounts: { // practice: { // changeDetectionSameLocation: 1, // changeDetectionExpand: 1, // changeDetectionRandom: 1, // focalDetection: 1 // }, // experiment: { // changeDetectionSameLocation: 2, // changeDetectionExpand: 2, // changeDetectionRandom: 2, // focalDetection: 2 // } // } }; // additional variables that the experiment needs in order to function var vars = { // variable that we use to hold the event chain ch: null, // the phase of the test we are on, 'practice' or 'experiment' phase: 'practice', // the participant id participant_id: 0, // the country that the participant currently lives in participant_country: "Wonderland", // the colours of the squares in the change detection tasks square_colors: ["#000000", "#000000", "#000000", "#000000"] }; // configuration var config = { stimDuration: 500, itiDuration: 500, focusCrossDuration: 1007, changeDetectionDuration: 150, changeDetectionBlankDuration: 907, canvasWidth: 450, canvasHeight: 450, changeDetectionSquareColors: ["red", "green", "blue", "purple", "cyan", "yellow", "magenta", "white", "black"], frameCrossColor: "#cccccc" } var progressHTML = '
Progress:
' + "
" + "
" + "
" + "
"; // init the test function init() { $$$("inst").innerHTML = "
" + "


" + "

Quick Instructions on How to Test Your Reaction Time

" + "

" + 'In this test, you will be given a series of tasks in which you are to determine the changes in simple images of geometric shapes. You should try to react as quickly as possible on each trial. ' + "" + 'Please maximize your browser window before you begin.' + "

" + "

" + "There will be a short practice trial before the actual test to familiarize you with what you will have to do. As you go along, you will receive detailed instructions on what to do for the different tasks. In all, the test should take about 5 minutes." + "

" + "

" + "After the test, you will be presented some test results to show your average reaction time" + ". " + // "You\'ll learn about how fast you react and how they compare to others with similar backgrounds to you!" + "

" + "Note: You can also press [space] instead of clicking [next] to continue." + "
"; // remove loader $$$("ajax_working").style.display = "none"; $$$("next_button").style.opacity = '1'; showSlide("inst"); showNextButton(nextTrial); } // function to finish the experiment function finish() { // console.log(JSON.stringify(expment.results)); $.ajax({ type: 'POST', url: 'includes/data.php', data: { participant_id: vars.participant_id, participant_country: vars.participant_country, results: JSON.stringify(expment.results), exp_trial_count: expment.trialCounts["experiment"]["total"] } }).done( function (data) { console.log("data=" + data); if ((typeof data) == "string" && data.slice(0,6) == "Failed") { // console.log("first"); handleError(data); } else { try { results(JSON.parse(data)); } catch (e) { // console.log("second"); handleError("ERR: " + e.toString() + " DATA: " + data); } } }); } // processes the user's response, and records the data for the trial if necessary function processResponse(input, trial) { // if there's a chain of timeouts, clear them all if(vars.ch) clearChain(vars.ch); // record data // `response` should be either 0 (incorrect), 1 (correct), or // -1 (don't know) for change detection tasks, and 1 for // focal detection tasks. expment.results[vars.phase].push({ task_type: trial.taskType, response: input.response, reaction_time: input.reaction_time, color_changed: input.color_changed ? 1 : 0 }); // console.log(expment.results); $.ajax({ type: 'POST', url: 'includes/dropout.php', data: { participant_id: vars.participant_id, trial_num: trial.num, phase: vars.phase, task_type: trial.taskType, response: input.response, reaction_time: input.reaction_time, color_changed: input.color_changed } }).done(function() {}); // and this trial is done; go to the next trial. nextTrial(); } // The main function to handle running the trials. // This contains the chain of events and the end of experiment functionality. function nextTrial() { var trial; // Shift the next trial's information off the front of the trials array. if (trial = expment.trials[vars.phase].shift()) { // Randomize colors for change detection var colorChoices; if(trial.taskType !== 'focalDetection') { // work with copy colorChoices = arrayShuffle(config.changeDetectionSquareColors.slice()); for(var i = 0; i < vars.square_colors.length; i++) { vars.square_colors[i] = colorChoices.shift(); } } // Change prompt to reflect instructions by task type var focalDetectionName = 'Flashing Square'; var changeDetectionName = 'Changing Color'; var taskName = trial.taskType == 'focalDetection' ? focalDetectionName : changeDetectionName; var focalDetectionTaskDirective = 'Hit the spacebar as soon as the cross changes into a square.'; var changeDetectionTaskDirective = 'Click on the square that changes color.'; var directive = trial.taskType == 'focalDetection' ? focalDetectionTaskDirective : changeDetectionTaskDirective; var illustration = trial.taskType == 'focalDetection' ? "" : "" $$$("prompt").innerHTML = '
' + "

" + taskName + "

" + 'Focus on the cross in the next screen. ' + directive + '

' + illustration + '
' + '
' + 'Ready?' + '
' + // '
' + 'Click here to proceed' + '
'; '
'; // Show instructions, wait for user response if (trial.type === "inst") { $$$("progress").innerHTML = ""; $$$("heading").innerHTML = ""; switch (vars.phase) { case 'practice': $$$("inst").innerHTML = "

" + 'Practice Trial' + "

" + "

" + "Remember, this is only for practice. " + 'Your results will be discarded.' + "

" + "

" + 'You will receive instructions on what to do on each task. ' + "Press next when you\'re ready to begin the practice. Get ready! You have to be fast!" + "

"; break; case 'experiment': $$$("inst").innerHTML = "

" + 'Great Job! :)' + "

" + "baby cheetah" + "

" + "Now for the real deal. It\'ll be just like the practice you just took, but with more trials. Don\'t worry, it will just take a few minutes." + "

" + "When you\'re ready, press next to begin the test." + "

"; break; case 2: $$$("inst").innerHTML = "

" + "

" + 'You\'re Doing Great!' + "

" + "

" + "Time for a break! You\'re half-way there!" + "

" + "When you\'re ready, press next to begin part 2, which is identical to the part you just completed." + "
" + "Note that some websites are repeated intentionally. Please rate them again honestly." + "


" + "baby cheetah"; break; } showSlide("inst"); showNextButton(nextTrial); } else { // `response` should be either 0 (incorrect), 1 (correct), or // -1 (don't know) for change detection tasks, and null for // focal detection tasks. var input = { reaction_time: null, response: null, color_changed: null } // $$$("progress").innerHTML = progressHTML; // do a trial here var focalDetectionSequence = [ function () { hideNextButton(); showSlide("empty"); $("#stims").children().hide(); $("#" + trial.stim_type + "_" + trial.stim_index).show(); if(vars.phase == 'practice') { $$$("heading").innerHTML = "

" + "Practice" + "

"; } // $$$("progress").innerHTML = "Progress: " + trial.num + " / " + expment.trialCounts[vars.phase]["total"]; $$$("progress").innerHTML = progressHTML; $("#progressbar > div").css({ width: ((trial.num / expment.trialCounts[vars.phase]["total"]) * 100) + "%" }); $$$('response-buttons').style.visibility = 'hidden'; }, config.itiDuration, function () { showSlide("task"); // Draw focus cross var canvas = document.getElementById("task-canvas"); var ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, config.canvasWidth, config.canvasHeight); ctx.strokeStyle = "#000000"; ctx.fillStyle = "#ffffff"; ctx.beginPath(); ctx.moveTo(0.4 * config.canvasWidth, 0.5 * config.canvasHeight); ctx.lineTo(0.6 * config.canvasWidth, 0.5 * config.canvasHeight); ctx.moveTo(0.5 * config.canvasWidth, 0.4 * config.canvasHeight); ctx.lineTo(0.5 * config.canvasWidth, 0.6 * config.canvasHeight); ctx.stroke(); }, config.focusCrossDuration, function () { var canvas = document.getElementById("task-canvas"); var ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, config.canvasWidth, config.canvasHeight); // Square drawRect(ctx, "black", "white", 0.4 * config.canvasWidth, 0.4 * config.canvasHeight, 0.2 * config.canvasWidth, 0.2 * config.canvasHeight); // $$$('response-buttons').innerHTML = ""; $$$('response-buttons').style.visibility = 'hidden'; var rt_start = new Date(); input.color_changed = false; if(withTouch) { $(canvas).off('click.focal-detection') .on('click.focal-detection', function() { input.reaction_time = (new Date()) - rt_start; $(canvas).off('click.focal-detection'); input.response = 1; processResponse(input, trial); }); } getKeyboardInput(["space"], function() { input.reaction_time = (new Date()) - rt_start; disableKeyboard(); input.response = 1; processResponse(input, trial); }); } ]; var changeDetectionSequence = [ function () { hideNextButton(); showSlide("empty"); $("#stims").children().hide(); $("#" + trial.stim_type + "_" + trial.stim_index).show(); if(vars.phase == 'practice') { $$$("heading").innerHTML = "

" + "Practice" + "

"; } // $$$("progress").innerHTML = "Progress: " + trial.num + " / " + expment.trialCounts[vars.phase]["total"]; $$$("progress").innerHTML = progressHTML; $("#progressbar > div").css({ width: ((trial.num / expment.trialCounts[vars.phase]["total"]) * 100) + "%" }); $$$('response-buttons').style.visibility = 'hidden'; }, config.itiDuration, function () { showSlide("task"); // Draw focus cross var canvas = document.getElementById("task-canvas"); var ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, config.canvasWidth, config.canvasHeight); ctx.strokeStyle = "#000000"; ctx.fillStyle = "#ffffff"; ctx.beginPath(); ctx.moveTo(0.4 * config.canvasWidth, 0.5 * config.canvasHeight); ctx.lineTo(0.6 * config.canvasWidth, 0.5 * config.canvasHeight); ctx.moveTo(0.5 * config.canvasWidth, 0.4 * config.canvasHeight); ctx.lineTo(0.5 * config.canvasWidth, 0.6 * config.canvasHeight); ctx.stroke(); }, config.focusCrossDuration, function () { var canvas = document.getElementById("task-canvas"); var ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, config.canvasWidth, config.canvasHeight); // Big cross ctx.strokeStyle = config.frameCrossColor; ctx.fillStyle = "#ffffff"; ctx.beginPath(); ctx.moveTo(0.2 * config.canvasWidth, 0.5 * config.canvasHeight); ctx.lineTo(0.8 * config.canvasWidth, 0.5 * config.canvasHeight); ctx.moveTo(0.5 * config.canvasWidth, 0.2 * config.canvasHeight); ctx.lineTo(0.5 * config.canvasWidth, 0.8 * config.canvasHeight); ctx.stroke(); // Squares drawRect(ctx, "black", vars.square_colors[0], 0.2 * config.canvasWidth, 0.2 * config.canvasHeight, 0.1 * config.canvasWidth, 0.1 * config.canvasHeight); drawRect(ctx, "black", vars.square_colors[1], 0.7 * config.canvasWidth, 0.2 * config.canvasHeight, 0.1 * config.canvasWidth, 0.1 * config.canvasHeight); drawRect(ctx, "black", vars.square_colors[2], 0.2 * config.canvasWidth, 0.7 * config.canvasHeight, 0.1 * config.canvasWidth, 0.1 * config.canvasHeight); drawRect(ctx, "black", vars.square_colors[3], 0.7 * config.canvasWidth, 0.7 * config.canvasHeight, 0.1 * config.canvasWidth, 0.1 * config.canvasHeight); }, config.changeDetectionDuration, function () { trial.st = (new Date()) - trial.st; // showSlide("empty"); var canvas = document.getElementById("task-canvas"); var ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, config.canvasWidth, config.canvasHeight); }, config.changeDetectionBlankDuration, function () { // var rt_start = new Date(); // var input = { response: 0, rt: 0 }; // showSlide("task"); // $$$('response-buttons').innerHTML = // "" + // ""; $$$('response-buttons').style.visibility = 'visible'; var canvas = document.getElementById("task-canvas"); var ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, config.canvasWidth, config.canvasHeight); // Big cross ctx.strokeStyle = config.frameCrossColor; ctx.fillStyle = "#ffffff"; ctx.beginPath(); ctx.moveTo(0.2 * config.canvasWidth, 0.5 * config.canvasHeight); ctx.lineTo(0.8 * config.canvasWidth, 0.5 * config.canvasHeight); ctx.moveTo(0.5 * config.canvasWidth, 0.2 * config.canvasHeight); ctx.lineTo(0.5 * config.canvasWidth, 0.8 * config.canvasHeight); ctx.stroke(); // Squares var getRandomArbitary = function (min, max) { return Math.random() * (max - min) + min; } var offset = function() { var result; switch(trial.taskType) { case 'changeDetectionExpand': result = 0.1 * config.canvasWidth; break; case 'changeDetectionShrink': result = -0.1 * config.canvasWidth; break; case 'changeDetectionRandom': result = getRandomArbitary(-0.1 * config.canvasWidth, 0.1 * config.canvasWidth); // console.log(result); break; case 'changeDetectionSameLocation': result = 0; } return result; } var shouldChangeSquare = Math.floor(Math.random() * 2) == 1; var changedSquare = shouldChangeSquare ? Math.round((Math.random() * 10) % vars.square_colors.length - 1) : -1; if(shouldChangeSquare) { vars.square_colors[changedSquare] = colorChoices.shift(); // console.log("Square " + changedSquare + " changed"); } var squareWidth = 0.1 * config.canvasWidth; var squareHeight = 0.1 * config.canvasHeight; var squareCoords = [ { x: 0.2 * config.canvasWidth - offset(), y: 0.2 * config.canvasHeight - offset() }, { x: 0.7 * config.canvasWidth + offset(), y: 0.2 * config.canvasHeight - offset() }, { x: 0.2 * config.canvasWidth - offset(), y: 0.7 * config.canvasHeight + offset() }, { x: 0.7 * config.canvasWidth + offset(), y: 0.7 * config.canvasHeight + offset() } ] // console.log(squareCoords); drawRect(ctx, "black", vars.square_colors[0], squareCoords[0].x, squareCoords[0].y, squareWidth, squareHeight); drawRect(ctx, "black", vars.square_colors[1], squareCoords[1].x, squareCoords[1].y, squareWidth, squareHeight); drawRect(ctx, "black", vars.square_colors[2], squareCoords[2].x, squareCoords[2].y, squareWidth, squareHeight); drawRect(ctx, "black", vars.square_colors[3], squareCoords[3].x, squareCoords[3].y, squareWidth, squareHeight); var rt_start = new Date(); input.color_changed = shouldChangeSquare; // console.log(input.color_changed); $('#no-change-btn').off('click').on('click', function() { input.reaction_time = (new Date()) - rt_start; // Check if there's actually no change if(shouldChangeSquare == false) { input.response = 1; } else { input.response = 0; } processResponse(input, trial); }); $('#dont-know-btn').off('click').on('click', function() { input.reaction_time = (new Date()) - rt_start; input.response = -1; processResponse(input, trial); }); $(canvas).off('click.change-detection') .on('click.change-detection', function(e) { input.reaction_time = (new Date()) - rt_start; var cursorPos = getCanvasCursorPosition(canvas, e); var x = cursorPos.x; var y = cursorPos.y; var hitSquareIndex; for(var i = 0; i < squareCoords.length; i++) { var hit = isPointInRect(x, y, squareCoords[i].x, squareCoords[i].y, squareWidth, squareHeight); if(hit) { hitSquareIndex = i; } } if(hitSquareIndex !== undefined) { $(canvas).off('click.change-detection'); // console.log(hitSquareIndex == changedSquare); if(hitSquareIndex == changedSquare) { // alert("Yep"); input.response = 1; } else { input.response = 0; } processResponse(input, trial); } }); } ]; $$$("progress").innerHTML = ""; $$$("heading").innerHTML = ""; showSlide("prompt"); hideNextButton(); var proceedWithTask = function() { if(trial.taskType == 'focalDetection') { vars.ch = chain.apply(this, focalDetectionSequence); } else { vars.ch = chain.apply(this, changeDetectionSequence) } } $('#ready-proceed-button').on('click.prompt.proceed', function() { $('#ready-proceed-button').off('click.prompt.proceed'); proceedWithTask(); }); // getKeyboardInput(["space"], function() { // disableKeyboard(); // proceedWithTask(); // }); } } else { // not completely done yet? go on to next phase! if (vars.phase == 'practice') { vars.phase = 'experiment'; nextTrial(); return; } // else, finish the test showCommentsPage(); // showDemographics(); } } function drawRect(ctx, stroke, fill, x, y, w, h) { ctx.lineWidth = 1; ctx.strokeStyle = stroke; ctx.fillStyle = fill; ctx.beginPath(); ctx.rect(x, y, w, h); ctx.fill(); ctx.stroke(); } function getCanvasCursorPosition(canvas, event) { var rect = canvas.getBoundingClientRect(); var x = event.clientX - rect.left; var y = event.clientY - rect.top; return {x: x, y: y} } function isPointInRect(x, y, rx, ry, rw, rh) { var inX = false; var inY = false; var bottomLeftX = rx; var bottomLeftY = ry + rh; var topRightX = rx + rw; var topRightY = ry; if(x > bottomLeftX && x < topRightX) { inX = true; } if(y < bottomLeftY && y > topRightY) { inY = true; } return (inX && inY); } // setup function function setup() { // force orientation fixOrientation("landscape"); // first do a browser detect var browserDetectResults = browserDetect(); withTouch = browserDetectResults.is_mobile; if (!browserDetectResults.pass) { $("#error").html(getBrowserErrorHTML(browserDetectResults)); showFailPage(); } // set up html $("#header").html("" + 'Tell your friends about this test!' + ""); $("#share_this").detach().appendTo("#header"); $("#share_this").show(); $("#header").append("
 
"); var why = 'We are trying to understand perceptual differences between people from varying cultural backgrounds. More specifically, the goal of this study is to find out the extent of the role our cultural background has in shaping the our visual perception.'; var what = 'You will be given tasks to identify as quickly as possible which square out of four, if any, changes color, and also tasks to respond as quickly as possible when a square appears on screen. Pressing the back button or leaving the test will cause all progress to be lost. You will receive more specific instructions shortly.'; var min = "5"; var details = 'If you have questions about your rights as a research participant, or wish to obtain information, ask questions or discuss any concerns about this study with someone other than the researcher(s), please contact the University of Washington Human Subjects Division at 206-543-0098 (for international calls include the US Calling Code: +1-206-543-0098).'; var study_info = "

"+'Welcome to the Reaction Time Study!'+"

"+'Please carefully read the following information.' + "

"; study_info += ""+'Why we are doing this research'+": "+why+"

"; study_info += ""+'What you will have to do'+": "+what+"

"; study_info += 'What you will get out of it: We will give you feedback on how your results compare to those of other participants. The final results from this experiment will be posted on our blog page. The experiment is not designed to benefit you, but you may enjoy it and enjoy comparing your results with those of other participants.'+"

"; study_info += 'Privacy and Data Collection: We will not ask you for your name. Any data that we collect will be securely stored on our servers.'+"

"; study_info += ""+'Length of the experiment:'+" "+min+" min

"; study_info += ""+'Contact information: ' +"" + 'If you have questions about this research, you may contact Professor Katharina Reinecke, Paul G. Allen Center for Computer Science & Engineering, Box 352350, Seattle, WA 98195, ' + "reinecke@cs.washington.edu

"; study_info += details+"

"; study_info += "
"; study_info += ""; study_info += "
"; $$$("studyinfo").innerHTML = study_info; //$$$("likert").innerHTML = // '
' + 'Please rate the website you have just seen based on ' + // 'visual appeal.
' + // getLikertScaleHTML("very
unappealing", "very
appealing"); $$$("ajax_working").innerHTML = getAJAXWorkingHTML(); $$$("error").innerHTML = getFatalErrorHTML(); $$$("focus_cross").innerHTML = '
' + "cross" + '
'; $$$("task").innerHTML = '
' + "
"; $('#task-canvas').prop({ width: config.canvasWidth, height: config.canvasHeight }); // Create all the trials var taskTrials = { practice: [], experiment: [] } for(phase in expment.trialCounts) { var total = 0; for(taskType in expment.trialCounts[phase]) { for(var i = 0; i < expment.trialCounts[phase][taskType]; i++) { taskTrials[phase].push(taskType); total++; } } expment.trialCounts[phase]["total"] = total; } // shuffle for(phase in taskTrials) { taskTrials[phase] = arrayShuffle(taskTrials[phase]); } // Populate the `trials` array for(phase in taskTrials) { // Add in an instruction trial as the first one expment.trials[phase].push({ num: 0, type: "inst" }); for(var i = 0; i < taskTrials[phase].length; i++) { expment.trials[phase].push({ num: i + 1, type: "trial", taskType: taskTrials[phase][i] }) } } // preload all images // var preloadArr = getCommonImageLinksArray().concat(["img/inst_stim.png", "img/inst_likert.png", "img/person_blue.png", "img/person_gray.png", // "img/cute-cat.jpg"]); // for (var i = 0; i < 2; i++) { // for (var j = 0; j < stims[i].length; j++) { // preloadArr.push(stims[i][j].url); // } // } // load images into DOM in order to just hide and unhide stuff // var preload_helper = function () { // for (var i = 0; i < 2; i++) { // for (var j = 0; j < stims[i].length; j++) { // $("#stims").append(""); // } // } // // studyInfo(); // } // preload(preloadArr, preload_helper); // showDemographics(); // init(); studyInfo(); $.ajax({ type: 'POST', url: 'includes/experiment_loaded.php', data: {} }).done(function() {}); } // prevent accidental closing of test window.onbeforeunload = function () { return 'You are leaving the test, and all your progress on this test will be lost.'; } // window loaded? let's set up! window.onload = setup;