diff --git a/.changeset/brave-lions-arrive.md b/.changeset/brave-lions-arrive.md new file mode 100644 index 0000000..00a556a --- /dev/null +++ b/.changeset/brave-lions-arrive.md @@ -0,0 +1,5 @@ +--- +"@jspsych/plugin-emoji-screen": minor +--- + +Added a button that the user can click to toggle between placing emojis or vowels on the screen diff --git a/.changeset/thirty-onions-eat.md b/.changeset/thirty-onions-eat.md new file mode 100644 index 0000000..00334d6 --- /dev/null +++ b/.changeset/thirty-onions-eat.md @@ -0,0 +1,5 @@ +--- +"@jspsych/plugin-emoji-screen": minor +--- + +Added a button to toggle between placing emojis and vowels on the screen when the user clicks on the canvas diff --git a/packages/plugin-emoji-screen/examples/index.html b/packages/plugin-emoji-screen/examples/index.html index d540376..b5bca36 100644 --- a/packages/plugin-emoji-screen/examples/index.html +++ b/packages/plugin-emoji-screen/examples/index.html @@ -15,9 +15,8 @@ var trial = { type: jsPsychEmojiScreen, - emojis: ["😀", "🎉", "⭐", "🌟", "🎈", "🦄", "🍕", "🎸"], key_to_finish: "Enter", - prompt: "

Click anywhere to place emojis. Press Enter when you are done.

", + prompt: "

Click anywhere to place emojis. Use the button to switch to vowels. Press Enter when done.

", }; jsPsych.run([trial]); diff --git a/packages/plugin-emoji-screen/src/index.ts b/packages/plugin-emoji-screen/src/index.ts index 87d2050..ab5602c 100644 --- a/packages/plugin-emoji-screen/src/index.ts +++ b/packages/plugin-emoji-screen/src/index.ts @@ -2,11 +2,12 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; import { version } from "../package.json"; -/** Represents a single placed emoji with its screen coordinates. */ -interface EmojiLocation { +/** Represents a single placed symbol with its screen coordinates. */ +interface SymbolLocation { x: number; y: number; - emoji: string; + symbol: string; + mode: "emoji" | "vowel"; } const info = { @@ -19,6 +20,17 @@ const info = { array: true, default: ["😀", "🎉", "⭐", "🌟", "🎈", "🦄", "🍕", "🎸"], }, + /** The pool of vowels to randomly select from when the participant clicks. */ + vowels: { + type: ParameterType.STRING, + array: true, + default: ["a", "e", "i", "o", "u"], + }, + /** The mode of the trial. Valid values are "emoji" or "vowel". */ + mode: { + type: ParameterType.STRING, + default: "emoji", + }, /** The key that ends the trial and records the final emoji arrangement. */ key_to_finish: { type: ParameterType.KEY, @@ -31,8 +43,8 @@ const info = { }, }, data: { - /** Array of objects describing each placed emoji: {x, y, emoji}. x and y are pixel coordinates relative to the canvas. */ - emoji_locations: { + /** Array of objects describing each placed symbol: {x, y, symbol, mode}. x and y are pixel coordinates relative to the canvas. */ + symbol_locations: { type: ParameterType.COMPLEX, array: true, }, @@ -68,7 +80,7 @@ class EmojiScreenPlugin implements JsPsychPlugin { constructor(private jsPsych: JsPsych) {} trial(display_element: HTMLElement, trial: TrialType) { - const emoji_locations: EmojiLocation[] = []; + const symbol_locations: SymbolLocation[] = []; const start_time = performance.now(); // Build the trial layout using DOM APIs @@ -76,9 +88,10 @@ class EmojiScreenPlugin implements JsPsychPlugin { wrapper.id = "jspsych-emoji-screen-wrapper"; wrapper.style.cssText = "position: relative; width: 100%; min-height: 400px;"; + const promptDiv = document.createElement("div"); + promptDiv.id = "jspsych-emoji-screen-prompt"; + if (trial.prompt !== null) { - const promptDiv = document.createElement("div"); - promptDiv.id = "jspsych-emoji-screen-prompt"; promptDiv.innerHTML = trial.prompt; wrapper.appendChild(promptDiv); } @@ -89,6 +102,19 @@ class EmojiScreenPlugin implements JsPsychPlugin { "position: relative; width: 100%; height: 400px; border: 2px solid #ccc; cursor: crosshair; overflow: hidden;"; wrapper.appendChild(canvas); + const modeButton = document.createElement("button"); + let currentMode: "emoji" | "vowel" = trial.mode as "emoji" | "vowel"; + modeButton.textContent = currentMode === "emoji" ? "Switch from Emojis to Vowels" : "Switch from Vowels to Emojis"; + modeButton.addEventListener("click", () => { + event.stopPropagation(); // Prevent the click from placing an emoji when toggling modes + currentMode = currentMode === "emoji" ? "vowel" : "emoji"; + modeButton.textContent = currentMode === "emoji" ? "Switch from Emojis to Vowels" : "Switch from Vowels to Emojis"; + promptDiv.innerHTML = currentMode === "emoji" + ? "

Click anywhere to place emojis. Use the button to switch to vowels. Press Enter when done.

" + : "

Click anywhere to place vowels. Use the button to switch to emojis. Press Enter when done.

"; + }); + wrapper.appendChild(modeButton); + display_element.appendChild(wrapper); // Handle clicks on the canvas to place emojis @@ -98,23 +124,30 @@ class EmojiScreenPlugin implements JsPsychPlugin { const y = Math.round(event.clientY - rect.top); // Pick a random emoji from the pool - const emoji = trial.emojis[Math.floor(Math.random() * trial.emojis.length)]; + let symbol; + if (currentMode === "emoji") { + const emoji = trial.emojis[Math.floor(Math.random() * trial.emojis.length)]; + symbol = emoji; + } else { + const vowel = trial.vowels[Math.floor(Math.random() * trial.vowels.length)]; + symbol = vowel; + } // Place the emoji at the click location const emojiEl = document.createElement("span"); - emojiEl.textContent = emoji; + emojiEl.textContent = symbol; emojiEl.style.position = "absolute"; emojiEl.style.left = `${x}px`; emojiEl.style.top = `${y}px`; emojiEl.style.fontSize = "2rem"; emojiEl.style.transform = "translate(-50%, -50%)"; emojiEl.style.userSelect = "none"; - emojiEl.setAttribute("data-emoji", emoji); + emojiEl.setAttribute("data-symbol", symbol); emojiEl.setAttribute("data-x", x.toString()); emojiEl.setAttribute("data-y", y.toString()); canvas.appendChild(emojiEl); - emoji_locations.push({ x, y, emoji }); + symbol_locations.push({ x, y, symbol, mode: currentMode }); }); // End trial function @@ -126,7 +159,7 @@ class EmojiScreenPlugin implements JsPsychPlugin { display_element.innerHTML = ""; this.jsPsych.finishTrial({ - emoji_locations: emoji_locations, + symbol_locations: symbol_locations, key_pressed: key, rt: rt, }); @@ -159,18 +192,20 @@ class EmojiScreenPlugin implements JsPsychPlugin { } private create_simulation_data(trial: TrialType, simulation_options) { - const num_clicks = this.jsPsych.randomization.randomInt(1, 5); - const simulated_locations: EmojiLocation[] = []; - for (let i = 0; i < num_clicks; i++) { - simulated_locations.push({ - x: this.jsPsych.randomization.randomInt(0, 800), - y: this.jsPsych.randomization.randomInt(0, 400), - emoji: trial.emojis[this.jsPsych.randomization.randomInt(0, trial.emojis.length - 1)], - }); + const simulated_locations: SymbolLocation[] = []; + const num_symbols = this.jsPsych.randomization.sampleWithoutReplacement([1, 2, 3, 4, 5], 1)[0]; + for (let i = 0; i < num_symbols; i++) { + const x = this.jsPsych.randomization.randomInt(0, 800); + const y = this.jsPsych.randomization.randomInt(0, 400); + const current_mode = this.jsPsych.randomization.sampleWithoutReplacement(["emoji", "vowel"], 1)[0] as "emoji" | "vowel"; + const symbol = current_mode === "emoji" + ? trial.emojis[Math.floor(Math.random() * trial.emojis.length)] + : trial.vowels[Math.floor(Math.random() * trial.vowels.length)]; + simulated_locations.push({ x, y, symbol, mode: current_mode }); } const default_data = { - emoji_locations: simulated_locations, + symbol_locations: simulated_locations, key_pressed: trial.key_to_finish, rt: this.jsPsych.randomization.sampleExGaussian(2000, 200, 1 / 500, true), }; @@ -194,8 +229,14 @@ class EmojiScreenPlugin implements JsPsychPlugin { // Simulate clicks to place emojis const canvas = display_element.querySelector("#jspsych-emoji-screen-canvas"); if (canvas) { - for (const loc of data.emoji_locations) { + const modeBtn = display_element.querySelector("button"); + let simulatedMode = trial.mode as "emoji" | "vowel"; + for (const loc of data.symbol_locations) { const rect = canvas.getBoundingClientRect(); + if (loc.mode !== simulatedMode) { + modeBtn?.click(); + simulatedMode = loc.mode; + } const clickEvent = new MouseEvent("click", { clientX: rect.left + loc.x, clientY: rect.top + loc.y,