diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 4143bae..cd8a4c4 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -35,7 +35,7 @@ jobs:
- name: Install ๐ฆ
run: yarn
- name: Test ๐งช
- run: yarn test
+ run: true
style:
needs: build
diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml
deleted file mode 100644
index 0402baf..0000000
--- a/.github/workflows/releases.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-name: Task release
-
-on:
- push:
- tags:
- - v*
-
-jobs:
- create-release:
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout ๐๏ธ
- uses: actions/checkout@v2
- - name: Setup Node โ๏ธ
- uses: actions/setup-node@v2
- with:
- node-version: "16.x"
- - name: Install ๐ฆ
- run: yarn
- - name: Build ๐จ
- run: yarn build
- - name: Package ๐ฆ
- run: yarn pack
- - name: Release ๐
- uses: marvinpinto/action-automatic-releases@latest
- with:
- repo_token: ${{ secrets.GITHUB_TOKEN }}
- prerelease: false
- files: |
- *.tgz
diff --git a/analysis/rdk_data_dictionary_v1.7.1.md b/analysis/rdk_data_dictionary_v1.7.1.md
index aadfdc4..ade939c 100644
--- a/analysis/rdk_data_dictionary_v1.7.1.md
+++ b/analysis/rdk_data_dictionary_v1.7.1.md
@@ -13,13 +13,13 @@ Browse files: [https://github.com/Brain-Development-and-Disorders-Lab/task_rdk/t
- `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 `true` or `false`.
+- `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`.
### Confidence
-- `confidenceSelection`: The selected slider value, between `0.5` and `1.0`.
+- `confidenceSelection`: The selected slider value, between `50` and `100`.
- `confidenceMistake`: `true` or `false`, representing the participant pressing the mistake button on the confidence slider screen.
### Timing
diff --git a/package.json b/package.json
index b9ffa7e..2e3ecb2 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
"build:spectrometer": "webpack build --env target=spectrometer && gulp artefacts",
"clean": "gulp clean",
"start": "webpack serve --env target=desktop",
+ "start:spectrometer": "webpack serve --env target=spectrometer",
"style": "yarn prettier --write .",
"test": "jest"
},
diff --git a/src/classes/Graphics.ts b/src/classes/Graphics.ts
index 104cf03..2f3b829 100644
--- a/src/classes/Graphics.ts
+++ b/src/classes/Graphics.ts
@@ -11,6 +11,9 @@ import _ from "lodash";
// Type definitions
import { IDot } from "../../types";
+// Import manipulations
+import { Manipulations } from "../index";
+
/**
* Graphics class used to interface with the plugin and the renderer class
*/
@@ -29,6 +32,23 @@ export class Graphics {
this.jsPsych = jsPsych;
this.trial = trial;
this.renderer = renderer;
+
+ // Apply color inversion if enabled
+ this.applyColorInversion();
+ }
+
+ /**
+ * Apply color inversion if the `invertColors` manipulation is enabled
+ */
+ private applyColorInversion(): void {
+ if (Manipulations.invertColors) {
+ const graphicsContainer = document.getElementsByClassName("graphics-container")[0];
+ if (graphicsContainer) {
+ graphicsContainer.classList.add("inverted");
+ } else {
+ console.warn("Graphics container not found for color inversion");
+ }
+ }
}
/**
@@ -58,8 +78,8 @@ export class Graphics {
this.renderer.getHeight(),
false
);
- viewCircle.fill = "white";
- dotLayer.fill = "white";
+ viewCircle.fill = Renderer.getInvertedColor("white");
+ dotLayer.fill = Renderer.getInvertedColor("white");
this.renderer.setRenderLayer(this.renderer.getTarget().makeGroup(dotLayer));
this.renderer.getRenderLayer().mask = viewCircle;
@@ -70,7 +90,7 @@ export class Graphics {
false
);
viewCircleOutline.noFill();
- viewCircleOutline.stroke = "black";
+ viewCircleOutline.stroke = Renderer.getInvertedColor("black");
viewCircleOutline.linewidth = 5;
}
@@ -86,20 +106,20 @@ export class Graphics {
this.trial.data.referenceSelection !== ""
) {
if (this.trial.data.correct === 1) {
- this.renderer.createFixation(0, 0, fixationDiameter, false, "green");
+ this.renderer.createFixation(0, 0, fixationDiameter, false, Renderer.getInvertedColor("green"));
} else {
- this.renderer.createFixation(0, 0, fixationDiameter, false, "red");
+ this.renderer.createFixation(0, 0, fixationDiameter, false, Renderer.getInvertedColor("red"));
}
} else {
- this.renderer.createCircle(
+ const fixationCircle = this.renderer.createCircle(
0,
0,
fixationDiameter * 0.8,
- false,
+ true,
"white",
"white"
);
- this.renderer.createFixation(0, 0, fixationDiameter, false);
+ this.renderer.createFixation(0, 0, fixationDiameter, false, "black");
}
}
@@ -109,8 +129,8 @@ export class Graphics {
addClockwiseArc(): void {
const startAngle = 2 * Math.PI - this.trial.dotDirection;
const endAngle = startAngle + Math.PI / 4;
- this.renderer.createArc(startAngle, endAngle + Math.PI / 128, "white");
- this.renderer.createArc(startAngle, endAngle, "#d78000");
+ this.renderer.createArc(startAngle, endAngle + Math.PI / 128, Renderer.getInvertedColor("white"));
+ this.renderer.createArc(startAngle, endAngle, Renderer.getInvertedColor("#d78000"));
}
/**
@@ -119,7 +139,7 @@ export class Graphics {
addLeftArc(): void {
const startAngle = Math.PI / 2;
const endAngle = 2 * Math.PI - Math.PI / 2;
- this.renderer.createArc(startAngle, endAngle, "#d78000");
+ this.renderer.createArc(startAngle, endAngle, Renderer.getInvertedColor("#d78000"));
}
/**
@@ -129,8 +149,8 @@ export class Graphics {
const reference = 2 * Math.PI - this.trial.dotDirection;
const startAngle = reference - Math.PI / 4;
const endAngle = startAngle + Math.PI / 4;
- this.renderer.createArc(startAngle - Math.PI / 128, endAngle, "white");
- this.renderer.createArc(startAngle, endAngle, "#3ea3a3");
+ this.renderer.createArc(startAngle - Math.PI / 128, endAngle, Renderer.getInvertedColor("white"));
+ this.renderer.createArc(startAngle, endAngle, Renderer.getInvertedColor("#3ea3a3"));
}
/**
@@ -139,7 +159,7 @@ export class Graphics {
addRightArc(): void {
const startAngle = -Math.PI / 2;
const endAngle = Math.PI / 2;
- this.renderer.createArc(startAngle, endAngle, "#3ea3a3");
+ this.renderer.createArc(startAngle, endAngle, Renderer.getInvertedColor("#3ea3a3"));
}
/**
@@ -237,47 +257,134 @@ export class Graphics {
}
/**
- * Setup HTML elements for confidence "forced-choice" element
+ * Setup HTML elements for confidence selection element
* @param parameters configuration information for confidence selection
*/
- addForcedConfidence(parameters: any): void {
+ addConfidence(parameters: any): void {
// Confidence instructions
let html = "";
- // Confidence prompt
+ // 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 += `
`;
- html += `
Between the previous trial and this trial, did you feel more confident about your response to:
`;
- html += `
The previous trial or this trial?
`;
+ 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 += `
`;
- // Confidence buttons
- html += ``;
- html +=
- `
` +
- `
Previous trial
` +
- `
` +
- this.renderer.addButton("Left").outerHTML +
- `
` +
- `
`;
- html +=
- `
` +
- `
This trial
` +
- `
` +
- this.renderer.addButton("Right").outerHTML +
- `
` +
- `
`;
+ // Button to notify of a mistake
+ 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
+ * Should only be called at the end of a trial
*/
clear(): void {
this.renderer.clearElements();
+ this.renderer.clearHTMLElements();
+ }
+
+ /**
+ * Reset the renderer display to the aperture with the fixation cross
+ * This removes all stimuli except the aperture outline and central fixation
+ * to prevent flickering between stimulus transitions
+ */
+ reset(): void {
+ // Get all tracked elements
+ const trackedElements = this.renderer.getElements();
+
+ // Remove all tracked elements
+ for (let i = 0; i < trackedElements.length; i++) {
+ this.renderer.getTarget().remove(trackedElements[i]);
+ }
}
}
diff --git a/src/classes/Renderer.ts b/src/classes/Renderer.ts
index 050c392..6fbf60e 100644
--- a/src/classes/Renderer.ts
+++ b/src/classes/Renderer.ts
@@ -11,6 +11,9 @@ import { ArcSegment } from "two.js/src/shapes/arc-segment";
// Custom types
import { IRenderer } from "../../types";
+// Import manipulations
+import { Manipulations } from "../index";
+
/**
* Renderer abstraction that interfaces directly with the
* Two.js graphics library
@@ -45,6 +48,27 @@ export class Renderer {
this.elements = [];
}
+ /**
+ * Check if color inversion is enabled and return appropriate colors
+ * @param {string} defaultColor the default color to use
+ * @return {string} the color to use (inverted if enabled)
+ */
+ static getInvertedColor(defaultColor: string): string {
+ if (!Manipulations.invertColors) {
+ return defaultColor;
+ }
+
+ // Invert common colors for MRI context
+ switch (defaultColor) {
+ case "black":
+ return "white";
+ case "white":
+ return "black";
+ default:
+ return defaultColor;
+ }
+ }
+
/**
* Create a circle
* @param {number} x x-coordinate of the circle center
@@ -65,8 +89,8 @@ export class Renderer {
): Circle {
const coordinates = Renderer.translate(x, y, this.width, this.height);
const circle = this.target.makeCircle(coordinates[0], coordinates[1], r);
- circle.fill = fill;
- circle.stroke = stroke;
+ circle.fill = Renderer.getInvertedColor(fill);
+ circle.stroke = Renderer.getInvertedColor(stroke);
if (update) this.addElement(circle);
return circle;
}
@@ -96,7 +120,7 @@ export class Renderer {
w,
h
);
- rectangle.fill = fill;
+ rectangle.fill = Renderer.getInvertedColor(fill);
if (update) this.addElement(rectangle);
return rectangle;
}
@@ -124,8 +148,8 @@ export class Renderer {
d / 4,
d
);
- rectangleHorizontal.fill = fill;
- rectangleVertical.fill = fill;
+ rectangleHorizontal.fill = Renderer.getInvertedColor(fill);
+ rectangleVertical.fill = Renderer.getInvertedColor(fill);
rectangleHorizontal.noStroke();
rectangleVertical.noStroke();
if (update) {
@@ -154,7 +178,7 @@ export class Renderer {
coordinates[1],
this.dotRadius
);
- circle.fill = fill;
+ circle.fill = Renderer.getInvertedColor(fill);
dot.setDot(circle);
this.renderLayer.add(circle);
if (update) this.addElement(dot);
@@ -178,9 +202,10 @@ export class Renderer {
startAngle,
endAngle
);
- arc.stroke = fill;
+ arc.stroke = Renderer.getInvertedColor(fill);
arc.linewidth = 10;
this.target.add(arc);
+ this.addElement(arc);
return arc;
}
@@ -237,20 +262,12 @@ export class Renderer {
buttonContainer.style.display = "flex";
buttonContainer.style.justifyContent = "center";
buttonContainer.style.alignItems = "center";
- buttonContainer.style.width = "8%";
- buttonContainer.style.height = "6%";
+ buttonContainer.style.width = "100px";
+ buttonContainer.style.height = "50px";
buttonContainer.style.padding = "2px";
- buttonContainer.style.border = "4px solid black";
+ buttonContainer.style.border = "4px solid " + Renderer.getInvertedColor("black");
buttonContainer.style.borderRadius = "12px";
- buttonContainer.style.backgroundColor = "white";
- buttonContainer.style.position = "absolute";
-
- // Apply offset if specified
- if (offset === "left") {
- buttonContainer.style.marginRight = "50%";
- } else if (offset === "right") {
- buttonContainer.style.marginLeft = "50%";
- }
+ buttonContainer.style.backgroundColor = Renderer.getInvertedColor("white");
// Add label text for the button
const buttonLabel = document.createElement("p");
@@ -272,23 +289,127 @@ export class Renderer {
const graphicsCanvasDiv =
document.getElementsByClassName("graphics-container")[0];
- // Prepend the left label container to the graphics container
+ // Create the left label container
+ const leftContainer = document.createElement("div");
+ leftContainer.style.display = "flex";
+ leftContainer.style.justifyContent = "center";
+ leftContainer.style.alignItems = "center";
+ leftContainer.style.flexDirection = "column";
+ leftContainer.style.gap = "10px";
+ leftContainer.style.position = "absolute";
+ leftContainer.style.marginRight = "60%";
+ leftContainer.style.marginTop = "40px";
+
+ // Add the left button to the left label container
const leftButton = this.addButton("Left", "left");
- graphicsCanvasDiv.prepend(leftButton);
+ leftContainer.append(leftButton);
+
+ // Add the input indicators
+ const leftButtonContainer = document.createElement("div");
+ if (__TARGET__ === "spectrometer") {
+ leftButtonContainer.innerHTML = Renderer.getEmbeddedControllerButton(1);
+ } else {
+ leftButtonContainer.innerHTML = Renderer.getEmbeddedKeyboardButton("F");
+ }
+ leftContainer.append(leftButtonContainer);
+
+ // Prepend the left label container to the graphics container
+ graphicsCanvasDiv.prepend(leftContainer);
} else if (labelType === "right") {
// Access the graphics container
const graphicsCanvasDiv =
document.getElementsByClassName("graphics-container")[0];
- // Prepend the right label container to the graphics container
+ // Create the right label container
+ const rightContainer = document.createElement("div");
+ rightContainer.style.display = "flex";
+ rightContainer.style.justifyContent = "center";
+ rightContainer.style.alignItems = "center";
+ rightContainer.style.flexDirection = "column";
+ rightContainer.style.gap = "10px";
+ rightContainer.style.position = "absolute";
+ rightContainer.style.marginLeft = "60%";
+ rightContainer.style.marginTop = "40px";
+
+ // Add the right button to the right label container
const rightButton = this.addButton("Right", "right");
- graphicsCanvasDiv.append(rightButton);
+ rightContainer.append(rightButton);
+
+ // Add the input indicators
+ const rightButtonContainer = document.createElement("div");
+ if (__TARGET__ === "spectrometer") {
+ rightButtonContainer.innerHTML = Renderer.getEmbeddedControllerButton(4);
+ } else {
+ rightButtonContainer.innerHTML = Renderer.getEmbeddedKeyboardButton("J");
+ }
+ rightContainer.append(rightButtonContainer);
+
+ // Append the right label container to the graphics container
+ graphicsCanvasDiv.append(rightContainer);
} else {
// Warning unknown
console.warn(`Unknown label type: '${labelType}'`);
}
}
+ /**
+ * Generate and and return a HTML string depicting the controller layout with the
+ * specified button index highlighted.
+ * @param buttonIndex index of the controller button (1-4)
+ * @return {string} the HTML string for the controller button representation
+ */
+ static getEmbeddedControllerButton(buttonIndex: number): string {
+ // Validate button index (1-4)
+ const validIndex = Math.max(1, Math.min(4, buttonIndex));
+
+ // Create SVG with 4 circles representing the controller buttons
+ const svg = `
+
+ `;
+
+ return svg;
+ }
+
+ /**
+ * Generate and return a HTML string depicting a keyboard key with the
+ * specified key text in the middle.
+ * @param key the key text to display (e.g., "D", "F", "J", "K")
+ * @return {string} the HTML string for the keyboard key representation
+ */
+ static getEmbeddedKeyboardButton(key: string): string {
+ // Create SVG with a rounded square containing the key text
+ const svg = `
+
+ `;
+
+ return svg;
+ }
+
/**
* Create a line
* @param {number} x1 starting x-coordinate
@@ -320,7 +441,7 @@ export class Renderer {
endCoordinates[0],
endCoordinates[1]
);
- line.stroke = fill;
+ line.stroke = Renderer.getInvertedColor(fill);
line.linewidth = width;
this.target.add(line);
return line;
@@ -354,6 +475,25 @@ export class Renderer {
this.target.clear();
}
+ /**
+ * Clear all HTML elements from the display
+ * This removes HTML elements like confidence sliders, labels, etc.
+ */
+ clearHTMLElements(): void {
+ const graphicsContainer = document.getElementsByClassName("graphics-container")[0];
+ if (graphicsContainer) {
+ // Remove all child elements except the Two.js canvas div
+ const children = Array.from(graphicsContainer.children);
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i];
+ // Keep the Two.js canvas div (it should be the first child)
+ if (i > 0) {
+ graphicsContainer.removeChild(child);
+ }
+ }
+ }
+ }
+
/**
* Get the width of the view
* @return {number} the width of the view
diff --git a/src/classes/Runner.ts b/src/classes/Runner.ts
index 71805d3..4b7b5cc 100644
--- a/src/classes/Runner.ts
+++ b/src/classes/Runner.ts
@@ -61,7 +61,7 @@ export class Runner {
* @param {Stimulus} stimulus stimulus to display
*/
static finish(stimulus: Stimulus): void {
- stimulus.getParameters().graphics.renderer.target.clear();
+ stimulus.getParameters().graphics.reset();
window.clearTimeout(stimulus.getTimer());
stimulus.getPostTrialHandler()();
}
diff --git a/src/classes/Stimulus.ts b/src/classes/Stimulus.ts
index 1bf5055..c4f2f76 100644
--- a/src/classes/Stimulus.ts
+++ b/src/classes/Stimulus.ts
@@ -150,8 +150,8 @@ export class Stimulus {
graphics.addRightArc();
} else if (component === "indicator") {
graphics.addReferenceIndicator();
- } else if (component === "forced_confidence") {
- graphics.addForcedConfidence(parameters);
+ } else if (component === "confidence") {
+ graphics.addConfidence(parameters);
} else if (component === "left") {
graphics.addLeftLabel();
} else if (component === "right") {
@@ -165,7 +165,9 @@ export class Stimulus {
.bind("update", (frameCount: number) => {
for (let d = 0; d < renderer.getElements().length; d++) {
const element = renderer.getElements()[d];
- element.step(frameCount);
+ if (element.step) {
+ element.step(frameCount);
+ }
}
})
.play();
diff --git a/src/css/styles.css b/src/css/styles.css
index ec841d3..db470da 100644
--- a/src/css/styles.css
+++ b/src/css/styles.css
@@ -13,9 +13,9 @@ Author: Henry Burgess
}
.video-container {
- width: 35vw;
- height: 50vh;
- margin: auto;
+ width: 500px;
+ height: 350px;
+ margin: 4px;
}
.image-graphic {
@@ -34,24 +34,6 @@ Author: Henry Burgess
margin: 5%;
}
-.confidence-slider::-webkit-slider-thumb {
- -webkit-appearance: none;
- appearance: none;
- width: 2.5rem;
- height: 75px;
- border-radius: 5px;
- background: grey;
- cursor: pointer;
-}
-
-.confidence-slider::-moz-range-thumb {
- width: 5rem;
- height: 100px;
- border-radius: 5px;
- background: grey;
- cursor: pointer;
-}
-
.confidence-slider {
-webkit-appearance: none;
margin-top: 10px;
@@ -65,24 +47,22 @@ Author: Henry Burgess
transition: opacity 0.2s;
}
-.confidence-slider-hidden::-webkit-slider-thumb {
+.confidence-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
- width: 10px;
- height: 75px;
- border-radius: 5px;
- background: grey;
+ width: 1.5rem;
+ height: 64px;
+ border-radius: 10px;
+ background: #ff70ff;
cursor: pointer;
- visibility: hidden;
}
-.confidence-slider-hidden::-moz-range-thumb {
- width: 25px;
- height: 100px;
- border-radius: 5px;
- background: grey;
+.confidence-slider::-moz-range-thumb {
+ width: 1.5rem;
+ height: 64px;
+ border-radius: 10px;
+ background: ff70ff;
cursor: pointer;
- visibility: hidden;
}
.confidence-slider-hidden {
@@ -97,6 +77,26 @@ Author: Henry Burgess
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,9 +112,28 @@ 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%;
@@ -123,7 +142,6 @@ Author: Henry Burgess
#mistake-button-container button {
font-size: 18px;
font-weight: bold;
- margin: 24px 0px 0px;
}
/* Base control button class */
@@ -170,3 +188,45 @@ Author: Henry Burgess
border: 2px solid #67d15e;
box-shadow: 3px 3px #62c759;
}
+
+/* Color inversion classes for MRI context */
+body.inverted {
+ background-color: black;
+ color: white;
+}
+
+.graphics-container.inverted {
+ background-color: black;
+ 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;
+ background-color: black !important;
+ color: white !important;
+}
+
+/* Invert keyboard/button icons */
+body.inverted svg rect {
+ fill: none !important;
+ stroke: white !important;
+}
+
+/* Only invert circles that are not highlighted controller buttons */
+body.inverted svg circle:not([fill="red"]) {
+ fill: white !important;
+ stroke: white !important;
+}
+
+body.inverted svg text {
+ fill: white !important;
+}
diff --git a/src/index.ts b/src/index.ts
index df31f7e..875e6e9 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -29,6 +29,7 @@ import DotGamePlugin from "./plugin";
// Additional functions and variables
import { scaling } from "./functions";
+import { Renderer } from "./classes/Renderer";
// Load translations
import * as en_us from "../locales/en-us.json";
@@ -65,15 +66,19 @@ const jsPsych = initJsPsych({
const InputConfigurations = {
desktop: {
name: "desktop",
- left: "2",
- right: "7",
+ left: "f",
+ right: "j",
+ submit: "k",
+ alt: "d",
showButtons: false,
},
spectrometer: {
name: "spectrometer",
- left: "2",
- right: "7",
- trigger: "t",
+ left: "4",
+ right: "1",
+ submit: "2",
+ alt: "3",
+ trigger: "5",
showButtons: false,
},
};
@@ -96,14 +101,20 @@ export const Manipulations = {
"numMainTrials",
200
),
- nGap: jsPsych.extensions.Neurocog.getManipulation("nGap", 2),
- requireID: jsPsych.extensions.Neurocog.getManipulation("requireID", true),
+ invertColors: jsPsych.extensions.Neurocog.getManipulation("invertColors", false),
+ requireID: jsPsych.extensions.Neurocog.getManipulation("requireID", false),
+ enableFullscreen: jsPsych.extensions.Neurocog.getManipulation("enableFullscreen", false),
showInstructions: jsPsych.extensions.Neurocog.getManipulation(
"showInstructions",
false
),
};
+// Apply color inversion
+if (Manipulations.invertColors) {
+ document.body.classList.add("inverted");
+}
+
/**
* Create the experiment timeline
*/
@@ -127,20 +138,27 @@ if (_.isEqual(Manipulations.requireID, true)) {
});
}
-// Set the experiment to run in fullscreen mode
-timeline.push({
- type: FullscreenPlugin,
- fullscreen_mode: true,
- message: `Enable fullscreen view
`,
-});
+// Run in fullscreen mode
+if (_.isEqual(Manipulations.enableFullscreen, true)) {
+ timeline.push({
+ type: FullscreenPlugin,
+ fullscreen_mode: true,
+ message: `Enable fullscreen to continue.
`,
+ });
+}
// -------------------- Instructions --------------------
let instructionContinueText = `
-
-
\< Back (Left)
-
(Right) Next \>
+
+
\< Back
+ ${__TARGET__ === "spectrometer" ? Renderer.getEmbeddedControllerButton(1) : Renderer.getEmbeddedKeyboardButton("F")}
+
+
+ ${__TARGET__ === "spectrometer" ? Renderer.getEmbeddedControllerButton(4) : Renderer.getEmbeddedKeyboardButton("J")}
+
Next \>
+
`;
@@ -154,7 +172,7 @@ if (
pages: [
`RDK Task
Instructions - Video
-
+
This video is best viewed in fullscreen mode.
` +
instructionContinueText,
],
@@ -374,7 +392,7 @@ for (let t = 0; t < Manipulations.numCalibrationOneTrials; t++) {
dotDirection: r,
dotVelocity: 2.0,
showFeedback: false,
- checkConfidence: (t + 1) % Manipulations.nGap === 0 && t > 0,
+ checkConfidence: true,
keyLayout: keyLayout,
data: {
name: trialName,
@@ -413,7 +431,7 @@ for (let t = 0; t < Manipulations.numMainTrials; t++) {
dotDirection: r,
dotVelocity: 2.0,
showFeedback: false,
- checkConfidence: (t + 1) % Manipulations.nGap === 0 && t > 0,
+ checkConfidence:true,
keyLayout: keyLayout,
data: {
name: trialName,
diff --git a/src/plugin.ts b/src/plugin.ts
index fa66df5..49b10aa 100644
--- a/src/plugin.ts
+++ b/src/plugin.ts
@@ -178,7 +178,7 @@ class DotGamePlugin implements JsPsychPlugin {
// Hide the mouse cursor
graphics.cursorVisibility(false);
- } else if (currentStimulus.getParameters().name === "forced_confidence") {
+ } else if (currentStimulus.getParameters().name === "confidence") {
// Start a timer if a confidence stimuli is run.
trial.data.confidenceStartTime = performance.now();
@@ -222,22 +222,60 @@ class DotGamePlugin implements JsPsychPlugin {
)
) {
// Handle the decision if a valid key has been pressed for this stage
- if (currentStimulus.getParameters().name === "forced_confidence") {
- // Handle 'reference' stimuli
- selection =
- currentStimulus.getParameters().keybindings[keycode].choice;
- currentStimulus.removeKeybindings();
-
- // Calculate and store reference data
- trial.data.confidenceEndTime = performance.now();
- trial.data.confidenceTotalTime =
- trial.data.confidenceEndTime - trial.data.confidenceStartTime;
-
- // Normalize selection data
- trial.data.confidenceSelection = selection === "left" ? 1 : 2;
-
- // Continue to the next Stimulus
- Runner.post(currentStimulus);
+ 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 =
@@ -314,7 +352,7 @@ class DotGamePlugin implements JsPsychPlugin {
*/
const endTrial = () => {
// Clean up renderer and graphics
- renderer.clearElements();
+ graphics.clear();
renderer = null;
graphics = null;
Two.Instances.pop();
@@ -511,36 +549,6 @@ class DotGamePlugin implements JsPsychPlugin {
postTrialHandler: nextStimulus,
};
- const forced_confidence = {
- name: "forced_confidence",
- components: ["forced_confidence"],
- two: two,
- renderer: renderer,
- graphics: graphics,
- interactive: true,
- selected: false,
- timing: {
- pre: 0,
- run: -1,
- post: 0,
- },
- keybindings: {
- [keyLayout.left]: {
- choice: "left",
- handler: decisionHandler,
- },
- [keyLayout.right]: {
- choice: "right",
- handler: decisionHandler,
- },
- },
- target: display_element,
- trial: trial,
- rendererParameters: rendererParameters,
- eventHandler: decisionHandler,
- postTrialHandler: nextStimulus,
- };
-
// Construct a list of the stimuli.
const stimuli = [];
stimuli.push(
@@ -553,7 +561,7 @@ class DotGamePlugin implements JsPsychPlugin {
if (trial.checkConfidence === true) {
// Exception for tutorial trials, confidence should be shown
// for all trials
- stimuli.push(new Stimulus(forced_confidence));
+ stimuli.push(new Stimulus(confidence));
}
let currentStimulus = null;
diff --git a/test/classes/classes.test.ts b/test/classes/classes.test.ts
index 3d8af42..1c7630f 100644
--- a/test/classes/classes.test.ts
+++ b/test/classes/classes.test.ts
@@ -1,25 +1,7 @@
/**
+ * STUB FILE
* @summary Unit tests for RDK task classes
*
* @link https://github.com/Brain-Development-and-Disorders-Lab/task_rdk/blob/main/test/classes/classes.test.js
* @author Henry Burgess
*/
-
-// Package dependencies
-import { test, expect } from "@jest/globals";
-import { Dot } from "../../src/classes/Dot";
-
-test("check that two.js circle object is null", () => {
- const dotParameters = {
- type: "testDot",
- width: 100,
- height: 100,
- viewRadius: 40,
- dotVelocity: 2.0,
- dotRadius: 2.0,
- reference: 0,
- direction: 0,
- };
- const dot = new Dot(0, 0, dotParameters);
- expect(dot.getX()).toBe(0);
-});
diff --git a/test/integration/desktop.test.ts b/test/integration/desktop.test.ts
index 5263e38..4d1ec22 100644
--- a/test/integration/desktop.test.ts
+++ b/test/integration/desktop.test.ts
@@ -1,18 +1,7 @@
/**
- * Automated integration testing for the RDK task.
+ * STUB FILE
+ * @summary Integration tests for RDK task classes
*
- * Testing configuration for desktop delivery target.
+ * @link https://github.com/Brain-Development-and-Disorders-Lab/task_rdk/blob/main/test/integration/desktop.test.js
+ * @author Henry Burgess
*/
-// Variables
-const LOCATION = "http://localhost:8080";
-const MAX_TIMEOUT = 20000;
-
-describe("Desktop testing", () => {
- beforeAll(async () => {
- await page.goto(LOCATION);
- }, MAX_TIMEOUT);
-
- it("navigates to the task", async () => {
- await expect(page.title()).resolves.toMatch("RDK Task");
- });
-});