From 1ea90db5e24789bad12fe8ded3316bd86c53701c Mon Sep 17 00:00:00 2001 From: Henry Burgess Date: Wed, 15 Oct 2025 15:29:50 -0500 Subject: [PATCH 01/12] RDK-85 Add new confidence buttons --- src/classes/Graphics.ts | 4 +-- src/classes/Renderer.ts | 76 +++++++++++++++++++++++++++-------------- 2 files changed, 53 insertions(+), 27 deletions(-) diff --git a/src/classes/Graphics.ts b/src/classes/Graphics.ts index 2f3b829..726fec0 100644 --- a/src/classes/Graphics.ts +++ b/src/classes/Graphics.ts @@ -150,7 +150,7 @@ export class Graphics { const startAngle = reference - Math.PI / 4; const endAngle = startAngle + Math.PI / 4; this.renderer.createArc(startAngle - Math.PI / 128, endAngle, Renderer.getInvertedColor("white")); - this.renderer.createArc(startAngle, endAngle, Renderer.getInvertedColor("#3ea3a3")); + this.renderer.createArc(startAngle, endAngle, Renderer.getInvertedColor("#1792fc")); } /** @@ -159,7 +159,7 @@ export class Graphics { addRightArc(): void { const startAngle = -Math.PI / 2; const endAngle = Math.PI / 2; - this.renderer.createArc(startAngle, endAngle, Renderer.getInvertedColor("#3ea3a3")); + this.renderer.createArc(startAngle, endAngle, Renderer.getInvertedColor("#1792fc")); } /** diff --git a/src/classes/Renderer.ts b/src/classes/Renderer.ts index 6fbf60e..4f266b9 100644 --- a/src/classes/Renderer.ts +++ b/src/classes/Renderer.ts @@ -256,24 +256,47 @@ export class Renderer { } } - addButton(buttonText: string, offset = "none"): HTMLDivElement { + addButton(buttonText: string, variant = "default"): HTMLDivElement { + // Create colorscheme based on variant + const colorscheme = { + default: { + background: "#88919e", + fill: "#575d66", + }, + orangeDark: { + background: "#ffc20a", + fill: "#ffe63b", + }, + orangeLight: { + background: "#ffe084", + fill: "#ffe79d", + }, + blueDark: { + background: "#1792fc", + fill: "#2c96f3", + }, + blueLight: { + background: "#7bbef8", + fill: "#95caf9", + }, + }; + // Create container const buttonContainer = document.createElement("div"); buttonContainer.style.display = "flex"; buttonContainer.style.justifyContent = "center"; buttonContainer.style.alignItems = "center"; - buttonContainer.style.width = "100px"; - buttonContainer.style.height = "50px"; - buttonContainer.style.padding = "2px"; - buttonContainer.style.border = "4px solid " + Renderer.getInvertedColor("black"); - buttonContainer.style.borderRadius = "12px"; - buttonContainer.style.backgroundColor = Renderer.getInvertedColor("white"); + buttonContainer.style.width = "140px"; + buttonContainer.style.height = "60px"; + buttonContainer.style.padding = "4px"; + buttonContainer.style.borderRadius = "8px"; + buttonContainer.style.backgroundColor = colorscheme[variant].background; // Add label text for the button const buttonLabel = document.createElement("p"); buttonLabel.textContent = buttonText; buttonLabel.style.fontWeight = "bold"; - buttonLabel.style.fontSize = "xx-large"; + buttonLabel.style.fontSize = "x-large"; buttonContainer.appendChild(buttonLabel); return buttonContainer; @@ -294,22 +317,24 @@ export class Renderer { leftContainer.style.display = "flex"; leftContainer.style.justifyContent = "center"; leftContainer.style.alignItems = "center"; - leftContainer.style.flexDirection = "column"; - leftContainer.style.gap = "10px"; + leftContainer.style.flexDirection = "row"; + leftContainer.style.gap = "16px"; leftContainer.style.position = "absolute"; - leftContainer.style.marginRight = "60%"; - leftContainer.style.marginTop = "40px"; + leftContainer.style.marginRight = "70%"; - // Add the left button to the left label container - const leftButton = this.addButton("Left", "left"); - leftContainer.append(leftButton); + // Add the left buttons to the left label container + // To-Do: 2-second hold to select + const leftVeryConfidentButton = this.addButton("Very Confident", "orangeDark"); + const leftSomewhatConfidentButton = this.addButton("Somewhat Confident", "orangeLight"); + leftContainer.append(leftVeryConfidentButton); + leftContainer.append(leftSomewhatConfidentButton); // Add the input indicators const leftButtonContainer = document.createElement("div"); if (__TARGET__ === "spectrometer") { - leftButtonContainer.innerHTML = Renderer.getEmbeddedControllerButton(1); + // leftButtonContainer.innerHTML = Renderer.getEmbeddedControllerButton(1); } else { - leftButtonContainer.innerHTML = Renderer.getEmbeddedKeyboardButton("F"); + // leftButtonContainer.innerHTML = Renderer.getEmbeddedKeyboardButton("F"); } leftContainer.append(leftButtonContainer); @@ -325,22 +350,23 @@ export class Renderer { rightContainer.style.display = "flex"; rightContainer.style.justifyContent = "center"; rightContainer.style.alignItems = "center"; - rightContainer.style.flexDirection = "column"; - rightContainer.style.gap = "10px"; + rightContainer.style.flexDirection = "row"; + rightContainer.style.gap = "16px"; rightContainer.style.position = "absolute"; - rightContainer.style.marginLeft = "60%"; - rightContainer.style.marginTop = "40px"; + rightContainer.style.marginLeft = "70%"; // Add the right button to the right label container - const rightButton = this.addButton("Right", "right"); - rightContainer.append(rightButton); + const rightButtonVeryConfident = this.addButton("Very Confident", "blueDark"); + const rightButtonSomewhatConfident = this.addButton("Somewhat Confident", "blueLight"); + rightContainer.append(rightButtonSomewhatConfident); + rightContainer.append(rightButtonVeryConfident); // Add the input indicators const rightButtonContainer = document.createElement("div"); if (__TARGET__ === "spectrometer") { - rightButtonContainer.innerHTML = Renderer.getEmbeddedControllerButton(4); + // rightButtonContainer.innerHTML = Renderer.getEmbeddedControllerButton(4); } else { - rightButtonContainer.innerHTML = Renderer.getEmbeddedKeyboardButton("J"); + // rightButtonContainer.innerHTML = Renderer.getEmbeddedKeyboardButton("J"); } rightContainer.append(rightButtonContainer); From 8b03773e177232280a4b7fbc46afe0f19f7c4dee Mon Sep 17 00:00:00 2001 From: Henry Burgess Date: Wed, 15 Oct 2025 16:05:18 -0500 Subject: [PATCH 02/12] RDK-85 Update confidence input graphics --- src/classes/Renderer.ts | 125 +++++++++++++++++++++++++++++++--------- 1 file changed, 99 insertions(+), 26 deletions(-) diff --git a/src/classes/Renderer.ts b/src/classes/Renderer.ts index 4f266b9..c7c6ca5 100644 --- a/src/classes/Renderer.ts +++ b/src/classes/Renderer.ts @@ -312,31 +312,67 @@ export class Renderer { const graphicsCanvasDiv = document.getElementsByClassName("graphics-container")[0]; - // Create the left label container + // Create the left container const leftContainer = document.createElement("div"); leftContainer.style.display = "flex"; - leftContainer.style.justifyContent = "center"; + leftContainer.style.flexDirection = "column"; leftContainer.style.alignItems = "center"; - leftContainer.style.flexDirection = "row"; - leftContainer.style.gap = "16px"; + leftContainer.style.justifyContent = "center"; leftContainer.style.position = "absolute"; leftContainer.style.marginRight = "70%"; - // Add the left buttons to the left label container + // Left label + const leftLabel = document.createElement("p"); + leftLabel.textContent = "Left"; + leftLabel.style.fontSize = "xx-large"; + leftLabel.style.fontWeight = "bold"; + leftContainer.append(leftLabel); + + // Create the left button container + const leftButtonContainer = document.createElement("div"); + leftButtonContainer.style.display = "flex"; + leftButtonContainer.style.justifyContent = "center"; + leftButtonContainer.style.alignItems = "center"; + leftButtonContainer.style.flexDirection = "row"; + leftButtonContainer.style.gap = "16px"; + leftContainer.append(leftButtonContainer); + // To-Do: 2-second hold to select + // Left "Very Confident" button + const leftVeryConfidentButtonContainer = document.createElement("div"); + leftVeryConfidentButtonContainer.style.display = "flex"; + leftVeryConfidentButtonContainer.style.justifyContent = "center"; + leftVeryConfidentButtonContainer.style.alignItems = "center"; + leftVeryConfidentButtonContainer.style.flexDirection = "column"; + leftVeryConfidentButtonContainer.style.gap = "16px"; const leftVeryConfidentButton = this.addButton("Very Confident", "orangeDark"); + leftVeryConfidentButtonContainer.append(leftVeryConfidentButton); + const leftVeryConfidentButtonLabelContainer = document.createElement("div"); + if (__TARGET__ === "spectrometer") { + leftVeryConfidentButtonLabelContainer.innerHTML = Renderer.getEmbeddedControllerButton(1); + } else { + leftVeryConfidentButtonLabelContainer.innerHTML = Renderer.getEmbeddedKeyboardButton("D"); + } + leftVeryConfidentButtonContainer.append(leftVeryConfidentButtonLabelContainer); + leftButtonContainer.append(leftVeryConfidentButtonContainer); + + // Left "Somewhat Confident" button + const leftSomewhatConfidentButtonContainer = document.createElement("div"); + leftSomewhatConfidentButtonContainer.style.display = "flex"; + leftSomewhatConfidentButtonContainer.style.justifyContent = "center"; + leftSomewhatConfidentButtonContainer.style.alignItems = "center"; + leftSomewhatConfidentButtonContainer.style.flexDirection = "column"; + leftSomewhatConfidentButtonContainer.style.gap = "16px"; const leftSomewhatConfidentButton = this.addButton("Somewhat Confident", "orangeLight"); - leftContainer.append(leftVeryConfidentButton); - leftContainer.append(leftSomewhatConfidentButton); - - // Add the input indicators - const leftButtonContainer = document.createElement("div"); + leftSomewhatConfidentButtonContainer.append(leftSomewhatConfidentButton); + const leftSomewhatConfidentButtonLabelContainer = document.createElement("div"); if (__TARGET__ === "spectrometer") { - // leftButtonContainer.innerHTML = Renderer.getEmbeddedControllerButton(1); + leftSomewhatConfidentButtonLabelContainer.innerHTML = Renderer.getEmbeddedControllerButton(2); } else { - // leftButtonContainer.innerHTML = Renderer.getEmbeddedKeyboardButton("F"); + leftSomewhatConfidentButtonLabelContainer.innerHTML = Renderer.getEmbeddedKeyboardButton("F"); } - leftContainer.append(leftButtonContainer); + leftSomewhatConfidentButtonContainer.append(leftSomewhatConfidentButtonLabelContainer); + leftButtonContainer.append(leftSomewhatConfidentButtonContainer); // Prepend the left label container to the graphics container graphicsCanvasDiv.prepend(leftContainer); @@ -345,30 +381,67 @@ export class Renderer { const graphicsCanvasDiv = document.getElementsByClassName("graphics-container")[0]; - // Create the right label container + // Create the right container const rightContainer = document.createElement("div"); rightContainer.style.display = "flex"; - rightContainer.style.justifyContent = "center"; + rightContainer.style.flexDirection = "column"; rightContainer.style.alignItems = "center"; - rightContainer.style.flexDirection = "row"; - rightContainer.style.gap = "16px"; + rightContainer.style.justifyContent = "center"; rightContainer.style.position = "absolute"; rightContainer.style.marginLeft = "70%"; - // Add the right button to the right label container - const rightButtonVeryConfident = this.addButton("Very Confident", "blueDark"); - const rightButtonSomewhatConfident = this.addButton("Somewhat Confident", "blueLight"); - rightContainer.append(rightButtonSomewhatConfident); - rightContainer.append(rightButtonVeryConfident); + // Right label + const rightLabel = document.createElement("p"); + rightLabel.textContent = "Right"; + rightLabel.style.fontSize = "xx-large"; + rightLabel.style.fontWeight = "bold"; + rightContainer.append(rightLabel); - // Add the input indicators + // Create the right button container const rightButtonContainer = document.createElement("div"); + rightButtonContainer.style.display = "flex"; + rightButtonContainer.style.justifyContent = "center"; + rightButtonContainer.style.alignItems = "center"; + rightButtonContainer.style.flexDirection = "row"; + rightButtonContainer.style.gap = "16px"; + rightContainer.append(rightButtonContainer); + + // To-Do: 2-second hold to select + // Right "Somewhat Confident" button + const rightSomewhatConfidentButtonContainer = document.createElement("div"); + rightSomewhatConfidentButtonContainer.style.display = "flex"; + rightSomewhatConfidentButtonContainer.style.justifyContent = "center"; + rightSomewhatConfidentButtonContainer.style.alignItems = "center"; + rightSomewhatConfidentButtonContainer.style.flexDirection = "column"; + rightSomewhatConfidentButtonContainer.style.gap = "16px"; + const rightSomewhatConfidentButton = this.addButton("Somewhat Confident", "blueLight"); + rightSomewhatConfidentButtonContainer.append(rightSomewhatConfidentButton); + const rightSomewhatConfidentButtonLabelContainer = document.createElement("div"); if (__TARGET__ === "spectrometer") { - // rightButtonContainer.innerHTML = Renderer.getEmbeddedControllerButton(4); + rightSomewhatConfidentButtonLabelContainer.innerHTML = Renderer.getEmbeddedControllerButton(3); } else { - // rightButtonContainer.innerHTML = Renderer.getEmbeddedKeyboardButton("J"); + rightSomewhatConfidentButtonLabelContainer.innerHTML = Renderer.getEmbeddedKeyboardButton("J"); } - rightContainer.append(rightButtonContainer); + rightSomewhatConfidentButtonContainer.append(rightSomewhatConfidentButtonLabelContainer); + rightButtonContainer.append(rightSomewhatConfidentButtonContainer); + + // Right "Very Confident" button + const rightVeryConfidentButtonContainer = document.createElement("div"); + rightVeryConfidentButtonContainer.style.display = "flex"; + rightVeryConfidentButtonContainer.style.justifyContent = "center"; + rightVeryConfidentButtonContainer.style.alignItems = "center"; + rightVeryConfidentButtonContainer.style.flexDirection = "column"; + rightVeryConfidentButtonContainer.style.gap = "16px"; + const rightVeryConfidentButton = this.addButton("Very Confident", "blueDark"); + rightVeryConfidentButtonContainer.append(rightVeryConfidentButton); + const rightVeryConfidentButtonLabelContainer = document.createElement("div"); + if (__TARGET__ === "spectrometer") { + rightVeryConfidentButtonLabelContainer.innerHTML = Renderer.getEmbeddedControllerButton(4); + } else { + rightVeryConfidentButtonLabelContainer.innerHTML = Renderer.getEmbeddedKeyboardButton("K"); + } + rightVeryConfidentButtonContainer.append(rightVeryConfidentButtonLabelContainer); + rightButtonContainer.append(rightVeryConfidentButtonContainer); // Append the right label container to the graphics container graphicsCanvasDiv.append(rightContainer); From fe8cd0da865fc9eec926b87f3a72b1470ec67df3 Mon Sep 17 00:00:00 2001 From: Henry Burgess Date: Wed, 15 Oct 2025 16:14:01 -0500 Subject: [PATCH 03/12] RDK-85 Increase sizes of controller glyphs --- src/classes/Renderer.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/classes/Renderer.ts b/src/classes/Renderer.ts index c7c6ca5..71122df 100644 --- a/src/classes/Renderer.ts +++ b/src/classes/Renderer.ts @@ -463,13 +463,13 @@ export class Renderer { // Create SVG with 4 circles representing the controller buttons const svg = ` - - + ${[1, 2, 3, 4].map((index) => { - const centerX = 16 * index; - const centerY = 20; + const centerX = 18 * index; + const centerY = 22.5; const radius = 6; const isHighlighted = index === validIndex; From 9b805a7311a12526c9d862eb71fb0f913d3fc854 Mon Sep 17 00:00:00 2001 From: Henry Burgess Date: Wed, 15 Oct 2025 16:31:13 -0500 Subject: [PATCH 04/12] RDK-85 Remove old slider confidence mechanics --- src/classes/Graphics.ts | 2 +- src/index.ts | 46 ++++----- src/plugin.ts | 206 +++++++++++----------------------------- types/index.d.ts | 4 +- 4 files changed, 76 insertions(+), 182 deletions(-) diff --git a/src/classes/Graphics.ts b/src/classes/Graphics.ts index 726fec0..0067810 100644 --- a/src/classes/Graphics.ts +++ b/src/classes/Graphics.ts @@ -103,7 +103,7 @@ export class Graphics { ); if ( this.trial.showFeedback === true && - this.trial.data.referenceSelection !== "" + this.trial.data.selection !== "" ) { if (this.trial.data.correct === 1) { this.renderer.createFixation(0, 0, fixationDiameter, false, Renderer.getInvertedColor("green")); diff --git a/src/index.ts b/src/index.ts index 875e6e9..8484a77 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,18 +66,18 @@ const jsPsych = initJsPsych({ const InputConfigurations = { desktop: { name: "desktop", - left: "f", - right: "j", - submit: "k", - alt: "d", + 1: "d", + 2: "f", + 3: "j", + 4: "k", showButtons: false, }, spectrometer: { name: "spectrometer", - left: "4", - right: "1", - submit: "2", - alt: "3", + 1: "1", + 2: "2", + 3: "3", + 4: "4", trigger: "5", showButtons: false, }, @@ -177,8 +177,8 @@ if ( instructionContinueText, ], allow_keys: !keyLayout.showButtons, - key_forward: keyLayout.right.charAt(keyLayout.right.length - 1), - key_backward: keyLayout.left.charAt(keyLayout.left.length - 1), + key_forward: keyLayout["3"].charAt(keyLayout["3"].length - 1), + key_backward: keyLayout["2"].charAt(keyLayout["2"].length - 1), show_page_number: true, show_clickable_nav: keyLayout.showButtons, }); @@ -198,8 +198,8 @@ timeline.push({ type: InstructionsPlugin, pages: tutorialGames, allow_keys: !keyLayout.showButtons, - key_forward: keyLayout.right.charAt(keyLayout.right.length - 1), - key_backward: keyLayout.left.charAt(keyLayout.left.length - 1), + key_forward: keyLayout["3"].charAt(keyLayout["3"].length - 1), + key_backward: keyLayout["2"].charAt(keyLayout["2"].length - 1), show_page_number: true, show_clickable_nav: keyLayout.showButtons, }); @@ -234,10 +234,9 @@ for (let t = 0; t < Manipulations.numTutorialTrials; t++) { coherence: k, stimulusDuration: d, dotDirection: r, - referenceSelection: "", + selection: "", deviation: "left", correct: false, - confidenceSelection: 0, }, extensions: [{ type: NeurocogExtension }], }; @@ -264,8 +263,8 @@ timeline.push({ type: InstructionsPlugin, pages: practice, allow_keys: !keyLayout.showButtons, - key_forward: keyLayout.right.charAt(keyLayout.right.length - 1), - key_backward: keyLayout.left.charAt(keyLayout.left.length - 1), + key_forward: keyLayout["3"].charAt(keyLayout["3"].length - 1), + key_backward: keyLayout["2"].charAt(keyLayout["2"].length - 1), show_page_number: true, show_clickable_nav: keyLayout.showButtons, }); @@ -297,10 +296,9 @@ for (let t = 0; t < Manipulations.numPracticeTrials; t++) { coherence: k, stimulusDuration: 1500, dotDirection: r, - referenceSelection: "", + selection: "", deviation: deviation, correct: false, - confidenceSelection: 0, }, extensions: [{ type: NeurocogExtension }], }; @@ -366,8 +364,8 @@ if (_.isEqual(keyLayout.name, "spectrometer")) { type: InstructionsPlugin, pages: main, allow_keys: !keyLayout.showButtons, - key_forward: keyLayout.right.charAt(keyLayout.right.length - 1), - key_backward: keyLayout.left.charAt(keyLayout.left.length - 1), + key_forward: keyLayout["3"].charAt(keyLayout["3"].length - 1), + key_backward: keyLayout["2"].charAt(keyLayout["2"].length - 1), show_page_number: true, show_clickable_nav: keyLayout.showButtons, }); @@ -400,10 +398,9 @@ for (let t = 0; t < Manipulations.numCalibrationOneTrials; t++) { coherence: k, stimulusDuration: 1500, dotDirection: r, - referenceSelection: "", + selection: "", deviation: deviation, correct: false, - confidenceSelection: 0, }, extensions: [{ type: NeurocogExtension }], }; @@ -439,10 +436,9 @@ for (let t = 0; t < Manipulations.numMainTrials; t++) { coherence: k, stimulusDuration: 1500, dotDirection: r, - referenceSelection: "", + selection: "", deviation: deviation, correct: false, - confidenceSelection: 0, }, extensions: [{ type: NeurocogExtension }], }; @@ -462,7 +458,7 @@ timeline.push({ allow_backward: false, button_label_next: "Finish", show_clickable_nav: false, - key_forward: keyLayout.right, + key_forward: keyLayout["3"], }); jsPsych.run(timeline); diff --git a/src/plugin.ts b/src/plugin.ts index 49b10aa..18d386e 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -61,10 +61,6 @@ const info = { type: ParameterType.INT, default: undefined, }, - checkConfidence: { - type: ParameterType.BOOL, - default: false, - }, }, }; @@ -172,18 +168,12 @@ class DotGamePlugin implements JsPsychPlugin { currentStimulus = stimuli.shift(); // Configure stimulus-specific parameters - if (currentStimulus.getParameters().name === "reference") { + if (currentStimulus.getParameters().name === "decision") { // Reference stimulus trial.data.referenceStartTime = performance.now(); // Hide the mouse cursor graphics.cursorVisibility(false); - } else if (currentStimulus.getParameters().name === "confidence") { - // Start a timer if a confidence stimuli is run. - trial.data.confidenceStartTime = performance.now(); - - // Show the mouse cursor - graphics.cursorVisibility(true); } else if (currentStimulus.getParameters().name === "motion") { trial.data.stimulusDuration = currentStimulus.getParameters().timing.run; @@ -193,7 +183,7 @@ class DotGamePlugin implements JsPsychPlugin { } else if (currentStimulus.getParameters().name === "initial") { // Hide the mouse cursor graphics.cursorVisibility(false); - } else if (currentStimulus.getParameters().name === "decision") { + } else if (currentStimulus.getParameters().name === "post-decision") { // Increase the run time of fixation cross to 1250ms if feedback // is to be shown if (trial.showFeedback === true) { @@ -216,97 +206,44 @@ class DotGamePlugin implements JsPsychPlugin { // Record the keycode to process the event const keycode = event.key; - if ( - Object.keys(currentStimulus.getParameters().keybindings).includes( - keycode - ) - ) { - // Handle the decision if a valid key has been pressed for this stage - if (currentStimulus.getParameters().name === "confidence") { - // Handle 'confidence' stimuli - const slider = document.getElementById( - "confidence-slider" - ) as HTMLInputElement; - if (slider) { - // Show the thumb if it is currently hidden when adjusting confidence - if ( - slider.className === "confidence-slider-hidden" && - (keycode === keyLayout.left || keycode === keyLayout.right) - ) { - slider.className = "confidence-slider"; - } - - // Handle incrementing and decrementing slider - if (keycode === keyLayout.left) { - slider.stepDown(1); - } else if (keycode === keyLayout.right) { - slider.stepUp(1); - } - - // Ensure we handle a mistake notification - if (keycode === keyLayout.alt || event.type === "click") { - // Store mistake boolean - trial.data.confidenceMistake = true; - - // Calculate and store confidence data - trial.data.confidenceEndTime = performance.now(); - trial.data.confidenceTotalTime = - trial.data.confidenceEndTime - trial.data.confidenceStartTime; - trial.data.confidenceSelection = slider.value; - - // Continue to the next Stimulus - console.warn("Mistake stored in trial data"); - Runner.post(currentStimulus); - } - - // Finally, we ignore any submissions if the slider is hidden, only submit if slider is visible - if ( - keycode === keyLayout.submit && - slider.className === "confidence-slider" - ) { - // Calculate and store confidence data - trial.data.confidenceEndTime = performance.now(); - trial.data.confidenceTotalTime = - trial.data.confidenceEndTime - trial.data.confidenceStartTime; - trial.data.confidenceSelection = slider.value; - - // Continue to the next Stimulus - Runner.post(currentStimulus); - } - } else { - console.warn("Did not get reference to slider element"); - } - } else if (currentStimulus.getParameters().name === "reference") { - // Handle 'reference' stimuli - selection = - currentStimulus.getParameters().keybindings[keycode].choice; - currentStimulus.removeKeybindings(); - - // Calculate and store reference data - trial.data.referenceEndTime = performance.now(); - trial.data.referenceTotalTime = - trial.data.referenceEndTime - trial.data.referenceStartTime; - - // Normalize selection data - trial.data.referenceSelection = selection === "left" ? 1 : 2; - - // Normalize correct data - trial.data.correct = selection === trial.data.deviation ? 1 : 0; - - // Increment score if correct - if (trial.data.correct === 1 && trial.name === "main") { - trial.data.score++; - } - - // Continue to the next Stimulus - Runner.post(currentStimulus); - } - } else { + // Filter out invalid keycodes + if (!Object.keys(currentStimulus.getParameters().keybindings).includes( + keycode + )) { console.warn( `Invalid key "${keycode}" for stimulus type "${ currentStimulus.getParameters().name }"` ); + return; + } + + if (currentStimulus.getParameters().name === "decision") { + // Handle 'decision' stimuli + selection = + currentStimulus.getParameters().keybindings[keycode].choice; + currentStimulus.removeKeybindings(); + + console.log("decision", selection); + + // Calculate and store reference data + trial.data.referenceEndTime = performance.now(); + trial.data.referenceTotalTime = + trial.data.referenceEndTime - trial.data.referenceStartTime; + + // Normalize selection data + trial.data.selection = selection; + + // Normalize correct data + trial.data.correct = selection.endsWith(trial.data.deviation) ? 1 : 0; + + // Increment score if correct + if (trial.data.correct === 1 && trial.name === "main") { + trial.data.score++; + } + + // Continue to the next Stimulus + Runner.post(currentStimulus); } }; @@ -452,8 +389,8 @@ class DotGamePlugin implements JsPsychPlugin { // Display of the reference angle and the coloured arcs for // user selection. - const reference = { - name: "reference", + const decision = { + name: "decision", components: [ "outline", "fixation", @@ -473,12 +410,20 @@ class DotGamePlugin implements JsPsychPlugin { post: 0, }, keybindings: { - [keyLayout.left]: { - choice: "left", + [keyLayout["1"]]: { + choice: "vc_l", + handler: decisionHandler, + }, + [keyLayout["2"]]: { + choice: "sc_l", handler: decisionHandler, }, - [keyLayout.right]: { - choice: "right", + [keyLayout["3"]]: { + choice: "sc_r", + handler: decisionHandler, + }, + [keyLayout["4"]]: { + choice: "vc_r", handler: decisionHandler, }, }, @@ -490,8 +435,8 @@ class DotGamePlugin implements JsPsychPlugin { }; // Brief display of the fixation cross. - const decision = { - name: "decision", + const postDecision = { + name: "post-decision", components: ["outline", "fixation"], two: two, renderer: renderer, @@ -510,60 +455,15 @@ class DotGamePlugin implements JsPsychPlugin { postTrialHandler: nextStimulus, }; - // Display of the confidence slider used to rate confidence. - const confidence = { - name: "confidence", - components: ["confidence"], - two: two, - renderer: renderer, - graphics: graphics, - interactive: true, - selected: false, - timing: { - pre: 0, - run: -1, - post: 0, - }, - keybindings: { - [keyLayout.submit]: { - choice: "submit", - handler: decisionHandler, - }, - [keyLayout.left]: { - choice: "decrease", - handler: decisionHandler, - }, - [keyLayout.right]: { - choice: "increase", - handler: decisionHandler, - }, - [keyLayout.alt]: { - choice: "mistake", - handler: decisionHandler, - }, - }, - target: display_element, - trial: trial, - rendererParameters: rendererParameters, - eventHandler: decisionHandler, - postTrialHandler: nextStimulus, - }; - // Construct a list of the stimuli. const stimuli = []; stimuli.push( new Stimulus(initial), new Stimulus(motion), - new Stimulus(reference), - new Stimulus(decision) + new Stimulus(decision), + new Stimulus(postDecision) ); - if (trial.checkConfidence === true) { - // Exception for tutorial trials, confidence should be shown - // for all trials - stimuli.push(new Stimulus(confidence)); - } - let currentStimulus = null; trial.data.trialStartTime = performance.now(); diff --git a/types/index.d.ts b/types/index.d.ts index 8eac43d..98ca5d7 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -9,7 +9,6 @@ export type RDKTrial = { dotDirection: number; dotVelocity: number; showFeedback: boolean; - checkConfidence: boolean; keyLayout: IKeys; data: { name: string; @@ -17,10 +16,9 @@ export type RDKTrial = { coherence: number; stimulusDuration: number; dotDirection: number; - referenceSelection: string; + selection: "vc_l" | "sc_l" | "sc_r" | "vc_r"; deviation: "left" | "right"; correct: boolean; - confidenceSelection: number; }; }; From 6d6b312ea7e9880d000064b16f6d8ccb893aa821 Mon Sep 17 00:00:00 2001 From: Henry Burgess Date: Wed, 15 Oct 2025 16:52:32 -0500 Subject: [PATCH 05/12] RDK-85 Add progress bar --- src/classes/Renderer.ts | 44 +++++++++++++++- src/classes/Stimulus.ts | 22 ++++++-- src/plugin.ts | 114 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 166 insertions(+), 14 deletions(-) diff --git a/src/classes/Renderer.ts b/src/classes/Renderer.ts index 71122df..db5c281 100644 --- a/src/classes/Renderer.ts +++ b/src/classes/Renderer.ts @@ -291,17 +291,55 @@ export class Renderer { buttonContainer.style.padding = "4px"; buttonContainer.style.borderRadius = "8px"; buttonContainer.style.backgroundColor = colorscheme[variant].background; + buttonContainer.style.position = "relative"; + buttonContainer.style.overflow = "hidden"; + + // Create progress bar + const progressBar = document.createElement("div"); + progressBar.style.position = "absolute"; + progressBar.style.bottom = "0"; + progressBar.style.left = "0"; + progressBar.style.width = "0%"; + progressBar.style.height = "100%"; + progressBar.style.backgroundColor = colorscheme[variant].fill; + progressBar.style.transition = "width 2s linear"; + progressBar.className = "progress-bar"; + buttonContainer.appendChild(progressBar); // Add label text for the button const buttonLabel = document.createElement("p"); buttonLabel.textContent = buttonText; buttonLabel.style.fontWeight = "bold"; buttonLabel.style.fontSize = "x-large"; + buttonLabel.style.position = "relative"; + buttonLabel.style.zIndex = "1"; buttonContainer.appendChild(buttonLabel); return buttonContainer; } + /** + * Start progress animation for a button + * @param {HTMLDivElement} buttonElement the button element to animate + */ + startProgress(buttonElement: HTMLDivElement): void { + const progressBar = buttonElement.querySelector('.progress-bar') as HTMLElement; + if (progressBar) { + progressBar.style.width = "100%"; + } + } + + /** + * Stop progress animation for a button + * @param {HTMLDivElement} buttonElement the button element to reset + */ + stopProgress(buttonElement: HTMLDivElement): void { + const progressBar = buttonElement.querySelector('.progress-bar') as HTMLElement; + if (progressBar) { + progressBar.style.width = "0%"; + } + } + /** * Add a label onto the target * @param {string} labelType the type of label to add @@ -337,7 +375,6 @@ export class Renderer { leftButtonContainer.style.gap = "16px"; leftContainer.append(leftButtonContainer); - // To-Do: 2-second hold to select // Left "Very Confident" button const leftVeryConfidentButtonContainer = document.createElement("div"); leftVeryConfidentButtonContainer.style.display = "flex"; @@ -346,6 +383,7 @@ export class Renderer { leftVeryConfidentButtonContainer.style.flexDirection = "column"; leftVeryConfidentButtonContainer.style.gap = "16px"; const leftVeryConfidentButton = this.addButton("Very Confident", "orangeDark"); + leftVeryConfidentButton.setAttribute("data-button-id", "vc_l"); leftVeryConfidentButtonContainer.append(leftVeryConfidentButton); const leftVeryConfidentButtonLabelContainer = document.createElement("div"); if (__TARGET__ === "spectrometer") { @@ -364,6 +402,7 @@ export class Renderer { leftSomewhatConfidentButtonContainer.style.flexDirection = "column"; leftSomewhatConfidentButtonContainer.style.gap = "16px"; const leftSomewhatConfidentButton = this.addButton("Somewhat Confident", "orangeLight"); + leftSomewhatConfidentButton.setAttribute("data-button-id", "sc_l"); leftSomewhatConfidentButtonContainer.append(leftSomewhatConfidentButton); const leftSomewhatConfidentButtonLabelContainer = document.createElement("div"); if (__TARGET__ === "spectrometer") { @@ -406,7 +445,6 @@ export class Renderer { rightButtonContainer.style.gap = "16px"; rightContainer.append(rightButtonContainer); - // To-Do: 2-second hold to select // Right "Somewhat Confident" button const rightSomewhatConfidentButtonContainer = document.createElement("div"); rightSomewhatConfidentButtonContainer.style.display = "flex"; @@ -415,6 +453,7 @@ export class Renderer { rightSomewhatConfidentButtonContainer.style.flexDirection = "column"; rightSomewhatConfidentButtonContainer.style.gap = "16px"; const rightSomewhatConfidentButton = this.addButton("Somewhat Confident", "blueLight"); + rightSomewhatConfidentButton.setAttribute("data-button-id", "sc_r"); rightSomewhatConfidentButtonContainer.append(rightSomewhatConfidentButton); const rightSomewhatConfidentButtonLabelContainer = document.createElement("div"); if (__TARGET__ === "spectrometer") { @@ -433,6 +472,7 @@ export class Renderer { rightVeryConfidentButtonContainer.style.flexDirection = "column"; rightVeryConfidentButtonContainer.style.gap = "16px"; const rightVeryConfidentButton = this.addButton("Very Confident", "blueDark"); + rightVeryConfidentButton.setAttribute("data-button-id", "vc_r"); rightVeryConfidentButtonContainer.append(rightVeryConfidentButton); const rightVeryConfidentButtonLabelContainer = document.createElement("div"); if (__TARGET__ === "spectrometer") { diff --git a/src/classes/Stimulus.ts b/src/classes/Stimulus.ts index c4f2f76..2c0a939 100644 --- a/src/classes/Stimulus.ts +++ b/src/classes/Stimulus.ts @@ -56,12 +56,21 @@ export class Stimulus { createKeybindings(): void { // Check if the stimulus can be interacted with if (this.interactive) { - // Bind keys + // Bind keydown events for (const binding in this.keybindings) { if (this.keybindings[binding]) { - document.addEventListener("keyup", this.keybindings[binding].handler); + document.addEventListener("keydown", this.keybindings[binding].handler); } } + + // Also bind keyup events for decision stimulus + if (this.name === "decision") { + document.addEventListener("keyup", (event: KeyboardEvent) => { + if (window.decisionKeyUpHandler) { + window.decisionKeyUpHandler(event); + } + }); + } } } @@ -69,15 +78,20 @@ export class Stimulus { * Remove keybindings for the stimulus */ removeKeybindings(): void { - // Unbind keys + // Unbind keydown events for (const binding in this.keybindings) { if (this.keybindings[binding]) { document.removeEventListener( - "keyup", + "keydown", this.keybindings[binding].handler ); } } + + // Also remove keyup events for decision stimulus + if (this.name === "decision") { + document.removeEventListener("keyup", window.decisionKeyUpHandler); + } } /** diff --git a/src/plugin.ts b/src/plugin.ts index 18d386e..a3937b7 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -25,6 +25,13 @@ import { IRenderer } from "../types"; import Two from "two.js"; import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +// Global keyup handler for decision input +declare global { + interface Window { + decisionKeyUpHandler?: (event: KeyboardEvent) => void; + } +} + const info = { name: "dot-game", parameters: { @@ -198,13 +205,103 @@ class DotGamePlugin implements JsPsychPlugin { Runner.start(currentStimulus); }; + // Add variables for key hold functionality + let keyHoldTimer: number | null = null; + let currentKey: string | null = null; + + /** + * An event handler for keydown during decision input + */ + const decisionKeyDownHandler = (event: KeyboardEvent) => { + const keycode = event.key.toLowerCase(); // Convert to lowercase for consistent comparison + + // Filter out invalid keycodes + if (!Object.keys(currentStimulus.getParameters().keybindings).includes(keycode)) { + return; + } + + // Prevent default to avoid key repeat + event.preventDefault(); + + // If already holding a key, ignore (prevents multiple simultaneous key holds) + if (currentKey !== null) { + return; + } + + currentKey = keycode; + + // Find the corresponding button element + const buttonMapping = { + [keyLayout["1"]]: "vc_l", + [keyLayout["2"]]: "sc_l", + [keyLayout["3"]]: "sc_r", + [keyLayout["4"]]: "vc_r" + }; + + const buttonId = buttonMapping[keycode]; + if (buttonId) { + const buttonElement = document.querySelector(`[data-button-id="${buttonId}"]`) as HTMLDivElement; + if (buttonElement) { + // Start the progress bar animation + renderer.startProgress(buttonElement); + } + } + + // Set timer for 2 seconds + keyHoldTimer = window.setTimeout(() => { + if (currentKey === keycode) { + // After 2 seconds, process the decision + decisionHandler(event); + resetKeyHold(); + } + }, 2000); + }; + + /** + * An event handler for keyup during decision input + */ + const decisionKeyUpHandler = (event: KeyboardEvent) => { + if (currentKey === event.key.toLowerCase()) { + resetKeyHold(); + } + }; + + const resetKeyHold = () => { + // Clear the timer + if (keyHoldTimer) { + clearTimeout(keyHoldTimer); + keyHoldTimer = null; + } + + // Reset progress bar if there's a current key + if (currentKey) { + const buttonMapping = { + [keyLayout["1"]]: "vc_l", + [keyLayout["2"]]: "sc_l", + [keyLayout["3"]]: "sc_r", + [keyLayout["4"]]: "vc_r" + }; + + const buttonId = buttonMapping[currentKey]; + if (buttonId) { + const buttonElement = document.querySelector(`[data-button-id="${buttonId}"]`) as HTMLDivElement; + if (buttonElement) { + // Stop and reset the progress bar + renderer.stopProgress(buttonElement); + } + } + } + + currentKey = null; + }; + /** * An event handler for decision made during a trial * @param {KeyboardEvent} event the particular event or keypress */ const decisionHandler = (event: KeyboardEvent) => { // Record the keycode to process the event - const keycode = event.key; + const keycode = event.key.toLowerCase(); // Filter out invalid keycodes if (!Object.keys(currentStimulus.getParameters().keybindings).includes( @@ -224,8 +321,6 @@ class DotGamePlugin implements JsPsychPlugin { currentStimulus.getParameters().keybindings[keycode].choice; currentStimulus.removeKeybindings(); - console.log("decision", selection); - // Calculate and store reference data trial.data.referenceEndTime = performance.now(); trial.data.referenceTotalTime = @@ -412,25 +507,25 @@ class DotGamePlugin implements JsPsychPlugin { keybindings: { [keyLayout["1"]]: { choice: "vc_l", - handler: decisionHandler, + handler: decisionKeyDownHandler, }, [keyLayout["2"]]: { choice: "sc_l", - handler: decisionHandler, + handler: decisionKeyDownHandler, }, [keyLayout["3"]]: { choice: "sc_r", - handler: decisionHandler, + handler: decisionKeyDownHandler, }, [keyLayout["4"]]: { choice: "vc_r", - handler: decisionHandler, + handler: decisionKeyDownHandler, }, }, target: display_element, trial: trial, rendererParameters: rendererParameters, - eventHandler: decisionHandler, + eventHandler: decisionKeyDownHandler, postTrialHandler: nextStimulus, }; @@ -467,6 +562,9 @@ class DotGamePlugin implements JsPsychPlugin { let currentStimulus = null; trial.data.trialStartTime = performance.now(); + // Make the keyup handler globally accessible + window.decisionKeyUpHandler = decisionKeyUpHandler; + nextStimulus(); } } From d6b017357725f7eac7c068589398f51b8d97682b Mon Sep 17 00:00:00 2001 From: Henry Burgess Date: Wed, 15 Oct 2025 17:08:09 -0500 Subject: [PATCH 06/12] RDK-85 Ensure minimum display * Wait for key release --- src/classes/Graphics.ts | 3 +++ src/classes/Renderer.ts | 18 +++++++++++++++ src/classes/Stimulus.ts | 6 +++++ src/plugin.ts | 50 ++++++++++++++++++++++++++++++++++++----- 4 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/classes/Graphics.ts b/src/classes/Graphics.ts index 0067810..b7d2d45 100644 --- a/src/classes/Graphics.ts +++ b/src/classes/Graphics.ts @@ -386,5 +386,8 @@ export class Graphics { for (let i = 0; i < trackedElements.length; i++) { this.renderer.getTarget().remove(trackedElements[i]); } + + // Clear decision containers if they exist + this.renderer.clearDecisionContainers(); } } diff --git a/src/classes/Renderer.ts b/src/classes/Renderer.ts index db5c281..d56b2e6 100644 --- a/src/classes/Renderer.ts +++ b/src/classes/Renderer.ts @@ -352,6 +352,7 @@ export class Renderer { // Create the left container const leftContainer = document.createElement("div"); + leftContainer.id = "left-container"; leftContainer.style.display = "flex"; leftContainer.style.flexDirection = "column"; leftContainer.style.alignItems = "center"; @@ -422,6 +423,7 @@ export class Renderer { // Create the right container const rightContainer = document.createElement("div"); + rightContainer.id = "right-container"; rightContainer.style.display = "flex"; rightContainer.style.flexDirection = "column"; rightContainer.style.alignItems = "center"; @@ -633,6 +635,22 @@ export class Renderer { } } + /** + * Clear the left and right decision containers + * This removes only the decision UI elements without affecting other HTML elements + */ + clearDecisionContainers(): void { + const leftContainer = document.getElementById("left-container"); + if (leftContainer) { + leftContainer.remove(); + } + + const rightContainer = document.getElementById("right-container"); + if (rightContainer) { + rightContainer.remove(); + } + } + /** * Get the width of the view * @return {number} the width of the view diff --git a/src/classes/Stimulus.ts b/src/classes/Stimulus.ts index 2c0a939..e80feb2 100644 --- a/src/classes/Stimulus.ts +++ b/src/classes/Stimulus.ts @@ -92,6 +92,12 @@ export class Stimulus { if (this.name === "decision") { document.removeEventListener("keyup", window.decisionKeyUpHandler); } + + // Remove post-decision keyup handler if it exists + if (this.name === "post-decision" && window.postDecisionKeyUpHandler) { + document.removeEventListener("keyup", window.postDecisionKeyUpHandler); + window.postDecisionKeyUpHandler = undefined; + } } /** diff --git a/src/plugin.ts b/src/plugin.ts index a3937b7..6287658 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -29,6 +29,7 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; declare global { interface Window { decisionKeyUpHandler?: (event: KeyboardEvent) => void; + postDecisionKeyUpHandler?: (event: KeyboardEvent) => void; } } @@ -191,11 +192,10 @@ class DotGamePlugin implements JsPsychPlugin { // Hide the mouse cursor graphics.cursorVisibility(false); } else if (currentStimulus.getParameters().name === "post-decision") { - // Increase the run time of fixation cross to 1250ms if feedback - // is to be shown - if (trial.showFeedback === true) { - currentStimulus.getParameters().timing.run = 1250; - } + // Set up keyup handler to wait for all keys to be released + postDecisionKeyUpHandler = createPostDecisionKeyUpHandler(); + window.postDecisionKeyUpHandler = postDecisionKeyUpHandler; + document.addEventListener("keyup", postDecisionKeyUpHandler); } else { // Show the mouse cursor graphics.cursorVisibility(true); @@ -208,6 +208,7 @@ class DotGamePlugin implements JsPsychPlugin { // Add variables for key hold functionality let keyHoldTimer: number | null = null; let currentKey: string | null = null; + let postDecisionKeyUpHandler: ((event: KeyboardEvent) => void) | null = null; /** * An event handler for keydown during decision input @@ -295,6 +296,43 @@ class DotGamePlugin implements JsPsychPlugin { currentKey = null; }; + /** + * Handler for post-decision keyup events + * Finishes the post-decision stimulus when all keys are released + */ + const createPostDecisionKeyUpHandler = () => { + const postDecisionStartTime = performance.now(); + + return (event: KeyboardEvent) => { + // Check if this is one of our decision keys + const keycode = event.key.toLowerCase(); + const decisionKeys = [keyLayout["1"], keyLayout["2"], keyLayout["3"], keyLayout["4"]]; + + if (decisionKeys.includes(keycode)) { + // Check if any decision keys are still being held + setTimeout(() => { + if (currentStimulus && currentStimulus.getParameters().name === "post-decision") { + const elapsedTime = performance.now() - postDecisionStartTime; + // Minimum display time: 250ms base + 1000ms if feedback is enabled + const minDisplayTime = trial.showFeedback === true ? 1250 : 250; + + if (elapsedTime < minDisplayTime) { + // Wait for the remaining time before finishing + setTimeout(() => { + if (currentStimulus && currentStimulus.getParameters().name === "post-decision") { + Runner.post(currentStimulus); + } + }, minDisplayTime - elapsedTime); + } else { + // Already waited long enough, finish immediately + Runner.post(currentStimulus); + } + } + }, 50); + } + }; + }; + /** * An event handler for decision made during a trial * @param {KeyboardEvent} event the particular event or keypress @@ -540,7 +578,7 @@ class DotGamePlugin implements JsPsychPlugin { selected: false, timing: { pre: 0, - run: 250, + run: -1, // Wait indefinitely until all keys are released post: 0, }, target: display_element, From 8cd5180163d3c66375483f66c4be7bc7d9a35e7e Mon Sep 17 00:00:00 2001 From: Henry Burgess Date: Wed, 15 Oct 2025 17:16:03 -0500 Subject: [PATCH 07/12] RDK-85 Prepare for v1.8.2 release --- ...ry_v1.7.1.md => v1.7.1_data_dictionary.md} | 0 analysis/v1.8.2_data_dictionary.md | 47 ++++++++ package.json | 2 +- src/classes/Graphics.ts | 107 ------------------ src/classes/Renderer.ts | 2 +- src/classes/Stimulus.ts | 2 - src/css/styles.css | 107 +----------------- src/index.ts | 18 +-- 8 files changed, 55 insertions(+), 230 deletions(-) rename analysis/{rdk_data_dictionary_v1.7.1.md => v1.7.1_data_dictionary.md} (100%) create mode 100644 analysis/v1.8.2_data_dictionary.md diff --git a/analysis/rdk_data_dictionary_v1.7.1.md b/analysis/v1.7.1_data_dictionary.md similarity index 100% rename from analysis/rdk_data_dictionary_v1.7.1.md rename to analysis/v1.7.1_data_dictionary.md diff --git a/analysis/v1.8.2_data_dictionary.md b/analysis/v1.8.2_data_dictionary.md new file mode 100644 index 0000000..9c9109d --- /dev/null +++ b/analysis/v1.8.2_data_dictionary.md @@ -0,0 +1,47 @@ +# RDK Task - Data Dictionary + +> Version 1.8.2 + +Release generated October 16, 2025. + +Browse files: [https://github.com/Brain-Development-and-Disorders-Lab/task_rdk/tree/v1.8.2](https://github.com/Brain-Development-and-Disorders-Lab/task_rdk/tree/v1.8.2) + +## Data + +### General + +- `trialNumber`: Number of elapsed trials, indexed from `0`. +- `score`: Total number of correct responses, counting `dot-game`-type `main` trials only (not calibration or practice trials). +- `deviation`: The direction of the coherent dots, either `left` or `right`. +- `correct`: Whether the participant selected the correct direction of the coherent dots, either `1` or `0`. +- `coherence`: The current proportion of coherent dots being presented during the moving dots stimulus. Example: A value of `0.13` indicates 13% of dots on-screen will be coherent, the remaining 87% will be random distractor dots. +- `coherences`: Generated low and high coherence pairs, `[low, high]`. Calculated as `[kMedian * 0.5, kMedian * 2.0]`, where `kMedian <= 0.5 && kMedian >= 0.12`. + +### Decisions + +- `selection`: The selected button value, `vc_l`, `sc_l`, `sc_r`, or `vc_r`. + +### Timing + +- `trialStartTime`: Timestamp of when the start of the trial is invoked. +- `trialEndTime`: Timestamp of when the trial is ended and the next trial invoked. +- `trialTotalTime`: Total duration of the trial presentation. +- `referenceStartTime`: Timestamp of when the start of the dot direction decision is presented. +- `referenceEndTime`: Timestamp of when the direction is selected and the selection is stored. +- `referenceTotalTime`: Total duration of the direction decision being presented and the participant making a selection. +- `confidenceStartTime`: Timestamp of when the start of the confidence slider is presented. +- `confidenceEndTime`: Timestamp of when the confidence value has been selected and the trial continues. +- `confidenceTotalTime`: Total duration of the confidence slider being presented and the participant making a slider selection. +- `stimulusDuration`: Total duration that the moving dot stimulus is presented for (default is `1500` milliseconds). + +## Trial Structure + +1. `initial`: Presenting a fixation cross for `1000` milliseconds. +2. `motion`: Presenting the dot motion for `trial.stimulusDuration` milliseconds (default `1500`). +3. `reference`: Presenting the dot direction decision, no time limit. +4. `decision`: Presenting a fixation cross for `250` milliseconds. +5. `confidence`: Presenting the confidence selection slider **if enabled on that trial**, no time limit. + +## Notes + +- Due to inaccuracies when using millisecond-precision timing alongside complex 2D rendering on the web, the result for `trialTotalTime` is typically between `65`-`75` milliseconds **greater** than the sum of other `-TotalTime` values and fixed durations. diff --git a/package.json b/package.json index ea6e798..dd4b6ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "task_rdk", - "version": "1.8.1", + "version": "1.8.2", "description": "RDK Task", "scripts": { "build": "yarn build:desktop", diff --git a/src/classes/Graphics.ts b/src/classes/Graphics.ts index b7d2d45..56106db 100644 --- a/src/classes/Graphics.ts +++ b/src/classes/Graphics.ts @@ -256,113 +256,6 @@ export class Graphics { } } - /** - * Setup HTML elements for confidence selection element - * @param parameters configuration information for confidence selection - */ - addConfidence(parameters: any): void { - // Confidence instructions - let html = ""; - - // Controls for confidence slider and submission - if (__TARGET__ === "spectrometer") { - html += `
`; - // Decrease confidence - html += `
`; - html += Renderer.getEmbeddedControllerButton(1); - html += `

Decrease Confidence

`; - html += `
`; - - // Submit - html += `
`; - html += `

Continue

`; - html += Renderer.getEmbeddedControllerButton(3); - html += `
`; - - // Increase confidence - html += `
`; - html += `

Increase Confidence

`; - html += Renderer.getEmbeddedControllerButton(4); - html += `
`; - html += `
`; - } else { - html += `
`; - // Decrease confidence - html += `
`; - html += Renderer.getEmbeddedKeyboardButton("F"); - html += `

Decrease Confidence

`; - html += `
`; - - // Submit - html += `
`; - html += `

Continue

`; - html += Renderer.getEmbeddedKeyboardButton("K"); - html += `
`; - - // Increase confidence - html += `
`; - html += `

Increase Confidence

`; - html += Renderer.getEmbeddedKeyboardButton("J"); - html += `
`; - html += `
`; - } - - // Confidence slider - html += `
`; - html += `
`; - html += ``; - html += ``; - - // Add labels - const labels = ["50%", "60%", "70%", "80%", "90%", "100%"]; - html += `
`; - for (let i = 0; i < labels.length; i++) { - const width = 100 / (labels.length - 1); - const offset = i * width - width / 2; - html += `
`; - html += `${labels[i]}`; - html += `
`; - } - html += `
`; - html += `
`; - html += `
`; - html += `
`; - - // Button to notify of a mistake - html += `
`; - if (__TARGET__ === "spectrometer") { - html += `

`; - html += `I made a mistake`; - html += `

`; - html += Renderer.getEmbeddedControllerButton(2); - } else { - html += ``; - html += Renderer.getEmbeddedKeyboardButton("D"); - } - html += `
`; - - // Add the HTML to the display element - this.renderer.getDisplayElement().parentNode.innerHTML = html; - - // Try to hide the thumb - document - .getElementById("confidence-slider") - .addEventListener("click", () => { - const slider = document.getElementById("confidence-slider"); - slider.className = "confidence-slider"; - }); - - // Bind appropriate event listeners to actions - document.addEventListener("keyup", parameters.eventHandler); - if (__TARGET__ === "desktop") { - document - .getElementById("mistake-button") - .addEventListener("click", parameters.eventHandler); - } - } - /** * Clear all elements from the renderer * This removes everything including aperture outline and fixation cross diff --git a/src/classes/Renderer.ts b/src/classes/Renderer.ts index d56b2e6..8cd8b1e 100644 --- a/src/classes/Renderer.ts +++ b/src/classes/Renderer.ts @@ -618,7 +618,7 @@ export class Renderer { /** * Clear all HTML elements from the display - * This removes HTML elements like confidence sliders, labels, etc. + * This removes HTML elements like labels, etc. */ clearHTMLElements(): void { const graphicsContainer = document.getElementsByClassName("graphics-container")[0]; diff --git a/src/classes/Stimulus.ts b/src/classes/Stimulus.ts index e80feb2..e8017d0 100644 --- a/src/classes/Stimulus.ts +++ b/src/classes/Stimulus.ts @@ -170,8 +170,6 @@ export class Stimulus { graphics.addRightArc(); } else if (component === "indicator") { graphics.addReferenceIndicator(); - } else if (component === "confidence") { - graphics.addConfidence(parameters); } else if (component === "left") { graphics.addLeftLabel(); } else if (component === "right") { diff --git a/src/css/styles.css b/src/css/styles.css index db470da..e44af3a 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -1,7 +1,5 @@ /* -Stylesheet for Bang et al. RDK task. Primary -style features relate to slider input used for -confidence estimate. +Stylesheet for Bang et al. RDK task. Author: Henry Burgess */ @@ -34,69 +32,6 @@ Author: Henry Burgess margin: 5%; } -.confidence-slider { - -webkit-appearance: none; - margin-top: 10px; - width: 100%; - height: 15px; - border-radius: 5px; - background: #000000; - outline: none; - opacity: 0.7; - -webkit-transition: 0.2s; - transition: opacity 0.2s; -} - -.confidence-slider::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 1.5rem; - height: 64px; - border-radius: 10px; - background: #ff70ff; - cursor: pointer; -} - -.confidence-slider::-moz-range-thumb { - width: 1.5rem; - height: 64px; - border-radius: 10px; - background: ff70ff; - cursor: pointer; -} - -.confidence-slider-hidden { - -webkit-appearance: none; - width: 100%; - height: 15px; - border-radius: 5px; - background: #000000; - outline: none; - opacity: 0.7; - -webkit-transition: 0.2s; - transition: opacity 0.2s; -} - -.confidence-slider-hidden::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 1.5rem; - height: 64px; - border-radius: 10px; - background: ff70ff; - cursor: pointer; - visibility: hidden; -} - -.confidence-slider-hidden::-moz-range-thumb { - width: 1.5rem; - height: 64px; - border-radius: 10px; - background: ff70ff; - cursor: pointer; - visibility: hidden; -} - .reference-instructions-container { padding: 0; } @@ -112,38 +47,6 @@ Author: Henry Burgess text-align: center; } -#confidence-controls-container { - display: flex; - flex-direction: row; - width: 90vw; - justify-content: space-between; - align-items: center; -} - -#confidence-controls-container-element { - display: flex; - flex-direction: row; - gap: 10px; - justify-content: center; - align-items: center; - width: 100%; -} - -#mistake-button-container { - display: flex; - flex-direction: row; - gap: 10px; - margin-top: 24px; - justify-content: center; - align-items: center; - width: 100%; -} - -#mistake-button-container button { - font-size: 18px; - font-weight: bold; -} - /* Base control button class */ .control-button { font-family: Arial, Helvetica, sans-serif; @@ -200,14 +103,6 @@ body.inverted { color: white; } -.graphics-container.inverted .confidence-slider { - background: #ffffff; -} - -.graphics-container.inverted .confidence-slider-hidden { - background: #ffffff; -} - /* Invert label borders */ body.inverted .addButton { border-color: white !important; diff --git a/src/index.ts b/src/index.ts index 8484a77..8f292a0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -87,19 +87,19 @@ const InputConfigurations = { export const Manipulations = { numTutorialTrials: jsPsych.extensions.Neurocog.getManipulation( "numTutorialTrials", - 10 + 8 ), numPracticeTrials: jsPsych.extensions.Neurocog.getManipulation( "numPracticeTrials", - 10 + 8 ), numCalibrationOneTrials: jsPsych.extensions.Neurocog.getManipulation( "numCalibrationOneTrials", - 120 + 36 ), numMainTrials: jsPsych.extensions.Neurocog.getManipulation( "numMainTrials", - 200 + 40 ), invertColors: jsPsych.extensions.Neurocog.getManipulation("invertColors", false), requireID: jsPsych.extensions.Neurocog.getManipulation("requireID", false), @@ -226,7 +226,6 @@ for (let t = 0; t < Manipulations.numTutorialTrials; t++) { dotDirection: r, dotVelocity: 2.0, showFeedback: false, - checkConfidence: false, keyLayout: keyLayout, data: { name: trialName, @@ -250,8 +249,7 @@ const practice = [ `

Practice Games

` + `

You will now play another ${Manipulations.numPracticeTrials} ` + `practice games. ` + - `You won't have to rate your confidence after each game, ` + - `but you will be shown if your answer was correct or not.

` + + `You will be shown if your answer was correct or not.

` + `

If your answer was correct, the cross in the ` + `middle of the screen will briefly turn green.

` + `

If your answer was wrong, the cross in the ` + @@ -288,7 +286,6 @@ for (let t = 0; t < Manipulations.numPracticeTrials; t++) { dotDirection: r, dotVelocity: 2.0, showFeedback: true, - checkConfidence: false, keyLayout: keyLayout, data: { name: trialName, @@ -319,9 +316,6 @@ if (Manipulations.numCalibrationOneTrials + Manipulations.numMainTrials > 0) { Manipulations.numCalibrationOneTrials + Manipulations.numMainTrials } ` + `games.

` + - `

You will not be shown if you have correctly ` + - `answered or not, and you will be asked to rate your ` + - `confidence after some of the games.

` + `
` + `

Good luck!

` + instructionContinueText @@ -390,7 +384,6 @@ for (let t = 0; t < Manipulations.numCalibrationOneTrials; t++) { dotDirection: r, dotVelocity: 2.0, showFeedback: false, - checkConfidence: true, keyLayout: keyLayout, data: { name: trialName, @@ -428,7 +421,6 @@ for (let t = 0; t < Manipulations.numMainTrials; t++) { dotDirection: r, dotVelocity: 2.0, showFeedback: false, - checkConfidence:true, keyLayout: keyLayout, data: { name: trialName, From cc857cafff614ce1e8cd0de705ac91cfac5735e2 Mon Sep 17 00:00:00 2001 From: Henry Burgess Date: Wed, 15 Oct 2025 17:19:47 -0500 Subject: [PATCH 08/12] RDK-85 Fix progress colours --- src/classes/Renderer.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/classes/Renderer.ts b/src/classes/Renderer.ts index 8cd8b1e..af6f772 100644 --- a/src/classes/Renderer.ts +++ b/src/classes/Renderer.ts @@ -265,19 +265,19 @@ export class Renderer { }, orangeDark: { background: "#ffc20a", - fill: "#ffe63b", + fill: "#ffdb6e", }, orangeLight: { - background: "#ffe084", - fill: "#ffe79d", + background: "#ffd75e", + fill: "#fce7a9", }, blueDark: { background: "#1792fc", - fill: "#2c96f3", + fill: "#47a8fc", }, blueLight: { background: "#7bbef8", - fill: "#95caf9", + fill: "#9dd0fc", }, }; From 0f707000324ac061986d629bcfefe2dc1633914a Mon Sep 17 00:00:00 2001 From: Henry Burgess Date: Thu, 16 Oct 2025 12:31:50 -0500 Subject: [PATCH 09/12] RDK-85 Fix instructions button inputs --- src/classes/Renderer.ts | 1 + src/index.ts | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/classes/Renderer.ts b/src/classes/Renderer.ts index af6f772..f76c479 100644 --- a/src/classes/Renderer.ts +++ b/src/classes/Renderer.ts @@ -313,6 +313,7 @@ export class Renderer { buttonLabel.style.fontSize = "x-large"; buttonLabel.style.position = "relative"; buttonLabel.style.zIndex = "1"; + buttonLabel.style.color = "black"; buttonContainer.appendChild(buttonLabel); return buttonContainer; diff --git a/src/index.ts b/src/index.ts index 8f292a0..bc3be88 100644 --- a/src/index.ts +++ b/src/index.ts @@ -177,8 +177,8 @@ if ( instructionContinueText, ], allow_keys: !keyLayout.showButtons, - key_forward: keyLayout["3"].charAt(keyLayout["3"].length - 1), - key_backward: keyLayout["2"].charAt(keyLayout["2"].length - 1), + key_forward: _.isEqual(keyLayout.name, "spectrometer") ? keyLayout["4"] : keyLayout["3"], + key_backward: _.isEqual(keyLayout.name, "spectrometer") ? keyLayout["1"] : keyLayout["2"], show_page_number: true, show_clickable_nav: keyLayout.showButtons, }); @@ -198,8 +198,8 @@ timeline.push({ type: InstructionsPlugin, pages: tutorialGames, allow_keys: !keyLayout.showButtons, - key_forward: keyLayout["3"].charAt(keyLayout["3"].length - 1), - key_backward: keyLayout["2"].charAt(keyLayout["2"].length - 1), + key_forward: _.isEqual(keyLayout.name, "spectrometer") ? keyLayout["4"] : keyLayout["3"], + key_backward: _.isEqual(keyLayout.name, "spectrometer") ? keyLayout["1"] : keyLayout["2"], show_page_number: true, show_clickable_nav: keyLayout.showButtons, }); @@ -261,8 +261,8 @@ timeline.push({ type: InstructionsPlugin, pages: practice, allow_keys: !keyLayout.showButtons, - key_forward: keyLayout["3"].charAt(keyLayout["3"].length - 1), - key_backward: keyLayout["2"].charAt(keyLayout["2"].length - 1), + key_forward: _.isEqual(keyLayout.name, "spectrometer") ? keyLayout["4"] : keyLayout["3"], + key_backward: _.isEqual(keyLayout.name, "spectrometer") ? keyLayout["1"] : keyLayout["2"], show_page_number: true, show_clickable_nav: keyLayout.showButtons, }); @@ -350,7 +350,7 @@ if (_.isEqual(keyLayout.name, "spectrometer")) { type: InstructionsPlugin, pages: spectrometer, allow_keys: !keyLayout.showButtons, - key_forward: keyLayout.trigger.charAt(keyLayout.trigger.length - 1), + key_forward: keyLayout.trigger, show_clickable_nav: keyLayout.showButtons, }); } else { @@ -358,8 +358,8 @@ if (_.isEqual(keyLayout.name, "spectrometer")) { type: InstructionsPlugin, pages: main, allow_keys: !keyLayout.showButtons, - key_forward: keyLayout["3"].charAt(keyLayout["3"].length - 1), - key_backward: keyLayout["2"].charAt(keyLayout["2"].length - 1), + key_forward: _.isEqual(keyLayout.name, "spectrometer") ? keyLayout["4"] : keyLayout["3"], + key_backward: _.isEqual(keyLayout.name, "spectrometer") ? keyLayout["1"] : keyLayout["2"], show_page_number: true, show_clickable_nav: keyLayout.showButtons, }); @@ -450,7 +450,7 @@ timeline.push({ allow_backward: false, button_label_next: "Finish", show_clickable_nav: false, - key_forward: keyLayout["3"], + key_forward: _.isEqual(keyLayout.name, "spectrometer") ? keyLayout["4"] : keyLayout["3"], }); jsPsych.run(timeline); From 16cce2221a049b08b282d972cc3ce7e4221de028 Mon Sep 17 00:00:00 2001 From: Henry Burgess Date: Thu, 16 Oct 2025 13:07:41 -0500 Subject: [PATCH 10/12] RDK-85 Fix automatic color scheme --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index bc3be88..2308007 100644 --- a/src/index.ts +++ b/src/index.ts @@ -101,7 +101,7 @@ export const Manipulations = { "numMainTrials", 40 ), - invertColors: jsPsych.extensions.Neurocog.getManipulation("invertColors", false), + invertColors: __TARGET__ === "spectrometer", requireID: jsPsych.extensions.Neurocog.getManipulation("requireID", false), enableFullscreen: jsPsych.extensions.Neurocog.getManipulation("enableFullscreen", false), showInstructions: jsPsych.extensions.Neurocog.getManipulation( From 69e743a1ac58766850339f9c4b0dc15383321c0d Mon Sep 17 00:00:00 2001 From: Henry Burgess Date: Thu, 16 Oct 2025 14:47:00 -0500 Subject: [PATCH 11/12] RDK-85 Update README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 0ddf4da..183afbb 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,12 @@ To deliver this task offline, open `index.html` in a browser of choice. To deliv To deliver this task offline in a spectrometer, adjust the key layout defined in `index.ts` to map the signals generated by spectrometer controllers. Run `yarn build:spectrometer` to generate output with updated button icons and key layouts to suit spectrometer delivery and controller interaction. This task requires a minimum of four physical buttons for playing the game and a signal signifying spectrometer data collection. +> [!IMPORTANT] +> When using the Current Designs button box, use the following output modes for the corresponding task version: +> +> * **v1.8.2 and higher**: Use *Output mode* `HID_NAR_12345` +> * **v1.8.1 and earlier**: Use *Output mode* `HID_KEY_12345` + ## License From cf285ef90b24a006ec01d371c9712f3ccea80519 Mon Sep 17 00:00:00 2001 From: Henry Burgess Date: Thu, 16 Oct 2025 14:48:29 -0500 Subject: [PATCH 12/12] RDK-85 Update README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 183afbb..0878419 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,10 @@ To deliver this task offline, open `index.html` in a browser of choice. To deliv To deliver this task offline in a spectrometer, adjust the key layout defined in `index.ts` to map the signals generated by spectrometer controllers. Run `yarn build:spectrometer` to generate output with updated button icons and key layouts to suit spectrometer delivery and controller interaction. This task requires a minimum of four physical buttons for playing the game and a signal signifying spectrometer data collection. > [!IMPORTANT] -> When using the Current Designs button box, use the following output modes for the corresponding task version: +> When using the Current Designs button box, set the following *Output modes* for the corresponding task version: > -> * **v1.8.2 and higher**: Use *Output mode* `HID_NAR_12345` -> * **v1.8.1 and earlier**: Use *Output mode* `HID_KEY_12345` +> * **v1.8.2 and higher**: Set *Output mode* to `HID_NAR_12345` +> * **v1.8.1 and earlier**: Set *Output mode* to `HID_KEY_12345` ## License