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 += `
`; + 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 + * 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 = ` + + + ${[1, 2, 3, 4].map((index) => { + const centerX = 16 * index; + const centerY = 20; + const radius = 6; + const isHighlighted = index === validIndex; + + return ` + + `; + }).join('')} + + `; + + 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 = ` + + + ${key} + + `; + + 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"); - }); -});