From 96903e0266d47727c9be617078169ee946a35497 Mon Sep 17 00:00:00 2001 From: RebeccaHirst <30597180+RebeccaHirst@users.noreply.github.com> Date: Thu, 6 Jan 2022 16:39:08 +0000 Subject: [PATCH 01/41] add linspace to util.js --- src/util/Util.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/util/Util.js b/src/util/Util.js index 2e01674..3d93bbf 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -358,6 +358,24 @@ export function shuffle(array, randomNumberGenerator = undefined) return array; } +/** + * linspace + * + * @name module:util.linspace + * @function + * @public + * @param {Object[]} startValue, stopValue, cardinality + * @return {Object[]} an array from startValue to stopValue with cardinality steps + */ +export function linspace(startValue, stopValue, cardinality) { + var arr = []; + var step = (stopValue - startValue) / (cardinality - 1); + for (var i = 0; i < cardinality; i++) { + arr.push(startValue + (step * i)); + } + return arr; +} + /** * Pick a random value from an array, uses `util.shuffle` to shuffle the array and returns the last value. * From 04669d28b3d484d2f016f1372f96d69851c6b747 Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Thu, 14 Jul 2022 11:08:53 +0100 Subject: [PATCH 02/41] NF: Add "arrow" as recognised named shape in ShapeStim --- src/visual/ShapeStim.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/visual/ShapeStim.js b/src/visual/ShapeStim.js index cb74cef..3439b70 100644 --- a/src/visual/ShapeStim.js +++ b/src/visual/ShapeStim.js @@ -371,4 +371,14 @@ ShapeStim.KnownShapes = { [-0.39, 0.31], [-0.09, 0.18], ], + + arrow: [ + [0.0, 0.5], + [-0.5, 0.0], + [-1/6, 0.0], + [-1/6, -0.5], + [1/6, -0.5], + [1/6, 0.0], + [0.5, 0.0], + ], }; From 11bddceb804517e4bf431a2a276ae854301d4855 Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Thu, 14 Jul 2022 11:09:12 +0100 Subject: [PATCH 03/41] ENH: Add other shapes from Python to ShapeStim --- src/visual/ShapeStim.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/visual/ShapeStim.js b/src/visual/ShapeStim.js index 3439b70..b5925e3 100644 --- a/src/visual/ShapeStim.js +++ b/src/visual/ShapeStim.js @@ -372,6 +372,19 @@ ShapeStim.KnownShapes = { [-0.09, 0.18], ], + triangle: [ + [+0.0, 0.5], // Point + [-0.5, -0.5], // Bottom left + [+0.5, -0.5], // Bottom right + ], + + rectangle: [ + [-.5, .5], // Top left + [ .5, .5], // Top right + [ .5, -.5], // Bottom left + [-.5, -.5], // Bottom right + ], + arrow: [ [0.0, 0.5], [-0.5, 0.0], From 858bbb067c4a4bbf34e0bd8b64484f8b3efa30c7 Mon Sep 17 00:00:00 2001 From: Alain Pitiot Date: Mon, 12 Sep 2022 13:20:16 +0200 Subject: [PATCH 04/41] improved survey stimulus --- docs/AudioClip.html | 2 +- docs/AudioClipPlayer.html | 2 +- docs/BuilderKeyResponse.html | 2 +- docs/ButtonStim.html | 38 +-- docs/Clock.html | 2 +- docs/Color.html | 2 +- docs/CountdownTimer.html | 2 +- docs/EventEmitter.html | 2 +- docs/EventManager.html | 2 +- docs/ExperimentHandler.html | 2 +- docs/FaceDetector.html | 2 +- docs/Form.html | 2 +- docs/GUI.html | 2 +- docs/GratingStim.html | 30 +- docs/ImageStim.html | 2 +- docs/KeyPress.html | 2 +- docs/Keyboard.html | 2 +- docs/Logger.html | 2 +- docs/Microphone.html | 2 +- docs/MinimalStim.html | 2 +- docs/MonotonicClock.html | 2 +- docs/Mouse.html | 2 +- docs/MovieStim.html | 2 +- docs/MultiStairHandler.html | 2 +- docs/Polygon.html | 2 +- docs/QuestHandler.html | 2 +- docs/Rect.html | 2 +- docs/Scheduler.html | 2 +- docs/ServerManager.html | 2 +- docs/ShapeStim.html | 2 +- docs/Shelf.html | 2 +- docs/Slider.html | 38 +-- docs/SoundPlayer.html | 2 +- docs/SpeechRecognition.html | 2 +- docs/TextBox.html | 42 +-- docs/TextStim.html | 2 +- docs/TonePlayer.html | 2 +- docs/TrackPlayer.html | 2 +- docs/Transcript.html | 2 +- docs/Window.html | 2 +- docs/core_EventManager.js.html | 2 +- docs/core_GUI.js.html | 2 +- docs/core_Keyboard.js.html | 2 +- docs/core_Logger.js.html | 2 +- docs/core_MinimalStim.js.html | 2 +- docs/core_Mouse.js.html | 2 +- docs/core_PsychoJS.js.html | 2 +- docs/core_ServerManager.js.html | 2 +- docs/core_Window.js.html | 2 +- docs/core_WindowMixin.js.html | 2 +- docs/data_ExperimentHandler.js.html | 2 +- docs/data_MultiStairHandler.js.html | 2 +- docs/data_QuestHandler.js.html | 2 +- docs/data_Shelf.js.html | 2 +- docs/data_TrialHandler.js.html | 2 +- docs/hardware_Camera.js.html | 2 +- docs/index.html | 2 +- docs/module-core.PsychoJS.html | 2 +- docs/module-core.WindowMixin.html | 2 +- docs/module-core.html | 2 +- docs/module-data.TrialHandler.html | 2 +- docs/module-data.html | 2 +- docs/module-hardware.Camera.html | 2 +- docs/module-sound.Sound.html | 2 +- docs/module-sound.html | 2 +- docs/module-util.ColorMixin.html | 2 +- docs/module-util.PsychObject.html | 2 +- docs/module-util.html | 2 +- docs/module-visual.VisualStim.html | 2 +- docs/module-visual.html | 2 +- docs/sound_AudioClip.js.html | 2 +- docs/sound_AudioClipPlayer.js.html | 2 +- docs/sound_Microphone.js.html | 2 +- docs/sound_Sound.js.html | 2 +- docs/sound_SoundPlayer.js.html | 2 +- docs/sound_SpeechRecognition.js.html | 2 +- docs/sound_TonePlayer.js.html | 2 +- docs/sound_TrackPlayer.js.html | 2 +- docs/styles/jsdoc.css | 16 +- docs/util_Clock.js.html | 2 +- docs/util_Color.js.html | 2 +- docs/util_ColorMixin.js.html | 2 +- docs/util_EventEmitter.js.html | 2 +- docs/util_Pixi.js.html | 2 +- docs/util_PsychObject.js.html | 2 +- docs/util_Scheduler.js.html | 2 +- docs/util_Util.js.html | 2 +- docs/visual_ButtonStim.js.html | 2 +- docs/visual_FaceDetector.js.html | 2 +- docs/visual_Form.js.html | 2 +- docs/visual_GratingStim.js.html | 15 +- docs/visual_ImageStim.js.html | 2 +- docs/visual_MovieStim.js.html | 2 +- docs/visual_Polygon.js.html | 2 +- docs/visual_Rect.js.html | 2 +- docs/visual_ShapeStim.js.html | 2 +- docs/visual_Slider.js.html | 21 +- docs/visual_TextBox.js.html | 35 ++- docs/visual_TextStim.js.html | 2 +- docs/visual_VisualStim.js.html | 2 +- package.json | 2 +- src/core/ServerManager.js | 173 ++++++++++- src/util/Util.js | 26 +- src/visual/Survey.js | 431 +++++++++++++++++++++++++++ src/visual/index.js | 1 + 105 files changed, 838 insertions(+), 214 deletions(-) create mode 100644 src/visual/Survey.js diff --git a/docs/AudioClip.html b/docs/AudioClip.html index 0ed740c..ab8a897 100644 --- a/docs/AudioClip.html +++ b/docs/AudioClip.html @@ -2117,7 +2117,7 @@ transcription confidence


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/AudioClipPlayer.html b/docs/AudioClipPlayer.html index 6c8d83c..efe2159 100644 --- a/docs/AudioClipPlayer.html +++ b/docs/AudioClipPlayer.html @@ -1657,7 +1657,7 @@
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/BuilderKeyResponse.html b/docs/BuilderKeyResponse.html index 84b980d..e6f633a 100644 --- a/docs/BuilderKeyResponse.html +++ b/docs/BuilderKeyResponse.html @@ -291,7 +291,7 @@
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/ButtonStim.html b/docs/ButtonStim.html index c803113..8f5b513 100644 --- a/docs/ButtonStim.html +++ b/docs/ButtonStim.html @@ -1062,7 +1062,7 @@
Source:
@@ -1154,7 +1154,7 @@
Source:
@@ -1246,7 +1246,7 @@
Source:
@@ -1362,7 +1362,7 @@
Source:
@@ -1478,7 +1478,7 @@
Source:
@@ -1570,7 +1570,7 @@
Source:
@@ -1761,7 +1761,7 @@
Source:
@@ -2081,7 +2081,7 @@ - left + center @@ -2167,7 +2167,7 @@
Source:
@@ -2365,7 +2365,7 @@
Source:
@@ -2561,7 +2561,7 @@
Source:
@@ -2757,7 +2757,7 @@
Source:
@@ -2953,7 +2953,7 @@
Source:
@@ -3149,7 +3149,7 @@
Source:
@@ -3347,7 +3347,7 @@
Source:
@@ -3545,7 +3545,7 @@
Source:
@@ -3745,7 +3745,7 @@
Source:
@@ -3941,7 +3941,7 @@
Source:
@@ -4085,7 +4085,7 @@
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/Clock.html b/docs/Clock.html index 2685592..fd73b74 100644 --- a/docs/Clock.html +++ b/docs/Clock.html @@ -737,7 +737,7 @@ smaller). As a consequence, getTime() may return a negative number.


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/Color.html b/docs/Color.html index d111e6e..59c3370 100644 --- a/docs/Color.html +++ b/docs/Color.html @@ -6491,7 +6491,7 @@ static methods for converting colors from one space to another.


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/CountdownTimer.html b/docs/CountdownTimer.html index 596e88c..c9ec6ef 100644 --- a/docs/CountdownTimer.html +++ b/docs/CountdownTimer.html @@ -809,7 +809,7 @@ to newTime


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/EventEmitter.html b/docs/EventEmitter.html index c138eec..c7cca15 100644 --- a/docs/EventEmitter.html +++ b/docs/EventEmitter.html @@ -880,7 +880,7 @@ observable.emit("change", { a: 1 });
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/EventManager.html b/docs/EventManager.html index d9ec311..216a0e1 100644 --- a/docs/EventManager.html +++ b/docs/EventManager.html @@ -2371,7 +2371,7 @@ Right, or between Enter and Numpad Enter. Use at your own risk (or upgrade your
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/ExperimentHandler.html b/docs/ExperimentHandler.html index 798c243..0e77cd7 100644 --- a/docs/ExperimentHandler.html +++ b/docs/ExperimentHandler.html @@ -1995,7 +1995,7 @@ will be associated with the next trial.


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/FaceDetector.html b/docs/FaceDetector.html index 19a3d73..415f795 100644 --- a/docs/FaceDetector.html +++ b/docs/FaceDetector.html @@ -1729,7 +1729,7 @@ movie resource or a HTMLVideoElement or a Camera component


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/Form.html b/docs/Form.html index 8a19b90..08963fa 100644 --- a/docs/Form.html +++ b/docs/Form.html @@ -4654,7 +4654,7 @@ the bounding box


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/GUI.html b/docs/GUI.html index aebf661..c1386d9 100644 --- a/docs/GUI.html +++ b/docs/GUI.html @@ -2402,7 +2402,7 @@ of session.


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/GratingStim.html b/docs/GratingStim.html index 602e4e2..032ef7b 100644 --- a/docs/GratingStim.html +++ b/docs/GratingStim.html @@ -85,7 +85,7 @@
Source:
@@ -975,7 +975,7 @@
Source:
@@ -1062,7 +1062,7 @@
Source:
@@ -1172,7 +1172,7 @@ it contains.

Source:
@@ -1353,7 +1353,7 @@ it contains.

Source:
@@ -1440,7 +1440,7 @@ it contains.

Source:
@@ -1633,7 +1633,7 @@ it contains.

Source:
@@ -1826,7 +1826,7 @@ it contains.

Source:
@@ -2019,7 +2019,7 @@ it contains.

Source:
@@ -2212,7 +2212,7 @@ it contains.

Source:
@@ -2406,7 +2406,7 @@ it contains.

Source:
@@ -2601,7 +2601,7 @@ it contains.

Source:
@@ -2792,7 +2792,7 @@ it contains.

Source:
@@ -2983,7 +2983,7 @@ it contains.

Source:
@@ -3180,7 +3180,7 @@ it contains.


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/ImageStim.html b/docs/ImageStim.html index e992fd4..43fe403 100644 --- a/docs/ImageStim.html +++ b/docs/ImageStim.html @@ -1884,7 +1884,7 @@ it contains.


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/KeyPress.html b/docs/KeyPress.html index d6ee0b2..6f2ad69 100644 --- a/docs/KeyPress.html +++ b/docs/KeyPress.html @@ -286,7 +286,7 @@
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/Keyboard.html b/docs/Keyboard.html index 3ea6f5d..37c873d 100644 --- a/docs/Keyboard.html +++ b/docs/Keyboard.html @@ -1778,7 +1778,7 @@ waitRelease = false, key presses without a corresponding key release will have a
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/Logger.html b/docs/Logger.html index cd40f2f..3c1126c 100644 --- a/docs/Logger.html +++ b/docs/Logger.html @@ -2162,7 +2162,7 @@ See https://github.com/nodeca/pako for details.


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/Microphone.html b/docs/Microphone.html index 6a807aa..9fa12a3 100644 --- a/docs/Microphone.html +++ b/docs/Microphone.html @@ -1931,7 +1931,7 @@ data was made available


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/MinimalStim.html b/docs/MinimalStim.html index f4dd821..be8dc29 100644 --- a/docs/MinimalStim.html +++ b/docs/MinimalStim.html @@ -1198,7 +1198,7 @@
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/MonotonicClock.html b/docs/MonotonicClock.html index 0ebd80e..51ae4ed 100644 --- a/docs/MonotonicClock.html +++ b/docs/MonotonicClock.html @@ -972,7 +972,7 @@
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/Mouse.html b/docs/Mouse.html index 7c2923a..dcba2bc 100644 --- a/docs/Mouse.html +++ b/docs/Mouse.html @@ -1742,7 +1742,7 @@ call to getPos
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/MovieStim.html b/docs/MovieStim.html index 3046a12..7fa0abc 100644 --- a/docs/MovieStim.html +++ b/docs/MovieStim.html @@ -2348,7 +2348,7 @@ movie resource or of a HTMLVideoElement or of a Camera component


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/MultiStairHandler.html b/docs/MultiStairHandler.html index 137f7ad..7e3b581 100644 --- a/docs/MultiStairHandler.html +++ b/docs/MultiStairHandler.html @@ -1395,7 +1395,7 @@ we treat it as the name of a conditions resource


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/Polygon.html b/docs/Polygon.html index fe2dfb5..8f6b4e4 100644 --- a/docs/Polygon.html +++ b/docs/Polygon.html @@ -2110,7 +2110,7 @@
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/QuestHandler.html b/docs/QuestHandler.html index 453461e..5d843f7 100644 --- a/docs/QuestHandler.html +++ b/docs/QuestHandler.html @@ -2247,7 +2247,7 @@ and on the selected QUEST method.


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/Rect.html b/docs/Rect.html index d803f78..b71fe10 100644 --- a/docs/Rect.html +++ b/docs/Rect.html @@ -2199,7 +2199,7 @@
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/Scheduler.html b/docs/Scheduler.html index c7a90d7..6efc906 100644 --- a/docs/Scheduler.html +++ b/docs/Scheduler.html @@ -1463,7 +1463,7 @@ task would be by calling scheduler.add(subSch
diff --git a/docs/ServerManager.html b/docs/ServerManager.html index 7f35f0d..68ff35b 100644 --- a/docs/ServerManager.html +++ b/docs/ServerManager.html @@ -4656,7 +4656,7 @@ before returning


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/ShapeStim.html b/docs/ShapeStim.html index c85946e..be13260 100644 --- a/docs/ShapeStim.html +++ b/docs/ShapeStim.html @@ -1765,7 +1765,7 @@
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/Shelf.html b/docs/Shelf.html index 88566a8..43857c5 100644 --- a/docs/Shelf.html +++ b/docs/Shelf.html @@ -6999,7 +6999,7 @@ but it is locked or it is not of type LIST


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/Slider.html b/docs/Slider.html index 781b55f..1cbf4cd 100644 --- a/docs/Slider.html +++ b/docs/Slider.html @@ -1217,7 +1217,7 @@ which must be updated when this Slider is updated, e.g. a Form.

Source:
@@ -1411,7 +1411,7 @@ which must be updated when this Slider is updated, e.g. a Form.

Source:
@@ -1582,7 +1582,7 @@ which must be updated when this Slider is updated, e.g. a Form.

Source:
@@ -1958,7 +1958,7 @@ which must be updated when this Slider is updated, e.g. a Form.

Source:
@@ -2362,7 +2362,7 @@ which must be updated when this Slider is updated, e.g. a Form.

Source:
@@ -2449,7 +2449,7 @@ which must be updated when this Slider is updated, e.g. a Form.

Source:
@@ -2694,7 +2694,7 @@ which must be updated when this Slider is updated, e.g. a Form.

Source:
@@ -2781,7 +2781,7 @@ which must be updated when this Slider is updated, e.g. a Form.

Source:
@@ -2868,7 +2868,7 @@ which must be updated when this Slider is updated, e.g. a Form.

Source:
@@ -3200,7 +3200,7 @@ the bounding box

Source:
@@ -3358,7 +3358,7 @@ the bounding box

Source:
@@ -3517,7 +3517,7 @@ with 0 at the center of the Slider)

Source:
@@ -3691,7 +3691,7 @@ with 0 at the center of the Slider)

Source:
@@ -3778,7 +3778,7 @@ with 0 at the center of the Slider)

Source:
@@ -3865,7 +3865,7 @@ with 0 at the center of the Slider)

Source:
@@ -3952,7 +3952,7 @@ with 0 at the center of the Slider)

Source:
@@ -4039,7 +4039,7 @@ with 0 at the center of the Slider)

Source:
@@ -4126,7 +4126,7 @@ with 0 at the center of the Slider)

Source:
@@ -7154,7 +7154,7 @@ but does not change the actual rating returned by the slider.


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/SoundPlayer.html b/docs/SoundPlayer.html index 716df83..d39de67 100644 --- a/docs/SoundPlayer.html +++ b/docs/SoundPlayer.html @@ -1064,7 +1064,7 @@
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/SpeechRecognition.html b/docs/SpeechRecognition.html index 8bd9a3a..41c3baf 100644 --- a/docs/SpeechRecognition.html +++ b/docs/SpeechRecognition.html @@ -1421,7 +1421,7 @@ previously cleared by calls to getTranscripts with clear = true.


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/TextBox.html b/docs/TextBox.html index c3a61a6..f52597d 100644 --- a/docs/TextBox.html +++ b/docs/TextBox.html @@ -1279,7 +1279,7 @@
Source:
@@ -1342,7 +1342,7 @@
Source:
@@ -1415,7 +1415,7 @@
Source:
@@ -1502,7 +1502,7 @@
Source:
@@ -1589,7 +1589,7 @@
Source:
@@ -1700,7 +1700,7 @@
Source:
@@ -1811,7 +1811,7 @@
Source:
@@ -1898,7 +1898,7 @@
Source:
@@ -2079,7 +2079,7 @@
Source:
@@ -2384,7 +2384,7 @@ - left + center @@ -2470,7 +2470,7 @@
Source:
@@ -2663,7 +2663,7 @@
Source:
@@ -2854,7 +2854,7 @@
Source:
@@ -3045,7 +3045,7 @@
Source:
@@ -3236,7 +3236,7 @@
Source:
@@ -3427,7 +3427,7 @@
Source:
@@ -3620,7 +3620,7 @@
Source:
@@ -3813,7 +3813,7 @@
Source:
@@ -4008,7 +4008,7 @@
Source:
@@ -4199,7 +4199,7 @@
Source:
@@ -4338,7 +4338,7 @@
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:50 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/TextStim.html b/docs/TextStim.html index b77c618..215fc66 100644 --- a/docs/TextStim.html +++ b/docs/TextStim.html @@ -2281,7 +2281,7 @@ to be instantiated, unlike getSize().


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:50 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/TonePlayer.html b/docs/TonePlayer.html index 94dc9ce..6a14583 100644 --- a/docs/TonePlayer.html +++ b/docs/TonePlayer.html @@ -1642,7 +1642,7 @@ we throw an exception.


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:50 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/TrackPlayer.html b/docs/TrackPlayer.html index 952f3d0..3088bf4 100644 --- a/docs/TrackPlayer.html +++ b/docs/TrackPlayer.html @@ -1667,7 +1667,7 @@ file


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:50 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/Transcript.html b/docs/Transcript.html index b196e51..c97a1b6 100644 --- a/docs/Transcript.html +++ b/docs/Transcript.html @@ -308,7 +308,7 @@
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:50 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/Window.html b/docs/Window.html index abbc7eb..3cedd7b 100644 --- a/docs/Window.html +++ b/docs/Window.html @@ -2312,7 +2312,7 @@ Window.


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:50 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/core_EventManager.js.html b/docs/core_EventManager.js.html index 124bf9d..a176dfc 100644 --- a/docs/core_EventManager.js.html +++ b/docs/core_EventManager.js.html @@ -665,7 +665,7 @@ export class BuilderKeyResponse
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/core_GUI.js.html b/docs/core_GUI.js.html index 6d863b1..98e5325 100644 --- a/docs/core_GUI.js.html +++ b/docs/core_GUI.js.html @@ -838,7 +838,7 @@ export class GUI
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/core_Keyboard.js.html b/docs/core_Keyboard.js.html index 0b59d26..96d7d46 100644 --- a/docs/core_Keyboard.js.html +++ b/docs/core_Keyboard.js.html @@ -507,7 +507,7 @@ Keyboard.KeyStatus = {
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/core_Logger.js.html b/docs/core_Logger.js.html index e4989fd..9256721 100644 --- a/docs/core_Logger.js.html +++ b/docs/core_Logger.js.html @@ -470,7 +470,7 @@ Logger._ServerLevelValue = {
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/core_MinimalStim.js.html b/docs/core_MinimalStim.js.html index cd401da..6544b11 100644 --- a/docs/core_MinimalStim.js.html +++ b/docs/core_MinimalStim.js.html @@ -253,7 +253,7 @@ export class MinimalStim extends PsychObject
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/core_Mouse.js.html b/docs/core_Mouse.js.html index b5a8362..3b9cabd 100644 --- a/docs/core_Mouse.js.html +++ b/docs/core_Mouse.js.html @@ -388,7 +388,7 @@ export class Mouse extends PsychObject
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/core_PsychoJS.js.html b/docs/core_PsychoJS.js.html index de12cdf..413033c 100644 --- a/docs/core_PsychoJS.js.html +++ b/docs/core_PsychoJS.js.html @@ -842,7 +842,7 @@ PsychoJS.Status = {
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/core_ServerManager.js.html b/docs/core_ServerManager.js.html index 916654e..6681b71 100644 --- a/docs/core_ServerManager.js.html +++ b/docs/core_ServerManager.js.html @@ -1521,7 +1521,7 @@ ServerManager.ResourceStatus = {
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/core_Window.js.html b/docs/core_Window.js.html index 267b4a4..b3c6b26 100644 --- a/docs/core_Window.js.html +++ b/docs/core_Window.js.html @@ -591,7 +591,7 @@ export class Window extends PsychObject
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/core_WindowMixin.js.html b/docs/core_WindowMixin.js.html index 047687f..5ac14b9 100644 --- a/docs/core_WindowMixin.js.html +++ b/docs/core_WindowMixin.js.html @@ -252,7 +252,7 @@ export let WindowMixin = (superclass) =>
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/data_ExperimentHandler.js.html b/docs/data_ExperimentHandler.js.html index 6a59258..0ed0b69 100644 --- a/docs/data_ExperimentHandler.js.html +++ b/docs/data_ExperimentHandler.js.html @@ -505,7 +505,7 @@ ExperimentHandler.Environment = {
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/data_MultiStairHandler.js.html b/docs/data_MultiStairHandler.js.html index f77f3b8..9fbad0e 100644 --- a/docs/data_MultiStairHandler.js.html +++ b/docs/data_MultiStairHandler.js.html @@ -499,7 +499,7 @@ MultiStairHandler.StaircaseStatus = {
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/data_QuestHandler.js.html b/docs/data_QuestHandler.js.html index 56a736f..16e58a3 100644 --- a/docs/data_QuestHandler.js.html +++ b/docs/data_QuestHandler.js.html @@ -433,7 +433,7 @@ QuestHandler.Method = {
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/data_Shelf.js.html b/docs/data_Shelf.js.html index 0d58bac..b032519 100644 --- a/docs/data_Shelf.js.html +++ b/docs/data_Shelf.js.html @@ -896,7 +896,7 @@ Shelf.Type = {
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/data_TrialHandler.js.html b/docs/data_TrialHandler.js.html index df179b4..3695887 100644 --- a/docs/data_TrialHandler.js.html +++ b/docs/data_TrialHandler.js.html @@ -803,7 +803,7 @@ TrialHandler.Method = {
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/hardware_Camera.js.html b/docs/hardware_Camera.js.html index 917b8f8..cd01b60 100644 --- a/docs/hardware_Camera.js.html +++ b/docs/hardware_Camera.js.html @@ -709,7 +709,7 @@ export class Camera extends PsychObject
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/index.html b/docs/index.html index 601a1ba..0ffe5df 100644 --- a/docs/index.html +++ b/docs/index.html @@ -126,7 +126,7 @@ It is now a collaborative effort, supported by the diff --git a/docs/module-core.PsychoJS.html b/docs/module-core.PsychoJS.html index 23613cc..db16989 100644 --- a/docs/module-core.PsychoJS.html +++ b/docs/module-core.PsychoJS.html @@ -2981,7 +2981,7 @@ considered.


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/module-core.WindowMixin.html b/docs/module-core.WindowMixin.html index 66f3471..208483e 100644 --- a/docs/module-core.WindowMixin.html +++ b/docs/module-core.WindowMixin.html @@ -854,7 +854,7 @@
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/module-core.html b/docs/module-core.html index 8c5e019..7101249 100644 --- a/docs/module-core.html +++ b/docs/module-core.html @@ -141,7 +141,7 @@
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/module-data.TrialHandler.html b/docs/module-data.TrialHandler.html index 873316f..eca4204 100644 --- a/docs/module-data.TrialHandler.html +++ b/docs/module-data.TrialHandler.html @@ -3383,7 +3383,7 @@ for (const thisTrial of handler) { console.log(thisTrial); }

- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/module-data.html b/docs/module-data.html index fe7dae1..f1515a5 100644 --- a/docs/module-data.html +++ b/docs/module-data.html @@ -497,7 +497,7 @@
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/module-hardware.Camera.html b/docs/module-hardware.Camera.html index df6db87..ad8d4c8 100644 --- a/docs/module-hardware.Camera.html +++ b/docs/module-hardware.Camera.html @@ -2516,7 +2516,7 @@ data was made available


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/module-sound.Sound.html b/docs/module-sound.Sound.html index 7d14cba..336658c 100644 --- a/docs/module-sound.Sound.html +++ b/docs/module-sound.Sound.html @@ -2180,7 +2180,7 @@ Repeat calls to play may results in the sounds being played on top of each other
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/module-sound.html b/docs/module-sound.html index 458c490..449daf5 100644 --- a/docs/module-sound.html +++ b/docs/module-sound.html @@ -132,7 +132,7 @@
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/module-util.ColorMixin.html b/docs/module-util.ColorMixin.html index 0c9fd81..bc34d76 100644 --- a/docs/module-util.ColorMixin.html +++ b/docs/module-util.ColorMixin.html @@ -703,7 +703,7 @@
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/module-util.PsychObject.html b/docs/module-util.PsychObject.html index 7254e49..adbe04d 100644 --- a/docs/module-util.PsychObject.html +++ b/docs/module-util.PsychObject.html @@ -1782,7 +1782,7 @@ was not previously set)


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/module-util.html b/docs/module-util.html index fa15676..9274f04 100644 --- a/docs/module-util.html +++ b/docs/module-util.html @@ -14778,7 +14778,7 @@ missing the naught prefix, and is able to process several arrays, e.g. "[1,
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/module-visual.VisualStim.html b/docs/module-visual.VisualStim.html index 0aeed49..11a4137 100644 --- a/docs/module-visual.VisualStim.html +++ b/docs/module-visual.VisualStim.html @@ -2807,7 +2807,7 @@ the bounding box


- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/module-visual.html b/docs/module-visual.html index 7d5ff05..d5c24eb 100644 --- a/docs/module-visual.html +++ b/docs/module-visual.html @@ -143,7 +143,7 @@
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:56 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/sound_AudioClip.js.html b/docs/sound_AudioClip.js.html index 3e33b85..22eb543 100644 --- a/docs/sound_AudioClip.js.html +++ b/docs/sound_AudioClip.js.html @@ -530,7 +530,7 @@ AudioClip.Status = {
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/sound_AudioClipPlayer.js.html b/docs/sound_AudioClipPlayer.js.html index 3d6ea7b..d60ed02 100644 --- a/docs/sound_AudioClipPlayer.js.html +++ b/docs/sound_AudioClipPlayer.js.html @@ -227,7 +227,7 @@ export class AudioClipPlayer extends SoundPlayer
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/sound_Microphone.js.html b/docs/sound_Microphone.js.html index 3e148a8..74ef762 100644 --- a/docs/sound_Microphone.js.html +++ b/docs/sound_Microphone.js.html @@ -553,7 +553,7 @@ export class Microphone extends PsychObject
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/sound_Sound.js.html b/docs/sound_Sound.js.html index 6d9e89a..a50d0ae 100644 --- a/docs/sound_Sound.js.html +++ b/docs/sound_Sound.js.html @@ -303,7 +303,7 @@ export class Sound extends PsychObject
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/sound_SoundPlayer.js.html b/docs/sound_SoundPlayer.js.html index f8754f5..c7137e0 100644 --- a/docs/sound_SoundPlayer.js.html +++ b/docs/sound_SoundPlayer.js.html @@ -196,7 +196,7 @@ export class SoundPlayer extends PsychObject
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/sound_SpeechRecognition.js.html b/docs/sound_SpeechRecognition.js.html index 1e1d99c..a5f4d74 100644 --- a/docs/sound_SpeechRecognition.js.html +++ b/docs/sound_SpeechRecognition.js.html @@ -445,7 +445,7 @@ export class SpeechRecognition extends PsychObject
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/sound_TonePlayer.js.html b/docs/sound_TonePlayer.js.html index 7b6b738..6efc911 100644 --- a/docs/sound_TonePlayer.js.html +++ b/docs/sound_TonePlayer.js.html @@ -418,7 +418,7 @@ TonePlayer.SoundLibrary = {
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/sound_TrackPlayer.js.html b/docs/sound_TrackPlayer.js.html index 078fe3a..ccf9deb 100644 --- a/docs/sound_TrackPlayer.js.html +++ b/docs/sound_TrackPlayer.js.html @@ -261,7 +261,7 @@ export class TrackPlayer extends SoundPlayer
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/styles/jsdoc.css b/docs/styles/jsdoc.css index ff0d5e5..eb45265 100644 --- a/docs/styles/jsdoc.css +++ b/docs/styles/jsdoc.css @@ -64,24 +64,24 @@ h1 { } h1.page-title { - font-size: 30px; + font-size: 48px; margin: 1em 30px; line-height: 100%; word-wrap: break-word; } h2 { - font-size: 20px; + font-size: 24px; margin: 1.5em 0 .3em; } h3 { - font-size: 20px; + font-size: 24px; margin: 1.2em 0 .3em; } h4 { - font-size: 16px; + font-size: 18px; margin: 1em 0 .2em; color: #4d4e53; } @@ -169,8 +169,8 @@ tt, code, kbd, samp { } .class-description { - /*font-size: 130%; - line-height: 140%;*/ + font-size: 130%; + line-height: 140%; margin-bottom: 1em; margin-top: 1em; } @@ -257,7 +257,7 @@ nav ul a:active { line-height: 18px; padding: 0; display: block; - font-size: 14px; + font-size: 12px; } nav a:hover, @@ -762,4 +762,4 @@ html[data-search-mode] .level-hide { font-weight: 300; font-style: normal; -} +} \ No newline at end of file diff --git a/docs/util_Clock.js.html b/docs/util_Clock.js.html index fb9aac7..32b518c 100644 --- a/docs/util_Clock.js.html +++ b/docs/util_Clock.js.html @@ -280,7 +280,7 @@ export class CountdownTimer extends Clock
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/util_Color.js.html b/docs/util_Color.js.html index 4f34f32..cd2e50c 100644 --- a/docs/util_Color.js.html +++ b/docs/util_Color.js.html @@ -706,7 +706,7 @@ Color.NAMED_COLORS = {
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/util_ColorMixin.js.html b/docs/util_ColorMixin.js.html index 1258bb5..c2dbac3 100644 --- a/docs/util_ColorMixin.js.html +++ b/docs/util_ColorMixin.js.html @@ -135,7 +135,7 @@ export let ColorMixin = (superclass) =>
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/util_EventEmitter.js.html b/docs/util_EventEmitter.js.html index 861298c..cb64774 100644 --- a/docs/util_EventEmitter.js.html +++ b/docs/util_EventEmitter.js.html @@ -200,7 +200,7 @@ export class EventEmitter
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/util_Pixi.js.html b/docs/util_Pixi.js.html index f990815..89af443 100644 --- a/docs/util_Pixi.js.html +++ b/docs/util_Pixi.js.html @@ -99,7 +99,7 @@ export function to_pixiPoint(pos, posUnit, win, integerCoordinates = false)
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/util_PsychObject.js.html b/docs/util_PsychObject.js.html index 8ada66f..9820216 100644 --- a/docs/util_PsychObject.js.html +++ b/docs/util_PsychObject.js.html @@ -462,7 +462,7 @@ export class PsychObject extends EventEmitter
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/util_Scheduler.js.html b/docs/util_Scheduler.js.html index fad2435..d90dd1b 100644 --- a/docs/util_Scheduler.js.html +++ b/docs/util_Scheduler.js.html @@ -354,7 +354,7 @@ Scheduler.Status = {
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/util_Util.js.html b/docs/util_Util.js.html index 0accf08..8331df3 100644 --- a/docs/util_Util.js.html +++ b/docs/util_Util.js.html @@ -1478,7 +1478,7 @@ export const TEXT_DIRECTION = {
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/visual_ButtonStim.js.html b/docs/visual_ButtonStim.js.html index 73e7e7f..4ed56fd 100644 --- a/docs/visual_ButtonStim.js.html +++ b/docs/visual_ButtonStim.js.html @@ -202,7 +202,7 @@ export class ButtonStim extends TextBox
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/visual_FaceDetector.js.html b/docs/visual_FaceDetector.js.html index 36612ac..e0815c9 100644 --- a/docs/visual_FaceDetector.js.html +++ b/docs/visual_FaceDetector.js.html @@ -363,7 +363,7 @@ export class FaceDetector extends VisualStim
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/visual_Form.js.html b/docs/visual_Form.js.html index 7372f63..e72e28c 100644 --- a/docs/visual_Form.js.html +++ b/docs/visual_Form.js.html @@ -1223,7 +1223,7 @@ Form._defaultItems = {
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/visual_GratingStim.js.html b/docs/visual_GratingStim.js.html index b8ae42c..f0d6c7f 100644 --- a/docs/visual_GratingStim.js.html +++ b/docs/visual_GratingStim.js.html @@ -78,6 +78,7 @@ import gaussShader from "./shaders/gaussShader.frag"; import crossShader from "./shaders/crossShader.frag"; import radRampShader from "./shaders/radRampShader.frag"; import raisedCosShader from "./shaders/raisedCosShader.frag"; +import radialStim from "./shaders/radialShader.frag"; /** * Grating Stimulus. @@ -292,6 +293,15 @@ export class GratingStim extends VisualStim uColor: [1., 1., 1.], uAlpha: 1.0 } + }, + radialStim: { + shader: radialStim, + uniforms: { + uFreq: 20.0, + uPhase: 0.0, + uColor: [1., 1., 1.], + uAlpha: 1.0 + } } }; @@ -769,7 +779,8 @@ export class GratingStim extends VisualStim const maskMesh = this._getPixiMeshFromPredefinedShaders(this._mask); const rt = PIXI.RenderTexture.create({ width: this._size_px[0], - height: this._size_px[1] + height: this._size_px[1], + scaleMode: this._interpolate ? PIXI.SCALE_MODES.LINEAR : PIXI.SCALE_MODES.NEAREST }); this.win._renderer.render(maskMesh, { renderTexture: rt @@ -824,7 +835,7 @@ export class GratingStim extends VisualStim
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/visual_ImageStim.js.html b/docs/visual_ImageStim.js.html index 7d1f306..1359b6b 100644 --- a/docs/visual_ImageStim.js.html +++ b/docs/visual_ImageStim.js.html @@ -453,7 +453,7 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/visual_MovieStim.js.html b/docs/visual_MovieStim.js.html index c2399eb..18cd734 100644 --- a/docs/visual_MovieStim.js.html +++ b/docs/visual_MovieStim.js.html @@ -500,7 +500,7 @@ export class MovieStim extends VisualStim
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/visual_Polygon.js.html b/docs/visual_Polygon.js.html index 1c2b350..68c8872 100644 --- a/docs/visual_Polygon.js.html +++ b/docs/visual_Polygon.js.html @@ -199,7 +199,7 @@ export class Polygon extends ShapeStim
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/visual_Rect.js.html b/docs/visual_Rect.js.html index 98e5070..b682c7f 100644 --- a/docs/visual_Rect.js.html +++ b/docs/visual_Rect.js.html @@ -201,7 +201,7 @@ export class Rect extends ShapeStim
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/visual_ShapeStim.js.html b/docs/visual_ShapeStim.js.html index ddab829..1071ab4 100644 --- a/docs/visual_ShapeStim.js.html +++ b/docs/visual_ShapeStim.js.html @@ -431,7 +431,7 @@ ShapeStim.KnownShapes = {
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/visual_Slider.js.html b/docs/visual_Slider.js.html index 203bae6..a739124 100644 --- a/docs/visual_Slider.js.html +++ b/docs/visual_Slider.js.html @@ -870,15 +870,17 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin) * @protected */ _handlePointerDown (e) { - if (e.data.button === 0) + if (e.data.pointerType === "mouse" && e.data.button !== 0) { - this._markerDragging = true; - if (!this._frozenMarker) - { - const mouseLocalPos_px = e.data.getLocalPosition(this._pixi); - const rating = this._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]); - this.setMarkerPos(rating); - } + return; + } + + this._markerDragging = true; + if (!this._frozenMarker) + { + const mouseLocalPos_px = e.data.getLocalPosition(this._pixi); + const rating = this._posToRating([mouseLocalPos_px.x, mouseLocalPos_px.y]); + this.setMarkerPos(rating); } e.stopPropagation(); @@ -1366,6 +1368,7 @@ export class Slider extends util.mix(VisualStim).with(ColorMixin, WindowMixin) { this._barLineWidth_px = 0; this._tickType = Slider.Shape.DISC; + this.granularity = 1.0; if (!this._skin.MARKER_SIZE) { @@ -1579,7 +1582,7 @@ Slider.Skin = {
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/visual_TextBox.js.html b/docs/visual_TextBox.js.html index db2abfa..904f279 100644 --- a/docs/visual_TextBox.js.html +++ b/docs/visual_TextBox.js.html @@ -198,7 +198,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) this._addAttribute( "alignment", alignment, - "left" + "center" ); this._addAttribute( "languageStyle", @@ -289,11 +289,16 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) * @param {boolean} alignment - alignment of the text * @param {boolean} [log= false] - whether or not to log */ - setAlignment(alignment = "left", log = false) + setAlignment(alignment = "center", log = false) { this._setAttribute("alignment", alignment, log); if (this._pixi !== undefined) { - this._pixi.setInputStyle("textAlign", alignment); + let alignmentStyles = TextBox._alignmentToFlexboxMap.get(alignment); + if (!alignmentStyles) { + alignmentStyles = ["center", "center"]; + } + this._pixi.setInputStyle("justifyContent", alignmentStyles[0]); + this._pixi.setInputStyle("textAlign", alignmentStyles[1]); } } @@ -543,18 +548,24 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) const borderWidth_px = Math.round(this._getLengthPix(this._borderWidth)); const width_px = Math.abs(Math.round(this._getLengthPix(this._size[0]))); const height_px = Math.abs(Math.round(this._getLengthPix(this._size[1]))); + let alignmentStyles = TextBox._alignmentToFlexboxMap.get(this._alignment); + if (!alignmentStyles) { + alignmentStyles = ["center", "center"]; + } return { // input style properties eventually become CSS, so same syntax applies input: { - display: "inline-block", + display: "flex", + flexDirection: "column", fontFamily: this._font, fontSize: `${letterHeight_px}px`, color: this._color === undefined || this._color === null ? 'transparent' : new Color(this._color).hex, fontWeight: (this._bold) ? "bold" : "normal", fontStyle: (this._italic) ? "italic" : "normal", direction: util.TEXT_DIRECTION[this._languageStyle], - textAlign: this._alignment, + justifyContent: alignmentStyles[0], + textAlign: alignmentStyles[1], padding: `${padding_px}px`, multiline: this._multiline, text: this._text, @@ -738,6 +749,18 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) } } +TextBox._alignmentToFlexboxMap = new Map([ + ["center", ["center", "center"]], + ["top-center", ["flex-start", "center"]], + ["bottom-center", ["flex-end", "center"]], + ["center-left", ["center", "left"]], + ["center-right", ["center", "right"]], + ["top-left", ["flex-start", "left"]], + ["top-right", ["flex-start", "right"]], + ["bottom-left", ["flex-end", "left"]], + ["bottom-right", ["flex-end", "right"]] +]); + /** * <p>This map associates units to default letter height.</p> * @@ -787,7 +810,7 @@ TextBox._defaultSizeMap = new Map([
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/visual_TextStim.js.html b/docs/visual_TextStim.js.html index 0d38a83..4007ef4 100644 --- a/docs/visual_TextStim.js.html +++ b/docs/visual_TextStim.js.html @@ -577,7 +577,7 @@ TextStim._defaultWrapWidthMap = new Map([
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/docs/visual_VisualStim.js.html b/docs/visual_VisualStim.js.html index fc46e05..e8be617 100644 --- a/docs/visual_VisualStim.js.html +++ b/docs/visual_VisualStim.js.html @@ -356,7 +356,7 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
- Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 09:56:49 GMT+0200 (Central European Summer Time) using the docdash theme. + Documentation generated by JSDoc 3.6.7 on Mon Aug 01 2022 10:19:55 GMT+0200 (Central European Summer Time) using the docdash theme.
diff --git a/package.json b/package.json index 3797ef4..5ffb4ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "psychojs", - "version": "2022.2.3", + "version": "2022.3.0", "private": true, "description": "Helps run in-browser neuroscience, psychology, and psychophysics experiments", "license": "MIT", diff --git a/src/core/ServerManager.js b/src/core/ServerManager.js index 5fd8654..be49900 100644 --- a/src/core/ServerManager.js +++ b/src/core/ServerManager.js @@ -1,9 +1,11 @@ /** - * Manager responsible for the communication between the experiment running in the participant's browser and the pavlovia.org server. + * Manager responsible for the communication between the experiment running in the participant's browser and the + * pavlovia.org server. * * @author Alain Pitiot * @version 2022.2.3 - * @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) + * @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2022 Open Science Tools Ltd. + * (https://opensciencetools.org) * @license Distributed under the terms of the MIT License */ @@ -16,8 +18,10 @@ import { Scheduler } from "../util/Scheduler.js"; import { PsychoJS } from "./PsychoJS.js"; /** - *

This manager handles all communications between the experiment running in the participant's browser and the [pavlovia.org]{@link http://pavlovia.org} server, in an asynchronous manner.

- *

It is responsible for reading the configuration file of an experiment, for opening and closing a session, for listing and downloading resources, and for uploading results, logs, and audio recordings.

+ *

This manager handles all communications between the experiment running in the participant's browser and the + * [pavlovia.org]{@link http://pavlovia.org} server, in an asynchronous manner.

+ *

It is responsible for reading the configuration file of an experiment, for opening and closing a session, for + * listing and downloading resources, and for uploading results, logs, and audio recordings.

* * @extends PsychObject */ @@ -212,7 +216,8 @@ export class ServerManager extends PsychObject * @typedef ServerManager.CloseSessionPromise * @property {string} origin the calling method * @property {string} context the context - * @property {Object.} [error] an error message if we could not close the session (e.g. if it has not previously been opened) + * @property {Object.} [error] an error message if we could not close the session (e.g. if it has not + * previously been opened) */ /** * Close the session for this experiment on the remote PsychoJS manager. @@ -331,7 +336,8 @@ export class ServerManager extends PsychObject *

* * @param {string | string[]} names names of the resources whose statuses are requested - * @return {module:core.ServerManager.ResourceStatus} status of the resource if there is only one, or reduced status otherwise + * @return {module:core.ServerManager.ResourceStatus} status of the resource if there is only one, or reduced status + * otherwise * @throws {Object.} if at least one of the names is not that of a previously * registered resource */ @@ -433,7 +439,8 @@ export class ServerManager extends PsychObject *
  • If resources is null, then we do not download any resources
  • * * - * @param {String | Array.<{name: string, path: string, download: boolean} | String | Symbol>} [resources=[]] - the list of resources or a single resource + * @param {String | Array.<{name: string, path: string, download: boolean} | String | Symbol>} [resources=[]] - the + * list of resources or a single resource */ async prepareResources(resources = []) { @@ -502,17 +509,31 @@ export class ServerManager extends PsychObject throw "resources must be manually specified when the experiment is running locally: ALL_RESOURCES cannot be used"; } - // convert those resources that are only a string to an object with name and path: + // pre-process the resources: for (let r = 0; r < resources.length; ++r) { const resource = resources[r]; + + // convert those resources that are only a string to an object with name and path: if (typeof resource === "string") { resources[r] = { name: resource, path: resource, download: true - } + }; + } + + // deal with survey models: + if ("surveyId" in resource) + { + // we add a .sid extension so _downloadResources knows what to download the associated + // survey model from the server + resources[r] = { + name: `${resource["surveyId"]}.sid`, + path: resource["surveyId"], + download: true + }; } } @@ -729,7 +750,6 @@ export class ServerManager extends PsychObject { key, value }, "FORM" ); - const uploadDataResponse = await postResponse.json(); if (postResponse.status !== 200) @@ -818,8 +838,10 @@ export class ServerManager extends PsychObject * @param {string} options.tag - additional tag * @param {boolean} [options.waitForCompletion=false] - whether or not to wait for completion * before returning - * @param {boolean} [options.showDialog=false] - whether or not to open a dialog box to inform the participant to wait for the data to be uploaded to the server - * @param {string} [options.dialogMsg="Please wait a few moments while the data is uploading to the server"] - default message informing the participant to wait for the data to be uploaded to the server + * @param {boolean} [options.showDialog=false] - whether or not to open a dialog box to inform the participant to + * wait for the data to be uploaded to the server + * @param {string} [options.dialogMsg="Please wait a few moments while the data is uploading to the server"] - + * default message informing the participant to wait for the data to be uploaded to the server * @returns {Promise} the response */ async uploadAudioVideo({mediaBlob, tag, waitForCompletion = false, showDialog = false, dialogMsg = "Please wait a few moments while the data is uploading to the server"}) @@ -942,6 +964,69 @@ export class ServerManager extends PsychObject } } + /** + * Asynchronously upload a survey response to the pavlovia server. + * + * @returns {Promise} a promise resolved when the survey response has been uploaded + */ + async uploadSurveyResponse(surveyId, surveyResponse, isComplete) + { + const response = { + origin: "ServerManager.uploadSurveyResponse", + context: `when uploading the survey response for experiment: ${this._psychoJS.config.experiment.fullpath} and survey: ${surveyId}` + }; + + if (this._psychoJS.getEnvironment() !== ExperimentHandler.Environment.SERVER || + this._psychoJS.config.experiment.status !== "RUNNING" || + this._psychoJS._serverMsg.has("__pilotToken")) + { + throw "survey responses can only be uploaded to the server for experiments running on the server"; + } + + this._psychoJS.logger.debug(`uploading a survey response for experiment: ${this._psychoJS.config.experiment.fullpath} and survey: ${surveyId}`); + this.setStatus(ServerManager.Status.BUSY); + + const self = this; + return new Promise(async (resolve, reject) => + { + try + { + const info = this._psychoJS.experiment.extraInfo; + const participant = (typeof info.participant === "string" && info.participant.length > 0) ? + info.participant : + "PARTICIPANT"; + + const postResponse = await this._queryServerAPI( + "POST", + `surveys/${surveyId}`, + { + experimentId: this._psychoJS.config.gitlab.projectId, + sessionToken: this._psychoJS.config.session.token, + participant: participant, + surveyResponse, + isComplete + }, + "JSON" + ); + const uploadDataResponse = await postResponse.json(); + + if (postResponse.status !== 200) + { + throw ('error' in uploadDataResponse) ? uploadDataResponse.error : uploadDataResponse; + } + + self.setStatus(ServerManager.Status.READY); + resolve({ ...response, ...uploadDataResponse }); + } + catch (error) + { + console.error(error); + self.setStatus(ServerManager.Status.ERROR); + reject({...response, error}); + } + }); + } + /** * List the resources available to the experiment. * @@ -1023,10 +1108,12 @@ export class ServerManager extends PsychObject }); // based on the resource extension either (a) add it to the preload manifest, (b) mark it for - // download by howler, or (c) add it to the document fonts + // download by howler, (c) add it to the document fonts, or (d) download the associated survey model + // from the server const preloadManifest = []; const soundResources = new Set(); const fontResources = []; + const surveyModelResources = []; for (const name of resources) { const nameParts = name.toLowerCase().split("."); @@ -1079,12 +1166,18 @@ export class ServerManager extends PsychObject } } - // font files + // font files: else if (["ttf", "otf", "woff", "woff2"].indexOf(pathExtension) > -1) { fontResources.push(name); } + // survey models: + else if (["sid"].indexOf(extension) > -1) + { + surveyModelResources.push(name); + } + // all other extensions handled by preload.js (download type decided by preload.js): else { @@ -1155,8 +1248,55 @@ export class ServerManager extends PsychObject } } - // start loading resources marked for howler.js: + // start loading the survey models: + // TODO load them sequentially, not all at once! const self = this; + for (const name of surveyModelResources) + { + const pathStatusData = this._resources.get(name); + pathStatusData.status = ServerManager.ResourceStatus.DOWNLOADING; + this.emit(ServerManager.Event.RESOURCE, { + message: ServerManager.Event.DOWNLOADING_RESOURCE, + resource: name, + }); + + this._queryServerAPI("GET", `surveys/${pathStatusData.path}/model`, "JSON") + .then(async getResponse => + { + const getModelResponse = await getResponse.json(); + + if (getResponse.status !== 200) + { + const error = ("error" in getModelResponse) ? getModelResponse.error : getModelResponse; + throw { ...response, error: `unable to download resource: ${name}: ${util.toString(error)}` }; + } + + ++self._nbLoadedResources; + + // note: we encode the json model as a string since it will be decoded in Survey.setModel, + // just like the model loaded directly from a resource by preloadJS + pathStatusData.data = new TextEncoder().encode(JSON.stringify(getModelResponse['model'])); + + pathStatusData.status = ServerManager.ResourceStatus.DOWNLOADED; + self.emit(ServerManager.Event.RESOURCE, { + message: ServerManager.Event.RESOURCE_DOWNLOADED, + resource: name, + }); + + if (self._nbLoadedResources === resources.size) + { + self.setStatus(ServerManager.Status.READY); + self.emit(ServerManager.Event.RESOURCE, { + message: ServerManager.Event.DOWNLOAD_COMPLETED, + }); + } + + }); + + } + + // start loading resources marked for howler.js: + // TODO load them sequentially, not all at once! for (const name of soundResources) { const pathStatusData = this._resources.get(name); @@ -1368,7 +1508,8 @@ export class ServerManager extends PsychObject /** * Server event * - *

    A server event is emitted by the manager to inform its listeners of either a change of status, or of a resource related event (e.g. download started, download is completed).

    + *

    A server event is emitted by the manager to inform its listeners of either a change of status, or of a resource + * related event (e.g. download started, download is completed).

    * * @enum {Symbol} * @readonly diff --git a/src/util/Util.js b/src/util/Util.js index 2b01f96..3d7a7f1 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -8,6 +8,8 @@ * @license Distributed under the terms of the MIT License */ +import seedrandom from "seedrandom"; + /** * Syntactic sugar for Mixins * @@ -55,18 +57,30 @@ export function promiseToTupple(promise) } /** - * Get a Universally Unique Identifier (RFC4122 version 4) + * Get a Universally Unique Identifier (RFC4122 version 4) or a pseudo-uuid based on a root *

    See details here: https://www.ietf.org/rfc/rfc4122.txt

    * + * @param {string} [root] - the root, for string dependent pseudo uuid's * @return {string} the uuid */ -export function makeUuid() +export function makeUuid(root) { - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) + // bonafide uuid v4 generator: + if (typeof root === "undefined") { - const r = Math.random() * 16 | 0, v = (c === "x") ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { + const r = Math.random() * 16 | 0, v = (c === "x") ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } + else + { + // our in-house pseudo uuid generator: + const generator = seedrandom(root); + let digits = generator().toString().substring(2); + digits += generator().toString().substring(2); + return `${digits.substring(0, 8)}-${digits.substring(8, 12)}-4${digits.substring(12, 15)}-8${digits.substring(15, 18)}-${digits.substring(18, 30)}`; + } } /** diff --git a/src/visual/Survey.js b/src/visual/Survey.js new file mode 100644 index 0000000..f9d5e30 --- /dev/null +++ b/src/visual/Survey.js @@ -0,0 +1,431 @@ +/** + * Survey Stimulus. + * + * @author Alain Pitiot + * @version 2022.3 + * @copyright (c) 2022 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + */ + +import * as PIXI from "pixi.js-legacy"; +import { VisualStim } from "./VisualStim.js"; +import {PsychoJS} from "../core/PsychoJS.js"; +import * as util from "../util/Util.js"; +import {ExperimentHandler} from "../data/index.js"; + +/** + * Survey Stimulus. + * + * @extends VisualStim + */ +export class Survey extends VisualStim +{ + /** + * @memberOf module:visual + * @param {Object} options + * @param {String} options.name - the name used when logging messages from this stimulus + * @param {Window} options.win - the associated Window + * @param {string} [options.surveyId] - the survey id + * @param {Object | string} [options.model] - the survey model + * @param {Object[] | string} [options.items] - the survey items + * @param {string} [options.units= "norm"] - the units of the stimulus (e.g. for size, position, vertices) + * @param {Array.} [options.pos= [0, 0]] - the position of the center of the stimulus + * @param {number} [options.ori= 0.0] - the orientation (in degrees) + * @param {number} [options.size] - the size of the rendered survey + * @param {number} [options.depth= 0] - the depth (i.e. the z order) + * @param {boolean} [options.autoDraw= false] - whether the stimulus should be automatically drawn + * on every frame flip + * @param {boolean} [options.autoLog= false] - whether to log + */ + constructor({ name, win, items, model, surveyId, pos, units, ori, size, depth, autoDraw, autoLog } = {}) + { + super({ name, win, units, ori, depth, pos, size, autoDraw, autoLog }); + + this._addAttribute( + "items", + items + ); + this._addAttribute( + "model", + model + ); + + // the default surveyId is an uuid based on the experiment id (or name) and the survey name: + // this way, it is always the same within a given experiment + this._hasSelfGeneratedSurveyId = (typeof surveyId === "undefined"); + const defaultSurveyId = (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER) ? + util.makeUuid(`${name}@${this._psychoJS.config.gitlab.projectId}`) : + util.makeUuid(`${name}@${this._psychoJS.config.experiment.name}`); + this._addAttribute( + "surveyId", + surveyId, + defaultSurveyId + ); + + // whether the user is done with the survey (completed or not): + this.isFinished = false; + // whether the user completed the survey: + this.isCompleted = false; + + // estimate the bounding box: + this._estimateBoundingBox(); + + // load the Survey.js libraries, if necessary: + // TODO + + if (this._autoLog) + { + this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`); + } + } + + /** + * Setter for the items attribute. + * + * @param {Object[] | string} items - the form items + * @param {boolean} [log= false] - whether of not to log + * @return {void} + * + * @todo this is the old approach, which need to be retrofitted for SurveyJS + */ + setItems(items, log = false) + { + const response = { + origin: "Survey.setItems", + context: `when setting the items of Survey: ${this._name}`, + }; + + try + { + // items is undefined: that's fine but we raise a warning in case this is a symptom of an actual problem + if (typeof items === "undefined") + { + this.psychoJS.logger.warn(`setting the items of Survey: ${this._name} with argument: undefined.`); + this.psychoJS.logger.debug(`set the items of Survey: ${this._name} as: undefined`); + } + else + { + // items is a string: it should be the name of a resource, which we load + if (typeof items === "string") + { + items = this.psychoJS.serverManager.getResource(items); + } + + // items should now be an array of objects: + if (!Array.isArray(items)) + { + throw "items is neither the name of a resource nor an array"; + } + + this._processItems(); + this._setAttribute("items", items, log); + this._onChange(true, true)(); + } + } + catch (error) + { + throw { ...response, error }; + } + } + + /** + * Setter for the model attribute. + * + * @param {Object | string} model - the survey model + * @param {boolean} [log= false] - whether to log + * @return {void} + */ + setModel(model, log = false) + { + const response = { + origin: "Survey.setModel", + context: `when setting the model of Survey: ${this._name}`, + }; + + try + { + // model is undefined: that's fine, but we raise a warning in case this is a symptom of an actual problem + if (typeof model === "undefined") + { + this.psychoJS.logger.warn(`setting the model of Survey: ${this._name} with argument: undefined.`); + this.psychoJS.logger.debug(`set the model of Survey: ${this._name} as: undefined`); + } + else + { + // model is a string: it should be the name of a resource, which we load + if (typeof model === "string") + { + const encodedModel = this.psychoJS.serverManager.getResource(model); + const decodedModel = new TextDecoder("utf-8").decode(encodedModel); + model = JSON.parse(decodedModel); + } + + // items should now be an object: + if (typeof model !== "object") + { + throw "model is neither the name of a resource nor an object"; + } + + this._surveyModelJson = Object.assign({}, model); + this._surveyModel = new window.Survey.Model(this._surveyModelJson); + + // when the participant is done with the survey: + this._surveyModel.onComplete.add(() => + { + // note: status is now set by the generated code + // this.status = PsychoJS.Status.FINISHED; + this.isFinished = true; + + // check whether the survey was completed: + const surveyVisibleQuestions = this._surveyModel.getAllQuestions(true); + const nbAnsweredQuestion = surveyVisibleQuestions.reduce( + (count, question) => count + (!question.isEmpty() ? 1 : 0), + 0 + ); + this.isCompleted = (nbAnsweredQuestion === surveyVisibleQuestions.length); + }); + + this._setAttribute("model", model, log); + this._onChange(true, true)(); + } + } + catch (error) + { + throw { ...response, error }; + } + } + + /** + * Setter for the surveyId attribute. + * + * @param {string} surveyId - the survey Id + * @param {boolean} [log= false] - whether to log + * @return {void} + */ + setSurveyId(surveyId, log = false) + { + this._setAttribute("surveyId", surveyId, log); + if (!this._hasSelfGeneratedSurveyId) + { + this.setModel(`${surveyId}.sid`, log); + } + } + + /** + * Get the survey response. + */ + getResponse() + { + if (typeof this._surveyModel === "undefined") + { + return {}; + } + + return this._surveyModel.data; + } + + /** + * Upload the survey response to the pavlovia.org server. + * + * @returns {Promise} a promise resolved when the survey response has been saved + */ + save() + { + this._psychoJS.logger.info("[PsychoJS] Save survey response."); + + const response = this.getResponse(); + + // if the response cannot be uploaded, e.g. the experiment is running locally, or + // if it is piloting mode, then we offer the response as a file for download: + if (this._psychoJS.getEnvironment() !== ExperimentHandler.Environment.SERVER || + this._psychoJS.config.experiment.status !== "RUNNING" || + this._psychoJS._serverMsg.has("__pilotToken")) + { + const filename = `survey_${this._surveyId}.json`; + const blob = new Blob([JSON.stringify(response)], { type: "application/json" }); + + const anchor = document.createElement("a"); + anchor.href = window.URL.createObjectURL(blob); + anchor.download = filename; + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); + + return Promise.resolve({}); + } + + // otherwise, we do upload the survey response + // note: if the surveyId was self-generated instead of being a parameter of the constructor, + // we need to also upload the survey model, as a new survey might need to be created on the fly + // by the server for this experiment. + if (!this._hasSelfGeneratedSurveyId) + { + return this._psychoJS.serverManager.uploadSurveyResponse( + this._surveyId, response, this.isCompleted + ); + } + else + { + return this._psychoJS.serverManager.uploadSurveyResponse( + this._surveyId, response, this.isCompleted, this._surveyModelJson + ); + } + } + + /** + * Hide this stimulus on the next frame draw. + * + * @override + * @note We over-ride MinimalStim.hide such that we can remove the survey DOM element + */ + hide() + { + // if a survey div already does not exist already, create it: + const surveyId = `survey-${this._name}`; + const surveyDiv = document.getElementById(surveyId); + if (surveyDiv !== null) + { + document.body.removeChild(surveyDiv); + } + + super.hide(); + } + + + /** + * Process the items: check the syntax, turn them into a survey model. + * + * @protected + * @return {void} + */ + _processItems() + { + const response = { + origin: "Survey._processItems", + context: "when processing the form items", + }; + + try + { + if (this._autoLog) + { + // note: we use the same log message as PsychoPy even though we called this method differently + this._psychoJS.experimentLogger.exp("Importing items..."); + } + + // TODO + /* + // import the items: + this._importItems(); + + // sanitize the items (check that keys are valid, fill in default values): + this._sanitizeItems(); + + // randomise the items if need be: + if (this._randomize) + { + util.shuffle(this._items); + } +*/ + + this._surveyModelJson = { + elements: [{ + name: "FirstName", + title: "First name:", + type: "text" + }, { + name: "LastName", + title: "Last name:", + type: "text" + }], + showCompletedPage: false + }; + this._surveyModel = new Survey.Model(this._surveyModelJson); + + // when the participant has completed the survey, the Survey status changes to FINISHED: + this._surveyModel.onComplete.add(() => + { + this.status = PsychoJS.Status.FINISHED; + }); + } + catch (error) + { + throw { ...response, error }; + } + } + + /** + * Estimate the bounding box. + * + * @override + * @protected + */ + _estimateBoundingBox() + { + this._boundingBox = new PIXI.Rectangle( + this._pos[0] - this._size[0] / 2, + this._pos[1] - this._size[1] / 2, + this._size[0], + this._size[1], + ); + + // TODO take the orientation into account + } + + /** + * Update the stimulus, if necessary. + * + * @protected + */ + _updateIfNeeded() + { + if (!this._needUpdate) + { + return; + } + this._needUpdate = false; + + // update the PIXI representation, if need be: + if (this._needPixiUpdate) + { + this._needPixiUpdate = false; + + // if a survey div already does not exist already, create it: + const surveyId = `survey-${this._name}`; + let surveyDiv = document.getElementById(surveyId); + if (surveyDiv === null) + { + surveyDiv = document.createElement("div"); + surveyDiv.id = surveyId; + document.body.appendChild(surveyDiv); + } + + // start the survey: + if (typeof this._surveyModel !== "undefined") + { + jQuery(`#${surveyId}`).Survey({model: this._surveyModel}); + } + } + + // TODO change the position, scale, anchor, z-index, etc. + // TODO update the size, taking into account the actual size of the survey + /* + this._pixi.zIndex = -this._depth; + this._pixi.alpha = this.opacity; + + // set the scale: + const displaySize = this._getDisplaySize(); + const size_px = util.to_px(displaySize, this.units, this.win); + const scaleX = size_px[0] / this._texture.width; + const scaleY = size_px[1] / this._texture.height; + this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX; + this._pixi.scale.y = this.flipVert ? scaleY : -scaleY; + + // set the position, rotation, and anchor (image centered on pos): + this._pixi.position = to_pixiPoint(this.pos, this.units, this.win); + this._pixi.rotation = -this.ori * Math.PI / 180; + this._pixi.anchor.x = 0.5; + this._pixi.anchor.y = 0.5; +*/ + } +} diff --git a/src/visual/index.js b/src/visual/index.js index fb96f41..8c604fa 100644 --- a/src/visual/index.js +++ b/src/visual/index.js @@ -12,3 +12,4 @@ export * from "./TextInput.js"; export * from "./TextStim.js"; export * from "./VisualStim.js"; export * from "./FaceDetector.js"; +export * from "./Survey.js"; From 442b9a079f3265ae0dcdba25298e83720639dd73 Mon Sep 17 00:00:00 2001 From: Alain Pitiot Date: Thu, 15 Sep 2022 10:43:42 +0200 Subject: [PATCH 05/41] ENH Sound values can be changed without instantiating a new SoundPlayer --- src/core/PsychoJS.js | 2 +- src/sound/AudioClipPlayer.js | 42 ++++++++----- src/sound/Sound.js | 110 ++++++++++++++++++++++++++++------- src/sound/SoundPlayer.js | 16 ----- src/sound/TonePlayer.js | 63 +++++++++++--------- src/sound/TrackPlayer.js | 78 +++++++++++++++++++------ 6 files changed, 211 insertions(+), 100 deletions(-) diff --git a/src/core/PsychoJS.js b/src/core/PsychoJS.js index aabd33c..65ac719 100644 --- a/src/core/PsychoJS.js +++ b/src/core/PsychoJS.js @@ -170,7 +170,7 @@ export class PsychoJS } this.logger.info("[PsychoJS] Initialised."); - this.logger.info("[PsychoJS] @version 2022.2.1"); + this.logger.info("[PsychoJS] @version 2022.2.3"); // hide the initialisation message: const root = document.getElementById("root"); diff --git a/src/sound/AudioClipPlayer.js b/src/sound/AudioClipPlayer.js index 10dd99b..81792f6 100644 --- a/src/sound/AudioClipPlayer.js +++ b/src/sound/AudioClipPlayer.js @@ -53,28 +53,21 @@ export class AudioClipPlayer extends SoundPlayer /** * Determine whether this player can play the given sound. * - * @param {module:sound.Sound} sound - the sound object, which should be an AudioClip - * @return {Object|undefined} an instance of AudioClipPlayer if sound is an AudioClip or undefined otherwise + * @param {module:core.PsychoJS} psychoJS - the PsychoJS instance + * @param {string} value - the sound value, which should be the name of an audio resource + * file + * @return {Object|boolean} argument needed to instantiate a AudioClipPlayer that can play the given sound + * or false otherwise */ - static accept(sound) + static accept(psychoJS, value) { - if (sound.value instanceof AudioClip) + if (value instanceof AudioClip) { - // build the player: - const player = new AudioClipPlayer({ - psychoJS: sound.psychoJS, - audioClip: sound.value, - startTime: sound.startTime, - stopTime: sound.stopTime, - stereo: sound.stereo, - loops: sound.loops, - volume: sound.volume, - }); - return player; + return { audioClip: value }; } // AudioClipPlayer is not an appropriate player for the given sound: - return undefined; + return false; } /** @@ -129,6 +122,23 @@ export class AudioClipPlayer extends SoundPlayer // TODO } + /** + * Set the audio clip. + * + * @param {Object} options.audioClip - the module:sound.AudioClip. + */ + setAudioClip(audioClip) + { + if (audioClip instanceof AudioClip) + { + if (this._audioClip !== undefined) + { + this.stop(); + } + this._audioClip = audioClip; + } + } + /** * Start playing the sound. * diff --git a/src/sound/Sound.js b/src/sound/Sound.js index cf4ef45..084c9d6 100644 --- a/src/sound/Sound.js +++ b/src/sound/Sound.js @@ -2,7 +2,7 @@ /** * Sound stimulus. * - * @author Alain Pitiot + * @author Alain Pitiot, Nikita Agafonov * @version 2022.2.3 * @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) * @license Distributed under the terms of the MIT License @@ -74,7 +74,6 @@ export class Sound extends PsychObject this._player = undefined; this._addAttribute("win", win); - this._addAttribute("value", value); this._addAttribute("octave", octave); this._addAttribute("secs", secs); this._addAttribute("startTime", startTime); @@ -84,8 +83,9 @@ export class Sound extends PsychObject this._addAttribute("loops", loops); this._addAttribute("autoLog", autoLog); - // identify an appropriate player: - this._getPlayer(); + // note: setValue will identify the appropriate SoundPlayer and possibly instantiate it + // consequently _addAtribute("value") needs to be the last one so the other attributes are already set + this._addAttribute("value", value); this.status = PsychoJS.Status.NOT_STARTED; } @@ -97,7 +97,7 @@ export class Sound extends PsychObject * Repeat calls to play may results in the sounds being played on top of each other.

    * * @param {number} loops how many times to repeat the sound after it plays once. If loops == -1, the sound will repeat indefinitely until stopped. - * @param {boolean} [log= true] whether or not to log + * @param {boolean} [log= true] whether to log */ play(loops, log = true) { @@ -109,7 +109,7 @@ export class Sound extends PsychObject * Stop playing the sound immediately. * * @param {Object} options - * @param {boolean} [options.log= true] - whether or not to log + * @param {boolean} [options.log= true] - whether to log */ stop({ log = true, @@ -134,7 +134,7 @@ export class Sound extends PsychObject * * @param {number} volume - the volume (values should be between 0 and 1) * @param {boolean} [mute= false] - whether or not to mute the sound - * @param {boolean} [log= true] - whether of not to log + * @param {boolean} [log= true] - whether to log */ setVolume(volume, mute = false, log = true) { @@ -147,38 +147,108 @@ export class Sound extends PsychObject } /** - * Set the sound value on demand past initialisation. + * Set the sound value. * * @param {object} sound - a sound instance to replace the current one - * @param {boolean} [log= true] - whether or not to log + * @param {boolean} [log= true] - whether to log */ setSound(sound, log = true) { - if (sound instanceof Sound) + if (!(sound instanceof Sound)) { - this._setAttribute("value", sound.value, log); + throw { + origin: "Sound.setSound", + context: "when setting the sound", + error: "the argument should be an instance of the Sound class.", + }; + } - if (typeof this._player !== "undefined") + this._setAttribute("value", sound.value, log); + + if (typeof this._player !== "undefined") + { + this._player = this._player.constructor.accept(this); + } + + return this; + } + + /** + * Set the sound value. + * + * @param {number|string} [value = "C"] - the sound value + * @param {number} [octave = 4] - the octave corresponding to the tone (if applicable) + * @param {boolean} [log=true] - whether to log + */ + setValue(value = "C", octave = 4, log = true) + { + this._setAttribute("value", value, log); + + const args = { + psychoJS: this._psychoJS, + stereo: this._stereo, + volume: this._volume, + loops: this._loops, + startTime: this._startTime, + stopTime: this._stopTime, + secs: this._secs + } + + let playerArgs = TonePlayer.accept(value, octave); + if (typeof playerArgs !== "undefined") + { + if (this._player instanceof TonePlayer) { - this._player = this._player.constructor.accept(this); + this._player.setTone(value, octave); } + else + { + this._player = new TonePlayer(Object.assign(args, playerArgs)); + } + return; + } - // Be fluent? - return this; + playerArgs = TrackPlayer.accept(this._psychoJS, value); + if (playerArgs !== false) + { + if (this._player instanceof TrackPlayer) + { + this._player.setTrack(value); + } + else + { + this._player = new TrackPlayer(Object.assign(args, playerArgs)); + } + return; + } + + playerArgs = AudioClipPlayer.accept(this._psychoJS, value); + if (typeof playerArgs !== "undefined") + { + if (this._player instanceof AudioClipPlayer) + { + this._player.setAudioClip(value); + } + else + { + this._player = new AudioClipPlayer(Object.assign(args, playerArgs)); + } + return; } throw { - origin: "Sound.setSound", - context: "when replacing the current sound", - error: "invalid input, need an instance of the Sound class.", + origin: "Sound.setValue", + context: "when setting the sound value", + error: "could not find an appropriate player.", }; + } /** * Set the number of loops. * * @param {number} [loops=0] - how many times to repeat the sound after it has played once. If loops == -1, the sound will repeat indefinitely until stopped. - * @param {boolean} [log=true] - whether of not to log + * @param {boolean} [log=true] - whether to log */ setLoops(loops = 0, log = true) { @@ -194,7 +264,7 @@ export class Sound extends PsychObject * Set the duration (in seconds) * * @param {number} [secs=0.5] - duration of the tone (in seconds) If secs == -1, the sound will play indefinitely. - * @param {boolean} [log=true] - whether or not to log + * @param {boolean} [log=true] - whether to log */ setSecs(secs = 0.5, log = true) { diff --git a/src/sound/SoundPlayer.js b/src/sound/SoundPlayer.js index 512c365..77d610c 100644 --- a/src/sound/SoundPlayer.js +++ b/src/sound/SoundPlayer.js @@ -26,22 +26,6 @@ export class SoundPlayer extends PsychObject super(psychoJS); } - /** - * Determine whether this player can play the given sound. - * - * @abstract - * @param {module:sound.Sound} - the sound - * @return {Object|undefined} an instance of the SoundPlayer that can play the sound, or undefined if none could be found - */ - static accept(sound) - { - throw { - origin: "SoundPlayer.accept", - context: "when evaluating whether this player can play a given sound", - error: "this method is abstract and should not be called.", - }; - } - /** * Start playing the sound. * diff --git a/src/sound/TonePlayer.js b/src/sound/TonePlayer.js index 488016a..75c7d08 100644 --- a/src/sound/TonePlayer.js +++ b/src/sound/TonePlayer.js @@ -24,7 +24,7 @@ export class TonePlayer extends SoundPlayer * @memberOf module:sound * @param {Object} options * @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance - * @param {number} [options.duration_s= 0.5] - duration of the tone (in seconds). If duration_s == -1, the sound will play indefinitely. + * @param {number} [options.secs= 0.5] - duration of the tone (in seconds). If secs == -1, the sound will play indefinitely. * @param {string|number} [options.note= 'C4'] - note (if string) or frequency (if number) * @param {number} [options.volume= 1.0] - volume of the tone (must be between 0 and 1.0) * @param {number} [options.loops= 0] - how many times to repeat the tone after it has played once. If loops == -1, the tone will repeat indefinitely until stopped. @@ -32,7 +32,7 @@ export class TonePlayer extends SoundPlayer constructor({ psychoJS, note = "C4", - duration_s = 0.5, + secs = 0.5, volume = 1.0, loops = 0, soundLibrary = TonePlayer.SoundLibrary.TONE_JS, @@ -42,7 +42,7 @@ export class TonePlayer extends SoundPlayer super(psychoJS); this._addAttribute("note", note); - this._addAttribute("duration_s", duration_s); + this._addAttribute("duration_s", secs); this._addAttribute("volume", volume); this._addAttribute("loops", loops); this._addAttribute("soundLibrary", soundLibrary); @@ -66,25 +66,21 @@ export class TonePlayer extends SoundPlayer *

    Note: if TonePlayer accepts the sound but Tone.js is not available, e.g. if the browser is IE11, * we throw an exception.

    * - * @param {module:sound.Sound} sound - the sound - * @return {Object|undefined} an instance of TonePlayer that can play the given sound or undefined otherwise + * @param {string|number} value - potential frequency or note + * @param {number} octave - the octave corresponding to the tone + * @return {Object|boolean} argument needed to instantiate a TonePlayer that can play the given sound + * or false otherwise */ - static accept(sound) + static accept(value, octave) { // if the sound's value is an integer, we interpret it as a frequency: - if (isNumeric(sound.value)) + if (isNumeric(value)) { - return new TonePlayer({ - psychoJS: sound.psychoJS, - note: sound.value, - duration_s: sound.secs, - volume: sound.volume, - loops: sound.loops, - }); + return { note: value } } // if the sound's value is a string, we check whether it is a note: - if (typeof sound.value === "string") + if (typeof value === "string") { // mapping between the PsychoPY notes and the standard ones: let psychopyToToneMap = new Map(); @@ -96,21 +92,15 @@ export class TonePlayer extends SoundPlayer } // check whether the sound's value is a recognised note: - const note = psychopyToToneMap.get(sound.value); + const note = psychopyToToneMap.get(value); if (typeof note !== "undefined") { - return new TonePlayer({ - psychoJS: sound.psychoJS, - note: note + sound.octave, - duration_s: sound.secs, - volume: sound.volume, - loops: sound.loops, - }); + return { note: note + octave }; } } - // TonePlayer is not an appropriate player for the given sound: - return undefined; + // the value does not seem to correspond to a tone we can play: + return false; } /** @@ -126,11 +116,11 @@ export class TonePlayer extends SoundPlayer /** * Set the duration of the tone. * - * @param {number} duration_s - the duration of the tone (in seconds) If duration_s == -1, the sound will play indefinitely. + * @param {number} secs - the duration of the tone (in seconds) If secs == -1, the sound will play indefinitely. */ - setDuration(duration_s) + setDuration(secs) { - this.duration_s = duration_s; + this.duration_s = secs; } /** @@ -172,6 +162,23 @@ export class TonePlayer extends SoundPlayer } } + /** + * Set the note for tone. + * + * @param {string|number} value - potential frequency or note + * @param {number} octave - the octave corresponding to the tone + */ + setTone(value = "C", octave = 4) + { + const args = TonePlayer.accept(value, octave); + this._note = args.note; + + if (typeof this._synth !== "undefined") + { + this._synth.setNote(this._note); + } + } + /** * Start playing the sound. * diff --git a/src/sound/TrackPlayer.js b/src/sound/TrackPlayer.js index 7451ae6..dd9774c 100644 --- a/src/sound/TrackPlayer.js +++ b/src/sound/TrackPlayer.js @@ -8,6 +8,7 @@ */ import { SoundPlayer } from "./SoundPlayer.js"; +import { Howl } from "howler"; /** *

    This class handles the playback of sound tracks.

    @@ -54,34 +55,43 @@ export class TrackPlayer extends SoundPlayer /** * Determine whether this player can play the given sound. * - * @param {module:sound.Sound} sound - the sound, which should be the name of an audio resource - * file - * @return {Object|undefined} an instance of TrackPlayer that can play the given track or undefined otherwise + * @param {string} value - the sound, which should be the name of an audio resource file + * @return {boolean} whether or not value is supported */ - static accept(sound) + static checkValueSupport (value) { - // if the sound's value is a string, we check whether it is the name of a resource: - if (typeof sound.value === "string") + if (typeof value === "string") { - const howl = sound.psychoJS.serverManager.getResource(sound.value); + return true; + } + + return false; + } + + /** + * Determine whether this player can play the given sound. + * + * @param {module:core.PsychoJS} psychoJS - the PsychoJS instance + * @param {string} value - the sound value, which should be the name of an audio resource + * file + * @return {Object|boolean} argument needed to instantiate a TrackPlayer that can play the given sound + * or false otherwise + */ + static accept(psychoJS, value) + { + // value should be a string: + if (typeof value === "string") + { + // check whether the value is the name of a resource: + const howl = psychoJS.serverManager.getResource(value); if (typeof howl !== "undefined") { - // build the player: - const player = new TrackPlayer({ - psychoJS: sound.psychoJS, - howl: howl, - startTime: sound.startTime, - stopTime: sound.stopTime, - stereo: sound.stereo, - loops: sound.loops, - volume: sound.volume, - }); - return player; + return { howl }; } } // TonePlayer is not an appropriate player for the given sound: - return undefined; + return false; } /** @@ -142,6 +152,36 @@ export class TrackPlayer extends SoundPlayer } } + /** + * Set new track to play. + * + * @param {Object|string} track - a track resource name or Howl object (see {@link https://howlerjs.com/}) + */ + setTrack(track) + { + let newHowl = undefined; + + if (typeof track === "string") + { + newHowl = this.psychoJS.serverManager.getResource(track); + } + else if (track instanceof Howl) + { + newHowl = track; + } + + if (newHowl !== undefined) + { + this._howl.once("fade", (id) => + { + this._howl.stop(id); + this._howl.off("end"); + this._howl = newHowl; + }); + this._howl.fade(this._howl.volume(), 0, 17, this._id); + } + } + /** * Start playing the sound. * From 6a0cefb5da3439c60204ea7f75be5533080a028b Mon Sep 17 00:00:00 2001 From: Alain Pitiot Date: Thu, 15 Sep 2022 13:21:41 +0200 Subject: [PATCH 06/41] small edits --- package.json | 2 +- src/core/PsychoJS.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3797ef4..7a9b92c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "psychojs", - "version": "2022.2.3", + "version": "2022.2.4", "private": true, "description": "Helps run in-browser neuroscience, psychology, and psychophysics experiments", "license": "MIT", diff --git a/src/core/PsychoJS.js b/src/core/PsychoJS.js index 65ac719..bf66bef 100644 --- a/src/core/PsychoJS.js +++ b/src/core/PsychoJS.js @@ -170,7 +170,7 @@ export class PsychoJS } this.logger.info("[PsychoJS] Initialised."); - this.logger.info("[PsychoJS] @version 2022.2.3"); + this.logger.info("[PsychoJS] @version 2022.2.4"); // hide the initialisation message: const root = document.getElementById("root"); From 25aef3d5008ae12ed4cfaa41bfba26cc51e909b5 Mon Sep 17 00:00:00 2001 From: lgtst Date: Tue, 20 Sep 2022 02:01:03 +0100 Subject: [PATCH 07/41] Fix for incorrect mask position, when grating vertices centered around (0, 0); --- src/visual/GratingStim.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/visual/GratingStim.js b/src/visual/GratingStim.js index 140a78c..36fc6a1 100644 --- a/src/visual/GratingStim.js +++ b/src/visual/GratingStim.js @@ -279,6 +279,7 @@ export class GratingStim extends VisualStim * @param {number} [options.sf=1.0] - spatial frequency of the function used in grating stimulus * @param {number} [options.phase=0.0] - phase of the function used in grating stimulus, multiples of period of that function * @param {Array.} [options.pos= [0, 0]] - the position of the center of the stimulus + * @param {string} [options.anchor = "center"] - sets the origin point of the stim * @param {number} [options.ori= 0.0] - the orientation (in degrees) * @param {number} [options.size] - the size of the rendered image (DEFAULT_STIM_SIZE_PX will be used if size is not specified) * @param {Color} [options.color= "white"] - Foreground color of the stimulus. Can be String like "red" or "#ff0000" or Number like 0xff0000. @@ -296,6 +297,7 @@ export class GratingStim extends VisualStim win, mask, pos, + anchor, units, sf = 1.0, ori, @@ -313,7 +315,7 @@ export class GratingStim extends VisualStim maskParams } = {}) { - super({ name, win, units, ori, opacity, depth, pos, size, autoDraw, autoLog }); + super({ name, win, units, ori, opacity, depth, pos, anchor, size, autoDraw, autoLog }); this._adjustmentFilter = new AdjustmentFilter({ contrast @@ -741,9 +743,13 @@ export class GratingStim extends VisualStim } else { - // for some reason setting PIXI.Mesh as .mask doesn't do anything, + // For some reason setting PIXI.Mesh as .mask doesn't do anything, // rendering mask to texture for further use. const maskMesh = this._getPixiMeshFromPredefinedShaders(this._mask); + + // Since mesh is centered around [0, 0] (has vertices going around it), + // offsetting maskMesh position to properly cover render target texture. + maskMesh.position.set(this._size_px[0] * 0.5, this._size_px[1] * 0.5); const rt = PIXI.RenderTexture.create({ width: this._size_px[0], height: this._size_px[1], @@ -756,6 +762,8 @@ export class GratingStim extends VisualStim this._pixi.mask = maskSprite; this._pixi.addChild(maskSprite); } + // Since mesh is centered around [0, 0], setting mask's anchor to center to properly cover target image. + this._pixi.mask.anchor.set(0.5); } // since _pixi.width may not be immediately available but the rest of the code needs its value From b89b33e29446b68610e6da383bfe670c4a1aff1a Mon Sep 17 00:00:00 2001 From: lgtst Date: Wed, 21 Sep 2022 14:03:49 +0100 Subject: [PATCH 08/41] final tweaks in the comments; --- src/visual/GratingStim.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/visual/GratingStim.js b/src/visual/GratingStim.js index 36fc6a1..98cd524 100644 --- a/src/visual/GratingStim.js +++ b/src/visual/GratingStim.js @@ -743,13 +743,15 @@ export class GratingStim extends VisualStim } else { - // For some reason setting PIXI.Mesh as .mask doesn't do anything, - // rendering mask to texture for further use. const maskMesh = this._getPixiMeshFromPredefinedShaders(this._mask); - // Since mesh is centered around [0, 0] (has vertices going around it), - // offsetting maskMesh position to properly cover render target texture. + // Since maskMesh is centered around (0, 0) (has vertices going around it), + // offsetting maskMesh position to properly cover render target texture, + // which created with top-left corner at (0, 0). maskMesh.position.set(this._size_px[0] * 0.5, this._size_px[1] * 0.5); + + // For some reason setting PIXI.Mesh as .mask doesn't do anything, + // rendering mask to texture for further use. const rt = PIXI.RenderTexture.create({ width: this._size_px[0], height: this._size_px[1], @@ -762,7 +764,7 @@ export class GratingStim extends VisualStim this._pixi.mask = maskSprite; this._pixi.addChild(maskSprite); } - // Since mesh is centered around [0, 0], setting mask's anchor to center to properly cover target image. + // Since grating mesh is centered around (0, 0), setting mask's anchor to center to properly cover target image. this._pixi.mask.anchor.set(0.5); } From e1ffc9550a41e9f5b38bf99bd4f6d3f35ad65243 Mon Sep 17 00:00:00 2001 From: Alain Pitiot Date: Thu, 22 Sep 2022 10:26:20 +0200 Subject: [PATCH 09/41] BF SoundPlayer.accept outputs false instead of undefined, which needed to be reflected in Sound.setValue --- src/core/PsychoJS.js | 1 + src/sound/Sound.js | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/core/PsychoJS.js b/src/core/PsychoJS.js index bf66bef..87c5f6a 100644 --- a/src/core/PsychoJS.js +++ b/src/core/PsychoJS.js @@ -171,6 +171,7 @@ export class PsychoJS this.logger.info("[PsychoJS] Initialised."); this.logger.info("[PsychoJS] @version 2022.2.4"); + this.logger.info("[PsychoJS] @version 2022.2.4"); // hide the initialisation message: const root = document.getElementById("root"); diff --git a/src/sound/Sound.js b/src/sound/Sound.js index 084c9d6..145eb78 100644 --- a/src/sound/Sound.js +++ b/src/sound/Sound.js @@ -195,7 +195,7 @@ export class Sound extends PsychObject } let playerArgs = TonePlayer.accept(value, octave); - if (typeof playerArgs !== "undefined") + if (playerArgs) { if (this._player instanceof TonePlayer) { @@ -209,7 +209,7 @@ export class Sound extends PsychObject } playerArgs = TrackPlayer.accept(this._psychoJS, value); - if (playerArgs !== false) + if (playerArgs) { if (this._player instanceof TrackPlayer) { @@ -223,7 +223,7 @@ export class Sound extends PsychObject } playerArgs = AudioClipPlayer.accept(this._psychoJS, value); - if (typeof playerArgs !== "undefined") + if (playerArgs) { if (this._player instanceof AudioClipPlayer) { From 8eee61cf4e577f30432b78be358e6a8c230026e8 Mon Sep 17 00:00:00 2001 From: lgtst Date: Thu, 6 Oct 2022 19:40:31 +0100 Subject: [PATCH 10/41] wgl1 shaders for gratingStim; --- src/visual/GratingStim.js | 152 ++++++++++++++++++- src/visual/shaders/wgl1/circleShader.frag | 26 ++++ src/visual/shaders/wgl1/crossShader.frag | 28 ++++ src/visual/shaders/wgl1/defaultQuad.vert | 13 ++ src/visual/shaders/wgl1/gaussShader.frag | 32 ++++ src/visual/shaders/wgl1/imageShader.frag | 29 ++++ src/visual/shaders/wgl1/radRampShader.frag | 27 ++++ src/visual/shaders/wgl1/radialShader.frag | 35 +++++ src/visual/shaders/wgl1/raisedCosShader.frag | 38 +++++ src/visual/shaders/wgl1/sawShader.frag | 29 ++++ src/visual/shaders/wgl1/sinShader.frag | 27 ++++ src/visual/shaders/wgl1/sinXsinShader.frag | 30 ++++ src/visual/shaders/wgl1/sqrShader.frag | 27 ++++ src/visual/shaders/wgl1/sqrXsqrShader.frag | 30 ++++ src/visual/shaders/wgl1/triShader.frag | 30 ++++ 15 files changed, 550 insertions(+), 3 deletions(-) create mode 100644 src/visual/shaders/wgl1/circleShader.frag create mode 100644 src/visual/shaders/wgl1/crossShader.frag create mode 100644 src/visual/shaders/wgl1/defaultQuad.vert create mode 100644 src/visual/shaders/wgl1/gaussShader.frag create mode 100644 src/visual/shaders/wgl1/imageShader.frag create mode 100644 src/visual/shaders/wgl1/radRampShader.frag create mode 100644 src/visual/shaders/wgl1/radialShader.frag create mode 100644 src/visual/shaders/wgl1/raisedCosShader.frag create mode 100644 src/visual/shaders/wgl1/sawShader.frag create mode 100644 src/visual/shaders/wgl1/sinShader.frag create mode 100644 src/visual/shaders/wgl1/sinXsinShader.frag create mode 100644 src/visual/shaders/wgl1/sqrShader.frag create mode 100644 src/visual/shaders/wgl1/sqrXsqrShader.frag create mode 100644 src/visual/shaders/wgl1/triShader.frag diff --git a/src/visual/GratingStim.js b/src/visual/GratingStim.js index 98cd524..4b239cf 100644 --- a/src/visual/GratingStim.js +++ b/src/visual/GratingStim.js @@ -28,6 +28,21 @@ import radRampShader from "./shaders/radRampShader.frag"; import raisedCosShader from "./shaders/raisedCosShader.frag"; import radialStim from "./shaders/radialShader.frag"; +import defaultQuadVertWGL1 from "./shaders/wgl1/defaultQuad.vert"; +import imageShaderWGL1 from "./shaders/wgl1/imageShader.frag"; +import sinShaderWGL1 from "./shaders/wgl1/sinShader.frag"; +import sqrShaderWGL1 from "./shaders/wgl1/sqrShader.frag"; +import sawShaderWGL1 from "./shaders/wgl1/sawShader.frag"; +import triShaderWGL1 from "./shaders/wgl1/triShader.frag"; +import sinXsinShaderWGL1 from "./shaders/wgl1/sinXsinShader.frag"; +import sqrXsqrShaderWGL1 from "./shaders/wgl1/sqrXsqrShader.frag"; +import circleShaderWGL1 from "./shaders/wgl1/circleShader.frag"; +import gaussShaderWGL1 from "./shaders/wgl1/gaussShader.frag"; +import crossShaderWGL1 from "./shaders/wgl1/crossShader.frag"; +import radRampShaderWGL1 from "./shaders/wgl1/radRampShader.frag"; +import raisedCosShaderWGL1 from "./shaders/wgl1/raisedCosShader.frag"; +import radialStimWGL1 from "./shaders/wgl1/radialShader.frag"; + /** * Grating Stimulus. * @@ -253,6 +268,125 @@ export class GratingStim extends VisualStim } }; + static #SHADERSWGL1 = { + imageShader: { + shader: imageShaderWGL1, + uniforms: { + uFreq: 1.0, + uPhase: 0.0, + uColor: [1., 1., 1.], + uAlpha: 1.0 + } + }, + sin: { + shader: sinShaderWGL1, + uniforms: { + uFreq: 1.0, + uPhase: 0.0, + uColor: [1., 1., 1.], + uAlpha: 1.0 + } + }, + sqr: { + shader: sqrShaderWGL1, + uniforms: { + uFreq: 1.0, + uPhase: 0.0, + uColor: [1., 1., 1.], + uAlpha: 1.0 + } + }, + saw: { + shader: sawShaderWGL1, + uniforms: { + uFreq: 1.0, + uPhase: 0.0, + uColor: [1., 1., 1.], + uAlpha: 1.0 + } + }, + tri: { + shader: triShaderWGL1, + uniforms: { + uFreq: 1.0, + uPhase: 0.0, + uPeriod: 1.0, + uColor: [1., 1., 1.], + uAlpha: 1.0 + } + }, + sinXsin: { + shader: sinXsinShaderWGL1, + uniforms: { + uFreq: 1.0, + uPhase: 0.0, + uColor: [1., 1., 1.], + uAlpha: 1.0 + } + }, + sqrXsqr: { + shader: sqrXsqrShaderWGL1, + uniforms: { + uFreq: 1.0, + uPhase: 0.0, + uColor: [1., 1., 1.], + uAlpha: 1.0 + } + }, + circle: { + shader: circleShaderWGL1, + uniforms: { + uRadius: 1.0, + uColor: [1., 1., 1.], + uAlpha: 1.0 + } + }, + gauss: { + shader: gaussShaderWGL1, + uniforms: { + uA: 1.0, + uB: 0.0, + uC: 0.16, + uColor: [1., 1., 1.], + uAlpha: 1.0 + } + }, + cross: { + shader: crossShaderWGL1, + uniforms: { + uThickness: 0.2, + uColor: [1., 1., 1.], + uAlpha: 1.0 + } + }, + radRamp: { + shader: radRampShaderWGL1, + uniforms: { + uSqueeze: 1.0, + uColor: [1., 1., 1.], + uAlpha: 1.0 + } + }, + raisedCos: { + shader: raisedCosShaderWGL1, + uniforms: { + uBeta: 0.25, + uPeriod: 0.625, + uColor: [1., 1., 1.], + uAlpha: 1.0 + } + }, + radialStim: { + shader: radialStimWGL1, + uniforms: { + uFreq: 20.0, + uPhase: 0.0, + uColor: [1., 1., 1.], + uAlpha: 1.0 + } + } + }; + /** * Default size of the Grating Stimuli in pixels. * @@ -533,9 +667,21 @@ export class GratingStim extends VisualStim 2 ); geometry.addIndex([0, 1, 2, 0, 2, 3]); - const vertexSrc = defaultQuadVert; - const fragmentSrc = GratingStim.#SHADERS[shaderName].shader; - const uniformsFinal = Object.assign({}, GratingStim.#SHADERS[shaderName].uniforms, uniforms); + let vertexSrc; + let fragmentSrc; + let uniformsFinal; + if (this._win._renderer.context.webGLVersion >= 2) + { + vertexSrc = defaultQuadVert; + fragmentSrc = GratingStim.#SHADERS[shaderName].shader; + uniformsFinal = Object.assign({}, GratingStim.#SHADERS[shaderName].uniforms, uniforms); + } + else + { + vertexSrc = defaultQuadVertWGL1; + fragmentSrc = GratingStim.#SHADERSWGL1[shaderName].shader; + uniformsFinal = Object.assign({}, GratingStim.#SHADERSWGL1[shaderName].uniforms, uniforms); + } const shader = PIXI.Shader.from(vertexSrc, fragmentSrc, uniformsFinal); return new PIXI.Mesh(geometry, shader); } diff --git a/src/visual/shaders/wgl1/circleShader.frag b/src/visual/shaders/wgl1/circleShader.frag new file mode 100644 index 0000000..ca321e5 --- /dev/null +++ b/src/visual/shaders/wgl1/circleShader.frag @@ -0,0 +1,26 @@ +/** + * Circle Shape. + * + * @author Nikita Agafonov + * @copyright (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + * @description Creates a filled circle shape with sharp edges. + * @usedby GratingStim.js + */ + +precision mediump float; + +varying vec2 vUvs; + +#define M_PI 3.14159265358979 +uniform float uRadius; +uniform vec3 uColor; +uniform float uAlpha; + +void main() { + vec2 uv = vUvs; + // converting first to [-1, 1] space to get the proper color functionality + // then back to [0, 1] + float s = (1. - step(uRadius, length(uv * 2. - 1.))) * 2. - 1.; + gl_FragColor = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; +} diff --git a/src/visual/shaders/wgl1/crossShader.frag b/src/visual/shaders/wgl1/crossShader.frag new file mode 100644 index 0000000..1a00b78 --- /dev/null +++ b/src/visual/shaders/wgl1/crossShader.frag @@ -0,0 +1,28 @@ +/** + * Cross Shape. + * + * @author Nikita Agafonov + * @copyright (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + * @description Creates a filled cross shape with sharp edges. + * @usedby GratingStim.js + */ + +precision mediump float; + +varying vec2 vUvs; + +#define M_PI 3.14159265358979 +uniform float uThickness; +uniform vec3 uColor; +uniform float uAlpha; + +void main() { + vec2 uv = vUvs; + float sx = step(uThickness, length(uv.x * 2. - 1.)); + float sy = step(uThickness, length(uv.y * 2. - 1.)); + // converting first to [-1, 1] space to get the proper color functionality + // then back to [0, 1] + float s = (1. - sx * sy) * 2. - 1.; + gl_FragColor = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; +} diff --git a/src/visual/shaders/wgl1/defaultQuad.vert b/src/visual/shaders/wgl1/defaultQuad.vert new file mode 100644 index 0000000..b4dc9ae --- /dev/null +++ b/src/visual/shaders/wgl1/defaultQuad.vert @@ -0,0 +1,13 @@ +precision mediump float; + +attribute vec2 aVertexPosition; +attribute vec2 aUvs; +varying vec2 vUvs; + +uniform mat3 translationMatrix; +uniform mat3 projectionMatrix; + +void main() { + vUvs = aUvs; + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); +} diff --git a/src/visual/shaders/wgl1/gaussShader.frag b/src/visual/shaders/wgl1/gaussShader.frag new file mode 100644 index 0000000..27ca741 --- /dev/null +++ b/src/visual/shaders/wgl1/gaussShader.frag @@ -0,0 +1,32 @@ +/** + * Gaussian Function. + * https://en.wikipedia.org/wiki/Gaussian_function + * + * @author Nikita Agafonov + * @copyright (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + * @description Creates a 2d Gaussian image as if 1d Gaussian graph was rotated arount Y axis and observed from above. + * @usedby GratingStim.js + */ + +precision mediump float; + +varying vec2 vUvs; + +uniform float uA; +uniform float uB; +uniform float uC; +uniform vec3 uColor; +uniform float uAlpha; + +#define M_PI 3.14159265358979 + +void main() { + vec2 uv = vUvs; + float c2 = uC * uC; + float x = length(uv - .5); + // converting first to [-1, 1] space to get the proper color functionality + // then back to [0, 1] + float g = uA * exp(-pow(x - uB, 2.) / c2 * .5) * 2. - 1.; + gl_FragColor = vec4(vec3(g) * uColor * .5 + .5, 1.) * uAlpha; +} diff --git a/src/visual/shaders/wgl1/imageShader.frag b/src/visual/shaders/wgl1/imageShader.frag new file mode 100644 index 0000000..b2f876c --- /dev/null +++ b/src/visual/shaders/wgl1/imageShader.frag @@ -0,0 +1,29 @@ +/** + * Image shader. + * + * @author Nikita Agafonov + * @copyright (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + * @description Renders passed in image with applied effects. + * @usedby GratingStim.js + */ + +precision mediump float; + +varying vec2 vUvs; + +#define M_PI 3.14159265358979 +uniform sampler2D uTex; +uniform float uFreq; +uniform float uPhase; +uniform vec3 uColor; +uniform float uAlpha; + +void main() { + vec2 uv = vUvs; + // converting first to [-1, 1] space to get the proper color functionality + // then back to [0, 1] + vec4 s = texture2D(uTex, vec2(uv.x * uFreq + uPhase, uv.y)); + s.xyz = s.xyz * 2. - 1.; + gl_FragColor = vec4(s.xyz * uColor * .5 + .5, s.a) * uAlpha; +} diff --git a/src/visual/shaders/wgl1/radRampShader.frag b/src/visual/shaders/wgl1/radRampShader.frag new file mode 100644 index 0000000..8849bc8 --- /dev/null +++ b/src/visual/shaders/wgl1/radRampShader.frag @@ -0,0 +1,27 @@ +/** + * Radial Ramp. + * + * @author Nikita Agafonov + * @copyright (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + * @description Creates 2d radial ramp image. + * @usedby GratingStim.js + */ + +precision mediump float; + +varying vec2 vUvs; + +uniform float uSqueeze; +uniform vec3 uColor; +uniform float uAlpha; + +#define M_PI 3.14159265358979 + +void main() { + vec2 uv = vUvs; + // converting first to [-1, 1] space to get the proper color functionality + // then back to [0, 1] + float s = (1. - length(uv * 2. - 1.) * uSqueeze) * 2. - 1.; + gl_FragColor = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; +} diff --git a/src/visual/shaders/wgl1/radialShader.frag b/src/visual/shaders/wgl1/radialShader.frag new file mode 100644 index 0000000..e26edde --- /dev/null +++ b/src/visual/shaders/wgl1/radialShader.frag @@ -0,0 +1,35 @@ +/** + * Radial grating. + * + * @author Nikita Agafonov + * @copyright (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + * @description Creates 2d radial grating image. Based on https://www.shadertoy.com/view/wtjGzt + * @usedby GratingStim.js + */ + +precision mediump float; + +varying vec2 vUvs; + +uniform float uFreq; +uniform float uPhase; +uniform vec3 uColor; +uniform float uAlpha; + +#define M_PI 3.14159265358979 +#define PI2 2.* M_PI + +float aastep(float x) { // --- antialiased step(.5) + float w = fwidth(x); // pixel width. NB: x must not be discontinuous or factor discont out + return smoothstep(.7,-.7,(abs(fract(x-.25)-.5)-.25)/w); // just use (offseted) smooth squares +} + +void main() { + vec2 uv = vUvs * 2. - 1.; + // converting first to [-1, 1] space to get the proper color functionality + // then back to [0, 1] + float v = uFreq * atan(uv.y, uv.x) / 6.28; + float s = aastep(v) * 2. - 1.; + gl_FragColor = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; +} diff --git a/src/visual/shaders/wgl1/raisedCosShader.frag b/src/visual/shaders/wgl1/raisedCosShader.frag new file mode 100644 index 0000000..58c7764 --- /dev/null +++ b/src/visual/shaders/wgl1/raisedCosShader.frag @@ -0,0 +1,38 @@ +/** + * Raised-cosine. + * https://en.wikipedia.org/wiki/Raised-cosine_filter + * + * @author Nikita Agafonov + * @copyright (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + * @description Creates 2d raised-cosine image as if 1d raised-cosine graph was rotated around Y axis and observed from above. + * @usedby GratingStim.js + */ + +precision mediump float; + +varying vec2 vUvs; + +#define M_PI 3.14159265358979 +uniform float uBeta; +uniform float uPeriod; +uniform vec3 uColor; +uniform float uAlpha; + +void main() { + vec2 uv = vUvs; + float absX = length(uv * 2. - 1.); + float edgeArgument1 = (1. - uBeta) / (2. * uPeriod); + float edgeArgument2 = (1. + uBeta) / (2. * uPeriod); + float frequencyFactor = (M_PI * uPeriod) / uBeta; + float s = .5 * (1. + cos(frequencyFactor * (absX - edgeArgument1))); + if (absX <= edgeArgument1) { + s = 1.; + } else if (absX > edgeArgument2) { + s = 0.; + } + // converting first to [-1, 1] space to get the proper color functionality + // then back to [0, 1] + s = s * 2. - 1.; + gl_FragColor = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; +} diff --git a/src/visual/shaders/wgl1/sawShader.frag b/src/visual/shaders/wgl1/sawShader.frag new file mode 100644 index 0000000..239755d --- /dev/null +++ b/src/visual/shaders/wgl1/sawShader.frag @@ -0,0 +1,29 @@ +/** + * Sawtooth wave. + * https://en.wikipedia.org/wiki/Sawtooth_wave + * + * @author Nikita Agafonov + * @copyright (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + * @description Creates 2d sawtooth wave image as if 1d sawtooth graph was extended across Z axis and observed from above. + * @usedby GratingStim.js + */ + +precision mediump float; + +varying vec2 vUvs; + +#define M_PI 3.14159265358979 +uniform float uFreq; +uniform float uPhase; +uniform vec3 uColor; +uniform float uAlpha; + +void main() { + vec2 uv = vUvs; + float s = uFreq * uv.x + uPhase; + // converting first to [-1, 1] space to get the proper color functionality + // then back to [0, 1] + s = mod(s, 1.) * 2. - 1.; + gl_FragColor = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; +} diff --git a/src/visual/shaders/wgl1/sinShader.frag b/src/visual/shaders/wgl1/sinShader.frag new file mode 100644 index 0000000..25f4594 --- /dev/null +++ b/src/visual/shaders/wgl1/sinShader.frag @@ -0,0 +1,27 @@ +/** + * Sine wave. + * https://en.wikipedia.org/wiki/Sine_wave + * + * @author Nikita Agafonov + * @copyright (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + * @description Creates 2d sine wave image as if 1d sine graph was extended across Z axis and observed from above. + * @usedby GratingStim.js + */ + +precision mediump float; + +varying vec2 vUvs; + +#define M_PI 3.14159265358979 +uniform float uFreq; +uniform float uPhase; +uniform vec3 uColor; +uniform float uAlpha; + +void main() { + vec2 uv = vUvs - .25; + float s = sin((uFreq * uv.x + uPhase) * 2. * M_PI); + // it's important to convert to [0, 1] while multiplying to uColor, not before, to preserve desired coloring functionality + gl_FragColor = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; +} diff --git a/src/visual/shaders/wgl1/sinXsinShader.frag b/src/visual/shaders/wgl1/sinXsinShader.frag new file mode 100644 index 0000000..74915b6 --- /dev/null +++ b/src/visual/shaders/wgl1/sinXsinShader.frag @@ -0,0 +1,30 @@ +/** + * Sine wave multiplied by another sine wave. + * https://en.wikipedia.org/wiki/Sine_wave + * + * @author Nikita Agafonov + * @copyright (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + * @description Creates an image of two 2d sine waves multiplied with each other. + * @usedby GratingStim.js + */ + +precision mediump float; + +varying vec2 vUvs; + +#define M_PI 3.14159265358979 +#define PI2 2.* M_PI +uniform float uFreq; +uniform float uPhase; +uniform vec3 uColor; +uniform float uAlpha; + +void main() { + vec2 uv = vec2(vUvs.x - .25, vUvs.y * -1. - .25); + float sx = sin((uFreq * uv.x + uPhase) * PI2); + float sy = sin((uFreq * uv.y + uPhase) * PI2); + float s = sx * sy; + // it's important to convert to [0, 1] while multiplying to uColor, not before, to preserve desired coloring functionality + gl_FragColor = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; +} diff --git a/src/visual/shaders/wgl1/sqrShader.frag b/src/visual/shaders/wgl1/sqrShader.frag new file mode 100644 index 0000000..3e169fc --- /dev/null +++ b/src/visual/shaders/wgl1/sqrShader.frag @@ -0,0 +1,27 @@ +/** + * Square wave. + * https://en.wikipedia.org/wiki/Square_wave + * + * @author Nikita Agafonov + * @copyright (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + * @description Creates 2d square wave image as if 1d square graph was extended across Z axis and observed from above. + * @usedby GratingStim.js + */ + +precision mediump float; + +varying vec2 vUvs; + +#define M_PI 3.14159265358979 +uniform float uFreq; +uniform float uPhase; +uniform vec3 uColor; +uniform float uAlpha; + +void main() { + vec2 uv = vUvs - .25; + float s = sign(sin((uFreq * uv.x + uPhase) * 2. * M_PI)); + // it's important to convert to [0, 1] while multiplying to uColor, not before, to preserve desired coloring functionality + gl_FragColor = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; +} diff --git a/src/visual/shaders/wgl1/sqrXsqrShader.frag b/src/visual/shaders/wgl1/sqrXsqrShader.frag new file mode 100644 index 0000000..56f145c --- /dev/null +++ b/src/visual/shaders/wgl1/sqrXsqrShader.frag @@ -0,0 +1,30 @@ +/** + * Square wave multiplied by another square wave. + * https://en.wikipedia.org/wiki/Square_wave + * + * @author Nikita Agafonov + * @copyright (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + * @description Creates an image of two 2d square waves multiplied with each other. + * @usedby GratingStim.js + */ + +precision mediump float; + +varying vec2 vUvs; + +#define M_PI 3.14159265358979 +#define PI2 2.* M_PI +uniform float uFreq; +uniform float uPhase; +uniform vec3 uColor; +uniform float uAlpha; + +void main() { + vec2 uv = vec2(vUvs.x - .25, vUvs.y * -1. - .25); + float sx = sign(sin((uFreq * uv.x + uPhase) * PI2)); + float sy = sign(sin((uFreq * uv.y + uPhase) * PI2)); + float s = sx * sy; + // it's important to convert to [0, 1] while multiplying to uColor, not before, to preserve desired coloring functionality + gl_FragColor = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; +} diff --git a/src/visual/shaders/wgl1/triShader.frag b/src/visual/shaders/wgl1/triShader.frag new file mode 100644 index 0000000..30469b5 --- /dev/null +++ b/src/visual/shaders/wgl1/triShader.frag @@ -0,0 +1,30 @@ +/** + * Triangle wave. + * https://en.wikipedia.org/wiki/Triangle_wave + * + * @author Nikita Agafonov + * @copyright (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + * @description Creates 2d triangle wave image as if 1d triangle graph was extended across Z axis and observed from above. + * @usedby GratingStim.js + */ + +precision mediump float; + +varying vec2 vUvs; + +#define M_PI 3.14159265358979 +uniform float uFreq; +uniform float uPhase; +uniform float uPeriod; +uniform vec3 uColor; +uniform float uAlpha; + +void main() { + vec2 uv = vUvs; + float s = uFreq * uv.x + uPhase; + // converting first to [-1, 1] space to get the proper color functionality + // then back to [0, 1] + s = (2. * abs(s / uPeriod - floor(s / uPeriod + .5))) * 2. - 1.; + gl_FragColor = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; +} From 0d9ca6ba14761b4cd4fb706925b0c352fed15fd8 Mon Sep 17 00:00:00 2001 From: lgtst Date: Fri, 7 Oct 2022 23:21:31 +0100 Subject: [PATCH 11/41] manual derivatives calculation for radial stim; --- src/visual/GratingStim.js | 4 +++- src/visual/shaders/wgl1/radialShader.frag | 16 +++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/visual/GratingStim.js b/src/visual/GratingStim.js index 4b239cf..e8a6df0 100644 --- a/src/visual/GratingStim.js +++ b/src/visual/GratingStim.js @@ -2,7 +2,7 @@ * Grating Stimulus. * * @author Nikita Agafonov - * @version 2021.2.3 + * @version 2022.3.0 * @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) * @license Distributed under the terms of the MIT License */ @@ -380,6 +380,8 @@ export class GratingStim extends VisualStim shader: radialStimWGL1, uniforms: { uFreq: 20.0, + uStep: .0017, + uDX: 1., uPhase: 0.0, uColor: [1., 1., 1.], uAlpha: 1.0 diff --git a/src/visual/shaders/wgl1/radialShader.frag b/src/visual/shaders/wgl1/radialShader.frag index e26edde..ce38588 100644 --- a/src/visual/shaders/wgl1/radialShader.frag +++ b/src/visual/shaders/wgl1/radialShader.frag @@ -7,7 +7,6 @@ * @description Creates 2d radial grating image. Based on https://www.shadertoy.com/view/wtjGzt * @usedby GratingStim.js */ - precision mediump float; varying vec2 vUvs; @@ -16,20 +15,27 @@ uniform float uFreq; uniform float uPhase; uniform vec3 uColor; uniform float uAlpha; +uniform float uStep; +uniform float uDX; #define M_PI 3.14159265358979 #define PI2 2.* M_PI -float aastep(float x) { // --- antialiased step(.5) - float w = fwidth(x); // pixel width. NB: x must not be discontinuous or factor discont out +float aastep(float x, float w) { // --- antialiased step(.5) return smoothstep(.7,-.7,(abs(fract(x-.25)-.5)-.25)/w); // just use (offseted) smooth squares } void main() { vec2 uv = vUvs * 2. - 1.; + float v = uFreq * atan(uv.y, uv.x) / 6.28; + // WGL1 has dFdx, dFdy and fwidth() defined as part of OES_standard_derivatives extension. + // BUT using this extension fails due to how currently used version of PIXI goes about shader program compilation. + // Calculating derivatives manually instead. + float dF_dx = (uFreq * (atan(uv.y, uv.x + uStep) - atan(uv.y, uv.x - uStep)) / 6.28) / uDX; + float dF_dy = (uFreq * (atan(uv.y + uStep, uv.x) - atan(uv.y - uStep, uv.x)) / 6.28) / uDX; + float w = abs(dF_dx) + abs(dF_dy); // converting first to [-1, 1] space to get the proper color functionality // then back to [0, 1] - float v = uFreq * atan(uv.y, uv.x) / 6.28; - float s = aastep(v) * 2. - 1.; + float s = aastep(v, w) * 2. - 1.; gl_FragColor = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; } From 2dbfc9c43c4a8e0c65c3273181f1b0671a064cb2 Mon Sep 17 00:00:00 2001 From: lgtst Date: Mon, 24 Oct 2022 13:19:44 +0100 Subject: [PATCH 12/41] xlsx upgrade due to a string parsing bug; --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7a9b92c..6477532 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "pixi.js-legacy": "^6.0.4", "seedrandom": "^3.0.5", "tone": "^14.7.77", - "xlsx": "^0.17.0" + "xlsx": "^0.18.5" }, "devDependencies": { "csslint": "^1.0.5", From ac19ad0c19c852f0cfe814e275b42f48978b652c Mon Sep 17 00:00:00 2001 From: Alain Pitiot Date: Tue, 22 Nov 2022 14:04:02 +0100 Subject: [PATCH 13/41] Update ServerManager.js --- src/core/ServerManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/ServerManager.js b/src/core/ServerManager.js index 07a49cb..8c82457 100644 --- a/src/core/ServerManager.js +++ b/src/core/ServerManager.js @@ -650,7 +650,7 @@ export class ServerManager extends PsychObject && (path.indexOf("pavlovia.org") === -1) ) { - path = "https://devlovia.org/api/v2/proxy/" + path; + path = "https://pavlovia.org/api/v2/proxy/" + path; } const pathStatusData = this._resources.get(name); From bb36bee54296e353dc75883b56557ebda5fc4acf Mon Sep 17 00:00:00 2001 From: Alain Pitiot Date: Tue, 29 Nov 2022 09:45:07 +0100 Subject: [PATCH 14/41] NF: conditional check for hardware accelerated WebGL support in PsychoJS constructor --- src/core/GUI.js | 35 +++++++++++++++++++++++++++++--- src/core/PsychoJS.js | 42 ++++++++++++++++++++++++++++++++------- src/core/ServerManager.js | 2 +- src/util/Scheduler.js | 2 +- 4 files changed, 69 insertions(+), 12 deletions(-) diff --git a/src/core/GUI.js b/src/core/GUI.js index 2630a26..90bd3d1 100644 --- a/src/core/GUI.js +++ b/src/core/GUI.js @@ -266,6 +266,9 @@ export class GUI /** * @callback GUI.onOK */ + /** + * @callback GUI.onCancel + */ /** * Show a message to the participant in a dialog box. * @@ -275,15 +278,19 @@ export class GUI * @param {string} options.message - the message to be displayed * @param {Object.} options.error - an exception * @param {string} options.warning - a warning message - * @param {boolean} [options.showOK=true] - specifies whether to show the OK button + * @param {boolean} [options.showOK=true] - whether to show the OK button * @param {GUI.onOK} [options.onOK] - function called when the participant presses the OK button + * @param {boolean} [options.showCancel=false] - whether to show the Cancel button + * @param {GUI.onCancel} [options.onCancel] - function called when the participant presses the Cancel button */ dialog({ message, warning, error, showOK = true, - onOK + onOK, + showCancel = false, + onCancel } = {}) { // close the previously opened dialog box, if there is one: @@ -364,9 +371,17 @@ export class GUI markup += `

    ${message}

    `; } + if (showOK || showCancel) + { + markup += "
    "; + } + if (showCancel) + { + markup += ""; + } if (showOK) { - markup += "
    "; + markup += ""; } markup += ""; @@ -394,6 +409,20 @@ export class GUI } }; } + if (showCancel) + { + this._cancelButton = document.getElementById("dialogCancel"); + this._cancelButton.onclick = () => + { + this.closeDialog(); + + // execute callback function: + if (typeof onCancel !== "undefined") + { + onCancel(); + } + }; + } } /** diff --git a/src/core/PsychoJS.js b/src/core/PsychoJS.js index 31eaad4..ab32fdf 100644 --- a/src/core/PsychoJS.js +++ b/src/core/PsychoJS.js @@ -119,7 +119,8 @@ export class PsychoJS topLevelStatus = true, autoStartScheduler = true, saveResults = true, - captureErrors = true + captureErrors = true, + checkWebGLSupport = false } = {}) { // logging: @@ -178,6 +179,9 @@ export class PsychoJS // whether to start the scheduler when the experiment starts: this._autoStartScheduler = autoStartScheduler; + // whether to check for actual hardware accelerated WebGL support: + this._checkWebGLSupport = checkWebGLSupport; + // whether to save results at the end of the experiment: this._saveResults = saveResults; @@ -415,19 +419,43 @@ export class PsychoJS // start the asynchronous download of resources: this._serverManager.prepareResources(resources); - // start the experiment: - this.status = PsychoJS.Status.STARTED; - this.logger.info("[PsychoJS] Start Experiment."); - if (this._autoStartScheduler) + // if WebGL is not actually available, warn the participant and ask them whether they want to go ahead + if (this._checkWebGLSupport && !Window.checkWebGLSupport()) { - await this._scheduler.start(); + // add an entry to experiment results to warn the designer about a potential WebGL issue: + this._experiment.addData('hardware_acceleration', 'NOT SUPPORTED'); + this._experiment.nextEntry(); + + this._gui.dialog({ + warning: "It appears that hardware acceleration is either not supported by your browser or currently switched off.
    As a consequence, this experiment will be rendered using software emulation and advanced features, such as gratings and gamma correction, will not be available.

    You may want to press Cancel, change your browser settings, and reload the experiment. Otherwise press OK to proceed as is.", + showCancel: true, + onCancel: () => + { + this.quit(); + }, + onOK: () => + { + this.status = PsychoJS.Status.STARTED; + this.logger.info("[PsychoJS] Start Experiment (software emulation mode)."); + this._scheduler.start(); + } + }); } + else + { + if (this._autoStartScheduler) + { + this.status = PsychoJS.Status.STARTED; + this.logger.info("[PsychoJS] Start Experiment."); + this._scheduler.start(); + } + } + } catch (error) { this.status = PsychoJS.Status.ERROR; throw { ...response, error }; - // this._gui.dialog({ error: { ...response, error } }); } } diff --git a/src/core/ServerManager.js b/src/core/ServerManager.js index 07a49cb..8c82457 100644 --- a/src/core/ServerManager.js +++ b/src/core/ServerManager.js @@ -650,7 +650,7 @@ export class ServerManager extends PsychObject && (path.indexOf("pavlovia.org") === -1) ) { - path = "https://devlovia.org/api/v2/proxy/" + path; + path = "https://pavlovia.org/api/v2/proxy/" + path; } const pathStatusData = this._resources.get(name); diff --git a/src/util/Scheduler.js b/src/util/Scheduler.js index c7002ec..bad709c 100644 --- a/src/util/Scheduler.js +++ b/src/util/Scheduler.js @@ -118,7 +118,7 @@ export class Scheduler * *

    Note: tasks are run after each animation frame.

    */ - async start() + start() { const self = this; const update = async (timestamp) => From 395e7fba41e2a9ee5574cae31feedb99adf44c8d Mon Sep 17 00:00:00 2001 From: lgtst Date: Tue, 29 Nov 2022 13:24:02 +0000 Subject: [PATCH 15/41] added checkWebGLSupport flag check in openWindow(); --- src/core/Window.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/Window.js b/src/core/Window.js index 6d961b3..135eb24 100644 --- a/src/core/Window.js +++ b/src/core/Window.js @@ -108,6 +108,12 @@ export class Window extends PsychObject this._addAttribute("autoLog", autoLog); this._addAttribute("size", []); + if (this._psychoJS._checkWebGLSupport) + { + // see checkWebGLSupport() method for details. + PIXI.settings.FAIL_IF_MAJOR_PERFORMANCE_CAVEAT = true; + } + // setup PIXI: this._setupPixi(); From 1f6a816fbf3158698d869f2f5a6863cbdef57dda Mon Sep 17 00:00:00 2001 From: lgtst Date: Wed, 30 Nov 2022 09:49:29 +0000 Subject: [PATCH 16/41] wgl fail if perf issues flag moved to setupPixi(); --- src/core/Window.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/core/Window.js b/src/core/Window.js index 135eb24..cb6acbe 100644 --- a/src/core/Window.js +++ b/src/core/Window.js @@ -108,12 +108,6 @@ export class Window extends PsychObject this._addAttribute("autoLog", autoLog); this._addAttribute("size", []); - if (this._psychoJS._checkWebGLSupport) - { - // see checkWebGLSupport() method for details. - PIXI.settings.FAIL_IF_MAJOR_PERFORMANCE_CAVEAT = true; - } - // setup PIXI: this._setupPixi(); @@ -442,6 +436,12 @@ export class Window extends PsychObject this._size[0] = window.innerWidth; this._size[1] = window.innerHeight; + if (this._psychoJS._checkWebGLSupport) + { + // see checkWebGLSupport() method for details. + PIXI.settings.FAIL_IF_MAJOR_PERFORMANCE_CAVEAT = true; + } + // create a PIXI renderer and add it to the document: this._renderer = PIXI.autoDetectRenderer({ width: this._size[0], From be2f24f4c595e750e0bd635493d2779100177762 Mon Sep 17 00:00:00 2001 From: Alain Pitiot Date: Thu, 5 Jan 2023 11:29:30 +0100 Subject: [PATCH 17/41] ENH Survey: better management of vendor libraries --- src/core/PsychoJS.js | 5 +++-- src/core/ServerManager.js | 31 +++++++++++++++++++++++++++++++ src/util/Clock.js | 13 ++++++------- src/util/Util.js | 5 ----- src/visual/Survey.js | 7 ++++++- 5 files changed, 46 insertions(+), 15 deletions(-) diff --git a/src/core/PsychoJS.js b/src/core/PsychoJS.js index ab32fdf..ef5ef7f 100644 --- a/src/core/PsychoJS.js +++ b/src/core/PsychoJS.js @@ -789,10 +789,11 @@ export class PsychoJS const self = this; window.onerror = function(message, source, lineno, colno, error) - { + {console.log('@@@', message) // check for ResizeObserver loop limit exceeded error: // ref: https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded - if (message === "ResizeObserver loop limit exceeded") + if (message === "ResizeObserver loop limit exceeded" || + message === "ResizeObserver loop completed with undelivered notifications.") { console.warn(message); return true; diff --git a/src/core/ServerManager.js b/src/core/ServerManager.js index 8c82457..1b59a99 100644 --- a/src/core/ServerManager.js +++ b/src/core/ServerManager.js @@ -535,6 +535,37 @@ export class ServerManager extends PsychObject download: true }; } + + // deal with survey libraries: + if ("surveyLibrary" in resource) + { + // add the SurveyJS and PsychoJS Survey .js and .css resources: + resources[r] = { + name: "jquery-3.6.0.min.js", + path: "./lib/vendors/jquery-3.6.0.min.js", + download: true + }; + resources.push({ + name: "survey.jquery-1.9.50.min.js", + path: "./lib/vendors/survey.jquery-1.9.50.min.js", + download: true + }); + resources.push({ + name: "survey.defaultV2-1.9.50.min.css", + path: "./lib/vendors/survey.defaultV2-1.9.50.min.css", + download: true + }); + resources.push({ + name: "survey.widgets.css", + path: "./lib/vendors/survey.widgets.css", + download: true + }); + resources.push({ + name: "survey.grey_style.css", + path: "./lib/vendors/survey.grey_style.css", + download: true + }); + } } for (let { name, path, download } of resources) diff --git a/src/util/Clock.js b/src/util/Clock.js index cd89800..3e92b5d 100644 --- a/src/util/Clock.js +++ b/src/util/Clock.js @@ -58,13 +58,12 @@ export class MonotonicClock *

    Note: This is just a convenience wrapper around `Intl.DateTimeFormat()`.

    * * @param {string|array.string} locales - A string with a BCP 47 language tag, or an array of such strings. - * @param {object} options - An object with detailed date and time styling information. + * @param {object} [options] - An object with detailed date and time styling information. * @return {string} The current timestamp in the chosen format. */ - static getDate(locales = "en-CA", optionsMaybe) + static getDate(locales = "en-CA", options) { - const date = new Date(); - const options = Object.assign({ + const dataTimeOptions = Object.assign({ hour12: false, year: "numeric", month: "2-digit", @@ -73,10 +72,10 @@ export class MonotonicClock minute: "numeric", second: "numeric", fractionalSecondDigits: 3, - }, optionsMaybe); - - const dateTimeFormat = new Intl.DateTimeFormat(locales, options); + }, options); + const dateTimeFormat = new Intl.DateTimeFormat(locales, dataTimeOptions); + const date = new Date(); return dateTimeFormat.format(date); } diff --git a/src/util/Util.js b/src/util/Util.js index 8d82efc..02a6133 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -1434,11 +1434,6 @@ export function loadCss(cssId, cssPath) link.media = "all"; head.appendChild(link); } - - /* document.getElementsByTagName("head")[0].insertAdjacentHTML( - "beforeend", - `` - ); */ } /** diff --git a/src/visual/Survey.js b/src/visual/Survey.js index 2d19909..f0261d1 100644 --- a/src/visual/Survey.js +++ b/src/visual/Survey.js @@ -52,6 +52,12 @@ export class Survey extends VisualStim { super({ name, win, units, ori, depth, pos, size, autoDraw, autoLog }); + // default size: + if (typeof size === "undefined") + { + this.size = (this.unit === "norm") ? [2.0, 2.0] : [1.0, 1.0]; + } + // init SurveyJS this._initSurveyJS(); @@ -392,7 +398,6 @@ export class Survey extends VisualStim if (typeof this._surveyModel !== "undefined") { this._startSurvey(surveyId, this._surveyModel); - // jQuery(`#${surveyId}`).Survey({model: this._surveyModel}); } } From be5292480b10c47470cff865b74c4c15284b8935 Mon Sep 17 00:00:00 2001 From: Alain Pitiot Date: Thu, 5 Jan 2023 11:31:26 +0100 Subject: [PATCH 18/41] _ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5ffb4ca..3cfae9b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "psychojs", - "version": "2022.3.0", + "version": "2022.2.5", "private": true, "description": "Helps run in-browser neuroscience, psychology, and psychophysics experiments", "license": "MIT", From da9b892ead2abffd3bff12411d75a13cbe419d65 Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Fri, 13 Jan 2023 14:46:07 +0000 Subject: [PATCH 19/41] ENH: Alias "star" and "star7" --- src/visual/ShapeStim.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/visual/ShapeStim.js b/src/visual/ShapeStim.js index b5925e3..dad2807 100644 --- a/src/visual/ShapeStim.js +++ b/src/visual/ShapeStim.js @@ -395,3 +395,5 @@ ShapeStim.KnownShapes = { [0.5, 0.0], ], }; +// Alias some names for convenience +ShapeStim.KnownShapes['star'] = ShapeStim.KnownShapes['star7'] From a074ed34f0cfff8fa44d239a118e8ec740e1b4a9 Mon Sep 17 00:00:00 2001 From: lgtst Date: Thu, 26 Jan 2023 09:45:31 +0000 Subject: [PATCH 20/41] blurfilter for image stim v0; --- src/visual/ImageStim.js | 64 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/src/visual/ImageStim.js b/src/visual/ImageStim.js index f043579..8d73be3 100644 --- a/src/visual/ImageStim.js +++ b/src/visual/ImageStim.js @@ -47,10 +47,34 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) * @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip * @param {boolean} [options.autoLog= false] - whether or not to log */ - constructor({ name, win, image, mask, pos, anchor, units, ori, size, color, opacity, contrast, texRes, depth, interpolate, flipHoriz, flipVert, autoDraw, autoLog } = {}) + constructor({ + name, + win, + image, + mask, + pos, + anchor, + units, + ori, + size, + color, + opacity, + contrast, + texRes, + depth, + interpolate, + flipHoriz, + flipVert, + autoDraw, + autoLog, + blurVal + } = {}) { super({ name, win, units, ori, opacity, depth, pos, anchor, size, autoDraw, autoLog }); + // Holds an instance of PIXI blur filter. Used if blur value is passed. + this._blurFilter = undefined; + this._addAttribute( "image", image, @@ -94,6 +118,11 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) false, this._onChange(false, false), ); + this._addAttribute( + "blurVal", + blurVal, + 0 + ); // estimate the bounding box: this._estimateBoundingBox(); @@ -234,6 +263,33 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) } } + setBlurVal (blurVal = 0, log = false) + { + this._setAttribute("blurVal", blurVal, log); + if (this._pixi instanceof PIXI.Sprite) + { + if (this._blurFilter === undefined) + { + this._blurFilter = new PIXI.filters.BlurFilter(); + this._blurFilter.blur = blurVal; + } + else + { + this._blurFilter.blur = blurVal; + } + + // this._pixi might get destroyed and recreated again with no filters. + if (this._pixi.filters instanceof Array && this._pixi.filters.indexOf(this._blurFilter) === -1) + { + this._pixi.filters.push(this._blurFilter); + } + else + { + this._pixi.filters = [this._blurFilter]; + } + } + } + /** * Estimate the bounding box. * @@ -276,6 +332,7 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) if (typeof this._pixi !== "undefined") { + this._pixi.filters = null; this._pixi.destroy(true); } this._pixi = undefined; @@ -359,6 +416,11 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) this._pixi.position = to_pixiPoint(this.pos, this.units, this.win); this._pixi.rotation = -this.ori * Math.PI / 180; + if (this._blurVal > 0) + { + this.setBlurVal(this._blurVal); + } + // re-estimate the bounding box, as the texture's width may now be available: this._estimateBoundingBox(); } From c2be1a04ec74cb95ed54022e1f4a73aa7aa6c984 Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Thu, 26 Jan 2023 12:47:28 +0000 Subject: [PATCH 21/41] FF: Allow TextBox to accept "placeholder" as an input --- src/visual/TextBox.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/visual/TextBox.js b/src/visual/TextBox.js index 3930cbf..4d8e2bc 100644 --- a/src/visual/TextBox.js +++ b/src/visual/TextBox.js @@ -65,6 +65,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) opacity, depth, text, + placeholder, font, letterHeight, bold, @@ -98,7 +99,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) ); this._addAttribute( "placeholder", - text, + placeholder, "", this._onChange(true, true), ); From 9a294f8367dc027bbcd88ea37f76efefb8f24e2e Mon Sep 17 00:00:00 2001 From: lgtst Date: Sat, 28 Jan 2023 15:42:00 +0000 Subject: [PATCH 22/41] particle system v01. --- src/visual/ParticleSystem.js | 238 +++++++++++++++++++++++++++++++++++ src/visual/index.js | 1 + 2 files changed, 239 insertions(+) create mode 100644 src/visual/ParticleSystem.js diff --git a/src/visual/ParticleSystem.js b/src/visual/ParticleSystem.js new file mode 100644 index 0000000..c6c3074 --- /dev/null +++ b/src/visual/ParticleSystem.js @@ -0,0 +1,238 @@ +/** + * Grating Stimulus. + * + * @author Nikita Agafonov + * @version 2022.3.0 + * @copyright (c) 2020-2023 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + */ + +import * as PIXI from "pixi.js-legacy"; + +const DEFAULT_POOL_SIZE = 1024; +const DEFAULT_PARTICLE_WIDTH = 10; +const DEFAULT_PARTICLE_HEIGHT = 10; +const DEFAULT_PARTICLE_LIFETIME = 3; // ms +const DEFAULT_PARTICLE_COLOR = 0xffffff; +const DEFAULT_PARTICLES_PER_SEC = 60; +const DEFAULT_PARTICLE_ACCELERATION = 2500; + +class Particle +{ + constructor (cfg) + { + this.x = 0; + this.y = 0; + this.ax = 0; + this.ay = 0; + this.vx = 0; + this.vy = 0; + this.lifeTime = 0; + this.widthChange = 0; + this.heightChange = 0; + this.sprite = undefined; + this.inUse = false; + + if (cfg.particleImage !== undefined) + { + this.sprite = PIXI.Sprite.from(PIXI.Texture.from(cfg.particleImage)); + } + else + { + this.sprite = new PIXI.Sprite(PIXI.Texture.WHITE); + this.sprite.tint = cfg.particleColor || DEFAULT_PARTICLE_COLOR; + } + + // TODO: Should we instead incorporate that in position calculation? + // Consider: accurate spawn position of the particle confined by spawnArea. + this.sprite.anchor.set(0.5); + + this.width = cfg.width || DEFAULT_PARTICLE_WIDTH; + this.height = cfg.height || DEFAULT_PARTICLE_HEIGHT; + } + + set width (w) + { + this._width = w; + this.sprite.width = w; + } + + get width () + { + return this._width; + } + + set height (h) + { + this._height = h; + this.sprite.height = h; + } + + get height () + { + return this._height; + } + + update (dt) + { + const dt2 = dt ** 2; + + // Update position with current velocity. + this.x = this.x + this.vx * dt + this.ax * dt2 * .5; + this.y = this.y + this.vy * dt + this.ay * dt2 * .5; + + // Update velocity with current acceleration. + this.vx = this.ax * dt; + this.vy = this.ay * dt; + + this.sprite.x = this.x; + this.sprite.y = this.y; + + if (this.width > 0) + { + this.width = Math.max(0, this.width + this.widthChange); + } + + if (this.height > 0) + { + this.height = Math.max(0, this.height + this.heightChange); + } + this.lifeTime -= dt; + + if (this.width <= 0 && this.height <= 0) + { + this.lifeTime = 0; + } + + if (this.lifeTime <= 0) + { + this.inUse = false; + } + } +} + +export class ParticleSystem +{ + constructor (cfg = {}) + { + this.x = 0; + this.y = 0; + this._cfg = cfg; + this._particlesPerSec = cfg.particlesPerSec || DEFAULT_PARTICLES_PER_SEC; + this._spawnCoolDown = 0; + this._parentObj = undefined; + this._particlePool = new Array(DEFAULT_POOL_SIZE); + + if (cfg.parentObject !== undefined) + { + this._parentObj = cfg.parentObject; + } + + this._fillParticlePool(cfg); + } + + _fillParticlePool (cfg) + { + let i; + for (i = 0; i < this._particlePool.length; i++) + { + this._particlePool[i] = new Particle(cfg); + } + } + + _setupParticle (p) + { + let spawnAreaWidth = this._cfg.spawnAreaWidth || 0; + let spawnAreaHeight = this._cfg.spawnAreaHeight || 0; + + if (this._parentObj !== undefined && this._cfg.useParentSizeAsSpawnArea) + { + spawnAreaWidth = this._parentObj.width; + spawnAreaHeight = this._parentObj.height; + } + + const spawnOffsetX = Math.random() * spawnAreaWidth - spawnAreaWidth * .5; + const spawnOffsetY = Math.random() * spawnAreaHeight - spawnAreaHeight * .5; + const x = this.x + spawnOffsetX; + const y = this.y + spawnOffsetY; + + p.x = x; + p.y = y; + + p.ax = this._cfg.initialAx || Math.random() * DEFAULT_PARTICLE_ACCELERATION * 2.0 - DEFAULT_PARTICLE_ACCELERATION; + p.ay = this._cfg.initialAy || Math.random() * DEFAULT_PARTICLE_ACCELERATION * 2.0 - DEFAULT_PARTICLE_ACCELERATION; + p.vx = this._cfg.initialVx || 0; + p.vy = this._cfg.initialVy || 0; + p.lifeTime = this._cfg.lifeTime || DEFAULT_PARTICLE_LIFETIME; + p.width = this._cfg.width || DEFAULT_PARTICLE_WIDTH; + p.height = this._cfg.height || DEFAULT_PARTICLE_HEIGHT; + p.widthChange = this._cfg.widthChange || 0; + p.heightChange = this._cfg.heightChange || 0; + + if (this._cfg.particleColor !== undefined) + { + p.sprite.tint = this._cfg.particleColor; + } + else + { + p.sprite.tint = 0xffffff; + } + } + + _spawnParticles (n = 0) + { + let i; + for (i = 0; i < this._particlePool.length && n > 0; i++) + { + if (this._particlePool[i].inUse === false) + { + this._particlePool[i].inUse = true; + n--; + + this._setupParticle(this._particlePool[i]); + this._cfg.container.addChild(this._particlePool[i].sprite); + } + } + } + + update (dt) + { + // Sync with parent object if it exists. + if (this._parentObj !== undefined) + { + this.x = this._parentObj.x; + this.y = this._parentObj.y; + } + + if (this._spawnCoolDown <= 0) + { + this._spawnCoolDown = 1 / this._particlesPerSec; + + // Assuming that we have at least 60FPS. + const frameTime = Math.min(dt, 1 / 60); + const particlesPerFrame = Math.ceil(frameTime / this._spawnCoolDown); + + // TODO: figure out how to calc amount of particles when it's more than 1 per frame. + this._spawnParticles(particlesPerFrame); + } + else + { + this._spawnCoolDown -= dt; + } + + let i; + for (i = 0; i < this._particlePool.length; i++) + { + if (this._particlePool[i].inUse) + { + this._particlePool[i].update(dt); + } + + // Check if particle should be removed. + if (this._particlePool[i].lifeTime <= 0 && this._particlePool[i].sprite.parent) + { + this._cfg.container.removeChild(this._particlePool[i].sprite); + } + } + } +} diff --git a/src/visual/index.js b/src/visual/index.js index 8c604fa..9fd2574 100644 --- a/src/visual/index.js +++ b/src/visual/index.js @@ -13,3 +13,4 @@ export * from "./TextStim.js"; export * from "./VisualStim.js"; export * from "./FaceDetector.js"; export * from "./Survey.js"; +export * from "./ParticleSystem.js"; From 33967660ab8feefadf93f7d166a4ecfb315344b2 Mon Sep 17 00:00:00 2001 From: lgtst Date: Tue, 31 Jan 2023 07:21:38 +0000 Subject: [PATCH 23/41] particle emitter update. Now supports proper initial settings; particles orient themselves in the direction of velocity. --- .../{ParticleSystem.js => ParticleEmitter.js} | 145 ++++++++++++++---- src/visual/index.js | 2 +- 2 files changed, 120 insertions(+), 27 deletions(-) rename src/visual/{ParticleSystem.js => ParticleEmitter.js} (59%) diff --git a/src/visual/ParticleSystem.js b/src/visual/ParticleEmitter.js similarity index 59% rename from src/visual/ParticleSystem.js rename to src/visual/ParticleEmitter.js index c6c3074..ae7fb23 100644 --- a/src/visual/ParticleSystem.js +++ b/src/visual/ParticleEmitter.js @@ -12,10 +12,10 @@ import * as PIXI from "pixi.js-legacy"; const DEFAULT_POOL_SIZE = 1024; const DEFAULT_PARTICLE_WIDTH = 10; const DEFAULT_PARTICLE_HEIGHT = 10; -const DEFAULT_PARTICLE_LIFETIME = 3; // ms +const DEFAULT_PARTICLE_LIFETIME = 3; // Seconds. const DEFAULT_PARTICLE_COLOR = 0xffffff; const DEFAULT_PARTICLES_PER_SEC = 60; -const DEFAULT_PARTICLE_ACCELERATION = 2500; +const DEFAULT_PARTICLE_V = 100; class Particle { @@ -47,8 +47,8 @@ class Particle // Consider: accurate spawn position of the particle confined by spawnArea. this.sprite.anchor.set(0.5); - this.width = cfg.width || DEFAULT_PARTICLE_WIDTH; - this.height = cfg.height || DEFAULT_PARTICLE_HEIGHT; + this.width = cfg.particleWidth || DEFAULT_PARTICLE_WIDTH; + this.height = cfg.particleHeight || DEFAULT_PARTICLE_HEIGHT; } set width (w) @@ -75,15 +75,17 @@ class Particle update (dt) { - const dt2 = dt ** 2; + const dt2 = dt * dt; - // Update position with current velocity. + // Update velocity with current acceleration. + this.vx += this.ax * dt; + this.vy += this.ay * dt; + + // Update position with current velocity and acceleration. this.x = this.x + this.vx * dt + this.ax * dt2 * .5; this.y = this.y + this.vy * dt + this.ay * dt2 * .5; - // Update velocity with current acceleration. - this.vx = this.ax * dt; - this.vy = this.ay * dt; + this.sprite.rotation = Math.atan2(this.vy, this.vx); this.sprite.x = this.x; this.sprite.y = this.y; @@ -111,7 +113,7 @@ class Particle } } -export class ParticleSystem +export class ParticleEmitter { constructor (cfg = {}) { @@ -122,12 +124,7 @@ export class ParticleSystem this._spawnCoolDown = 0; this._parentObj = undefined; this._particlePool = new Array(DEFAULT_POOL_SIZE); - - if (cfg.parentObject !== undefined) - { - this._parentObj = cfg.parentObject; - } - + this.setParentObject(cfg.parentObject); this._fillParticlePool(cfg); } @@ -159,15 +156,50 @@ export class ParticleSystem p.x = x; p.y = y; - p.ax = this._cfg.initialAx || Math.random() * DEFAULT_PARTICLE_ACCELERATION * 2.0 - DEFAULT_PARTICLE_ACCELERATION; - p.ay = this._cfg.initialAy || Math.random() * DEFAULT_PARTICLE_ACCELERATION * 2.0 - DEFAULT_PARTICLE_ACCELERATION; - p.vx = this._cfg.initialVx || 0; - p.vy = this._cfg.initialVy || 0; + p.ax = 0; + p.ay = 0; + + if (Number.isFinite(this._cfg.initialVx)) + { + p.vx = this._cfg.initialVx; + } + else if (this._cfg.initialVx instanceof Array && this._cfg.initialVx.length >= 2) + { + p.vx = Math.random() * (this._cfg.initialVx[1] - this._cfg.initialVx[0]) + this._cfg.initialVx[0]; + } + else + { + p.vx = Math.random() * DEFAULT_PARTICLE_V - DEFAULT_PARTICLE_V * .5; + } + + if (Number.isFinite(this._cfg.initialVy)) + { + p.vy = this._cfg.initialVy; + } + else if (this._cfg.initialVy instanceof Array && this._cfg.initialVy.length >= 2) + { + p.vy = Math.random() * (this._cfg.initialVy[1] - this._cfg.initialVy[0]) + this._cfg.initialVy[0]; + } + else + { + p.vy = Math.random() * DEFAULT_PARTICLE_V - DEFAULT_PARTICLE_V * .5; + } + p.lifeTime = this._cfg.lifeTime || DEFAULT_PARTICLE_LIFETIME; - p.width = this._cfg.width || DEFAULT_PARTICLE_WIDTH; - p.height = this._cfg.height || DEFAULT_PARTICLE_HEIGHT; - p.widthChange = this._cfg.widthChange || 0; - p.heightChange = this._cfg.heightChange || 0; + p.width = this._cfg.particleWidth || DEFAULT_PARTICLE_WIDTH; + p.height = this._cfg.particleHeight || DEFAULT_PARTICLE_HEIGHT; + p.widthChange = this._cfg.particleWidthChange || 0; + p.heightChange = this._cfg.particleHeightChange || 0; + + // TODO: run proper checks here. + if (this._cfg.particleImage) + { + p.sprite.texture = PIXI.Texture.from(this._cfg.particleImage); + } + else + { + p.sprite.texture = PIXI.Texture.WHITE; + } if (this._cfg.particleColor !== undefined) { @@ -195,8 +227,58 @@ export class ParticleSystem } } + _getResultingExternalForce () + { + let externalForce = [0, 0]; + if (this._cfg.externalForces instanceof Array) + { + let i; + for (i = 0; i < this._cfg.externalForces.length; i++) + { + externalForce[0] += this._cfg.externalForces[i][0]; + externalForce[1] += this._cfg.externalForces[i][1]; + } + } + + return externalForce; + } + + setParentObject (po) + { + this._parentObj = po; + } + + /** + * @desc: Adds external force which acts on a particle + * @param: f - Array with two elements, first is x component, second is y component. + * It's a vector of length L which sets the direction and the margnitude of the force. + * */ + addExternalForce (f) + { + this._cfg.externalForces.push(f); + } + + removeExternalForce (f) + { + const i = this._cfg.externalForces.indexOf(f); + if (i !== -1) + { + this._cfg.externalForces.splice(i, 1); + } + } + + removeExternalForceByIdx (idx) + { + if (this._cfg.externalForces[idx] !== undefined) + { + this._cfg.externalForces.splice(idx, 1); + } + } + update (dt) { + let externalForce; + // Sync with parent object if it exists. if (this._parentObj !== undefined) { @@ -204,6 +286,16 @@ export class ParticleSystem this.y = this._parentObj.y; } + if (Number.isFinite(this._cfg.positionOffsetX)) + { + this.x += this._cfg.positionOffsetX; + } + + if (Number.isFinite(this._cfg.positionOffsetY)) + { + this.y += this._cfg.positionOffsetY; + } + if (this._spawnCoolDown <= 0) { this._spawnCoolDown = 1 / this._particlesPerSec; @@ -211,8 +303,6 @@ export class ParticleSystem // Assuming that we have at least 60FPS. const frameTime = Math.min(dt, 1 / 60); const particlesPerFrame = Math.ceil(frameTime / this._spawnCoolDown); - - // TODO: figure out how to calc amount of particles when it's more than 1 per frame. this._spawnParticles(particlesPerFrame); } else @@ -225,6 +315,9 @@ export class ParticleSystem { if (this._particlePool[i].inUse) { + externalForce = this._getResultingExternalForce(); + this._particlePool[i].ax = externalForce[0]; + this._particlePool[i].ay = externalForce[1]; this._particlePool[i].update(dt); } diff --git a/src/visual/index.js b/src/visual/index.js index 9fd2574..cbbf0a5 100644 --- a/src/visual/index.js +++ b/src/visual/index.js @@ -13,4 +13,4 @@ export * from "./TextStim.js"; export * from "./VisualStim.js"; export * from "./FaceDetector.js"; export * from "./Survey.js"; -export * from "./ParticleSystem.js"; +export * from "./ParticleEmitter.js"; From f87431e6af9ce3f8db73ced5f6a2e7fd52021f9d Mon Sep 17 00:00:00 2001 From: Alain Pitiot Date: Thu, 2 Feb 2023 12:49:26 +0100 Subject: [PATCH 24/41] ENH: Survey super-flow --- src/visual/Survey.js | 845 +++++++++++++++--- src/visual/survey/MaxDiffMatrix.js | 307 ------- src/visual/survey/SelectBox.js | 119 --- src/visual/survey/SideBySideMatrix.js | 424 --------- src/visual/survey/SliderStar.js | 289 ------ src/visual/survey/components/MatrixBipolar.js | 7 +- src/visual/survey/widgets/MaxDiffMatrix.js | 27 +- src/visual/survey/widgets/SelectBox.js | 22 +- src/visual/survey/widgets/SideBySideMatrix.js | 43 +- src/visual/survey/widgets/SliderStar.js | 8 +- src/visual/survey/widgets/SliderWidget.js | 1 + 11 files changed, 817 insertions(+), 1275 deletions(-) delete mode 100644 src/visual/survey/MaxDiffMatrix.js delete mode 100644 src/visual/survey/SelectBox.js delete mode 100644 src/visual/survey/SideBySideMatrix.js delete mode 100644 src/visual/survey/SliderStar.js diff --git a/src/visual/Survey.js b/src/visual/Survey.js index f0261d1..d4cf781 100644 --- a/src/visual/Survey.js +++ b/src/visual/Survey.js @@ -3,13 +3,12 @@ * * @author Alain Pitiot and Nikita Agafonov * @version 2022.3 - * @copyright (c) 2022 Open Science Tools Ltd. (https://opensciencetools.org) + * @copyright (c) 2023 Open Science Tools Ltd. (https://opensciencetools.org) * @license Distributed under the terms of the MIT License */ import * as PIXI from "pixi.js-legacy"; import { VisualStim } from "./VisualStim.js"; -import {PsychoJS} from "../core/PsychoJS.js"; import * as util from "../util/Util.js"; import {Clock} from "../util/Clock.js"; import {ExperimentHandler} from "../data/ExperimentHandler.js"; @@ -21,7 +20,29 @@ import registerSideBySideMatrix from "./survey/widgets/SideBySideMatrix.js"; import registerMaxDiffMatrix from "./survey/widgets/MaxDiffMatrix.js"; import registerSliderStar from "./survey/widgets/SliderStar.js"; import MatrixBipolar from "./survey/components/MatrixBipolar.js"; +import DropdownExtensions from "./survey/components/DropdownExtensions.js"; +import customExpressionFunctionsArray from "./survey/extensions/customExpressionFunctions.js"; +const CAPTIONS = { + NEXT: "Next" +}; + +const SURVEY_SETTINGS = { + minWidth: "100px" +}; + +const SURVEY_COMPLETION_CODES = +{ + NORMAL: 0, + SKIP_TO_END_OF_BLOCK: 1, + SKIP_TO_END_OF_SURVEY: 2 +}; + +const NODE_EXIT_CODES = +{ + NORMAL: 0, + BREAK_FLOW: 1 +}; /** * Survey Stimulus. @@ -32,6 +53,16 @@ export class Survey extends VisualStim { static SURVEY_EXPERIMENT_PARAMETERS = ["surveyId", "showStartDialog", "showEndDialog", "completionUrl", "cancellationUrl", "quitOnEsc"]; + static SURVEY_FLOW_PLAYBACK_TYPES = + { + DIRECT: "QUESTION_BLOCK", + CONDITIONAL: "IF_THEN_ELSE_GROUP", + EMBEDDED_DATA: "VARIABLES", + RANDOMIZER: "RANDOM_GROUP", + SEQUENTIAL: "SEQUENTIAL_GROUP", + ENDSURVEY: "END" + }; + /** * @memberOf module:visual * @param {Object} options @@ -52,11 +83,40 @@ export class Survey extends VisualStim { super({ name, win, units, ori, depth, pos, size, autoDraw, autoLog }); - // default size: - if (typeof size === "undefined") - { - this.size = (this.unit === "norm") ? [2.0, 2.0] : [1.0, 1.0]; - } + // the default surveyId is an uuid based on the experiment id (or name) and the survey name: + // this way, it is always the same within a given experiment + this._hasSelfGeneratedSurveyId = (typeof surveyId === "undefined"); + const defaultSurveyId = (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER) ? + util.makeUuid(`${name}@${this._psychoJS.config.gitlab.projectId}`) : + util.makeUuid(`${name}@${this._psychoJS.config.experiment.name}`); + + // whether the user is done with the survey, independently of whether the survey is completed: + this.isFinished = false; + + // Accumulated completion flag that is being set after completion of one survey node. + // This flag allows to track completion progress while moving through the survey flow. + // Initially set to true and will be flipped if at least one of the survey nodes were not fully completed. + this._isCompletedAll = true; + + // timestamps associated to each question: + this._questionAnswerTimestamps = {}; + // timestamps clock: + this._questionAnswerTimestampClock = new Clock(); + + this._totalSurveyResults = {}; + this._surveyData = undefined; + this._surveyModel = undefined; + this._signaturePadRO = undefined; + this._expressionsRunner = undefined; + this._lastPageSwitchHandledIdx = -1; + this._variables = {}; + + this._surveyRunningPromise = undefined; + this._surveyRunningPromiseResolve = undefined; + this._surveyRunningPromiseReject = undefined; + + // callback triggered when the user is done with the survey: nothing to do by default + this._onFinishedCallback = () => {}; // init SurveyJS this._initSurveyJS(); @@ -65,30 +125,12 @@ export class Survey extends VisualStim "model", model ); - - // the default surveyId is an uuid based on the experiment id (or name) and the survey name: - // this way, it is always the same within a given experiment - this._hasSelfGeneratedSurveyId = (typeof surveyId === "undefined"); - const defaultSurveyId = (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER) ? - util.makeUuid(`${name}@${this._psychoJS.config.gitlab.projectId}`) : - util.makeUuid(`${name}@${this._psychoJS.config.experiment.name}`); this._addAttribute( "surveyId", surveyId, defaultSurveyId ); - // whether the user is done with the survey, independently of whether the survey is completed: - this.isFinished = false; - // whether the user completed the survey, i.e. answered all the questions: - this.isCompleted = false; - // timestamps associated to each question: - this._questionAnswerTimestamps = {}; - // timestamps clock: - this._questionAnswerTimestampClock = new Clock(); - // callback triggered when the user is done with the survey: nothing to do by default - this._onFinishedCallback = () => {}; - // estimate the bounding box: this._estimateBoundingBox(); @@ -98,6 +140,11 @@ export class Survey extends VisualStim } } + get isCompleted () + { + return this.isFinished && this._isCompletedAll; + } + /** * Setter for the model attribute. * @@ -130,19 +177,46 @@ export class Survey extends VisualStim model = JSON.parse(decodedModel); } - // items should now be an object: + // model should now be an object: if (typeof model !== "object") { throw "model is neither the name of a resource nor an object"; } - this._surveyModelJson = Object.assign({}, model); - this._surveyModel = new window.Survey.Model(this._surveyModelJson); - this._surveyModel.isInitialized = false; + // if model is a straight-forward SurveyJS model, instead of a Pavlovia Survey super-flow model, + // convert it: + if (!('surveyFlow' in model)) + { + model = { + surveys: [model], + embeddedData: [], + surveysMap: {}, + questionMapsBySurvey: {}, + surveyFlow: { + name: "root", + type: "SEQUENTIAL_GROUP", + nodes: [{ + type: "QUESTION_BLOCK", + surveyIdx: 0 + }] + }, - // custom css: - // see https://surveyjs.io/form-library/examples/survey-cssclasses/jquery#content-js + surveySettings: { showPrevButton: false }, + surveyRunLogic: {}, + inQuestionRandomization: {}, + questionsOrderRandomization: [], + questionSkipLogic: {}, + + questionsConverted: -1, + questionsTotal: -1, + logs: [] + }; + + this.psychoJS.logger.debug(`converted the old model to the new super-flow model: ${JSON.stringify(model)}`); + } + + this._surveyData = model; this._setAttribute("model", model, log); this._onChange(true, true)(); } @@ -163,18 +237,26 @@ export class Survey extends VisualStim setVariables(variables, excludedNames) { // filter the variables and set them: - const filteredVariables = {}; + // const filteredVariables = {}; + // for (const name in variables) + // { + // if (excludedNames.indexOf(name) === -1) + // { + // filteredVariables[name] = variables[name]; + // this._surveyModel.setVariable(name, variables[name]); + // } + // } + + // // set the values: + // this._surveyModel.mergeData(filteredVariables); + for (const name in variables) { if (excludedNames.indexOf(name) === -1) { - filteredVariables[name] = variables[name]; - this._surveyModel.setVariable(name, variables[name]); + this._surveyData.variables[name] = variables[name]; } } - - // set the values: - this._surveyModel.mergeData(filteredVariables); } /** @@ -224,7 +306,7 @@ export class Survey extends VisualStim */ onFinished(callback) { - if (typeof this._surveyModel === "undefined") + if (typeof this._surveyData === "undefined") { throw { origin: "Survey.onFinished", @@ -247,12 +329,14 @@ export class Survey extends VisualStim */ getResponse() { - if (typeof this._surveyModel === "undefined") - { - return {}; - } + // if (typeof this._surveyModel === "undefined") + // { + // return {}; + // } - return this._surveyModel.data; + // return this._surveyModel.data; + + return this._totalSurveyResults; } /** @@ -323,7 +407,7 @@ export class Survey extends VisualStim else { return this._psychoJS.serverManager.uploadSurveyResponse( - this._surveyId, sortedResponses, this.isCompleted, this._surveyModelJson + this._surveyId, sortedResponses, this.isCompleted, this._surveyData ); } } @@ -383,21 +467,19 @@ export class Survey extends VisualStim { this._needPixiUpdate = false; - // if a survey div already does not exist already, create it: - const surveyId = "_survey"; - let surveyDiv = document.getElementById(surveyId); - if (surveyDiv === null) + // if a survey div does not exist, create it: + if (document.getElementById("_survey") === null) { - surveyDiv = document.createElement("div"); - surveyDiv.id = surveyId; - surveyDiv.className = "survey"; - document.body.appendChild(surveyDiv); + document.body.insertAdjacentHTML("beforeend", "
    ") } - // start the survey: - if (typeof this._surveyModel !== "undefined") + // start the survey flow: + if (typeof this._surveyData !== "undefined") { - this._startSurvey(surveyId, this._surveyModel); + // this._startSurvey(surveyId, this._surveyModel); + // jQuery(`#${surveyId}`).Survey({model: this._surveyModel}); + + this._runSurveyFlow(this._surveyData.surveyFlow, this._surveyData); } } @@ -424,26 +506,18 @@ export class Survey extends VisualStim } /** - * Init the SurveyJS.io library. + * Register custom SurveyJS expression functions. * * @protected + * @return {void} */ - _initSurveyJS() + _registerCustomExpressionFunctions (Survey, customFuncs = []) { - // load the Survey.js libraries, if necessary: - // TODO - - // setup the survey theme: - window.Survey.StylesManager.applyTheme("defaultV2"); - - // load the PsychoJS SurveyJS extensions: - this._expressionsRunner = new window.Survey.ExpressionRunner(); - this._registerWidgets(); - this._registerCustomSurveyProperties(); - - // load the desired style: - // TODO - // util.loadCss("./survey/css/grey_style.css"); + let i; + for (i = 0; i < customFuncs.length; i++) + { + Survey.FunctionFactory.Instance.register(customFuncs[i].func.name, customFuncs[i].func, customFuncs[i].isAsync); + } } /** @@ -452,57 +526,39 @@ export class Survey extends VisualStim * @protected * @return {void} */ - _registerWidgets() + _registerWidgets(Survey) { - registerSelectBoxWidget(window.Survey); - registerSliderWidget(window.Survey); - registerSideBySideMatrix(window.Survey); - registerMaxDiffMatrix(window.Survey); - registerSliderStar(window.Survey); + registerSelectBoxWidget(Survey); + registerSliderWidget(Survey); + registerSideBySideMatrix(Survey); + registerMaxDiffMatrix(Survey); + registerSliderStar(Survey); // load the widget style: // TODO // util.loadCss("./survey/css/widgets.css"); } - _registerCustomSurveyProperties() + /** + * Register custom Survey properties. Usially these are relevant for different question types. + * + * @protected + * @return {void} + */ + _registerCustomSurveyProperties(Survey) { - MatrixBipolar.registerSurveyProperties(window.Survey); + MatrixBipolar.registerSurveyProperties(Survey); + Survey.Serializer.addProperty("signaturepad", { + name: "maxSignatureWidth", + type: "number", + default: 500 + }); } _registerCustomComponentCallbacks(surveyModel) { MatrixBipolar.registerModelCallbacks(surveyModel); - } - - /** - * Run the survey using flow data provided. This method runs recursively. - * - * @protected - * @param {string} surveyId - the id of the DOM div - * @param {Object} surveyData - surveyData / model. - * @param {Object} prevBlockResults - survey results gathered from running previous block of questions. - * @return {void} - */ - _startSurvey(surveyId, surveyData, prevBlockResults = {}) - { - // initialise the survey model is need be: - if (!this._surveyModel.isInitialized) - { - this._registerCustomComponentCallbacks(this._surveyModel); - this._surveyModel.onValueChanged.add(this._onQuestionValueChanged.bind(this)); - this._surveyModel.onCurrentPageChanging.add(this._onCurrentPageChanging.bind(this)); - this._surveyModel.onTextMarkdown.add(this._onTextMarkdown.bind(this)); - this._surveyModel.onComplete.add(this._onSurveyComplete.bind(this)); - this._surveyModel.isInitialized = true; - } - - jQuery(`#${surveyId}`).Survey({ - model: this._surveyModel, - showItemsInOrder: "column" - }); - - this._questionAnswerTimestampClock.reset(); + DropdownExtensions.registerModelCallbacks(surveyModel); } /** @@ -523,20 +579,315 @@ export class Survey extends VisualStim this._questionAnswerTimestamps[questionData.name].timestamp = this._questionAnswerTimestampClock.getTime(); } + // This probably needs to be moved to some kind of utils.js. + // https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle + _FisherYatesShuffle (targetArray = []) + { + // Copying array to preserve initial data. + const out = Array.from(targetArray); + const len = targetArray.length; + let i, j, k; + for (i = len - 1; i >= 1; i--) + { + j = Math.floor(Math.random() * (i + 1)); + k = out[j]; + out[j] = out[i]; + out[i] = k; + } + + return out; + } + + // https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle + _InPlaceFisherYatesShuffle (inOutArray = [], startIdx, endIdx) + { + // Shuffling right in the input array. + let i, j, k; + for (i = endIdx; i >= startIdx; i--) + { + j = Math.floor(Math.random() * (i + 1)); + k = inOutArray[j]; + inOutArray[j] = inOutArray[i]; + inOutArray[i] = k; + } + + return inOutArray; + } + + _composeModelWithRandomizedQuestions (surveyModel, inBlockRandomizationSettings) + { + let t = performance.now(); + // Qualtrics's in-block randomization ignores presense of page breaks within the block. + // Hence creating a fresh survey data object with shuffled question order. + let questions = []; + let questionsMap = {}; + let shuffledQuestions; + let newSurveyModel = + { + pages:[{ elements: new Array(inBlockRandomizationSettings.questionsPerPage) }] + }; + let i, j, k; + for (i = 0; i < surveyModel.pages.length; i++) + { + for (j = 0; j < surveyModel.pages[i].elements.length; j++) + { + questions.push(surveyModel.pages[i].elements[j]); + k = questions.length - 1; + questionsMap[questions[k].name] = questions[k]; + } + } + + if (inBlockRandomizationSettings.layout.length > 0) + { + j = 0; + k = 0; + let curPage = 0; + let curElement = 0; + const shuffledSet0 = this._FisherYatesShuffle(inBlockRandomizationSettings.set0); + const shuffledSet1 = this._FisherYatesShuffle(inBlockRandomizationSettings.set1); + for (i = 0; i < inBlockRandomizationSettings.layout.length; i++) + { + // Create new page if questionsPerPage reached. + if (curElement === inBlockRandomizationSettings.questionsPerPage) + { + newSurveyModel.pages.push({ elements: new Array(inBlockRandomizationSettings.questionsPerPage) }); + curPage++; + curElement = 0; + } + + if (inBlockRandomizationSettings.layout[i] === "set0") + { + newSurveyModel.pages[curPage].elements[curElement] = questionsMap[shuffledSet0[j]]; + j++; + } + else if (inBlockRandomizationSettings.layout[i] === "set1") + { + newSurveyModel.pages[curPage].elements[curElement] = questionsMap[shuffledSet1[k]]; + k++; + } + else + { + newSurveyModel.pages[curPage].elements[curElement] = questionsMap[inBlockRandomizationSettings.layout[i]]; + } + curElement++; + } + } + else if (inBlockRandomizationSettings.showOnly > 0) + { + // TODO: Check if there can be questionsPerPage applicable in this case. + shuffledQuestions = this._FisherYatesShuffle(questions); + newSurveyModel.pages[0].elements = shuffledQuestions.splice(0, inBlockRandomizationSettings.showOnly); + } + else { + // TODO: Check if there can be questionsPerPage applicable in this case. + newSurveyModel.pages[0].elements = this._FisherYatesShuffle(questions); + } + console.log("model recomposition took", performance.now() - t); + console.log("recomposed model:", newSurveyModel); + return newSurveyModel; + } + + _applyInQuestionRandomization (questionData, inQuestionRandomizationSettings, surveyData) + { + let t = performance.now(); + let choicesFieldName; + let valueFieldName; + if (questionData.rows !== undefined) + { + choicesFieldName = "rows"; + valueFieldName = "value"; + } + else if (questionData.choices !== undefined) + { + choicesFieldName = "choices"; + valueFieldName = "value"; + } + else if (questionData.items !== undefined) + { + choicesFieldName = "items"; + valueFieldName = "name"; + } + else + { + console.log("[Survey runner]: Uknown choicesFieldName for", questionData); + } + + if (inQuestionRandomizationSettings.randomizeAll) + { + questionData[choicesFieldName] = this._FisherYatesShuffle(questionData[choicesFieldName]); + // Handle dynamic choices. + } + else if (inQuestionRandomizationSettings.showOnly > 0) + { + questionData[choicesFieldName] = this._FisherYatesShuffle(questionData[choicesFieldName]).splice(0, inQuestionRandomizationSettings.showOnly); + } + else if (inQuestionRandomizationSettings.reverse) + { + questionData[choicesFieldName] = Math.round(Math.random()) === 1 ? questionData[choicesFieldName].reverse() : questionData[choicesFieldName]; + } + else if (inQuestionRandomizationSettings.layout.length > 0) + { + const initialChoices = questionData[choicesFieldName]; + let choicesMap = {}; + // TODO: generalize further i.e. figure out how to calculate the length of array based on availability of sets. + const setIndices = [0, 0, 0]; + let i; + for (i = 0; i < questionData[choicesFieldName].length; i++) + { + choicesMap[questionData[choicesFieldName][i][valueFieldName]] = questionData[choicesFieldName][i]; + } + + // Creating new array of choices to which we're going to write from randomized/reversed sets. + questionData[choicesFieldName] = new Array(inQuestionRandomizationSettings.layout.length); + const shuffledSet0 = this._FisherYatesShuffle(inQuestionRandomizationSettings.set0); + const shuffledSet1 = this._FisherYatesShuffle(inQuestionRandomizationSettings.set1); + const reversedSet = Math.round(Math.random()) === 1 ? inQuestionRandomizationSettings.reverseOrder.reverse() : inQuestionRandomizationSettings.reverseOrder; + for (i = 0; i < inQuestionRandomizationSettings.layout.length; i++) + { + if (inQuestionRandomizationSettings.layout[i] === "set0") + { + questionData[choicesFieldName][i] = choicesMap[shuffledSet0[ setIndices[0] ]]; + setIndices[0]++; + } + else if (inQuestionRandomizationSettings.layout[i] === "set1") + { + questionData[choicesFieldName][i] = choicesMap[shuffledSet1[ setIndices[1] ]]; + setIndices[1]++; + } + else if (inQuestionRandomizationSettings.layout[i] === "reverseOrder") + { + questionData[choicesFieldName][i] = choicesMap[reversedSet[ setIndices[2] ]]; + setIndices[2]++; + } + else + { + questionData[choicesFieldName][i] = choicesMap[inQuestionRandomizationSettings.layout[i]]; + } + } + + if (inQuestionRandomizationSettings.layout.length < initialChoices.length) + { + // Compose unused choices set. + // TODO: This is potentially how data loss can be avoided and thus no need to deepcopy model. + if (surveyData.unusedChoices === undefined) + { + surveyData.unusedChoices = {}; + } + surveyData.unusedChoices[questionData.name] = { + // All other sets are always used entirely. + set1: shuffledSet1.splice(setIndices[1], shuffledSet1.length) + }; + console.log("unused choices", questionData.name, surveyData.unusedChoices[questionData.name]); + } + } + + console.log("applying question randomization took", performance.now() - t); + // console.log(questionData); + } + + /** + * @desc: Go over required surveyModelData and apply randomization settings. + */ + _processSurveyData (surveyData, surveyIdx) + { + let t = performance.now(); + let i, j; + let newSurveyModel = undefined; + if (surveyData.questionsOrderRandomization[surveyIdx] !== undefined) + { + // Qualtrics's in-block randomization ignores presense of page breaks within the block. + // Hence creating a fresh survey data object with shuffled question order. + newSurveyModel = this._composeModelWithRandomizedQuestions(surveyData.surveys[surveyIdx], surveyData.questionsOrderRandomization[surveyIdx]); + } + + // Checking if there's in-question randomization that needs to be applied. + for (i = 0; i < surveyData.surveys[surveyIdx].pages.length; i++) + { + for (j = 0; j < surveyData.surveys[surveyIdx].pages[i].elements.length; j++) + { + if (surveyData.inQuestionRandomization[surveyData.surveys[surveyIdx].pages[i].elements[j].name] !== undefined) + { + if (newSurveyModel === undefined) + { + // Marking a deep copy of survey model input data, to avoid data loss if randomization returns a subset of choices. + // TODO: think of somehting more optimal. + newSurveyModel = JSON.parse(JSON.stringify(surveyData.surveys[surveyIdx])); + } + this._applyInQuestionRandomization( + newSurveyModel.pages[i].elements[j], + surveyData.inQuestionRandomization[newSurveyModel.pages[i].elements[j].name], + surveyData + ); + } + } + } + + if (newSurveyModel === undefined) + { + // No changes were made, just return original data. + newSurveyModel = surveyData.surveys[surveyIdx]; + } + console.log("survey model preprocessing took", performance.now() - t); + return newSurveyModel; + } + /** * Callback triggered when the participant changed the page. * * @protected */ - _onCurrentPageChanging() + _onCurrentPageChanging (surveyModel, options) { - // console.log(arguments); - } + if (this._lastPageSwitchHandledIdx === options.oldCurrentPage.visibleIndex) + { + // When surveyModel.currentPage is called from this handler, pagechange event gets triggered again. + // Hence returning if we already handled this pagechange to avoid max callstack exceeded errors. + return; + } + this._lastPageSwitchHandledIdx = options.oldCurrentPage.visibleIndex; + const questions = surveyModel.getCurrentPageQuestions(); - _onTextMarkdown(survey, options) - { - // TODO add sanitization / checks if required. - options.html = options.text; + // It is guaranteed that the question with skip logic is always last on the page. + const lastQuestion = questions[questions.length - 1]; + const skipLogic = this._surveyData.questionSkipLogic[lastQuestion.name]; + if (skipLogic !== undefined) + { + this._expressionsRunner.expressionExecutor.setExpression(skipLogic.expression); + const result = this._expressionsRunner.run(surveyModel.data); + if (result) + { + options.allowChanging = false; + + if (skipLogic.destination === "ENDOFSURVEY") + { + surveyModel.setCompleted(); + this._surveyRunningPromiseResolve(SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY); + } + else if (skipLogic.destination === "ENDOFBLOCK") + { + surveyModel.setCompleted(); + this._surveyRunningPromiseResolve(SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_BLOCK); + } + else + { + // skipLogic.destination is a question within the current survey (qualtrics block). + const targetQuestion = surveyModel.getQuestionByName(skipLogic.destination); + const page = surveyModel.getPageByQuestion(targetQuestion); + const pageQuestions = page.questions; + let i; + for (i = 0; i < pageQuestions.length; i++) + { + if (pageQuestions[i] === targetQuestion) + { + break; + } + pageQuestions[i].visible = false; + } + targetQuestion.focus(); + surveyModel.currentPage = page; + } + } + } } /** @@ -549,7 +900,33 @@ export class Survey extends VisualStim */ _onSurveyComplete(surveyModel, options) { - this.isFinished = true; + Object.assign(this._totalSurveyResults, surveyModel.data); + this._detachResizeObservers(); + let completionCode = SURVEY_COMPLETION_CODES.NORMAL; + const questions = surveyModel.getAllQuestions(); + + // It is guaranteed that the question with skip logic is always last on the page. + const lastQuestion = questions[questions.length - 1]; + const skipLogic = this._surveyData.questionSkipLogic[lastQuestion.name]; + if (skipLogic !== undefined) + { + this._expressionsRunner.expressionExecutor.setExpression(skipLogic.expression); + const result = this._expressionsRunner.run(surveyModel.data); + if (result) + { + if (skipLogic.destination === "ENDOFSURVEY") + { + completionCode = SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY; + surveyModel.setCompleted(); + } + else if (skipLogic.destination === "ENDOFBLOCK") + { + completionCode = SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_BLOCK; + } + } + } + + surveyModel.stopTimer(); // check whether the survey was completed: const surveyVisibleQuestions = this._surveyModel.getAllQuestions(true); @@ -574,9 +951,241 @@ export class Survey extends VisualStim }, 0 ); - this.isCompleted = (nbAnsweredQuestions === surveyVisibleQuestions.length); + this._isCompletedAll = this._isCompletedAll && (nbAnsweredQuestions === surveyVisibleQuestions.length); + if (this._isCompletedAll === false) + { + this.psychoJS.logger.warn(`Flag _isCompletedAll is false!`); + } + this._surveyRunningPromiseResolve(completionCode); + } + + _onFlowComplete () + { + this.isFinished = true; this._onFinishedCallback(); } + _onTextMarkdown(survey, options) + { + // TODO add sanitization / checks if required. + options.html = options.text; + } + + /** + * Run the survey using flow data provided. This method runs recursively. + * + * @protected + * @param {string} surveyId - the id of the DOM div + * @param {Object} surveyData - surveyData / model. + * @param {Object} prevBlockResults - survey results gathered from running previous block of questions. + * @return {void} + */ + _beginSurvey(surveyData, surveyFlowBlock) + { + let j; + let surveyIdx; + this._lastPageSwitchHandledIdx = -1; + surveyIdx = surveyFlowBlock.surveyIdx; + console.log("playing survey with idx", surveyIdx); + let surveyModelInput = this._processSurveyData(surveyData, surveyIdx); + + this._surveyModel = new window.Survey.Model(surveyModelInput); + for (j in this._variables) + { + // Adding variables directly to hash to get higher performance (this is instantaneous compared to .setVariable()). + // At this stage we don't care to trigger all the callbacks like .setVariable() does, since this is very beginning of survey presentation. + this._surveyModel.variablesHash[j] = this._variables[j]; + // this._surveyModel.setVariable(j, this._variables[j]); + } + + if (!this._surveyModel.isInitialized) + { + this._registerCustomComponentCallbacks(this._surveyModel); + this._surveyModel.onValueChanged.add(this._onQuestionValueChanged.bind(this)); + this._surveyModel.onCurrentPageChanging.add(this._onCurrentPageChanging.bind(this)); + this._surveyModel.onComplete.add(this._onSurveyComplete.bind(this)); + this._surveyModel.onTextMarkdown.add(this._onTextMarkdown.bind(this)); + this._surveyModel.isInitialized = true; + this._surveyModel.onAfterRenderQuestion.add(this._handleAfterQuestionRender.bind(this)); + } + + const completeText = surveyIdx < this._surveyData.surveys.length - 1 ? (this._surveyModel.pageNextText || CAPTIONS.NEXT) : undefined; + jQuery(".survey").Survey({ + model: this._surveyModel, + showItemsInOrder: "column", + completeText, + ...surveyData.surveySettings, + }); + + this._questionAnswerTimestampClock.reset(); + + // TODO: should this be conditional? + this._surveyModel.startTimer(); + + this._surveyRunningPromise = new Promise((res, rej) => { + this._surveyRunningPromiseResolve = res; + this._surveyRunningPromiseReject = rej; + }); + + return this._surveyRunningPromise; + } + + async _runSurveyFlow(surveyBlock, surveyData, prevBlockResults = {}) + { + // let surveyBlock; + let surveyIdx; + let surveyCompletionCode; + let nodeExitCode = NODE_EXIT_CODES.NORMAL; + let i, j; + + if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.CONDITIONAL) + { + const dataset = Object.assign({}, this._totalSurveyResults, this._variables); + this._expressionsRunner.expressionExecutor.setExpression(surveyBlock.condition); + if (this._expressionsRunner.run(dataset) && surveyBlock.nodes[0] !== undefined) + { + nodeExitCode = await this._runSurveyFlow(surveyBlock.nodes[0], surveyData, prevBlockResults); + } + else if (surveyBlock.nodes[1] !== undefined) + { + nodeExitCode = await this._runSurveyFlow(surveyBlock.nodes[1], surveyData, prevBlockResults); + } + } + else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.RANDOMIZER) + { + this._InPlaceFisherYatesShuffle(surveyBlock.nodes, 0, surveyBlock.nodes.length - 1); + } + else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.EMBEDDED_DATA) + { + let t = performance.now(); + const surveyBlockData = surveyData.embeddedData[surveyBlock.dataIdx]; + for (j = 0; j < surveyBlockData.length; j++) + { + // TODO: handle the rest data types. + if (surveyBlockData[j].type === "Custom") + { + // Variable value can be an expression. Check if so and if valid - run it. + // surveyBlockData is an array so all the variables in it are in order they were declared in Qualtrics. + // This means this._variables is saturated gradually with the data necessary to perform a computation. + // It's guaranteed to be there, unless there are declaration order mistakes. + this._expressionsRunner.expressionExecutor.setExpression(surveyBlockData[j].value); + if (this._expressionsRunner.expressionExecutor.canRun()) + { + this._variables[surveyBlockData[j].key] = this._expressionsRunner.run(this._variables); + } + else + { + this._variables[surveyBlockData[j].key] = surveyBlockData[j].value; + } + } + } + console.log("embedded data variables accumulation took", performance.now() - t); + } + else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.ENDSURVEY) + { + if (this._surveyModel) + { + this._surveyModel.setCompleted(); + } + console.log("EndSurvey block encountered, exiting."); + nodeExitCode = NODE_EXIT_CODES.BREAK_FLOW; + } + else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.DIRECT) + { + surveyCompletionCode = await this._beginSurvey(surveyData, surveyBlock); + Object.assign({}, prevBlockResults, this._surveyModel.data); + + // SkipLogic had destination set to ENDOFSURVEY. + if (surveyCompletionCode === SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY) + { + nodeExitCode = NODE_EXIT_CODES.BREAK_FLOW; + } + } + + if (nodeExitCode === NODE_EXIT_CODES.NORMAL && + surveyBlock.type !== Survey.SURVEY_FLOW_PLAYBACK_TYPES.CONDITIONAL && + surveyBlock.nodes instanceof Array) + { + for (i = 0; i < surveyBlock.nodes.length; i++) + { + nodeExitCode = await this._runSurveyFlow(surveyBlock.nodes[i], surveyData, prevBlockResults); + if (nodeExitCode === NODE_EXIT_CODES.BREAK_FLOW) + { + break; + } + } + } + + if (surveyBlock.name === "root") + { + // At this point we went through the entire survey flow tree. + this._onFlowComplete(); + } + + return nodeExitCode; + } + + _resetState () + { + this._lastPageSwitchHandledIdx = -1; + } + + _handleSignaturePadResize (entries) + { + let signatureCanvas; + let q; + let i; + for (i = 0; i < entries.length; i++) + { + signatureCanvas = entries[i].target.querySelector("canvas"); + q = this._surveyModel.getQuestionByName(entries[i].target.dataset.name); + q.signatureWidth = Math.min(q.maxSignatureWidth, entries[i].contentBoxSize[0].inlineSize); + } + } + + _addEventListeners () + { + this._signaturePadRO = new ResizeObserver(this._handleSignaturePadResize.bind(this)); + } + + _handleAfterQuestionRender (sender, options) + { + if (options.question.getType() === "signaturepad") + { + this._signaturePadRO.observe(options.htmlElement); + } + } + + _detachResizeObservers () + { + this._signaturePadRO.disconnect(); + } + + /** + * Init the SurveyJS.io library. + * + * @protected + */ + _initSurveyJS() + { + // load the Survey.js libraries, if necessary: + // TODO + + // load the PsychoJS SurveyJS extensions: + this._expressionsRunner = new window.Survey.ExpressionRunner(); + this._registerCustomExpressionFunctions(window.Survey, customExpressionFunctionsArray); + this._registerWidgets(window.Survey); + this._registerCustomSurveyProperties(window.Survey); + this._addEventListeners(); + + // setup the survey theme: + window.Survey.Serializer.getProperty("expression", "minWidth").defaultValue = "100px"; + window.Survey.settings.minWidth = "100px"; + window.Survey.StylesManager.applyTheme("defaultV2"); + + // load the desired style: + // TODO + // util.loadCss("./survey/css/grey_style.css"); + } } diff --git a/src/visual/survey/MaxDiffMatrix.js b/src/visual/survey/MaxDiffMatrix.js deleted file mode 100644 index a539f54..0000000 --- a/src/visual/survey/MaxDiffMatrix.js +++ /dev/null @@ -1,307 +0,0 @@ -/** -* @desc "MaxDiff" matrix. -* */ - -class MaxDiffMatrix -{ - constructor (cfg = {}) - { - // surveyCSS contains css class names provided by the applied theme - // INCLUDING those added/modified by application's code. - const surveyCSS = cfg.question.css; - this._CSS_CLASSES = { - WRAPPER: `${surveyCSS.matrix.tableWrapper} matrix-maxdiff`, - TABLE: surveyCSS.matrix.root, - TABLE_ROW: surveyCSS.matrixdropdown.row, - TABLE_HEADER_CELL: surveyCSS.matrix.headerCell, - TABLE_CELL: surveyCSS.matrix.cell, - INPUT_TEXT: surveyCSS.text.root, - LABEL: surveyCSS.matrix.label, - ITEM_CHECKED: surveyCSS.matrix.itemChecked, - ITEM_VALUE: surveyCSS.matrix.itemValue, - ITEM_DECORATOR: surveyCSS.matrix.materialDecorator, - RADIO: surveyCSS.radiogroup.item, - SELECT: surveyCSS.dropdown.control, - CHECKBOX: surveyCSS.checkbox.item - }; - - // const CSS_CLASSES = { - // WRAPPER: "sv-matrix matrix-maxdiff", - // TABLE: "sv-table sv-matrix-root", - // TABLE_ROW: "sv-table__row", - // TABLE_HEADER_CELL: "sv-table__cell sv-table__cell--header", - // TABLE_CELL: "sv-table__cell sv-matrix__cell", - // INPUT_TEXT: "sv-text", - // RADIO: "sv-radio", - // SELECT: "sv-dropdown", - // CHECKBOX: "sv-checkbox" - // }; - this._question = cfg.question; - this._DOM = cfg.el; - this._DOM.classList.add(...this._CSS_CLASSES.WRAPPER.split(" ")); - - this._bindedHandlers = - { - _handleInput: this._handleInput.bind(this) - }; - - this._init(this._question, this._DOM); - } - - _handleInput (e) - { - const valueCoordinates = e.currentTarget.name.split("-"); - const row = valueCoordinates[0]; - const col = parseInt(e.currentTarget.dataset.column, 10); - const colRadioDOMS = this._DOM.querySelectorAll(`input[data-column="${col}"]`); - - if (this._question.value === undefined) - { - this._question.value = {}; - } - - const oldVal = this._question.value; - const newVal = {[row]: col}; - - // Handle case when exclusiveAnswer option is false? - let inputRow; - let i; - for (i = 0; i < colRadioDOMS.length; i++) - { - if (colRadioDOMS[i] !== e.currentTarget) - { - colRadioDOMS[i].checked = false; - inputRow = colRadioDOMS[i].name; - // Preserving previously ticked columns within other rows - if (oldVal[inputRow] !== undefined && oldVal[inputRow] !== col) - { - newVal[inputRow] = oldVal[inputRow]; - } - } - } - - this._question.value = newVal; - console.log(row, col, this._question.value); - } - - _init (question, el) - { - let t = performance.now(); - const CSS_CLASSES = this._CSS_CLASSES; - if (question.css.matrix.mainRoot) - { - // Replacing default mainRoot class with those used in matrix type questions, to achieve proper styling and overflow behavior - const rootClass = `${question.css.matrix.mainRoot} ${question.cssClasses.withFrame || ""}`; - question.setCssRoot(rootClass); - question.cssClasses.mainRoot = rootClass; - } - let html; - let headerCells = ""; - let subHeaderCells = ""; - let bodyCells = ""; - let bodyHTML = ""; - let cellGenerator; - let i, j; - - // Relying on a fact that there's always 2 columns. - // This is correct according current Qualtrics design for MaxDiff matrices. - // Header generation - headerCells = - `${question.columns[0].text} - - - - ${question.columns[1].text}`; - - // Body generation - for (i = 0; i < question.rows.length; i++) - { - bodyCells = - ` - - - - ${question.rows[i].text} - - - - `; - bodyHTML += `${bodyCells}`; - } - - html = ` - - ${headerCells} - - ${bodyHTML} -
    `; - - console.log("maxdiff matrix generation took", performance.now() - t); - el.insertAdjacentHTML("beforeend", html); - - let inputDOMS = el.querySelectorAll("input"); - - for (i = 0; i < inputDOMS.length; i++) - { - inputDOMS[i].addEventListener("input", this._bindedHandlers._handleInput); - } - } -} - -export default function init (Survey) { - var widget = { - //the widget name. It should be unique and written in lowcase. - name: "maxdiffmatrix", - - //the widget title. It is how it will appear on the toolbox of the SurveyJS Editor/Builder - title: "MaxDiff matrix", - - //the name of the icon on the toolbox. We will leave it empty to use the standard one - iconName: "", - - //If the widgets depends on third-party library(s) then here you may check if this library(s) is loaded - widgetIsLoaded: function () { - //return typeof $ == "function" && !!$.fn.select2; //return true if jQuery and select2 widget are loaded on the page - return true; //we do not require anything so we just return true. - }, - - //SurveyJS library calls this function for every question to check, if it should use this widget instead of default rendering/behavior - isFit: function (question) { - //we return true if the type of question is maxdiffmatrix - return question.getType() === 'maxdiffmatrix'; - //the following code will activate the widget for a text question with inputType equals to date - //return question.getType() === 'text' && question.inputType === "date"; - }, - - //Use this function to create a new class or add new properties or remove unneeded properties from your widget - //activatedBy tells how your widget has been activated by: property, type or customType - //property - it means that it will activated if a property of the existing question type is set to particular value, for example inputType = "date" - //type - you are changing the behaviour of entire question type. For example render radiogroup question differently, have a fancy radio buttons - //customType - you are creating a new type, like in our example "maxdiffmatrix" - activatedByChanged: function (activatedBy) { - //we do not need to check acticatedBy parameter, since we will use our widget for customType only - //We are creating a new class and derived it from text question type. It means that text model (properties and fuctions) will be available to us - Survey.JsonObject.metaData.addClass("maxdiffmatrix", [], null, "text"); - //signaturepad is derived from "empty" class - basic question class - //Survey.JsonObject.metaData.addClass("signaturepad", [], null, "empty"); - - //Add new property(s) - //For more information go to https://surveyjs.io/Examples/Builder/?id=addproperties#content-docs - Survey.JsonObject.metaData.addProperties("maxdiffmatrix", [ - { - name: "rows", - default: [] - }, - { - name: "columns", - default: [] - } - ]); - }, - - //If you want to use the default question rendering then set this property to true. We do not need any default rendering, we will use our our htmlTemplate - isDefaultRender: false, - - //You should use it if your set the isDefaultRender to false - htmlTemplate: "
    ", - - //The main function, rendering and two-way binding - afterRender: function (question, el) { - console.log("MaxDiff mat", question.rows, question.columns); - new MaxDiffMatrix({ question, el }); - - // let containers = el.querySelectorAll(".srv-slider-container"); - // let inputDOMS = el.querySelectorAll(".srv-slider"); - // let sliderDisplayDOMS = el.querySelectorAll(".srv-slider-display"); - // if (!(question.value instanceof Array)) - // { - // question.value = new Array(inputDOMS.length); - // question.value.fill(0); - // } - - // for (i = 0; i < inputDOMS.length; i++) - // { - // inputDOMS[i].min = question.minVal; - // inputDOMS[i].max = question.maxVal; - // inputDOMS[i].addEventListener("input", (e) => { - // let idx = parseInt(e.currentTarget.dataset.idx, 10); - // question.value[idx] = parseFloat(e.currentTarget.value); - // // using .value setter to trigger update properly. - // // otherwise on survey competion it returns array of nulls. - // question.value = question.value; - // onValueChangedCallback(); - // }); - - // // Handle grid lines? - // } - - - // function positionSliderDisplay (v, min, max, displayDOM) - // { - // v = parseFloat(v); - // min = parseFloat(min); - // max = parseFloat(max); - // // Formula is (halfThumbWidth - v * (fullThumbWidth / 100)), taking into account that display has translate(-50%, 0). - // // Size of thumb is set in CSS. - // displayDOM.style.left = `calc(${(v - min) / (max - min) * 100}% + ${10 - v * 0.2}px)` - // } - - - // var onValueChangedCallback = function () { - // let i; - // let v; - // for (i = 0; i < question.choices.length; i++) - // { - // v = question.value[i] || 0; - // inputDOMS[i].value = v; - // sliderDisplayDOMS[i].innerText = v; - // positionSliderDisplay(v, question.minVal, question.maxVal, sliderDisplayDOMS[i]); - // } - // } - - // var onReadOnlyChangedCallback = function() { - // let i; - // if (question.isReadOnly) { - // for (i = 0; i < question.choices.length; i++) - // { - // inputDOMS[i].setAttribute('disabled', 'disabled'); - // } - // } else { - // for (i = 0; i < question.choices.length; i++) - // { - // inputDOMS[i].removeAttribute("disabled"); - // } - // } - // }; - - // if question becomes readonly/enabled add/remove disabled attribute - // question.readOnlyChangedCallback = onReadOnlyChangedCallback; - - // if the question value changed in the code, for example you have changed it in JavaScript - // question.valueChangedCallback = onValueChangedCallback; - - // set initial value - // onValueChangedCallback(); - - // make elements disabled if needed - // onReadOnlyChangedCallback(); - }, - - //Use it to destroy the widget. It is typically needed by jQuery widgets - willUnmount: function (question, el) { - //We do not need to clear anything in our simple example - //Here is the example to destroy the image picker - //var $el = $(el).find("select"); - //$el.data('picker').destroy(); - } - } - - //Register our widget in singleton custom widget collection - Survey.CustomWidgetCollection.Instance.addCustomWidget(widget, "customtype"); -} diff --git a/src/visual/survey/SelectBox.js b/src/visual/survey/SelectBox.js deleted file mode 100644 index 1402298..0000000 --- a/src/visual/survey/SelectBox.js +++ /dev/null @@ -1,119 +0,0 @@ -/** -* @desc SelectBox widget for surveyJS. -* @type: SurveyJS widget. -*/ - -export default function init (Survey) { - var widget = { - //the widget name. It should be unique and written in lowcase. - name: "selectbox", - - //the widget title. It is how it will appear on the toolbox of the SurveyJS Editor/Builder - title: "My custom widg", - - //the name of the icon on the toolbox. We will leave it empty to use the standard one - iconName: "", - - //If the widgets depends on third-party library(s) then here you may check if this library(s) is loaded - widgetIsLoaded: function () { - //return typeof $ == "function" && !!$.fn.select2; //return true if jQuery and select2 widget are loaded on the page - return true; //we do not require anything so we just return true. - }, - - //SurveyJS library calls this function for every question to check, if it should use this widget instead of default rendering/behavior - isFit: function (question) { - //we return true if the type of question is selectbox - return question.getType() === 'selectbox'; - //the following code will activate the widget for a text question with inputType equals to date - //return question.getType() === 'text' && question.inputType === "date"; - }, - - //Use this function to create a new class or add new properties or remove unneeded properties from your widget - //activatedBy tells how your widget has been activated by: property, type or customType - //property - it means that it will activated if a property of the existing question type is set to particular value, for example inputType = "date" - //type - you are changing the behaviour of entire question type. For example render radiogroup question differently, have a fancy radio buttons - //customType - you are creating a new type, like in our example "selectbox" - activatedByChanged: function (activatedBy) { - //we do not need to check acticatedBy parameter, since we will use our widget for customType only - //We are creating a new class and derived it from text question type. It means that text model (properties and fuctions) will be available to us - Survey.JsonObject.metaData.addClass("selectbox", [], null, "text"); - //signaturepad is derived from "empty" class - basic question class - //Survey.JsonObject.metaData.addClass("signaturepad", [], null, "empty"); - - //Add new property(s) - //For more information go to https://surveyjs.io/Examples/Builder/?id=addproperties#content-docs - Survey.JsonObject.metaData.addProperties("selectbox", [ - { - name: "choices", - default: [] - } - ]); - }, - - //If you want to use the default question rendering then set this property to true. We do not need any default rendering, we will use our our htmlTemplate - isDefaultRender: false, - - //You should use it if your set the isDefaultRender to false - htmlTemplate: `
    `, - - //The main function, rendering and two-way binding - afterRender: function (question, el) { - let optionsHTML = ""; - let i; - for (i = 0; i < question.choices.length; i++) - { - optionsHTML += ``; - } - - let selectDOM = el.querySelector("select"); - selectDOM.innerHTML = optionsHTML; - - selectDOM.addEventListener('input', (e) => { - let i; - let opts = new Array(e.currentTarget.selectedOptions.length); - for (i = 0; i < e.currentTarget.selectedOptions.length; i++) - { - opts[i] = e.currentTarget.selectedOptions[i].value; - } - question.value = opts; - }); - - // var onValueChangedCallback = function () { - // text.value = question.value ? question.value : ""; - // } - - // var onReadOnlyChangedCallback = function() { - // if (question.isReadOnly) { - // text.setAttribute('disabled', 'disabled'); - // button.setAttribute('disabled', 'disabled'); - // } else { - // text.removeAttribute("disabled"); - // button.removeAttribute("disabled"); - // } - // }; - - //if question becomes readonly/enabled add/remove disabled attribute - // question.readOnlyChangedCallback = onReadOnlyChangedCallback; - - //if the question value changed in the code, for example you have changed it in JavaScript - // question.valueChangedCallback = onValueChangedCallback; - - //set initial value - // onValueChangedCallback(); - - //make elements disabled if needed - // onReadOnlyChangedCallback(); - }, - - //Use it to destroy the widget. It is typically needed by jQuery widgets - willUnmount: function (question, el) { - //We do not need to clear anything in our simple example - //Here is the example to destroy the image picker - //var $el = $(el).find("select"); - //$el.data('picker').destroy(); - } - } - - //Register our widget in singleton custom widget collection - Survey.CustomWidgetCollection.Instance.addCustomWidget(widget, "customtype"); -} diff --git a/src/visual/survey/SideBySideMatrix.js b/src/visual/survey/SideBySideMatrix.js deleted file mode 100644 index eb822fa..0000000 --- a/src/visual/survey/SideBySideMatrix.js +++ /dev/null @@ -1,424 +0,0 @@ -/** -* @desc Side By Side matrix. -* */ - -const CELL_TYPES = { - DROP_DOWN: "dropdown", - RADIO: "radio", - CHECKBOX: "checkbox", - TEXT: "text" -}; - -class SideBySideMatrix -{ - constructor (cfg = {}) - { - // surveyCSS contains css class names provided by the applied theme - // INCLUDING those added/modified by application's code. - const surveyCSS = cfg.question.css; - this._CSS_CLASSES = { - WRAPPER: surveyCSS.matrix.tableWrapper, - TABLE: surveyCSS.matrix.root, - TABLE_ROW: surveyCSS.matrixdropdown.row, - TABLE_HEADER_CELL: surveyCSS.matrix.headerCell, - TABLE_CELL: surveyCSS.matrix.cell, - INPUT_TEXT: surveyCSS.text.root, - LABEL: surveyCSS.matrix.label, - ITEM_CHECKED: surveyCSS.matrix.itemChecked, - ITEM_VALUE: surveyCSS.matrix.itemValue, - ITEM_DECORATOR: surveyCSS.matrix.materialDecorator, - RADIO: surveyCSS.radiogroup.item, - SELECT: surveyCSS.dropdown.control, - CHECKBOX: surveyCSS.checkbox.item, - CHECKBOX_CONTROL: surveyCSS.checkbox.itemControl, - CHECKBOX_DECORATOR: surveyCSS.checkbox.materialDecorator, - CHECKBOX_DECORATOR_SVG: surveyCSS.checkbox.itemDecorator - }; - this._question = cfg.question; - this._DOM = cfg.el; - this._DOM.classList.add(...this._CSS_CLASSES.WRAPPER.split(" ")); - - this._bindedHandlers = { - _handleInput: this._handleInput.bind(this), - _handleSelectChange: this._handleSelectChange.bind(this) - }; - - this._init(this._question, this._DOM); - } - - static CELL_GENERATORS = - { - [CELL_TYPES.DROP_DOWN]: "_generateDropdownCells", - [CELL_TYPES.RADIO]: "_generateRadioCells", - [CELL_TYPES.CHECKBOX]: "_generateCheckboxCells", - [CELL_TYPES.TEXT]: "_generateTextInputCells", - }; - - _generateDropdownCells (row, col, subColumns, CSS_CLASSES) - { - let bodyCells = ""; - let selectOptions = ""; - let i; - for (i = 0; i < subColumns.length; i++) - { - selectOptions += ``; - } - bodyCells = - ` - - `; - return bodyCells; - } - - _generateRadioCells (row, col, subColumns, CSS_CLASSES) - { - let bodyCells = ""; - let i; - for (i = 0; i < subColumns.length; i++) - { - bodyCells += - ` - - `; - } - return bodyCells; - } - - _generateCheckboxCells (row, col, subColumns, CSS_CLASSES) - { - let bodyCells = ""; - let i; - for (i = 0; i < subColumns.length; i++) - { - bodyCells += - ` - - `; - } - return bodyCells; - } - - _generateTextInputCells (row, col, subColumns, CSS_CLASSES) - { - let bodyCells = ""; - let i; - for (i = 0; i < subColumns.length; i++) - { - bodyCells += - ` - - `; - } - return bodyCells; - } - - _ensureQuestionValueFields (row, col) - { - if (this._question.value === undefined) - { - this._question.value = {}; - } - - if (this._question.value[row] === undefined) - { - this._question.value[row] = { - [col]: {} - } - } - - if (this._question.value[row][col] === undefined) - { - this._question.value[row][col] = {}; - } - } - - _handleInput (e) - { - const valueCoordinates = e.currentTarget.name.split("-"); - const row = valueCoordinates[0]; - const col = valueCoordinates[1]; - const subCol = valueCoordinates[2] !== undefined ? valueCoordinates[2] : e.currentTarget.value; - this._ensureQuestionValueFields(row, col); - - if (e.currentTarget.type === "text") - { - this._question.value[row][col][subCol] = e.currentTarget.value; - } - else if (e.currentTarget.type === "radio") - { - this._question.value[row][col] = e.currentTarget.value; - } - else if (e.currentTarget.type === "checkbox") - { - this._question.value[row][col][subCol] = e.currentTarget.checked; - } - - // Triggering internal SurveyJS mechanism for value update. - this._question.value = this._question.value; - } - - _handleSelectChange (e) - { - const valueCoordinates = e.currentTarget.name.split("-"); - const row = valueCoordinates[0]; - const col = valueCoordinates[1]; - this._ensureQuestionValueFields(row, col); - this._question.value[row][col]= e.currentTarget.value; - // Triggering internal SurveyJS mechanism for value update. - this._question.value = this._question.value; - } - - _init (question, el) - { - let t = performance.now(); - const CSS_CLASSES = this._CSS_CLASSES; - // TODO: Find out how it actually composed inside SurveyJS. - if (question.css.matrix.mainRoot) - { - // Replacing default mainRoot class with those used in matrix type questions, to achieve proper styling and overflow behavior - const rootClass = `${question.css.matrix.mainRoot} ${question.cssClasses.withFrame || ""}`; - question.setCssRoot(rootClass); - question.cssClasses.mainRoot = rootClass; - } - let html; - let headerCells = ""; - let subHeaderCells = ""; - let bodyCells = ""; - let bodyHTML = ""; - let cellGenerator; - let i, j; - - // Header generation - for (i = 0; i < question.columns.length; i++) - { - if (question.columns[i].cellType !== CELL_TYPES.DROP_DOWN) - { - headerCells += - ` - ${question.columns[i].title} - `; - for (j = 0; j < question.columns[i].subColumns.length; j++) - { - subHeaderCells += `${question.columns[i].subColumns[j].text}`; - } - } - else - { - headerCells += - ` - ${question.columns[i].title} - `; - subHeaderCells += ""; - } - headerCells += ""; - subHeaderCells += ""; - } - - // Body generation - for (i = 0; i < question.rows.length; i++) - { - bodyCells = ""; - for (j = 0; j < question.columns.length; j++) - { - cellGenerator = this[SideBySideMatrix.CELL_GENERATORS[question.columns[j].cellType]]; - if (typeof cellGenerator === "function") - { - // Passing rows, columns, subColumns as separate arguments - // to make generatorrs independent from table data-structure. - bodyCells += `${cellGenerator.call(this, question.rows[i], question.columns[j], question.columns[j].subColumns, CSS_CLASSES)}`; - } - else - { - console.log("No cell generator found for cellType", question.columns[j].cellType); - } - } - bodyHTML += `${question.rows[i].text}${bodyCells}`; - } - - html = ` - - ${headerCells} - ${subHeaderCells} - - ${bodyHTML} -
    `; - - // console.log("sbs matrix generation took", performance.now() - t); - el.insertAdjacentHTML("beforeend", html); - - let inputDOMS = el.querySelectorAll("input"); - let selectDOMS = el.querySelectorAll("select"); - - for (i = 0; i < inputDOMS.length; i++) - { - inputDOMS[i].addEventListener("input", this._bindedHandlers._handleInput); - } - - for (i = 0; i < selectDOMS.length; i++) - { - selectDOMS[i].addEventListener("change", this._bindedHandlers._handleSelectChange) - } - } -} - -export default function init (Survey) { - var widget = { - //the widget name. It should be unique and written in lowcase. - name: "sidebysidematrix", - - //the widget title. It is how it will appear on the toolbox of the SurveyJS Editor/Builder - title: "Side by side matrix", - - //the name of the icon on the toolbox. We will leave it empty to use the standard one - iconName: "", - - //If the widgets depends on third-party library(s) then here you may check if this library(s) is loaded - widgetIsLoaded: function () { - //return typeof $ == "function" && !!$.fn.select2; //return true if jQuery and select2 widget are loaded on the page - return true; //we do not require anything so we just return true. - }, - - //SurveyJS library calls this function for every question to check, if it should use this widget instead of default rendering/behavior - isFit: function (question) { - //we return true if the type of question is sidebysidematrix - return question.getType() === 'sidebysidematrix'; - //the following code will activate the widget for a text question with inputType equals to date - //return question.getType() === 'text' && question.inputType === "date"; - }, - - //Use this function to create a new class or add new properties or remove unneeded properties from your widget - //activatedBy tells how your widget has been activated by: property, type or customType - //property - it means that it will activated if a property of the existing question type is set to particular value, for example inputType = "date" - //type - you are changing the behaviour of entire question type. For example render radiogroup question differently, have a fancy radio buttons - //customType - you are creating a new type, like in our example "sidebysidematrix" - activatedByChanged: function (activatedBy) { - //we do not need to check acticatedBy parameter, since we will use our widget for customType only - //We are creating a new class and derived it from text question type. It means that text model (properties and fuctions) will be available to us - Survey.JsonObject.metaData.addClass("sidebysidematrix", [], null, "text"); - //signaturepad is derived from "empty" class - basic question class - //Survey.JsonObject.metaData.addClass("signaturepad", [], null, "empty"); - - //Add new property(s) - //For more information go to https://surveyjs.io/Examples/Builder/?id=addproperties#content-docs - Survey.JsonObject.metaData.addProperties("sidebysidematrix", [ - { - name: "rows", - default: [] - }, - { - name: "columns", - default: [] - } - ]); - }, - - //If you want to use the default question rendering then set this property to true. We do not need any default rendering, we will use our our htmlTemplate - isDefaultRender: false, - - //You should use it if your set the isDefaultRender to false - htmlTemplate: "
    ", - - //The main function, rendering and two-way binding - afterRender: function (question, el) { - new SideBySideMatrix({ question, el }); - // TODO: add readonly and enabled/disabled handlers. - - // let containers = el.querySelectorAll(".srv-slider-container"); - // let inputDOMS = el.querySelectorAll(".srv-slider"); - // let sliderDisplayDOMS = el.querySelectorAll(".srv-slider-display"); - // if (!(question.value instanceof Array)) - // { - // question.value = new Array(inputDOMS.length); - // question.value.fill(0); - // } - - // for (i = 0; i < inputDOMS.length; i++) - // { - // inputDOMS[i].min = question.minVal; - // inputDOMS[i].max = question.maxVal; - // inputDOMS[i].addEventListener("input", (e) => { - // let idx = parseInt(e.currentTarget.dataset.idx, 10); - // question.value[idx] = parseFloat(e.currentTarget.value); - // // using .value setter to trigger update properly. - // // otherwise on survey competion it returns array of nulls. - // question.value = question.value; - // onValueChangedCallback(); - // }); - - // // Handle grid lines? - // } - - - // function positionSliderDisplay (v, min, max, displayDOM) - // { - // v = parseFloat(v); - // min = parseFloat(min); - // max = parseFloat(max); - // // Formula is (halfThumbWidth - v * (fullThumbWidth / 100)), taking into account that display has translate(-50%, 0). - // // Size of thumb is set in CSS. - // displayDOM.style.left = `calc(${(v - min) / (max - min) * 100}% + ${10 - v * 0.2}px)` - // } - - - // var onValueChangedCallback = function () { - // let i; - // let v; - // for (i = 0; i < question.choices.length; i++) - // { - // v = question.value[i] || 0; - // inputDOMS[i].value = v; - // sliderDisplayDOMS[i].innerText = v; - // positionSliderDisplay(v, question.minVal, question.maxVal, sliderDisplayDOMS[i]); - // } - // } - - // var onReadOnlyChangedCallback = function() { - // let i; - // if (question.isReadOnly) { - // for (i = 0; i < question.choices.length; i++) - // { - // inputDOMS[i].setAttribute('disabled', 'disabled'); - // } - // } else { - // for (i = 0; i < question.choices.length; i++) - // { - // inputDOMS[i].removeAttribute("disabled"); - // } - // } - // }; - - // if question becomes readonly/enabled add/remove disabled attribute - // question.readOnlyChangedCallback = onReadOnlyChangedCallback; - - // if the question value changed in the code, for example you have changed it in JavaScript - // question.valueChangedCallback = onValueChangedCallback; - - // set initial value - // onValueChangedCallback(); - - // make elements disabled if needed - // onReadOnlyChangedCallback(); - }, - - //Use it to destroy the widget. It is typically needed by jQuery widgets - willUnmount: function (question, el) { - //We do not need to clear anything in our simple example - //Here is the example to destroy the image picker - //var $el = $(el).find("select"); - //$el.data('picker').destroy(); - } - } - - //Register our widget in singleton custom widget collection - Survey.CustomWidgetCollection.Instance.addCustomWidget(widget, "customtype"); -} diff --git a/src/visual/survey/SliderStar.js b/src/visual/survey/SliderStar.js deleted file mode 100644 index 1ff1014..0000000 --- a/src/visual/survey/SliderStar.js +++ /dev/null @@ -1,289 +0,0 @@ -/** -* @desc Slider Star. -* */ - -class SliderStar -{ - constructor (cfg = {}) - { - const surveyCSS = cfg.question.css; - this._CSS_CLASSES = { - // INPUT_TEXT: `${surveyCSS.text.root} slider-star-text-input` - INPUT_TEXT: `slider-star-text-input` - }; - this._question = cfg.question; - this._DOM = cfg.el; - this._engagedInputIdx = undefined; - this._pdowns = {}; - - this._bindedHandlers = - { - _handleInput: this._handleInput.bind(this), - _handlePointerDown: this._handlePointerDown.bind(this), - _handlePointerUp: this._handlePointerUp.bind(this), - _handlePointerMove: this._handlePointerMove.bind(this) - }; - - this._init(this._question, this._DOM); - } - - _markStarsActive (n, qIdx) - { - let stars = this._DOM.querySelectorAll(`.stars-container[data-idx="${qIdx}"] .star-slider-star-input`); - let i; - for (i = 0; i < stars.length; i++) - { - stars[i].classList.remove("active"); - if (i <= n - 1) - { - stars[i].classList.add("active"); - } - } - } - - _handleIndividualValueUpdate (v, qIdx) - { - if (this._question.value === undefined) - { - this._question.value = {}; - } - if (this._question.value[qIdx] !== v) - { - this._question.value[qIdx] = v; - this._DOM.querySelector(`.slider-star-text-input[name="${qIdx}"]`).value = v; - this._markStarsActive(v, qIdx); - // Triggering internal SurveyJS mechanism for value update. - this._question.value = this._question.value; - } - } - - _handleInput (e) - { - let v = parseInt(e.currentTarget.value, 10) || 0; - v = Math.max(0, Math.min(this._question.starCount, v)); - const qIdx = e.currentTarget.name; - this._handleIndividualValueUpdate(v, qIdx); - } - - _handlePointerDown (e) - { - e.preventDefault(); - this._engagedInputIdx = e.currentTarget.dataset.idx; - this._pdowns[this._engagedInputIdx] = true; - const starIdx = [].indexOf.call(e.target.parentElement.children, e.target); - this._handleIndividualValueUpdate(starIdx + 1, this._engagedInputIdx); - } - - _handlePointerUp (e) - { - if (this._engagedInputIdx !== undefined) - { - this._pdowns[this._engagedInputIdx] = false; - } - this._engagedInputIdx = undefined; - } - - _handlePointerMove (e) - { - if (this._pdowns[this._engagedInputIdx]) - { - e.preventDefault(); - const starIdx = [].indexOf.call(e.target.parentElement.children, e.target); - this._handleIndividualValueUpdate(starIdx + 1, this._engagedInputIdx); - } - } - - _init (question, el) - { - let t = performance.now(); - let starsHTML = new Array(question.starCount).fill(`
    ★
    `).join(""); - let html = ""; - let i; - for (i = 0; i < question.choices.length; i++) - { - html += - `
    -
    ${question.choices[i].text}
    -
    -
    ${starsHTML}
    - ${question.showValue ? - `` : - ""} -
    -
    `; - } - - el.insertAdjacentHTML("beforeend", html); - const inputDOMS = el.querySelectorAll(".slider-star-text-input"); - const starsContainers = el.querySelectorAll(".stars-container"); - - // Amount of inputDOMS and starsCointainer is the same. - // Also iterating over starContainers since text inputs might be absent. - for (i = 0; i < starsContainers.length; i++) - { - inputDOMS[i].addEventListener("input", this._bindedHandlers._handleInput); - starsContainers[i].addEventListener("pointerdown", this._bindedHandlers._handlePointerDown); - starsContainers[i].addEventListener("pointermove", this._bindedHandlers._handlePointerMove); - } - window.addEventListener("pointerup", this._bindedHandlers._handlePointerUp); - } -} - -export default function init (Survey) { - var widget = { - //the widget name. It should be unique and written in lowcase. - name: "sliderstar", - - //the widget title. It is how it will appear on the toolbox of the SurveyJS Editor/Builder - title: "Slider Star", - - //the name of the icon on the toolbox. We will leave it empty to use the standard one - iconName: "", - - //If the widgets depends on third-party library(s) then here you may check if this library(s) is loaded - widgetIsLoaded: function () { - //return typeof $ == "function" && !!$.fn.select2; //return true if jQuery and select2 widget are loaded on the page - return true; //we do not require anything so we just return true. - }, - - //SurveyJS library calls this function for every question to check, if it should use this widget instead of default rendering/behavior - isFit: function (question) { - //we return true if the type of question is sliderstar - return question.getType() === 'sliderstar'; - //the following code will activate the widget for a text question with inputType equals to date - //return question.getType() === 'text' && question.inputType === "date"; - }, - - //Use this function to create a new class or add new properties or remove unneeded properties from your widget - //activatedBy tells how your widget has been activated by: property, type or customType - //property - it means that it will activated if a property of the existing question type is set to particular value, for example inputType = "date" - //type - you are changing the behaviour of entire question type. For example render radiogroup question differently, have a fancy radio buttons - //customType - you are creating a new type, like in our example "sliderstar" - activatedByChanged: function (activatedBy) { - //we do not need to check acticatedBy parameter, since we will use our widget for customType only - //We are creating a new class and derived it from text question type. It means that text model (properties and fuctions) will be available to us - Survey.JsonObject.metaData.addClass("sliderstar", [], null, "text"); - //signaturepad is derived from "empty" class - basic question class - //Survey.JsonObject.metaData.addClass("signaturepad", [], null, "empty"); - - //Add new property(s) - //For more information go to https://surveyjs.io/Examples/Builder/?id=addproperties#content-docs - Survey.JsonObject.metaData.addProperties("sliderstar", [ - { - name: "choices", - default: [] - }, - { - name: "starCount", - default: 5 - }, - { - name: "showValue", - default: true - }, - { - name: "starType", - default: "descrete" - } - ]); - }, - - //If you want to use the default question rendering then set this property to true. We do not need any default rendering, we will use our our htmlTemplate - isDefaultRender: false, - - //You should use it if your set the isDefaultRender to false - htmlTemplate: "
    ", - - //The main function, rendering and two-way binding - afterRender: function (question, el) { - new SliderStar({ question, el }); - - // let containers = el.querySelectorAll(".srv-slider-container"); - // let inputDOMS = el.querySelectorAll(".srv-slider"); - // let sliderDisplayDOMS = el.querySelectorAll(".srv-slider-display"); - // if (!(question.value instanceof Array)) - // { - // question.value = new Array(inputDOMS.length); - // question.value.fill(0); - // } - - // for (i = 0; i < inputDOMS.length; i++) - // { - // inputDOMS[i].min = question.minVal; - // inputDOMS[i].max = question.maxVal; - // inputDOMS[i].addEventListener("input", (e) => { - // let idx = parseInt(e.currentTarget.dataset.idx, 10); - // question.value[idx] = parseFloat(e.currentTarget.value); - // // using .value setter to trigger update properly. - // // otherwise on survey competion it returns array of nulls. - // question.value = question.value; - // onValueChangedCallback(); - // }); - - // // Handle grid lines? - // } - - - // function positionSliderDisplay (v, min, max, displayDOM) - // { - // v = parseFloat(v); - // min = parseFloat(min); - // max = parseFloat(max); - // // Formula is (halfThumbWidth - v * (fullThumbWidth / 100)), taking into account that display has translate(-50%, 0). - // // Size of thumb is set in CSS. - // displayDOM.style.left = `calc(${(v - min) / (max - min) * 100}% + ${10 - v * 0.2}px)` - // } - - - // var onValueChangedCallback = function () { - // let i; - // let v; - // for (i = 0; i < question.choices.length; i++) - // { - // v = question.value[i] || 0; - // inputDOMS[i].value = v; - // sliderDisplayDOMS[i].innerText = v; - // positionSliderDisplay(v, question.minVal, question.maxVal, sliderDisplayDOMS[i]); - // } - // } - - // var onReadOnlyChangedCallback = function() { - // let i; - // if (question.isReadOnly) { - // for (i = 0; i < question.choices.length; i++) - // { - // inputDOMS[i].setAttribute('disabled', 'disabled'); - // } - // } else { - // for (i = 0; i < question.choices.length; i++) - // { - // inputDOMS[i].removeAttribute("disabled"); - // } - // } - // }; - - // if question becomes readonly/enabled add/remove disabled attribute - // question.readOnlyChangedCallback = onReadOnlyChangedCallback; - - // if the question value changed in the code, for example you have changed it in JavaScript - // question.valueChangedCallback = onValueChangedCallback; - - // set initial value - // onValueChangedCallback(); - - // make elements disabled if needed - // onReadOnlyChangedCallback(); - }, - - //Use it to destroy the widget. It is typically needed by jQuery widgets - willUnmount: function (question, el) { - //We do not need to clear anything in our simple example - //Here is the example to destroy the image picker - //var $el = $(el).find("select"); - //$el.data('picker').destroy(); - } - } - - //Register our widget in singleton custom widget collection - Survey.CustomWidgetCollection.Instance.addCustomWidget(widget, "customtype"); -} diff --git a/src/visual/survey/components/MatrixBipolar.js b/src/visual/survey/components/MatrixBipolar.js index 2b956b2..638ba09 100644 --- a/src/visual/survey/components/MatrixBipolar.js +++ b/src/visual/survey/components/MatrixBipolar.js @@ -22,7 +22,8 @@ function handleBipolarMatrixRendering (survey, options) let rowsDOM = options.htmlElement.querySelectorAll("tbody tr"); // let rowCaptionsDOM = options.htmlElement.querySelectorAll("tbody tr td:nth-child(1) .sv-string-viewer"); let rowCaptionsDOM = options.htmlElement.querySelectorAll("tbody tr td:nth-child(1) span"); - let captionsClassList = rowCaptionsDOM[0].classList.toString(); + let captionsClassList = rowCaptionsDOM[0].classList; + let cellClassList = rowsDOM[0].children[0].classList; let rowCaptions = new Array(options.question.rows.length); let rowCaptionOppositeHTML = ""; let i; @@ -30,7 +31,7 @@ function handleBipolarMatrixRendering (survey, options) { rowCaptions[i] = options.question.rows[i].text.split(":"); rowCaptionsDOM[i].innerText = rowCaptions[i][0]; - rowCaptionOppositeHTML = `${rowCaptions[i][1]}`; + rowCaptionOppositeHTML = `${rowCaptions[i][1]}`; rowsDOM[i].insertAdjacentHTML("beforeend", rowCaptionOppositeHTML); } } @@ -38,7 +39,7 @@ function handleBipolarMatrixRendering (survey, options) export default { registerSurveyProperties (Survey) { - Survey.Serializer.addProperty("question", + Survey.Serializer.addProperty("matrix", { name: "subType:text", default: "", diff --git a/src/visual/survey/widgets/MaxDiffMatrix.js b/src/visual/survey/widgets/MaxDiffMatrix.js index 8a9a8ec..d9958c5 100644 --- a/src/visual/survey/widgets/MaxDiffMatrix.js +++ b/src/visual/survey/widgets/MaxDiffMatrix.js @@ -16,6 +16,10 @@ class MaxDiffMatrix TABLE_HEADER_CELL: surveyCSS.matrix.headerCell, TABLE_CELL: surveyCSS.matrix.cell, INPUT_TEXT: surveyCSS.text.root, + LABEL: surveyCSS.matrix.label, + ITEM_CHECKED: surveyCSS.matrix.itemChecked, + ITEM_VALUE: surveyCSS.matrix.itemValue, + ITEM_DECORATOR: surveyCSS.matrix.materialDecorator, RADIO: surveyCSS.radiogroup.item, SELECT: surveyCSS.dropdown.control, CHECKBOX: surveyCSS.checkbox.item @@ -84,6 +88,13 @@ class MaxDiffMatrix { let t = performance.now(); const CSS_CLASSES = this._CSS_CLASSES; + if (question.css.matrix.mainRoot) + { + // Replacing default mainRoot class with those used in matrix type questions, to achieve proper styling and overflow behavior + const rootClass = `${question.css.matrix.mainRoot} ${question.cssClasses.withFrame || ""}`; + question.setCssRoot(rootClass); + question.cssClasses.mainRoot = rootClass; + } let html; let headerCells = ""; let subHeaderCells = ""; @@ -106,11 +117,21 @@ class MaxDiffMatrix for (i = 0; i < question.rows.length; i++) { bodyCells = - ` + ` + + ${question.rows[i].text} - `; + + + `; bodyHTML += `${bodyCells}`; } @@ -175,10 +196,12 @@ export default function init (Survey) { Survey.JsonObject.metaData.addProperties("maxdiffmatrix", [ { name: "rows", + isArray: true, default: [] }, { name: "columns", + isArray: true, default: [] } ]); diff --git a/src/visual/survey/widgets/SelectBox.js b/src/visual/survey/widgets/SelectBox.js index d57eeb0..18c2bec 100644 --- a/src/visual/survey/widgets/SelectBox.js +++ b/src/visual/survey/widgets/SelectBox.js @@ -45,7 +45,12 @@ export default function init (Survey) { Survey.JsonObject.metaData.addProperties("selectbox", [ { name: "choices", + isArray: true, default: [] + }, + { + name: "multipleAnswer", + default: true } ]); }, @@ -54,7 +59,7 @@ export default function init (Survey) { isDefaultRender: false, //You should use it if your set the isDefaultRender to false - htmlTemplate: "
    ", + htmlTemplate: `
    `, //The main function, rendering and two-way binding afterRender: function (question, el) { @@ -65,9 +70,20 @@ export default function init (Survey) { optionsHTML += ``; } - let selectDOM = el.querySelector("select"); - selectDOM.innerHTML = optionsHTML; + let additionalAttr = ""; + if (question.multipleAnswer) + { + additionalAttr = "multiple"; + } + else + { + additionalAttr = "size=\"4\""; + } + let selectHTML = ``; + el.insertAdjacentHTML("beforeend", selectHTML); + + let selectDOM = el.querySelector("select"); selectDOM.addEventListener('input', (e) => { let i; let opts = new Array(e.currentTarget.selectedOptions.length); diff --git a/src/visual/survey/widgets/SideBySideMatrix.js b/src/visual/survey/widgets/SideBySideMatrix.js index 6a8159a..c389c95 100644 --- a/src/visual/survey/widgets/SideBySideMatrix.js +++ b/src/visual/survey/widgets/SideBySideMatrix.js @@ -17,15 +17,22 @@ class SideBySideMatrix // INCLUDING those added/modified by application's code. const surveyCSS = cfg.question.css; this._CSS_CLASSES = { - WRAPPER: surveyCSS.matrix.tableWrapper, + WRAPPER: `${surveyCSS.matrix.tableWrapper} sbs-matrix`, TABLE: surveyCSS.matrix.root, TABLE_ROW: surveyCSS.matrixdropdown.row, TABLE_HEADER_CELL: surveyCSS.matrix.headerCell, TABLE_CELL: surveyCSS.matrix.cell, INPUT_TEXT: surveyCSS.text.root, + LABEL: surveyCSS.matrix.label, + ITEM_CHECKED: surveyCSS.matrix.itemChecked, + ITEM_VALUE: surveyCSS.matrix.itemValue, + ITEM_DECORATOR: surveyCSS.matrix.materialDecorator, RADIO: surveyCSS.radiogroup.item, SELECT: surveyCSS.dropdown.control, - CHECKBOX: surveyCSS.checkbox.item + CHECKBOX: surveyCSS.checkbox.item, + CHECKBOX_CONTROL: surveyCSS.checkbox.itemControl, + CHECKBOX_DECORATOR: surveyCSS.checkbox.materialDecorator, + CHECKBOX_DECORATOR_SVG: surveyCSS.checkbox.itemDecorator }; this._question = cfg.question; this._DOM = cfg.el; @@ -71,7 +78,10 @@ class SideBySideMatrix { bodyCells += ` - + `; } return bodyCells; @@ -85,7 +95,14 @@ class SideBySideMatrix { bodyCells += ` - + `; } return bodyCells; @@ -168,7 +185,10 @@ class SideBySideMatrix // TODO: Find out how it actually composed inside SurveyJS. if (question.css.matrix.mainRoot) { - question.setCssRoot(`${question.css.matrix.mainRoot} ${question.cssClasses.withFrame || ""}`); + // Replacing default mainRoot class with those used in matrix type questions, to achieve proper styling and overflow behavior + const rootClass = `${question.css.matrix.mainRoot} ${question.cssClasses.withFrame || ""}`; + question.setCssRoot(rootClass); + question.cssClasses.mainRoot = rootClass; } let html; let headerCells = ""; @@ -189,7 +209,10 @@ class SideBySideMatrix `; for (j = 0; j < question.columns[i].subColumns.length; j++) { - subHeaderCells += `${question.columns[i].subColumns[j].text}`; + subHeaderCells += ` + ${question.columns[i].subColumns[j].text} + `; } } else @@ -198,7 +221,7 @@ class SideBySideMatrix ` ${question.columns[i].title} `; - subHeaderCells += ""; + subHeaderCells += ``; } headerCells += ""; subHeaderCells += ""; @@ -227,8 +250,8 @@ class SideBySideMatrix html = ` - ${headerCells} - ${subHeaderCells} + ${headerCells} + ${subHeaderCells}${bodyHTML}
    `; @@ -293,10 +316,12 @@ export default function init (Survey) { Survey.JsonObject.metaData.addProperties("sidebysidematrix", [ { name: "rows", + isArray: true, default: [] }, { name: "columns", + isArray: true, default: [] } ]); diff --git a/src/visual/survey/widgets/SliderStar.js b/src/visual/survey/widgets/SliderStar.js index d9311e9..8c6c223 100644 --- a/src/visual/survey/widgets/SliderStar.js +++ b/src/visual/survey/widgets/SliderStar.js @@ -6,6 +6,11 @@ class SliderStar { constructor (cfg = {}) { + const surveyCSS = cfg.question.css; + this._CSS_CLASSES = { + // INPUT_TEXT: `${surveyCSS.text.root} slider-star-text-input` + INPUT_TEXT: `slider-star-text-input` + }; this._question = cfg.question; this._DOM = cfg.el; this._engagedInputIdx = undefined; @@ -102,7 +107,7 @@ class SliderStar
    ${starsHTML}
    ${question.showValue ? - `` : + `` : ""}
    `; @@ -166,6 +171,7 @@ export default function init (Survey) { Survey.JsonObject.metaData.addProperties("sliderstar", [ { name: "choices", + isArray: true, default: [] }, { diff --git a/src/visual/survey/widgets/SliderWidget.js b/src/visual/survey/widgets/SliderWidget.js index 33896d8..1d71359 100644 --- a/src/visual/survey/widgets/SliderWidget.js +++ b/src/visual/survey/widgets/SliderWidget.js @@ -44,6 +44,7 @@ export default function init (Survey) { Survey.JsonObject.metaData.addProperties("slider", [ { name: "choices", + isArray: true, default: [] }, { From 61a2f4286a4205ff1cb056311e6e37987542c2d3 Mon Sep 17 00:00:00 2001 From: Alain Pitiot Date: Thu, 2 Feb 2023 12:49:53 +0100 Subject: [PATCH 25/41] _ --- .../survey/components/DropdownExtensions.js | 48 ++++++++++ .../extensions/customExpressionFunctions.js | 89 +++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 src/visual/survey/components/DropdownExtensions.js create mode 100644 src/visual/survey/extensions/customExpressionFunctions.js diff --git a/src/visual/survey/components/DropdownExtensions.js b/src/visual/survey/components/DropdownExtensions.js new file mode 100644 index 0000000..a5f8704 --- /dev/null +++ b/src/visual/survey/components/DropdownExtensions.js @@ -0,0 +1,48 @@ +/** + * @desc: Extensions for default dropdown component of SurveyJS to make it more nice to interact with on mobile devices. + * @type: SurveyJS component modification. + */ + +function handleValueChange (survey, options, e) +{ + options.question.value = e.currentTarget.value; +} + +function handleValueChangeForDOM (survey, options) +{ + options.htmlElement.querySelector("select").value = options.question.value; +} + +function handleDropdownRendering (survey, options) +{ + // Default SurveyJS drop down is actually an with customly built options list + // It works well on desktop, but not that convenient on mobile. + // Adding native ${optionsHTML}`; + options.htmlElement.querySelector('.sd-selectbase').insertAdjacentHTML("beforebegin", selectHTML); + + const selectDOM = options.htmlElement.querySelector("select"); + selectDOM.addEventListener("change", handleValueChange.bind(this, survey, options)); + + options.question.valueChangedCallback = handleValueChangeForDOM.bind(this, survey, options); +} + +export default { + registerModelCallbacks (surveyModel) + { + surveyModel.onAfterRenderQuestion.add((survey, options) => { + if (options.question.getType() === "dropdown") + { + handleDropdownRendering(survey, options); + } + }); + } +}; diff --git a/src/visual/survey/extensions/customExpressionFunctions.js b/src/visual/survey/extensions/customExpressionFunctions.js new file mode 100644 index 0000000..43bae5f --- /dev/null +++ b/src/visual/survey/extensions/customExpressionFunctions.js @@ -0,0 +1,89 @@ +// Wrapping everything in Class and defining as static methods to prevent esbuild from renaming when bundling. +// NOTE! Survey stim uses property .name of these methods on registering stage. +// Methods are available inside SurveyJS expressions using their actual names. +class ExpressionFunctions { + static rnd () + { + return Math.random(); + } + + static arrayContains (params) + { + if (params[0] instanceof Array) + { + let searchValue = params[1]; + let searchResult = params[0].indexOf(searchValue) !== -1; + + // If no results at first, trying conversion combinations, since array of values sometimes might + // contain both string and number data types. + if (searchResult === false) + { + if (typeof searchValue === "string") + { + searchValue = parseFloat(searchValue); + searchResult = params[0].indexOf(searchValue) !== -1; + } + else if (typeof searchValue === "number") + { + searchValue = searchValue.toString(); + searchResult = params[0].indexOf(searchValue) !== -1; + } + } + + return searchResult + } + return false; + } + + static stringContains (params) + { + if (typeof params[0] === "string") + { + return params[0].indexOf(params[1]) !== -1; + } + return false; + } + + static isEmpty (params) + { + let questionIsEmpty = false; + if (params[0] instanceof Array || typeof params[0] === "string") + { + questionIsEmpty = params[0].length === 0; + } + else + { + questionIsEmpty = params[0] === undefined || params[0] === null; + } + return questionIsEmpty; + } + + static isNotEmpty (params) + { + return !ExpressionFunctions.isEmpty(params); + } +} + + +export default [ + { + func: ExpressionFunctions.rnd, + isAsync: false + }, + { + func: ExpressionFunctions.arrayContains, + isAsync: false + }, + { + func: ExpressionFunctions.stringContains, + isAsync: false + }, + { + func: ExpressionFunctions.isEmpty, + isAsync: false + }, + { + func: ExpressionFunctions.isNotEmpty, + isAsync: false + } +]; From 0c578e26d0ca826e7b77c64f6e0b4e0650fe6dc0 Mon Sep 17 00:00:00 2001 From: Alain Pitiot Date: Thu, 2 Feb 2023 12:56:23 +0100 Subject: [PATCH 26/41] FF: Allow TextBox to accept placeholder as input --- package.json | 2 +- src/visual/TextBox.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3cfae9b..8c7e2cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "psychojs", - "version": "2022.2.5", + "version": "2022.3.1", "private": true, "description": "Helps run in-browser neuroscience, psychology, and psychophysics experiments", "license": "MIT", diff --git a/src/visual/TextBox.js b/src/visual/TextBox.js index 3930cbf..4d8e2bc 100644 --- a/src/visual/TextBox.js +++ b/src/visual/TextBox.js @@ -65,6 +65,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) opacity, depth, text, + placeholder, font, letterHeight, bold, @@ -98,7 +99,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) ); this._addAttribute( "placeholder", - text, + placeholder, "", this._onChange(true, true), ); From 30c937b2138084bc8c996bd7b826aedaef2bcb26 Mon Sep 17 00:00:00 2001 From: lgtst Date: Mon, 13 Feb 2023 19:40:22 +0000 Subject: [PATCH 27/41] progress bar component prototype. --- src/visual/Progress.js | 133 +++++++++++++++++++++++++++++++++++++++++ src/visual/index.js | 1 + 2 files changed, 134 insertions(+) create mode 100644 src/visual/Progress.js diff --git a/src/visual/Progress.js b/src/visual/Progress.js new file mode 100644 index 0000000..279d4d4 --- /dev/null +++ b/src/visual/Progress.js @@ -0,0 +1,133 @@ +import * as PIXI from "pixi.js-legacy"; +import * as util from "../util/Util.js"; +import { Color } from "../util/Color.js"; +import { to_pixiPoint } from "../util/Pixi.js"; +import { VisualStim } from "./VisualStim.js"; + +export class Progress extends VisualStim +{ + constructor ( + { + name, + win, + units, + ori, + opacity, + depth, + pos, + anchor = "left", + size = [300, 30], + clipMask, + autoDraw, + autoLog, + progress = 1, + type, + fillColor, + fillTexture + }) + { + super({ + name, + win, + units, + ori, + opacity, + depth, + pos, + anchor, + size, + clipMask, + autoDraw, + autoLog + }); + + this._addAttribute("progress", progress, 0); + this._addAttribute("type", type, PROGRESS_TYPES.BAR); + this._addAttribute("fillColor", fillColor, "lightgreen"); + this._addAttribute("fillTexture", fillTexture, PIXI.Texture.WHITE); + } + + setProgress (progress = 0, log = false) + { + this._setAttribute("progress", Math.min(1.0, Math.max(0.0, progress)), log); + if (this._pixi !== undefined) + { + this._pixi.clear(); + const size_px = util.to_px(this.size, this.units, this.win); + const pos_px = util.to_px(this.pos, this.units, this.win); + const progressWidth = size_px[0] * this._progress; + if (this._fillTexture) + { + let t = PIXI.Texture.WHITE; + if (typeof this._fillTexture === "string") + { + t = PIXI.Texture.from(this._fillTexture); + t.baseTexture.scaleMode = PIXI.SCALE_MODES.NEAREST; + } + this._pixi.beginTextureFill({ + texture: t + }); + } + else + { + this._pixi.beginFill(new Color(this._fillColor).int, this._opacity); + } + if (this._type === PROGRESS_TYPES.BAR) + { + this._pixi.drawRect(pos_px[0], pos_px[1], progressWidth, size_px[1]); + } + // TODO: check out beginTextureFill(). Perhaps it will allow to use images as filling for progress. + this._pixi.endFill(); + + // TODO: is there a better way to ensure anchor works? + this.anchor = this._anchor; + } + } + + /** + * Update the stimulus, if necessary. + * + * @protected + */ + _updateIfNeeded() + { + // TODO: figure out what is the error with estimateBoundBox on resize? + if (!this._needUpdate) + { + return; + } + this._needUpdate = false; + + // update the PIXI representation, if need be: + if (this._needPixiUpdate) + { + this._needPixiUpdate = false; + + if (typeof this._pixi !== "undefined") + { + this._pixi.destroy(true); + } + this._pixi = new PIXI.Graphics(); + // TODO: Should we do this? + // this._pixi.lineStyle(this._lineWidth, this._lineColor.int, this._opacity, 0.5); + + // TODO: Should just .setProgress() be called? + this.setProgress(this._progress); + + this._pixi.scale.y = -1; + this._pixi.zIndex = -this._depth; + this.anchor = this._anchor; + } + + // set polygon position and rotation: + // TODO: what's the difference bw to_px and to_pixiPoint? + this._pixi.position = to_pixiPoint(this.pos, this.units, this.win); + this._pixi.rotation = -this.ori * Math.PI / 180.0; + } +} + +export const PROGRESS_TYPES = +{ + BAR: 0, + CIRCLE: 1 +} diff --git a/src/visual/index.js b/src/visual/index.js index 8c604fa..07c75b0 100644 --- a/src/visual/index.js +++ b/src/visual/index.js @@ -13,3 +13,4 @@ export * from "./TextStim.js"; export * from "./VisualStim.js"; export * from "./FaceDetector.js"; export * from "./Survey.js"; +export * from "./Progress.js"; From d9f5cfae6375d6434ea31bb1eee28058dca8a06f Mon Sep 17 00:00:00 2001 From: Alain Pitiot Date: Thu, 23 Mar 2023 15:19:08 +0100 Subject: [PATCH 28/41] ENH: improved super-flow survey; ENH: release of resources --- src/core/PsychoJS.js | 2 +- src/core/ServerManager.js | 38 +++++- src/index.css | 2 +- src/util/Clock.js | 1 + src/util/Util.js | 29 ++++- src/visual/Survey.js | 249 +++++++++++++++++++------------------- 6 files changed, 187 insertions(+), 134 deletions(-) diff --git a/src/core/PsychoJS.js b/src/core/PsychoJS.js index ef5ef7f..21d9f35 100644 --- a/src/core/PsychoJS.js +++ b/src/core/PsychoJS.js @@ -789,7 +789,7 @@ export class PsychoJS const self = this; window.onerror = function(message, source, lineno, colno, error) - {console.log('@@@', message) + { // check for ResizeObserver loop limit exceeded error: // ref: https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded if (message === "ResizeObserver loop limit exceeded" || diff --git a/src/core/ServerManager.js b/src/core/ServerManager.js index 1b59a99..2415c50 100644 --- a/src/core/ServerManager.js +++ b/src/core/ServerManager.js @@ -314,6 +314,34 @@ export class ServerManager extends PsychObject return pathStatusData.data; } + /** + * Release a resource. + * + * @param {string} name - the name of the resource to release + * @return {boolean} true if a resource with the given name was previously registered with the manager, + * false otherwise. + */ + releaseResource(name) + { + const response = { + origin: "ServerManager.releaseResource", + context: "when releasing resource: " + name, + }; + + const pathStatusData = this._resources.get(name); + + if (typeof pathStatusData === "undefined") + { + return false; + } + + // TODO check the current status: prevent the release of a resources currently downloading + + this._psychoJS.logger.debug(`releasing resource: ${name}`); + this._resources.delete(name); + return true; + } + /** * Get the status of a single resource or the reduced status of an array of resources. * @@ -506,18 +534,18 @@ export class ServerManager extends PsychObject // pre-process the resources: for (let r = 0; r < resources.length; ++r) { - const resource = resources[r]; - // convert those resources that are only a string to an object with name and path: - if (typeof resource === "string") + if (typeof resources[r] === "string") { resources[r] = { - name: resource, - path: resource, + name: resources[r], + path: resources[r], download: true }; } + const resource = resources[r]; + // deal with survey models: if ("surveyId" in resource) { diff --git a/src/index.css b/src/index.css index c903ea8..301aaa1 100644 --- a/src/index.css +++ b/src/index.css @@ -13,7 +13,7 @@ body { /* Initialisation message (which will disappear behind the canvas) */ #root::after { - content: "initialising the experiment..."; + content: "initialising..."; position: fixed; top: 50%; left: 50%; diff --git a/src/util/Clock.js b/src/util/Clock.js index 3e92b5d..f0e7874 100644 --- a/src/util/Clock.js +++ b/src/util/Clock.js @@ -90,6 +90,7 @@ export class MonotonicClock { // yyyy-mm-dd, hh:mm:ss.sss return MonotonicClock.getDate() + .replaceAll("/","-") // yyyy-mm-dd_hh:mm:ss.sss .replace(", ", "_") // yyyy-mm-dd_hh[h]mm:ss.sss diff --git a/src/util/Util.js b/src/util/Util.js index 02a6133..0845207 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -322,24 +322,43 @@ export function IsPointInsidePolygon(point, vertices) } /** - * Shuffle an array in place using the Fisher-Yastes's modern algorithm + * Shuffle an array, or a portion of that array, in place using the Fisher-Yastes's modern algorithm *

    See details here: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm

    * * @param {Object[]} array - the input 1-D array - * @param {Function} [randomNumberGenerator = undefined] - A function used to generated random numbers in the interal [0, 1). Defaults to Math.random + * @param {Function} [randomNumberGenerator= undefined] - A function used to generated random numbers in the interval [0, 1). Defaults to Math.random + * @param [startIndex= undefined] - start index in the array + * @param [endIndex= undefined] - end index in the array * @return {Object[]} the shuffled array */ -export function shuffle(array, randomNumberGenerator = undefined) +export function shuffle(array, randomNumberGenerator = undefined, startIndex = undefined, endIndex = undefined) { - if (randomNumberGenerator === undefined) + // if array is not an array, we return it untouched rather than throwing an exception: + if (!array || !Array.isArray(array)) + { + return array; + } + + if (typeof startIndex === "undefined") + { + startIndex = 0; + } + if (typeof endIndex === "undefined") + { + endIndex = array.length - 1; + } + + if (typeof randomNumberGenerator === "undefined") { randomNumberGenerator = Math.random; } - for (let i = array.length - 1; i > 0; i--) + + for (let i = endIndex; i > startIndex; i--) { const j = Math.floor(randomNumberGenerator() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } + return array; } diff --git a/src/visual/Survey.js b/src/visual/Survey.js index d4cf781..b573e16 100644 --- a/src/visual/Survey.js +++ b/src/visual/Survey.js @@ -23,26 +23,7 @@ import MatrixBipolar from "./survey/components/MatrixBipolar.js"; import DropdownExtensions from "./survey/components/DropdownExtensions.js"; import customExpressionFunctionsArray from "./survey/extensions/customExpressionFunctions.js"; -const CAPTIONS = { - NEXT: "Next" -}; -const SURVEY_SETTINGS = { - minWidth: "100px" -}; - -const SURVEY_COMPLETION_CODES = -{ - NORMAL: 0, - SKIP_TO_END_OF_BLOCK: 1, - SKIP_TO_END_OF_SURVEY: 2 -}; - -const NODE_EXIT_CODES = -{ - NORMAL: 0, - BREAK_FLOW: 1 -}; /** * Survey Stimulus. @@ -63,6 +44,24 @@ export class Survey extends VisualStim ENDSURVEY: "END" }; + static CAPTIONS = + { + NEXT: "Next" + }; + + static SURVEY_COMPLETION_CODES = + { + NORMAL: 0, + SKIP_TO_END_OF_BLOCK: 1, + SKIP_TO_END_OF_SURVEY: 2 + }; + + static NODE_EXIT_CODES = + { + NORMAL: 0, + BREAK_FLOW: 1 + }; + /** * @memberOf module:visual * @param {Object} options @@ -83,19 +82,12 @@ export class Survey extends VisualStim { super({ name, win, units, ori, depth, pos, size, autoDraw, autoLog }); - // the default surveyId is an uuid based on the experiment id (or name) and the survey name: - // this way, it is always the same within a given experiment - this._hasSelfGeneratedSurveyId = (typeof surveyId === "undefined"); - const defaultSurveyId = (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER) ? - util.makeUuid(`${name}@${this._psychoJS.config.gitlab.projectId}`) : - util.makeUuid(`${name}@${this._psychoJS.config.experiment.name}`); - // whether the user is done with the survey, independently of whether the survey is completed: this.isFinished = false; - // Accumulated completion flag that is being set after completion of one survey node. - // This flag allows to track completion progress while moving through the survey flow. - // Initially set to true and will be flipped if at least one of the survey nodes were not fully completed. + // accumulated completion flag updated after each survey node is completed + // note: this make it possible to track completion as we move through the survey flow. + // _isCompletedAll will be flipped to false whenever a survey node is not completed this._isCompletedAll = true; // timestamps associated to each question: @@ -103,10 +95,9 @@ export class Survey extends VisualStim // timestamps clock: this._questionAnswerTimestampClock = new Clock(); - this._totalSurveyResults = {}; + this._overallSurveyResults = {}; this._surveyData = undefined; this._surveyModel = undefined; - this._signaturePadRO = undefined; this._expressionsRunner = undefined; this._lastPageSwitchHandledIdx = -1; this._variables = {}; @@ -114,23 +105,36 @@ export class Survey extends VisualStim this._surveyRunningPromise = undefined; this._surveyRunningPromiseResolve = undefined; this._surveyRunningPromiseReject = undefined; - // callback triggered when the user is done with the survey: nothing to do by default this._onFinishedCallback = () => {}; - // init SurveyJS + // init SurveyJS: this._initSurveyJS(); + // default size: + if (typeof size === "undefined") + { + this.size = (this.unit === "norm") ? [2.0, 2.0] : [1.0, 1.0]; + } + this._addAttribute( "model", model ); + + // the default surveyId is an uuid based on the experiment id (or name) and the survey name: + // this way, it is always the same within a given experiment + this._hasSelfGeneratedSurveyId = (typeof surveyId === "undefined"); + const defaultSurveyId = (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER) ? + util.makeUuid(`${name}@${this._psychoJS.config.gitlab.projectId}`) : + util.makeUuid(`${name}@${this._psychoJS.config.experiment.name}`); this._addAttribute( "surveyId", surveyId, defaultSurveyId ); + // estimate the bounding box: this._estimateBoundingBox(); @@ -213,7 +217,7 @@ export class Survey extends VisualStim logs: [] }; - this.psychoJS.logger.debug(`converted the old model to the new super-flow model: ${JSON.stringify(model)}`); + this.psychoJS.logger.debug(`converted the legacy model to the new super-flow model: ${JSON.stringify(model)}`); } this._surveyData = model; @@ -227,6 +231,24 @@ export class Survey extends VisualStim } } + /** + * Setter for the surveyId attribute. + * + * @param {string} surveyId - the survey Id + * @param {boolean} [log= false] - whether to log + * @return {void} + */ + setSurveyId(surveyId, log = false) + { + this._setAttribute("surveyId", surveyId, log); + + // only update the model if a genuine surveyId was given as parameter to the Survey: + if (!this._hasSelfGeneratedSurveyId) + { + this.setModel(`${surveyId}.sid`, log); + } + } + /** * Set survey variables. * @@ -254,7 +276,8 @@ export class Survey extends VisualStim { if (excludedNames.indexOf(name) === -1) { - this._surveyData.variables[name] = variables[name]; + this._variables[name] = variables[name]; + // this._surveyData.variables[name] = variables[name]; } } } @@ -282,22 +305,6 @@ export class Survey extends VisualStim return this._surveyModel.runExpression(expression); } - /** - * Setter for the surveyId attribute. - * - * @param {string} surveyId - the survey Id - * @param {boolean} [log= false] - whether to log - * @return {void} - */ - setSurveyId(surveyId, log = false) - { - this._setAttribute("surveyId", surveyId, log); - if (!this._hasSelfGeneratedSurveyId) - { - this.setModel(`${surveyId}.sid`, log); - } - } - /** * Add a callback that will be triggered when the participant finishes the survey. * @@ -336,7 +343,7 @@ export class Survey extends VisualStim // return this._surveyModel.data; - return this._totalSurveyResults; + return this._overallSurveyResults; } /** @@ -374,7 +381,6 @@ export class Survey extends VisualStim {} ); - // if the response cannot be uploaded, e.g. the experiment is running locally, or // if it is piloting mode, then we offer the response as a file for download: if (this._psychoJS.getEnvironment() !== ExperimentHandler.Environment.SERVER || @@ -420,9 +426,7 @@ export class Survey extends VisualStim */ hide() { - // if a survey div already does not exist already, create it: - const surveyId = `survey-${this._name}`; - const surveyDiv = document.getElementById(surveyId); + const surveyDiv = document.getElementById(this._surveyDivId); if (surveyDiv !== null) { document.body.removeChild(surveyDiv); @@ -468,9 +472,9 @@ export class Survey extends VisualStim this._needPixiUpdate = false; // if a survey div does not exist, create it: - if (document.getElementById("_survey") === null) + if (document.getElementById(this._surveyDivId) === null) { - document.body.insertAdjacentHTML("beforeend", "
    ") + document.body.insertAdjacentHTML("beforeend", `
    `) } // start the survey flow: @@ -513,8 +517,7 @@ export class Survey extends VisualStim */ _registerCustomExpressionFunctions (Survey, customFuncs = []) { - let i; - for (i = 0; i < customFuncs.length; i++) + for (let i = 0; i < customFuncs.length; i++) { Survey.FunctionFactory.Instance.register(customFuncs[i].func.name, customFuncs[i].func, customFuncs[i].isAsync); } @@ -579,6 +582,7 @@ export class Survey extends VisualStim this._questionAnswerTimestamps[questionData.name].timestamp = this._questionAnswerTimestampClock.getTime(); } +/* // This probably needs to be moved to some kind of utils.js. // https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle _FisherYatesShuffle (targetArray = []) @@ -613,6 +617,7 @@ export class Survey extends VisualStim return inOutArray; } +*/ _composeModelWithRandomizedQuestions (surveyModel, inBlockRandomizationSettings) { @@ -621,31 +626,32 @@ export class Survey extends VisualStim // Hence creating a fresh survey data object with shuffled question order. let questions = []; let questionsMap = {}; - let shuffledQuestions; let newSurveyModel = { pages:[{ elements: new Array(inBlockRandomizationSettings.questionsPerPage) }] }; - let i, j, k; - for (i = 0; i < surveyModel.pages.length; i++) + for (let i = 0; i < surveyModel.pages.length; i++) { - for (j = 0; j < surveyModel.pages[i].elements.length; j++) + for (let j = 0; j < surveyModel.pages[i].elements.length; j++) { questions.push(surveyModel.pages[i].elements[j]); - k = questions.length - 1; + const k = questions.length - 1; questionsMap[questions[k].name] = questions[k]; } } if (inBlockRandomizationSettings.layout.length > 0) { - j = 0; - k = 0; + let j = 0; + let k = 0; let curPage = 0; let curElement = 0; - const shuffledSet0 = this._FisherYatesShuffle(inBlockRandomizationSettings.set0); - const shuffledSet1 = this._FisherYatesShuffle(inBlockRandomizationSettings.set1); - for (i = 0; i < inBlockRandomizationSettings.layout.length; i++) + + const shuffledSet0 = util.shuffle(Array.from(inBlockRandomizationSettings.set0)); + const shuffledSet1 = util.shuffle(Array.from(inBlockRandomizationSettings.set1)); + // const shuffledSet0 = this._FisherYatesShuffle(inBlockRandomizationSettings.set0); + // const shuffledSet1 = this._FisherYatesShuffle(inBlockRandomizationSettings.set1); + for (let i = 0; i < inBlockRandomizationSettings.layout.length; i++) { // Create new page if questionsPerPage reached. if (curElement === inBlockRandomizationSettings.questionsPerPage) @@ -675,12 +681,14 @@ export class Survey extends VisualStim else if (inBlockRandomizationSettings.showOnly > 0) { // TODO: Check if there can be questionsPerPage applicable in this case. - shuffledQuestions = this._FisherYatesShuffle(questions); + const shuffledQuestions = util.shuffle(Array.from(questions)); + // shuffledQuestions = this._FisherYatesShuffle(questions); newSurveyModel.pages[0].elements = shuffledQuestions.splice(0, inBlockRandomizationSettings.showOnly); } else { // TODO: Check if there can be questionsPerPage applicable in this case. - newSurveyModel.pages[0].elements = this._FisherYatesShuffle(questions); + newSurveyModel.pages[0].elements = util.shuffle(Array.from(questions)); + // newSurveyModel.pages[0].elements = this._FisherYatesShuffle(questions); } console.log("model recomposition took", performance.now() - t); console.log("recomposed model:", newSurveyModel); @@ -714,12 +722,14 @@ export class Survey extends VisualStim if (inQuestionRandomizationSettings.randomizeAll) { - questionData[choicesFieldName] = this._FisherYatesShuffle(questionData[choicesFieldName]); + questionData[choicesFieldName] = util.shuffle(Array.from(questionData[choicesFieldName])); + // questionData[choicesFieldName] = this._FisherYatesShuffle(questionData[choicesFieldName]); // Handle dynamic choices. } else if (inQuestionRandomizationSettings.showOnly > 0) { - questionData[choicesFieldName] = this._FisherYatesShuffle(questionData[choicesFieldName]).splice(0, inQuestionRandomizationSettings.showOnly); + questionData[choicesFieldName] = util.shuffle(Array.from(questionData[choicesFieldName]).splice(0, inQuestionRandomizationSettings.showOnly)); + // questionData[choicesFieldName] = this._FisherYatesShuffle(questionData[choicesFieldName]).splice(0, inQuestionRandomizationSettings.showOnly); } else if (inQuestionRandomizationSettings.reverse) { @@ -739,8 +749,10 @@ export class Survey extends VisualStim // Creating new array of choices to which we're going to write from randomized/reversed sets. questionData[choicesFieldName] = new Array(inQuestionRandomizationSettings.layout.length); - const shuffledSet0 = this._FisherYatesShuffle(inQuestionRandomizationSettings.set0); - const shuffledSet1 = this._FisherYatesShuffle(inQuestionRandomizationSettings.set1); + const shuffledSet0 = util.shuffle(Array.from(inQuestionRandomizationSettings.set0)); + const shuffledSet1 = util.shuffle(Array.from(inQuestionRandomizationSettings.set1)); + // const shuffledSet0 = this._FisherYatesShuffle(inQuestionRandomizationSettings.set0); + // const shuffledSet1 = this._FisherYatesShuffle(inQuestionRandomizationSettings.set1); const reversedSet = Math.round(Math.random()) === 1 ? inQuestionRandomizationSettings.reverseOrder.reverse() : inQuestionRandomizationSettings.reverseOrder; for (i = 0; i < inQuestionRandomizationSettings.layout.length; i++) { @@ -861,12 +873,12 @@ export class Survey extends VisualStim if (skipLogic.destination === "ENDOFSURVEY") { surveyModel.setCompleted(); - this._surveyRunningPromiseResolve(SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY); + this._surveyRunningPromiseResolve(Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY); } else if (skipLogic.destination === "ENDOFBLOCK") { surveyModel.setCompleted(); - this._surveyRunningPromiseResolve(SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_BLOCK); + this._surveyRunningPromiseResolve(Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_BLOCK); } else { @@ -896,13 +908,12 @@ export class Survey extends VisualStim * * @param surveyModel * @param options - * @private + * @protected */ _onSurveyComplete(surveyModel, options) { - Object.assign(this._totalSurveyResults, surveyModel.data); - this._detachResizeObservers(); - let completionCode = SURVEY_COMPLETION_CODES.NORMAL; + Object.assign(this._overallSurveyResults, surveyModel.data); + let completionCode = Survey.SURVEY_COMPLETION_CODES.NORMAL; const questions = surveyModel.getAllQuestions(); // It is guaranteed that the question with skip logic is always last on the page. @@ -916,12 +927,12 @@ export class Survey extends VisualStim { if (skipLogic.destination === "ENDOFSURVEY") { - completionCode = SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY; + completionCode = Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY; surveyModel.setCompleted(); } else if (skipLogic.destination === "ENDOFBLOCK") { - completionCode = SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_BLOCK; + completionCode = Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_BLOCK; } } } @@ -957,6 +968,8 @@ export class Survey extends VisualStim this.psychoJS.logger.warn(`Flag _isCompletedAll is false!`); } + this._detachResizeObservers(); + this._surveyRunningPromiseResolve(completionCode); } @@ -976,22 +989,18 @@ export class Survey extends VisualStim * Run the survey using flow data provided. This method runs recursively. * * @protected - * @param {string} surveyId - the id of the DOM div * @param {Object} surveyData - surveyData / model. - * @param {Object} prevBlockResults - survey results gathered from running previous block of questions. + * @param {Object} surveyFlowBlock - XXX * @return {void} */ _beginSurvey(surveyData, surveyFlowBlock) { - let j; - let surveyIdx; this._lastPageSwitchHandledIdx = -1; - surveyIdx = surveyFlowBlock.surveyIdx; - console.log("playing survey with idx", surveyIdx); + const surveyIdx = surveyFlowBlock.surveyIdx; let surveyModelInput = this._processSurveyData(surveyData, surveyIdx); this._surveyModel = new window.Survey.Model(surveyModelInput); - for (j in this._variables) + for (let j in this._variables) { // Adding variables directly to hash to get higher performance (this is instantaneous compared to .setVariable()). // At this stage we don't care to trigger all the callbacks like .setVariable() does, since this is very beginning of survey presentation. @@ -1010,7 +1019,7 @@ export class Survey extends VisualStim this._surveyModel.onAfterRenderQuestion.add(this._handleAfterQuestionRender.bind(this)); } - const completeText = surveyIdx < this._surveyData.surveys.length - 1 ? (this._surveyModel.pageNextText || CAPTIONS.NEXT) : undefined; + const completeText = surveyIdx < this._surveyData.surveys.length - 1 ? (this._surveyModel.pageNextText || Survey.CAPTIONS.NEXT) : undefined; jQuery(".survey").Survey({ model: this._surveyModel, showItemsInOrder: "column", @@ -1033,15 +1042,11 @@ export class Survey extends VisualStim async _runSurveyFlow(surveyBlock, surveyData, prevBlockResults = {}) { - // let surveyBlock; - let surveyIdx; - let surveyCompletionCode; - let nodeExitCode = NODE_EXIT_CODES.NORMAL; - let i, j; + let nodeExitCode = Survey.NODE_EXIT_CODES.NORMAL; if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.CONDITIONAL) { - const dataset = Object.assign({}, this._totalSurveyResults, this._variables); + const dataset = Object.assign({}, this._overallSurveyResults, this._variables); this._expressionsRunner.expressionExecutor.setExpression(surveyBlock.condition); if (this._expressionsRunner.run(dataset) && surveyBlock.nodes[0] !== undefined) { @@ -1054,13 +1059,14 @@ export class Survey extends VisualStim } else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.RANDOMIZER) { - this._InPlaceFisherYatesShuffle(surveyBlock.nodes, 0, surveyBlock.nodes.length - 1); + util.shuffle(surveyBlock.nodes, Math.random, 0, surveyBlock.nodes.length - 1); + // this._InPlaceFisherYatesShuffle(surveyBlock.nodes, 0, surveyBlock.nodes.length - 1); } else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.EMBEDDED_DATA) { let t = performance.now(); const surveyBlockData = surveyData.embeddedData[surveyBlock.dataIdx]; - for (j = 0; j < surveyBlockData.length; j++) + for (let j = 0; j < surveyBlockData.length; j++) { // TODO: handle the rest data types. if (surveyBlockData[j].type === "Custom") @@ -1089,28 +1095,28 @@ export class Survey extends VisualStim this._surveyModel.setCompleted(); } console.log("EndSurvey block encountered, exiting."); - nodeExitCode = NODE_EXIT_CODES.BREAK_FLOW; + nodeExitCode = Survey.NODE_EXIT_CODES.BREAK_FLOW; } else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.DIRECT) { - surveyCompletionCode = await this._beginSurvey(surveyData, surveyBlock); + const surveyCompletionCode = await this._beginSurvey(surveyData, surveyBlock); Object.assign({}, prevBlockResults, this._surveyModel.data); // SkipLogic had destination set to ENDOFSURVEY. - if (surveyCompletionCode === SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY) + if (surveyCompletionCode === Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY) { - nodeExitCode = NODE_EXIT_CODES.BREAK_FLOW; + nodeExitCode = Survey.NODE_EXIT_CODES.BREAK_FLOW; } } - if (nodeExitCode === NODE_EXIT_CODES.NORMAL && + if (nodeExitCode === Survey.NODE_EXIT_CODES.NORMAL && surveyBlock.type !== Survey.SURVEY_FLOW_PLAYBACK_TYPES.CONDITIONAL && surveyBlock.nodes instanceof Array) { - for (i = 0; i < surveyBlock.nodes.length; i++) + for (let i = 0; i < surveyBlock.nodes.length; i++) { nodeExitCode = await this._runSurveyFlow(surveyBlock.nodes[i], surveyData, prevBlockResults); - if (nodeExitCode === NODE_EXIT_CODES.BREAK_FLOW) + if (nodeExitCode === Survey.NODE_EXIT_CODES.BREAK_FLOW) { break; } @@ -1131,20 +1137,17 @@ export class Survey extends VisualStim this._lastPageSwitchHandledIdx = -1; } - _handleSignaturePadResize (entries) + _handleSignaturePadResize(entries) { - let signatureCanvas; - let q; - let i; - for (i = 0; i < entries.length; i++) + for (let i = 0; i < entries.length; i++) { - signatureCanvas = entries[i].target.querySelector("canvas"); - q = this._surveyModel.getQuestionByName(entries[i].target.dataset.name); - q.signatureWidth = Math.min(q.maxSignatureWidth, entries[i].contentBoxSize[0].inlineSize); + // const signatureCanvas = entries[i].target.querySelector("canvas"); + const question = this._surveyModel.getQuestionByName(entries[i].target.dataset.name); + question.signatureWidth = Math.min(question.maxSignatureWidth, entries[i].contentBoxSize[0].inlineSize); } } - _addEventListeners () + _addEventListeners() { this._signaturePadRO = new ResizeObserver(this._handleSignaturePadResize.bind(this)); } @@ -1157,27 +1160,29 @@ export class Survey extends VisualStim } } - _detachResizeObservers () + _detachResizeObservers() { this._signaturePadRO.disconnect(); } /** - * Init the SurveyJS.io library. + * Init the SurveyJS.io library and various extensions, setup the theme. * * @protected */ _initSurveyJS() { - // load the Survey.js libraries, if necessary: - // TODO + // note: the Survey.js libraries must be added to the list of resources in PsychoJS.start: + // psychoJS.start({ resources: [ {'surveyLibrary': true}, ... ], ...}); + + // id of the SurveyJS html div: + this._surveyDivId = `survey-${this._name}`; - // load the PsychoJS SurveyJS extensions: - this._expressionsRunner = new window.Survey.ExpressionRunner(); this._registerCustomExpressionFunctions(window.Survey, customExpressionFunctionsArray); this._registerWidgets(window.Survey); this._registerCustomSurveyProperties(window.Survey); this._addEventListeners(); + this._expressionsRunner = new window.Survey.ExpressionRunner(); // setup the survey theme: window.Survey.Serializer.getProperty("expression", "minWidth").defaultValue = "100px"; From bff887b79311d569e340f233c1c53959a32b44ed Mon Sep 17 00:00:00 2001 From: lgtst Date: Fri, 24 Mar 2023 13:20:34 +0000 Subject: [PATCH 29/41] src/visual --- src/visual/Survey.js | 58 +++++++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/src/visual/Survey.js b/src/visual/Survey.js index b573e16..d761224 100644 --- a/src/visual/Survey.js +++ b/src/visual/Survey.js @@ -82,6 +82,10 @@ export class Survey extends VisualStim { super({ name, win, units, ori, depth, pos, size, autoDraw, autoLog }); + // Storing all existing signaturePad questions to properly handle their resize. + // Unfortunately signaturepad question type can't handle resizing properly by itself. + this._signaturePads = []; + // whether the user is done with the survey, independently of whether the survey is completed: this.isFinished = false; @@ -968,8 +972,6 @@ export class Survey extends VisualStim this.psychoJS.logger.warn(`Flag _isCompletedAll is false!`); } - this._detachResizeObservers(); - this._surveyRunningPromiseResolve(completionCode); } @@ -1017,6 +1019,10 @@ export class Survey extends VisualStim this._surveyModel.onTextMarkdown.add(this._onTextMarkdown.bind(this)); this._surveyModel.isInitialized = true; this._surveyModel.onAfterRenderQuestion.add(this._handleAfterQuestionRender.bind(this)); + this._surveyModel.onQuestionRemoved.add(() => + { + console.log("question removed") + }) } const completeText = surveyIdx < this._surveyData.surveys.length - 1 ? (this._surveyModel.pageNextText || Survey.CAPTIONS.NEXT) : undefined; @@ -1137,34 +1143,58 @@ export class Survey extends VisualStim this._lastPageSwitchHandledIdx = -1; } - _handleSignaturePadResize(entries) + _getQuestionByNameIncludingInDesign(questionName = "") { - for (let i = 0; i < entries.length; i++) + const allQuestions = this._surveyModel.getAllQuestions(false, true); + for (const question of allQuestions) { - // const signatureCanvas = entries[i].target.querySelector("canvas"); - const question = this._surveyModel.getQuestionByName(entries[i].target.dataset.name); - question.signatureWidth = Math.min(question.maxSignatureWidth, entries[i].contentBoxSize[0].inlineSize); + if (question.name === questionName) + { + return question; + } + } + } + + _handleWindowResize(e) + { + if (this._surveyModel) + { + for (let i = this._signaturePads.length - 1; i >= 0; i--) + { + // As of writing this (24.03.2023). SurveyJS doesn't have a proper event + // for question being removed from nested locations, such as dynamic panel. + // However, surveyJS will set .signaturePad property to null once the question is removed. + // Utilising this knowledge to sync our lists. + if (this._signaturePads[ i ].question.signaturePad) + { + this._signaturePads[ i ].question.signatureWidth = Math.min( + this._signaturePads[i].question.maxSignatureWidth, + this._signaturePads[ i ].htmlElement.getBoundingClientRect().width + ); + } + else + { + // Signature pad was removed. Syncing list. + this._signaturePads.splice(i, 1); + } + } } } _addEventListeners() { - this._signaturePadRO = new ResizeObserver(this._handleSignaturePadResize.bind(this)); + window.addEventListener("resize", (e) => this._handleWindowResize(e)); } _handleAfterQuestionRender (sender, options) { if (options.question.getType() === "signaturepad") { - this._signaturePadRO.observe(options.htmlElement); + this._signaturePads.push(options); + options.question.signatureWidth = Math.min(options.question.maxSignatureWidth, options.htmlElement.getBoundingClientRect().width); } } - _detachResizeObservers() - { - this._signaturePadRO.disconnect(); - } - /** * Init the SurveyJS.io library and various extensions, setup the theme. * From 5f32881be273542d8dab1bf15c7347ff1d9c1c91 Mon Sep 17 00:00:00 2001 From: lgtst Date: Fri, 24 Mar 2023 13:23:50 +0000 Subject: [PATCH 30/41] removed dead code. --- src/visual/Survey.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/visual/Survey.js b/src/visual/Survey.js index d761224..57bf41f 100644 --- a/src/visual/Survey.js +++ b/src/visual/Survey.js @@ -1019,10 +1019,6 @@ export class Survey extends VisualStim this._surveyModel.onTextMarkdown.add(this._onTextMarkdown.bind(this)); this._surveyModel.isInitialized = true; this._surveyModel.onAfterRenderQuestion.add(this._handleAfterQuestionRender.bind(this)); - this._surveyModel.onQuestionRemoved.add(() => - { - console.log("question removed") - }) } const completeText = surveyIdx < this._surveyData.surveys.length - 1 ? (this._surveyModel.pageNextText || Survey.CAPTIONS.NEXT) : undefined; @@ -1143,18 +1139,6 @@ export class Survey extends VisualStim this._lastPageSwitchHandledIdx = -1; } - _getQuestionByNameIncludingInDesign(questionName = "") - { - const allQuestions = this._surveyModel.getAllQuestions(false, true); - for (const question of allQuestions) - { - if (question.name === questionName) - { - return question; - } - } - } - _handleWindowResize(e) { if (this._surveyModel) From 122250527c1cb0040e4b1c62c2ca0ed456db221c Mon Sep 17 00:00:00 2001 From: Alain Pitiot Date: Wed, 19 Jul 2023 09:55:49 +0200 Subject: [PATCH 31/41] log in constructor, additional comment --- src/visual/Progress.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/visual/Progress.js b/src/visual/Progress.js index 279d4d4..3963a54 100644 --- a/src/visual/Progress.js +++ b/src/visual/Progress.js @@ -45,8 +45,16 @@ export class Progress extends VisualStim this._addAttribute("type", type, PROGRESS_TYPES.BAR); this._addAttribute("fillColor", fillColor, "lightgreen"); this._addAttribute("fillTexture", fillTexture, PIXI.Texture.WHITE); + + if (this._autoLog) + { + this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`); + } } + /** + * Setter for the progress attribute. + */ setProgress (progress = 0, log = false) { this._setAttribute("progress", Math.min(1.0, Math.max(0.0, progress)), log); From 5dba92ab37c03d5ce52274ca1625d352c1061fc5 Mon Sep 17 00:00:00 2001 From: Alain Pitiot Date: Wed, 19 Jul 2023 16:07:43 +0200 Subject: [PATCH 32/41] ENH: various enhancements to the dialog box, resource manager, saving of data, capture of keys, scheduler --- package-lock.json | 815 ++++++++++++++++++++- package.json | 3 +- src/core/EventManager.js | 8 +- src/core/GUI.js | 79 +- src/core/Keyboard.js | 12 +- src/core/PsychoJS.js | 3 +- src/core/ServerManager.js | 2 +- src/core/Window.js | 14 +- src/data/ExperimentHandler.js | 14 + src/index.css | 60 +- src/util/Scheduler.js | 11 + src/util/Util.js | 46 ++ src/visual/ButtonStim.js | 10 +- src/visual/ImageStim.js | 81 +- src/visual/TextBox.js | 63 +- src/visual/survey/widgets/MaxDiffMatrix.js | 21 +- 16 files changed, 1148 insertions(+), 94 deletions(-) diff --git a/package-lock.json b/package-lock.json index cb6071b..eae98bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "psychojs", - "version": "2022.2.0", + "version": "2023.2.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "psychojs", - "version": "2022.2.0", + "version": "2023.2.1", "license": "MIT", "dependencies": { "@pixi/filter-adjustment": "^4.1.3", @@ -16,6 +16,7 @@ "howler": "^2.2.1", "log4javascript": "github:Ritzlgrmft/log4javascript", "pako": "^1.0.10", + "pixi-filters": "^5.0.0", "pixi.js-legacy": "^6.0.4", "seedrandom": "^3.0.5", "tone": "^14.7.77", @@ -288,6 +289,15 @@ "@pixi/text": "6.0.4" } }, + "node_modules/@pixi/color": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.2.4.tgz", + "integrity": "sha512-B/+9JRcXe2uE8wQfsueFRPZVayF2VEMRB7XGeRAsWCryOX19nmWhv0Nt3nOU2rvzI0niz9XgugJXsB6vVmDFSg==", + "peer": true, + "dependencies": { + "colord": "^2.9.3" + } + }, "node_modules/@pixi/compressed-textures": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@pixi/compressed-textures/-/compressed-textures-6.0.4.tgz", @@ -331,6 +341,12 @@ "@pixi/utils": "6.0.4" } }, + "node_modules/@pixi/extensions": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-7.2.4.tgz", + "integrity": "sha512-Mnqv9scbL1ARD3QFKfOWs2aSVJJfP1dL8g5UiqGImYO3rZbz/9QCzXOeMVIZ5n3iaRyKMNhFFr84/zUja2H7Dw==", + "peer": true + }, "node_modules/@pixi/extract": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@pixi/extract/-/extract-6.0.4.tgz", @@ -639,11 +655,23 @@ "url": "^0.11.0" } }, + "node_modules/@types/css-font-loading-module": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", + "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==", + "peer": true + }, "node_modules/@types/earcut": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.1.tgz", "integrity": "sha512-w8oigUCDjElRHRRrMvn/spybSMyX8MTkKA5Dv+tS1IE/TgmNZPqUYtvYBXGY8cieSE66gm+szeK+bnbxC2xHTQ==" }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.0", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.0.tgz", + "integrity": "sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==", + "peer": true + }, "node_modules/a11y-dialog": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/a11y-dialog/-/a11y-dialog-7.5.0.tgz", @@ -869,6 +897,12 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "peer": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -957,9 +991,9 @@ } }, "node_modules/earcut": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.2.tgz", - "integrity": "sha512-eZoZPPJcUHnfRZ0PjLvx2qBordSiO8ofC3vt+qACLM95u+4DovnbYNpQtJh0DNsWj8RnxrQytD4WA8gj5cRIaQ==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -1954,6 +1988,409 @@ "node": ">=8" } }, + "node_modules/pixi-filters": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pixi-filters/-/pixi-filters-5.0.0.tgz", + "integrity": "sha512-j90nvbiRpDozxalSUaQ2kTIyFNGAUKxJ2qhPs4ThmVLiR9lam5x+GpP+c1Yx5N+qc+u0tH5G3VRY1usB69atrw==", + "dependencies": { + "@pixi/filter-adjustment": "5.0.0", + "@pixi/filter-advanced-bloom": "5.0.0", + "@pixi/filter-ascii": "5.0.0", + "@pixi/filter-bevel": "5.0.0", + "@pixi/filter-bloom": "5.0.0", + "@pixi/filter-bulge-pinch": "5.0.0", + "@pixi/filter-color-map": "5.0.0", + "@pixi/filter-color-overlay": "5.0.0", + "@pixi/filter-color-replace": "5.0.0", + "@pixi/filter-convolution": "5.0.0", + "@pixi/filter-cross-hatch": "5.0.0", + "@pixi/filter-crt": "5.0.0", + "@pixi/filter-dot": "5.0.0", + "@pixi/filter-drop-shadow": "5.0.0", + "@pixi/filter-emboss": "5.0.0", + "@pixi/filter-glitch": "5.0.0", + "@pixi/filter-glow": "5.0.0", + "@pixi/filter-godray": "5.0.0", + "@pixi/filter-kawase-blur": "5.0.0", + "@pixi/filter-motion-blur": "5.0.0", + "@pixi/filter-multi-color-replace": "5.0.0", + "@pixi/filter-old-film": "5.0.0", + "@pixi/filter-outline": "5.0.0", + "@pixi/filter-pixelate": "5.0.0", + "@pixi/filter-radial-blur": "5.0.0", + "@pixi/filter-reflection": "5.0.0", + "@pixi/filter-rgb-split": "5.0.0", + "@pixi/filter-shockwave": "5.0.0", + "@pixi/filter-simple-lightmap": "5.0.0", + "@pixi/filter-tilt-shift": "5.0.0", + "@pixi/filter-twist": "5.0.0", + "@pixi/filter-zoom-blur": "5.0.0" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/constants": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-7.2.4.tgz", + "integrity": "sha512-hKuHBWR6N4Q0Sf5MGF3/9l+POg/G5rqhueHfzofiuelnKg7aBs3BVjjZ+6hZbd6M++vOUmxYelEX/NEFBxrheA==", + "peer": true + }, + "node_modules/pixi-filters/node_modules/@pixi/core": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/core/-/core-7.2.4.tgz", + "integrity": "sha512-0XtvrfxHlS2T+beBBSpo7GI8+QLyyTqMVQpNmPqB4woYxzrOEJ9JaUFBaBfCvycLeUkfVih1u6HAbtF+2d1EjQ==", + "peer": true, + "dependencies": { + "@pixi/color": "7.2.4", + "@pixi/constants": "7.2.4", + "@pixi/extensions": "7.2.4", + "@pixi/math": "7.2.4", + "@pixi/runner": "7.2.4", + "@pixi/settings": "7.2.4", + "@pixi/ticker": "7.2.4", + "@pixi/utils": "7.2.4", + "@types/offscreencanvas": "^2019.6.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/pixijs" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-adjustment": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-adjustment/-/filter-adjustment-5.0.0.tgz", + "integrity": "sha512-Epci8zSWCNWhFtnarvQqOcnmOqLfhXIJ7NNENEi2E1rom1Ar13RLM76CBGBbuDRK7flweqcWmZb0QZLxqwxTDg==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-advanced-bloom": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-advanced-bloom/-/filter-advanced-bloom-5.0.0.tgz", + "integrity": "sha512-P5Xt65GLBEqjZVUkLe4ZZk4D1/j9UEXYnYFG3JrLPYkdcniwD4Y+NIyNCJ+eP91ivgoCmK/+SyBRv0P0AEQkTw==", + "dependencies": { + "@pixi/filter-kawase-blur": "5.0.0" + }, + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-alpha": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-7.2.4.tgz", + "integrity": "sha512-UTUMSGyktUr+I9vmigqJo9iUhb0nwGyqTTME2xBWZvVGCnl5z+/wHxvIBBCe5pNZ66IM15pGXQ4cDcfqCuP2kA==", + "peer": true, + "peerDependencies": { + "@pixi/core": "7.2.4" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-ascii": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-ascii/-/filter-ascii-5.0.0.tgz", + "integrity": "sha512-A49yNhiye/aFDOnI11zwEm/td2xho0td/Cvzvru8FUgi1MzJvZE03W/JoLl04ToZczw143wFPxutl6V/Ohw5bQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-bevel": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-bevel/-/filter-bevel-5.0.0.tgz", + "integrity": "sha512-0Odat0tW/uoS/uyp0rigm07Q3YPgwKLTgkZZZSzIUVsPnwcJjiocSzWel73JkiY3m2ZjTrj+JZjkyGjkYH+2gQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-bloom": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-bloom/-/filter-bloom-5.0.0.tgz", + "integrity": "sha512-vOSNJNV5y+ifwQWfzEmml3owcgoJAQIQtMR17SELBUwfYP60qxy5bNWBdYBlipSJVwX2AuGi8Xk5Ia9dijcqZQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X", + "@pixi/filter-alpha": "^7.0.0-X", + "@pixi/filter-blur": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-blur": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/filter-blur/-/filter-blur-7.2.4.tgz", + "integrity": "sha512-aLyXIoxy14bTansCPtbY8x7Sdn2OrrqkF/pcKiRXHJGGhi7wPacvB/NcmYJdnI/n2ExQ6V5Njuj/nfrsejVwcA==", + "peer": true, + "peerDependencies": { + "@pixi/core": "7.2.4" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-bulge-pinch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-bulge-pinch/-/filter-bulge-pinch-5.0.0.tgz", + "integrity": "sha512-j1feWsCpyTZk4aHbYNjax52lt0OtyYDbHvYaePYzGO/SBb1t/spDnHQEkAP7R3bZ7Ud/GI4RgefAFnvsYeSetQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-color-map": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-map/-/filter-color-map-5.0.0.tgz", + "integrity": "sha512-w77mRi89sLUMwjhl7qL/q1YrhEKyOk2MJZQdKBksvGEV/Mf5mV2h3+EOC62wB18Q4iUVQy1MS4sANyVaCctu2w==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-color-overlay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-overlay/-/filter-color-overlay-5.0.0.tgz", + "integrity": "sha512-AjxVN6gnZ+xCryQUmI+TVy3yVF+CcLgDPv+nSVPDlQowuqYhZjD6qSzgRCl3Kezdi3AxrL1vi1fnBudEnzdDJg==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-color-replace": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-replace/-/filter-color-replace-5.0.0.tgz", + "integrity": "sha512-u4VOtKbY6SSr2P9v5AL8/2MVsUcAH9z92c1eaqeE3PXCPNyCgZKuNHWl8+FjBIDl/1UMQVhXH2zNrC3Vuqo3JA==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-convolution": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-convolution/-/filter-convolution-5.0.0.tgz", + "integrity": "sha512-SYjyKXODdHbjzBP9c5QGMOfowNwkNFi7zW1XzGwEadmv6mLHNanO3nm0PtRu/3B9B6AW1fvOaUecYmhjAZfQjg==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-cross-hatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-cross-hatch/-/filter-cross-hatch-5.0.0.tgz", + "integrity": "sha512-J4bcI3MUc/Ol3nQIsXZldYEtiLAl3ktU28zlidwffkANyl/XjP76bLEgFBoc4RE2iP/FQ+9ZeEqpsN8DIg6vVg==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-crt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-crt/-/filter-crt-5.0.0.tgz", + "integrity": "sha512-/kgjNW+BCCVtUa0s8Usk3WyxgBX8kelAiqkyVnM1g8xM19Dh2689gK2wjx0ibS0p74EHs42QpkJj7jTL+1MS7A==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-dot": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-dot/-/filter-dot-5.0.0.tgz", + "integrity": "sha512-kytardK58Ifl5D8Ss3kkfI29FMzV3+npJYr5GAKnA80R7XGOPOMoxrknhou8y+Dw9LUcOv8y643wryvL43P2vw==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-drop-shadow": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-drop-shadow/-/filter-drop-shadow-5.0.0.tgz", + "integrity": "sha512-kz2eL+ikCLL7/2RICyIkw3pZXkyMY0Ji6skhnPj7JaZSjH4V+7TiKqYXp532gTbwSRj/mzLCvFfOL3WwTDgZ1w==", + "dependencies": { + "@pixi/filter-kawase-blur": "5.0.0" + }, + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-emboss": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-emboss/-/filter-emboss-5.0.0.tgz", + "integrity": "sha512-wvrk9zB62lGaPcCWbTwoaO48FrLIE4+hi02BVS+exx5RvIniNUJD/ledGxdmUjcHX/2mDIIs7PH0kAs1L/ziZw==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-glitch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-glitch/-/filter-glitch-5.0.0.tgz", + "integrity": "sha512-yK3plqExyQp9eo3dwV03dnSHpQgh0xeD112ieAsqefrAOLc5AXSfTelPvEQaZ07ZkcxSDE5eqKcRvcIVi2IgLQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-glow": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-glow/-/filter-glow-5.0.0.tgz", + "integrity": "sha512-D+YE9DGSJXtmZa6aoWJfuNu+6MnSw90GP7oRRzr7S1/4moeFZ7EWbvQehl9Y9j98idHG87Cvuh6mmsRqpgS6ow==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-godray": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-godray/-/filter-godray-5.0.0.tgz", + "integrity": "sha512-L4PD3cysUMjTSDYk5q5xUtal9q6kfH8NVIdNT3aTDJpR0VW4b/ClanmOTFpJVzN6Ld/JlJbdg8ogUpXBe1gVuw==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-kawase-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-kawase-blur/-/filter-kawase-blur-5.0.0.tgz", + "integrity": "sha512-dKSTaPUOvdVkfx9x+kp0TzYjGAl8CLxIRGz6Wh43NKx96nVqd/lWqvlda+zloHVgZyQoJNHZZ4Spjcw2mYoaWg==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-motion-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-motion-blur/-/filter-motion-blur-5.0.0.tgz", + "integrity": "sha512-2av4dnVL1uyyCKF8RlZaMfeO8YnQwA893j24S15ubWHZaz4WlWH3lFIYmCMqlEqHPlFDBER4vLxpR1WjsUsX/Q==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-multi-color-replace": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-multi-color-replace/-/filter-multi-color-replace-5.0.0.tgz", + "integrity": "sha512-hcmCKFFQ1baGDrZc/blK9zWpe3f02rqWGsPx5VRRgc1sk44UYXHCKZnDjF80/g0ls8U4Lj+/5Xb7HOQq2LyyDg==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-old-film": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-old-film/-/filter-old-film-5.0.0.tgz", + "integrity": "sha512-XSHBz4JDbvYtUrf/NP5eKCw/wvaKTAKXQENDxk480tKYtDuteSCMg87ZjLrPlyKtGySW8KTmdzl58bZjSYpiyA==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-outline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-outline/-/filter-outline-5.0.0.tgz", + "integrity": "sha512-efS3Or7VQFXo2ZyPFR2M/JlZrcLAxeVbOTPYvgKe574yUghSQbQ/pyqDWE16tRB/W7+osMrTV0+C4/N/9wIxhQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-pixelate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-pixelate/-/filter-pixelate-5.0.0.tgz", + "integrity": "sha512-3g1ajOLsYy+x0FCC67WhDcjixrcBlhK3Zo+JP9zlHSxh0W4yNzfhsw9EsIb9XP4WnMtMAUMg5T0MLTnjbsrK4g==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-radial-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-radial-blur/-/filter-radial-blur-5.0.0.tgz", + "integrity": "sha512-zafBJCAiqRtsTNGKiQ8iMt00KbG20qtBi71h286wWbr0na37iXsRcg4EN76eyNbpfAOX+1ylBgIuSd9hLyQBFA==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-reflection": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-reflection/-/filter-reflection-5.0.0.tgz", + "integrity": "sha512-PuZe19XUq0gTdmAStu3hcyGKkNlKGrpblN4s6vJmV+vAKVcFv2OpfjtuGUXcP/oi2LmLakC/vKfEx4bDgZzz+w==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-rgb-split": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-rgb-split/-/filter-rgb-split-5.0.0.tgz", + "integrity": "sha512-zsWBrDkj9EdjJRPjGCt/0O2Vx/8Gt+8VTmjRA0ONoegcMD9slJdJMgL9EbH/1y5WHgmzGbgZIPvWULIqepVxBQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-shockwave": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-shockwave/-/filter-shockwave-5.0.0.tgz", + "integrity": "sha512-aL0ExAkJGcUo463Ktq4HXjZGlJDpoYcyZhwd87maJrFsBjQZl2gopse6bEsy7IJxbAKzlpUKFmAP9rxwZWqMVQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-simple-lightmap": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-simple-lightmap/-/filter-simple-lightmap-5.0.0.tgz", + "integrity": "sha512-0WIKQIGZ3aNafe2VZIbGQJWxSlBMbmjM9J+Tswjaeg8Z1dz6Qux5lYIC16wZOaIqVlWL5GTpfn8HU0BHCOvESA==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-tilt-shift": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-tilt-shift/-/filter-tilt-shift-5.0.0.tgz", + "integrity": "sha512-nIxYoTU9kFDx3EE1fyoIEOfAia9Tvoj+sakTKCJZUvTk+5tjpZdAm+Ump42cnb6UxTR8AMTQiwH54C7I0pbA4Q==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-twist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-twist/-/filter-twist-5.0.0.tgz", + "integrity": "sha512-YVtz3ZPfvaz22gZRZo+cOC0/L6SgSZmr/HEa6Ir+BRNVqLff6CpPx6YBVJqPREh+HFZjDomSP0kf5JasQYhzSg==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-zoom-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-zoom-blur/-/filter-zoom-blur-5.0.0.tgz", + "integrity": "sha512-Q1ftuY/KPgbVtJHCvl0p4hrwVWRMWZ/yX1YRjdLGSyOwMEN8u16MEEXFQUtixEHY7+MBRBWaPOaXBaQrd+Xq7A==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/math": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/math/-/math-7.2.4.tgz", + "integrity": "sha512-LJB+mozyEPllxa0EssFZrKNfVwysfaBun4b2dJKQQInp0DafgbA0j7A+WVg0oe51KhFULTJMpDqbLn/ITFc41A==", + "peer": true + }, + "node_modules/pixi-filters/node_modules/@pixi/runner": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-7.2.4.tgz", + "integrity": "sha512-YtyqPk1LA+0guEFKSFx6t/YSvbEQwajFwi4Ft8iDhioa6VK2MmTir1GjWwy7JQYLcDmYSAcQjnmFtVTZohyYSw==", + "peer": true + }, + "node_modules/pixi-filters/node_modules/@pixi/settings": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-7.2.4.tgz", + "integrity": "sha512-ZPKRar9EwibijGmH8EViu4Greq1I/O7V/xQx2rNqN23XA7g09Qo6yfaeQpufu5xl8+/lZrjuHtQSnuY7OgG1CA==", + "peer": true, + "dependencies": { + "@pixi/constants": "7.2.4", + "@types/css-font-loading-module": "^0.0.7", + "ismobilejs": "^1.1.0" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/ticker": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-7.2.4.tgz", + "integrity": "sha512-hQQHIHvGeFsP4GNezZqjzuhUgNQEVgCH9+qU05UX1Mc5UHC9l6OJnY4VTVhhcHxZjA6RnyaY+1zBxCnoXuazpg==", + "peer": true, + "dependencies": { + "@pixi/extensions": "7.2.4", + "@pixi/settings": "7.2.4", + "@pixi/utils": "7.2.4" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/utils": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-7.2.4.tgz", + "integrity": "sha512-VUGQHBOINIS4ePzoqafwxaGPVRTa3oM/mEutIIHbNGI3b+QvSO+1Dnk40M0zcH6Bo+MxQZbOZK5X/wO9oU5+LQ==", + "peer": true, + "dependencies": { + "@pixi/color": "7.2.4", + "@pixi/constants": "7.2.4", + "@pixi/settings": "7.2.4", + "@types/earcut": "^2.1.0", + "earcut": "^2.2.4", + "eventemitter3": "^4.0.0", + "url": "^0.11.0" + } + }, + "node_modules/pixi-filters/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "peer": true + }, "node_modules/pixi.js": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-6.0.4.tgz", @@ -2682,6 +3119,15 @@ "@pixi/text": "6.0.4" } }, + "@pixi/color": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.2.4.tgz", + "integrity": "sha512-B/+9JRcXe2uE8wQfsueFRPZVayF2VEMRB7XGeRAsWCryOX19nmWhv0Nt3nOU2rvzI0niz9XgugJXsB6vVmDFSg==", + "peer": true, + "requires": { + "colord": "^2.9.3" + } + }, "@pixi/compressed-textures": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@pixi/compressed-textures/-/compressed-textures-6.0.4.tgz", @@ -2721,6 +3167,12 @@ "@pixi/utils": "6.0.4" } }, + "@pixi/extensions": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-7.2.4.tgz", + "integrity": "sha512-Mnqv9scbL1ARD3QFKfOWs2aSVJJfP1dL8g5UiqGImYO3rZbz/9QCzXOeMVIZ5n3iaRyKMNhFFr84/zUja2H7Dw==", + "peer": true + }, "@pixi/extract": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@pixi/extract/-/extract-6.0.4.tgz", @@ -3026,11 +3478,23 @@ "url": "^0.11.0" } }, + "@types/css-font-loading-module": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", + "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==", + "peer": true + }, "@types/earcut": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.1.tgz", "integrity": "sha512-w8oigUCDjElRHRRrMvn/spybSMyX8MTkKA5Dv+tS1IE/TgmNZPqUYtvYBXGY8cieSE66gm+szeK+bnbxC2xHTQ==" }, + "@types/offscreencanvas": { + "version": "2019.7.0", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.0.tgz", + "integrity": "sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==", + "peer": true + }, "a11y-dialog": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/a11y-dialog/-/a11y-dialog-7.5.0.tgz", @@ -3207,6 +3671,12 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "peer": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3268,9 +3738,9 @@ "dev": true }, "earcut": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.2.tgz", - "integrity": "sha512-eZoZPPJcUHnfRZ0PjLvx2qBordSiO8ofC3vt+qACLM95u+4DovnbYNpQtJh0DNsWj8RnxrQytD4WA8gj5cRIaQ==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" }, "emoji-regex": { "version": "8.0.0", @@ -4044,6 +4514,335 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, + "pixi-filters": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pixi-filters/-/pixi-filters-5.0.0.tgz", + "integrity": "sha512-j90nvbiRpDozxalSUaQ2kTIyFNGAUKxJ2qhPs4ThmVLiR9lam5x+GpP+c1Yx5N+qc+u0tH5G3VRY1usB69atrw==", + "requires": { + "@pixi/filter-adjustment": "5.0.0", + "@pixi/filter-advanced-bloom": "5.0.0", + "@pixi/filter-ascii": "5.0.0", + "@pixi/filter-bevel": "5.0.0", + "@pixi/filter-bloom": "5.0.0", + "@pixi/filter-bulge-pinch": "5.0.0", + "@pixi/filter-color-map": "5.0.0", + "@pixi/filter-color-overlay": "5.0.0", + "@pixi/filter-color-replace": "5.0.0", + "@pixi/filter-convolution": "5.0.0", + "@pixi/filter-cross-hatch": "5.0.0", + "@pixi/filter-crt": "5.0.0", + "@pixi/filter-dot": "5.0.0", + "@pixi/filter-drop-shadow": "5.0.0", + "@pixi/filter-emboss": "5.0.0", + "@pixi/filter-glitch": "5.0.0", + "@pixi/filter-glow": "5.0.0", + "@pixi/filter-godray": "5.0.0", + "@pixi/filter-kawase-blur": "5.0.0", + "@pixi/filter-motion-blur": "5.0.0", + "@pixi/filter-multi-color-replace": "5.0.0", + "@pixi/filter-old-film": "5.0.0", + "@pixi/filter-outline": "5.0.0", + "@pixi/filter-pixelate": "5.0.0", + "@pixi/filter-radial-blur": "5.0.0", + "@pixi/filter-reflection": "5.0.0", + "@pixi/filter-rgb-split": "5.0.0", + "@pixi/filter-shockwave": "5.0.0", + "@pixi/filter-simple-lightmap": "5.0.0", + "@pixi/filter-tilt-shift": "5.0.0", + "@pixi/filter-twist": "5.0.0", + "@pixi/filter-zoom-blur": "5.0.0" + }, + "dependencies": { + "@pixi/constants": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-7.2.4.tgz", + "integrity": "sha512-hKuHBWR6N4Q0Sf5MGF3/9l+POg/G5rqhueHfzofiuelnKg7aBs3BVjjZ+6hZbd6M++vOUmxYelEX/NEFBxrheA==", + "peer": true + }, + "@pixi/core": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/core/-/core-7.2.4.tgz", + "integrity": "sha512-0XtvrfxHlS2T+beBBSpo7GI8+QLyyTqMVQpNmPqB4woYxzrOEJ9JaUFBaBfCvycLeUkfVih1u6HAbtF+2d1EjQ==", + "peer": true, + "requires": { + "@pixi/color": "7.2.4", + "@pixi/constants": "7.2.4", + "@pixi/extensions": "7.2.4", + "@pixi/math": "7.2.4", + "@pixi/runner": "7.2.4", + "@pixi/settings": "7.2.4", + "@pixi/ticker": "7.2.4", + "@pixi/utils": "7.2.4", + "@types/offscreencanvas": "^2019.6.4" + } + }, + "@pixi/filter-adjustment": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-adjustment/-/filter-adjustment-5.0.0.tgz", + "integrity": "sha512-Epci8zSWCNWhFtnarvQqOcnmOqLfhXIJ7NNENEi2E1rom1Ar13RLM76CBGBbuDRK7flweqcWmZb0QZLxqwxTDg==", + "requires": {} + }, + "@pixi/filter-advanced-bloom": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-advanced-bloom/-/filter-advanced-bloom-5.0.0.tgz", + "integrity": "sha512-P5Xt65GLBEqjZVUkLe4ZZk4D1/j9UEXYnYFG3JrLPYkdcniwD4Y+NIyNCJ+eP91ivgoCmK/+SyBRv0P0AEQkTw==", + "requires": { + "@pixi/filter-kawase-blur": "5.0.0" + } + }, + "@pixi/filter-alpha": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-7.2.4.tgz", + "integrity": "sha512-UTUMSGyktUr+I9vmigqJo9iUhb0nwGyqTTME2xBWZvVGCnl5z+/wHxvIBBCe5pNZ66IM15pGXQ4cDcfqCuP2kA==", + "peer": true, + "requires": {} + }, + "@pixi/filter-ascii": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-ascii/-/filter-ascii-5.0.0.tgz", + "integrity": "sha512-A49yNhiye/aFDOnI11zwEm/td2xho0td/Cvzvru8FUgi1MzJvZE03W/JoLl04ToZczw143wFPxutl6V/Ohw5bQ==", + "requires": {} + }, + "@pixi/filter-bevel": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-bevel/-/filter-bevel-5.0.0.tgz", + "integrity": "sha512-0Odat0tW/uoS/uyp0rigm07Q3YPgwKLTgkZZZSzIUVsPnwcJjiocSzWel73JkiY3m2ZjTrj+JZjkyGjkYH+2gQ==", + "requires": {} + }, + "@pixi/filter-bloom": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-bloom/-/filter-bloom-5.0.0.tgz", + "integrity": "sha512-vOSNJNV5y+ifwQWfzEmml3owcgoJAQIQtMR17SELBUwfYP60qxy5bNWBdYBlipSJVwX2AuGi8Xk5Ia9dijcqZQ==", + "requires": {} + }, + "@pixi/filter-blur": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/filter-blur/-/filter-blur-7.2.4.tgz", + "integrity": "sha512-aLyXIoxy14bTansCPtbY8x7Sdn2OrrqkF/pcKiRXHJGGhi7wPacvB/NcmYJdnI/n2ExQ6V5Njuj/nfrsejVwcA==", + "peer": true, + "requires": {} + }, + "@pixi/filter-bulge-pinch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-bulge-pinch/-/filter-bulge-pinch-5.0.0.tgz", + "integrity": "sha512-j1feWsCpyTZk4aHbYNjax52lt0OtyYDbHvYaePYzGO/SBb1t/spDnHQEkAP7R3bZ7Ud/GI4RgefAFnvsYeSetQ==", + "requires": {} + }, + "@pixi/filter-color-map": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-map/-/filter-color-map-5.0.0.tgz", + "integrity": "sha512-w77mRi89sLUMwjhl7qL/q1YrhEKyOk2MJZQdKBksvGEV/Mf5mV2h3+EOC62wB18Q4iUVQy1MS4sANyVaCctu2w==", + "requires": {} + }, + "@pixi/filter-color-overlay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-overlay/-/filter-color-overlay-5.0.0.tgz", + "integrity": "sha512-AjxVN6gnZ+xCryQUmI+TVy3yVF+CcLgDPv+nSVPDlQowuqYhZjD6qSzgRCl3Kezdi3AxrL1vi1fnBudEnzdDJg==", + "requires": {} + }, + "@pixi/filter-color-replace": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-replace/-/filter-color-replace-5.0.0.tgz", + "integrity": "sha512-u4VOtKbY6SSr2P9v5AL8/2MVsUcAH9z92c1eaqeE3PXCPNyCgZKuNHWl8+FjBIDl/1UMQVhXH2zNrC3Vuqo3JA==", + "requires": {} + }, + "@pixi/filter-convolution": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-convolution/-/filter-convolution-5.0.0.tgz", + "integrity": "sha512-SYjyKXODdHbjzBP9c5QGMOfowNwkNFi7zW1XzGwEadmv6mLHNanO3nm0PtRu/3B9B6AW1fvOaUecYmhjAZfQjg==", + "requires": {} + }, + "@pixi/filter-cross-hatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-cross-hatch/-/filter-cross-hatch-5.0.0.tgz", + "integrity": "sha512-J4bcI3MUc/Ol3nQIsXZldYEtiLAl3ktU28zlidwffkANyl/XjP76bLEgFBoc4RE2iP/FQ+9ZeEqpsN8DIg6vVg==", + "requires": {} + }, + "@pixi/filter-crt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-crt/-/filter-crt-5.0.0.tgz", + "integrity": "sha512-/kgjNW+BCCVtUa0s8Usk3WyxgBX8kelAiqkyVnM1g8xM19Dh2689gK2wjx0ibS0p74EHs42QpkJj7jTL+1MS7A==", + "requires": {} + }, + "@pixi/filter-dot": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-dot/-/filter-dot-5.0.0.tgz", + "integrity": "sha512-kytardK58Ifl5D8Ss3kkfI29FMzV3+npJYr5GAKnA80R7XGOPOMoxrknhou8y+Dw9LUcOv8y643wryvL43P2vw==", + "requires": {} + }, + "@pixi/filter-drop-shadow": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-drop-shadow/-/filter-drop-shadow-5.0.0.tgz", + "integrity": "sha512-kz2eL+ikCLL7/2RICyIkw3pZXkyMY0Ji6skhnPj7JaZSjH4V+7TiKqYXp532gTbwSRj/mzLCvFfOL3WwTDgZ1w==", + "requires": { + "@pixi/filter-kawase-blur": "5.0.0" + } + }, + "@pixi/filter-emboss": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-emboss/-/filter-emboss-5.0.0.tgz", + "integrity": "sha512-wvrk9zB62lGaPcCWbTwoaO48FrLIE4+hi02BVS+exx5RvIniNUJD/ledGxdmUjcHX/2mDIIs7PH0kAs1L/ziZw==", + "requires": {} + }, + "@pixi/filter-glitch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-glitch/-/filter-glitch-5.0.0.tgz", + "integrity": "sha512-yK3plqExyQp9eo3dwV03dnSHpQgh0xeD112ieAsqefrAOLc5AXSfTelPvEQaZ07ZkcxSDE5eqKcRvcIVi2IgLQ==", + "requires": {} + }, + "@pixi/filter-glow": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-glow/-/filter-glow-5.0.0.tgz", + "integrity": "sha512-D+YE9DGSJXtmZa6aoWJfuNu+6MnSw90GP7oRRzr7S1/4moeFZ7EWbvQehl9Y9j98idHG87Cvuh6mmsRqpgS6ow==", + "requires": {} + }, + "@pixi/filter-godray": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-godray/-/filter-godray-5.0.0.tgz", + "integrity": "sha512-L4PD3cysUMjTSDYk5q5xUtal9q6kfH8NVIdNT3aTDJpR0VW4b/ClanmOTFpJVzN6Ld/JlJbdg8ogUpXBe1gVuw==", + "requires": {} + }, + "@pixi/filter-kawase-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-kawase-blur/-/filter-kawase-blur-5.0.0.tgz", + "integrity": "sha512-dKSTaPUOvdVkfx9x+kp0TzYjGAl8CLxIRGz6Wh43NKx96nVqd/lWqvlda+zloHVgZyQoJNHZZ4Spjcw2mYoaWg==", + "requires": {} + }, + "@pixi/filter-motion-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-motion-blur/-/filter-motion-blur-5.0.0.tgz", + "integrity": "sha512-2av4dnVL1uyyCKF8RlZaMfeO8YnQwA893j24S15ubWHZaz4WlWH3lFIYmCMqlEqHPlFDBER4vLxpR1WjsUsX/Q==", + "requires": {} + }, + "@pixi/filter-multi-color-replace": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-multi-color-replace/-/filter-multi-color-replace-5.0.0.tgz", + "integrity": "sha512-hcmCKFFQ1baGDrZc/blK9zWpe3f02rqWGsPx5VRRgc1sk44UYXHCKZnDjF80/g0ls8U4Lj+/5Xb7HOQq2LyyDg==", + "requires": {} + }, + "@pixi/filter-old-film": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-old-film/-/filter-old-film-5.0.0.tgz", + "integrity": "sha512-XSHBz4JDbvYtUrf/NP5eKCw/wvaKTAKXQENDxk480tKYtDuteSCMg87ZjLrPlyKtGySW8KTmdzl58bZjSYpiyA==", + "requires": {} + }, + "@pixi/filter-outline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-outline/-/filter-outline-5.0.0.tgz", + "integrity": "sha512-efS3Or7VQFXo2ZyPFR2M/JlZrcLAxeVbOTPYvgKe574yUghSQbQ/pyqDWE16tRB/W7+osMrTV0+C4/N/9wIxhQ==", + "requires": {} + }, + "@pixi/filter-pixelate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-pixelate/-/filter-pixelate-5.0.0.tgz", + "integrity": "sha512-3g1ajOLsYy+x0FCC67WhDcjixrcBlhK3Zo+JP9zlHSxh0W4yNzfhsw9EsIb9XP4WnMtMAUMg5T0MLTnjbsrK4g==", + "requires": {} + }, + "@pixi/filter-radial-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-radial-blur/-/filter-radial-blur-5.0.0.tgz", + "integrity": "sha512-zafBJCAiqRtsTNGKiQ8iMt00KbG20qtBi71h286wWbr0na37iXsRcg4EN76eyNbpfAOX+1ylBgIuSd9hLyQBFA==", + "requires": {} + }, + "@pixi/filter-reflection": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-reflection/-/filter-reflection-5.0.0.tgz", + "integrity": "sha512-PuZe19XUq0gTdmAStu3hcyGKkNlKGrpblN4s6vJmV+vAKVcFv2OpfjtuGUXcP/oi2LmLakC/vKfEx4bDgZzz+w==", + "requires": {} + }, + "@pixi/filter-rgb-split": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-rgb-split/-/filter-rgb-split-5.0.0.tgz", + "integrity": "sha512-zsWBrDkj9EdjJRPjGCt/0O2Vx/8Gt+8VTmjRA0ONoegcMD9slJdJMgL9EbH/1y5WHgmzGbgZIPvWULIqepVxBQ==", + "requires": {} + }, + "@pixi/filter-shockwave": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-shockwave/-/filter-shockwave-5.0.0.tgz", + "integrity": "sha512-aL0ExAkJGcUo463Ktq4HXjZGlJDpoYcyZhwd87maJrFsBjQZl2gopse6bEsy7IJxbAKzlpUKFmAP9rxwZWqMVQ==", + "requires": {} + }, + "@pixi/filter-simple-lightmap": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-simple-lightmap/-/filter-simple-lightmap-5.0.0.tgz", + "integrity": "sha512-0WIKQIGZ3aNafe2VZIbGQJWxSlBMbmjM9J+Tswjaeg8Z1dz6Qux5lYIC16wZOaIqVlWL5GTpfn8HU0BHCOvESA==", + "requires": {} + }, + "@pixi/filter-tilt-shift": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-tilt-shift/-/filter-tilt-shift-5.0.0.tgz", + "integrity": "sha512-nIxYoTU9kFDx3EE1fyoIEOfAia9Tvoj+sakTKCJZUvTk+5tjpZdAm+Ump42cnb6UxTR8AMTQiwH54C7I0pbA4Q==", + "requires": {} + }, + "@pixi/filter-twist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-twist/-/filter-twist-5.0.0.tgz", + "integrity": "sha512-YVtz3ZPfvaz22gZRZo+cOC0/L6SgSZmr/HEa6Ir+BRNVqLff6CpPx6YBVJqPREh+HFZjDomSP0kf5JasQYhzSg==", + "requires": {} + }, + "@pixi/filter-zoom-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-zoom-blur/-/filter-zoom-blur-5.0.0.tgz", + "integrity": "sha512-Q1ftuY/KPgbVtJHCvl0p4hrwVWRMWZ/yX1YRjdLGSyOwMEN8u16MEEXFQUtixEHY7+MBRBWaPOaXBaQrd+Xq7A==", + "requires": {} + }, + "@pixi/math": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/math/-/math-7.2.4.tgz", + "integrity": "sha512-LJB+mozyEPllxa0EssFZrKNfVwysfaBun4b2dJKQQInp0DafgbA0j7A+WVg0oe51KhFULTJMpDqbLn/ITFc41A==", + "peer": true + }, + "@pixi/runner": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-7.2.4.tgz", + "integrity": "sha512-YtyqPk1LA+0guEFKSFx6t/YSvbEQwajFwi4Ft8iDhioa6VK2MmTir1GjWwy7JQYLcDmYSAcQjnmFtVTZohyYSw==", + "peer": true + }, + "@pixi/settings": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-7.2.4.tgz", + "integrity": "sha512-ZPKRar9EwibijGmH8EViu4Greq1I/O7V/xQx2rNqN23XA7g09Qo6yfaeQpufu5xl8+/lZrjuHtQSnuY7OgG1CA==", + "peer": true, + "requires": { + "@pixi/constants": "7.2.4", + "@types/css-font-loading-module": "^0.0.7", + "ismobilejs": "^1.1.0" + } + }, + "@pixi/ticker": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-7.2.4.tgz", + "integrity": "sha512-hQQHIHvGeFsP4GNezZqjzuhUgNQEVgCH9+qU05UX1Mc5UHC9l6OJnY4VTVhhcHxZjA6RnyaY+1zBxCnoXuazpg==", + "peer": true, + "requires": { + "@pixi/extensions": "7.2.4", + "@pixi/settings": "7.2.4", + "@pixi/utils": "7.2.4" + } + }, + "@pixi/utils": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-7.2.4.tgz", + "integrity": "sha512-VUGQHBOINIS4ePzoqafwxaGPVRTa3oM/mEutIIHbNGI3b+QvSO+1Dnk40M0zcH6Bo+MxQZbOZK5X/wO9oU5+LQ==", + "peer": true, + "requires": { + "@pixi/color": "7.2.4", + "@pixi/constants": "7.2.4", + "@pixi/settings": "7.2.4", + "@types/earcut": "^2.1.0", + "earcut": "^2.2.4", + "eventemitter3": "^4.0.0", + "url": "^0.11.0" + } + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "peer": true + } + } + }, "pixi.js": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-6.0.4.tgz", diff --git a/package.json b/package.json index 8c7e2cb..0e13461 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "psychojs", - "version": "2022.3.1", + "version": "2023.2.1", "private": true, "description": "Helps run in-browser neuroscience, psychology, and psychophysics experiments", "license": "MIT", @@ -34,6 +34,7 @@ "howler": "^2.2.1", "log4javascript": "github:Ritzlgrmft/log4javascript", "pako": "^1.0.10", + "pixi-filters": "^5.0.0", "pixi.js-legacy": "^6.0.4", "seedrandom": "^3.0.5", "tone": "^14.7.77", diff --git a/src/core/EventManager.js b/src/core/EventManager.js index c9f8255..245b3a7 100644 --- a/src/core/EventManager.js +++ b/src/core/EventManager.js @@ -302,7 +302,13 @@ export class EventManager { const timestamp = MonotonicClock.getReferenceTime(); - let code = event.code; + // Note: we are using event.key since we are interested in the input character rather than + // the physical key position on the keyboard, i.e. we need to take into account the keyboard + // layout + // See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code for a comment regarding + // event.code's lack of suitability + let code = EventManager._pygletMap[event.key]; + // let code = event.code; // take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge): if (typeof code === "undefined") diff --git a/src/core/GUI.js b/src/core/GUI.js index 90bd3d1..e3cc571 100644 --- a/src/core/GUI.js +++ b/src/core/GUI.js @@ -50,6 +50,9 @@ export class GUI { this._psychoJS = psychoJS; + // info fields excluded from the GUI: + this._excludedInfo = {}; + // gui listens to RESOURCE events from the server manager: psychoJS.serverManager.on(ServerManager.Event.RESOURCE, (signal) => { @@ -87,9 +90,6 @@ export class GUI requireParticipantClick = GUI.DEFAULT_SETTINGS.DlgFromDict.requireParticipantClick }) { - // get info from URL: - const infoFromUrl = util.getUrlParameters(); - this._progressBarMax = 0; this._allResourcesDownloaded = false; this._requiredKeys = []; @@ -113,6 +113,19 @@ export class GUI self._dialogComponent.tStart = t; self._dialogComponent.status = PsychoJS.Status.STARTED; + // prepare the info fields excluded from the GUI, including those from the URL: + const excludedInfo = {}; + for (let key in self._excludedInfo) + { + excludedInfo[key.trim().toLowerCase()] = self._excludedInfo[key]; + } + const infoFromUrl = util.getUrlParameters(); + infoFromUrl.forEach((value, key) => + { + excludedInfo[key.trim().toLowerCase()] = value; + }); + + // if the experiment is licensed, and running on the license rather than on credit, // we use the license logo: if (self._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER @@ -130,7 +143,13 @@ export class GUI markup += "
    "; // alert title and close button: - markup += `

    ${title}

    `; + markup += "
    "; + markup += `

    ${title}

    `; + markup += ""; + markup += "
    "; + + // everything above the buttons is in a scrollable container: + markup += "
    "; // logo, if need be: if (typeof logoUrl === "string") @@ -139,14 +158,16 @@ export class GUI } // add a combobox or text areas for each entry in the dictionary: + let atLeastOneIncludedKey = false; Object.keys(dictionary).forEach((key, keyIdx) => { const value = dictionary[key]; const keyId = "form-input-" + keyIdx; // only create an input if the key is not in the URL: - let inUrl = false; const cleanedDictKey = key.trim().toLowerCase(); + const isIncluded = !(cleanedDictKey in excludedInfo); + /*let inUrl = false; infoFromUrl.forEach((urlValue, urlKey) => { const cleanedUrlKey = urlKey.trim().toLowerCase(); @@ -155,10 +176,13 @@ export class GUI inUrl = true; // break; } - }); + });*/ - if (!inUrl) + if (isIncluded) + // if (!inUrl) { + atLeastOneIncludedKey = true; + markup += ``; // if the field is required: @@ -185,7 +209,7 @@ export class GUI markup += ""; } - // otherwise we use a single string input: + // otherwise we use a single string input: //if (typeof value === 'string') else { @@ -199,17 +223,27 @@ export class GUI markup += "

    Fields marked with an asterisk (*) are required.

    "; } + markup += "
    "; // scrollable-container + + // separator, if need be: + if (atLeastOneIncludedKey) + { + markup += "
    "; + } + // progress bar: - markup += `
    ${self._progressMessage}
    `; + markup += `
    ${self._progressMessage}
    `; markup += "
    "; // buttons: markup += "
    "; + markup += "
    "; markup += ""; if (self._requireParticipantClick) { markup += ""; } + markup += "
    "; // button-group markup += "
    "; @@ -346,14 +380,18 @@ export class GUI { const error = this._userFriendlyError(errorCode); markup += `

    ${error.title}

    `; + markup += "
    "; markup += `

    ${error.text}

    `; + markup += "
    "; } else { markup += `

    Error

    `; + markup += "
    "; markup += `

    Unfortunately we encountered the following error:

    `; markup += stackCode; markup += "

    Try to run the experiment again. If the error persists, contact the experiment designer.

    "; + markup += "
    "; } } @@ -361,27 +399,36 @@ export class GUI else if (typeof warning !== "undefined") { markup += `

    Warning

    `; + markup += "
    "; markup += `

    ${warning}

    `; + markup += "
    "; } // we are displaying a message: else if (typeof message !== "undefined") { - markup += `

    Message

    `; + markup += "

    Message

    "; + markup += "
    "; markup += `

    ${message}

    `; + markup += "
    "; } if (showOK || showCancel) { markup += "
    "; } - if (showCancel) + if (showCancel || showOK) { - markup += ""; - } - if (showOK) - { - markup += ""; + markup += "
    "; + if (showCancel) + { + markup += ""; + } + if (showOK) + { + markup += ""; + } + markup += "
    "; // button-group } markup += ""; diff --git a/src/core/Keyboard.js b/src/core/Keyboard.js index dd2427d..56df760 100644 --- a/src/core/Keyboard.js +++ b/src/core/Keyboard.js @@ -354,7 +354,13 @@ export class Keyboard extends PsychObject */ self._previousKeydownKey = event.key; - let code = event.code; + // Note: we are using event.key since we are interested in the input character rather than + // the physical key position on the keyboard, i.e. we need to take into account the keyboard + // layout + // See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code for a comment regarding + // event.code's lack of suitability + let code = EventManager._pygletMap[event.key]; + // let code = event.code; // take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge): if (typeof code === "undefined") @@ -394,7 +400,9 @@ export class Keyboard extends PsychObject self._previousKeydownKey = undefined; - let code = event.code; + // Note: see above for explanation regarding the use of event.key in lieu of event.code + let code = EventManager._pygletMap[event.key]; + // let code = event.code; // take care of legacy Microsoft Edge: if (typeof code === "undefined") diff --git a/src/core/PsychoJS.js b/src/core/PsychoJS.js index 21d9f35..c8ca9d1 100644 --- a/src/core/PsychoJS.js +++ b/src/core/PsychoJS.js @@ -530,6 +530,7 @@ export class PsychoJS const response = { origin: "PsychoJS.quit", context: "when terminating the experiment" }; this._experiment.experimentEnded = true; + this._experiment.isCompleted = isCompleted; this.status = PsychoJS.Status.STOPPED; const isServerEnv = (this.getEnvironment() === ExperimentHandler.Environment.SERVER); @@ -601,7 +602,7 @@ export class PsychoJS if (showOK) { - let text = "Thank you for your patience.

    "; + let text = "Thank you for your patience."; text += (typeof message !== "undefined") ? message : "Goodbye!"; this._gui.dialog({ message: text, diff --git a/src/core/ServerManager.js b/src/core/ServerManager.js index 2415c50..d522f6f 100644 --- a/src/core/ServerManager.js +++ b/src/core/ServerManager.js @@ -1293,7 +1293,7 @@ export class ServerManager extends PsychObject } // font files: - else if (["ttf", "otf", "woff", "woff2"].indexOf(pathExtension) > -1) + else if (["ttf", "otf", "woff", "woff2","eot"].indexOf(pathExtension) > -1) { fontResources.push(name); } diff --git a/src/core/Window.js b/src/core/Window.js index cb6acbe..16761a0 100644 --- a/src/core/Window.js +++ b/src/core/Window.js @@ -13,6 +13,7 @@ import { MonotonicClock } from "../util/Clock.js"; import { Color } from "../util/Color.js"; import { PsychObject } from "../util/PsychObject.js"; import { Logger } from "./Logger.js"; +import { hasTouchScreen } from "../util/Util.js"; /** *

    Window displays the various stimuli of the experiment.

    @@ -181,7 +182,7 @@ export class Window extends PsychObject { // gets updated frame by frame const lastDelta = this.psychoJS.scheduler._lastDelta; - const fps = lastDelta === 0 ? 60.0 : 1000 / lastDelta; + const fps = (lastDelta === 0) ? 60.0 : (1000.0 / lastDelta); return fps; } @@ -493,6 +494,17 @@ export class Window extends PsychObject // update the renderer size and the Window's stimuli whenever the browser's size or orientation change: this._resizeCallback = (e) => { + // if the user device is a mobile phone or tablet (we use the presence of a touch screen as a + // proxy), we need to detect whether the change in size is due to the appearance of a virtual keyboard + // in which case we do not want to resize the canvas. This is rather tricky and so we resort to + // the below trick. It would be better to use the VirtualKeyboard API, but it is not widely + // available just yet, as of 2023-06. + const keyboardHeight = 300; + if (hasTouchScreen() && (window.screen.height - window.visualViewport.height) > keyboardHeight) + { + return; + } + Window._resizePixiRenderer(this, e); this._backgroundSprite.width = this._size[0]; this._backgroundSprite.height = this._size[1]; diff --git a/src/data/ExperimentHandler.js b/src/data/ExperimentHandler.js index 7a30578..97692b5 100644 --- a/src/data/ExperimentHandler.js +++ b/src/data/ExperimentHandler.js @@ -276,6 +276,7 @@ export class ExperimentHandler extends PsychObject } let data = this._trialsData; + // if the experiment data have to be cleared, we first make a copy of them: if (clear) { @@ -351,6 +352,19 @@ export class ExperimentHandler extends PsychObject } } + /** + * Get the results of the experiment as a .csv string, ready to be uploaded or stored. + * + * @return {string} a .csv representation of the experiment results. + */ + getResultAsCsv() + { + // note: we use the XLSX library as it automatically deals with header, takes care of quotes, + // newlines, etc. + const worksheet = XLSX.utils.json_to_sheet(this._trialsData); + return "\ufeff" + XLSX.utils.sheet_to_csv(worksheet); + } + /** * Get the attribute names and values for the current trial of a given loop. *

    Only info relating to the trial execution are returned.

    diff --git a/src/index.css b/src/index.css index 301aaa1..8194d84 100644 --- a/src/index.css +++ b/src/index.css @@ -26,13 +26,12 @@ body { /* Project and resource dialogs */ - .dialog-container label, .dialog-container input, .dialog-container select { - box-sizing: border-box; - display: block; - padding-bottom: 0.5em; + box-sizing: border-box; + display: block; + padding-bottom: 0.5em; } .dialog-container input.text, @@ -40,6 +39,13 @@ body { margin-bottom: 1em; padding: 0.5em; width: 100%; + + height: 34px; + border: 1px solid #767676; + border-radius: 2px; + background: #ffffff; + color: #333; + font-size: 14px; } .dialog-container fieldset { @@ -71,12 +77,19 @@ body { } .dialog-content { + display: flex; + flex-direction: column; + row-gap: 0; + margin: auto; z-index: 2; position: relative; width: 500px; max-width: 88vw; + /*max-height: 90vh;*/ + max-height: 93%; + padding: 0.5em; border-radius: 2px; @@ -88,11 +101,24 @@ body { box-shadow: 1px 1px 3px #555555; } +.dialog-content .scrollable-container { + height: 100%; + padding: 0 0.5em; + + overflow-x: hidden; + overflow-y: auto; +} + +.dialog-content hr { + width: 100%; +} + .dialog-title { padding: 0.5em; margin-bottom: 1em; - background-color: #009900; + background-color: #00dd00; + /*background-color: #009900;*/ border-radius: 2px; } @@ -111,6 +137,11 @@ body { } .dialog-close { + display: flex; + justify-content: center; + align-items: center; + line-height: 1.1em; + position: absolute; top: 0.7em; right: 0.7em; @@ -153,7 +184,7 @@ body { .dialog-button { padding: 0.5em 1em 0.5em 1em; - margin: 0.5em 0.5em 0.5em 0; + /*margin: 0.5em 0.5em 0.5em 0;*/ border: 1px solid #555555; border-radius: 2px; @@ -176,6 +207,14 @@ body { border: 1px solid #000000; } +.dialog-button-group { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: flex-start; + column-gap: 0.5em; +} + .disabled { border: 1px solid #AAAAAA; color: #AAAAAA; @@ -186,10 +225,15 @@ body { } .logo { - display: block; + display: flex; + flex: 0 1 auto; + height: 100%; + width: auto; + + /*display: block; margin: 0 auto 1em; max-height: 20vh; - max-width: 100%; + max-width: 100%;*/ } a, diff --git a/src/util/Scheduler.js b/src/util/Scheduler.js index bad709c..4bbaf6e 100644 --- a/src/util/Scheduler.js +++ b/src/util/Scheduler.js @@ -117,9 +117,12 @@ export class Scheduler * Start this scheduler. * *

    Note: tasks are run after each animation frame.

    + * + * @return {Promise} a promise resolved when the scheduler stops, e.g. when the experiments finishes */ start() { + let shedulerResolve; const self = this; const update = async (timestamp) => { @@ -127,6 +130,7 @@ export class Scheduler if (self._stopAtNextUpdate) { self._status = Scheduler.Status.STOPPED; + shedulerResolve(); return; } @@ -137,6 +141,7 @@ export class Scheduler if (state === Scheduler.Event.QUIT) { self._status = Scheduler.Status.STOPPED; + shedulerResolve(); return; } @@ -155,6 +160,12 @@ export class Scheduler // start the animation: requestAnimationFrame(update); + + // return a promise resolved when the scheduler is stopped: + return new Promise((resolve, _) => + { + shedulerResolve = resolve; + }); } /** diff --git a/src/util/Util.js b/src/util/Util.js index 0845207..72b8514 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -629,6 +629,11 @@ export function toString(object) return object.toString(); } + if (typeof object === "function") + { + return ``; + } + try { const symbolReplacer = (key, value) => @@ -1455,6 +1460,47 @@ export function loadCss(cssId, cssPath) } } +/** + * Whether the user device has a touchscreen, e.g. it is a mobile phone or tablet. + * + * @return {boolean} true if the user device has a touchscreen. + * @note the code below is directly adapted from MDN + */ +export function hasTouchScreen() +{ + let hasTouchScreen = false; + + if ("maxTouchPoints" in navigator) + { + hasTouchScreen = navigator.maxTouchPoints > 0; + } + else if ("msMaxTouchPoints" in navigator) + { + hasTouchScreen = navigator.msMaxTouchPoints > 0; + } + else + { + const mQ = matchMedia?.("(pointer:coarse)"); + if (mQ?.media === "(pointer:coarse)") + { + hasTouchScreen = !!mQ.matches; + } + else if ("orientation" in window) + { + hasTouchScreen = true; + } + else + { + const UA = navigator.userAgent; + hasTouchScreen = + /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) || + /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA); + } + } + + return hasTouchScreen; +} + /** * Enum that stores possible text directions. * Note that Arabic is the same as RTL but added here to support PsychoPy's diff --git a/src/visual/ButtonStim.js b/src/visual/ButtonStim.js index c007b51..5b4d34f 100644 --- a/src/visual/ButtonStim.js +++ b/src/visual/ButtonStim.js @@ -9,6 +9,7 @@ import { Mouse } from "../core/Mouse.js"; import { TextBox } from "./TextBox.js"; +import * as util from "../util/Util"; /** *

    ButtonStim visual stimulus.

    @@ -32,6 +33,7 @@ export class ButtonStim extends TextBox * @param {Color} [options.borderColor= Color("white")] the border color * @param {Color} [options.borderWidth= 0] the border width * @param {number} [options.opacity= 1.0] - the opacity + * @param {number} [options.depth= 0] - the depth (i.e. the z order) * @param {number} [options.letterHeight= undefined] - the height of the text * @param {boolean} [options.bold= true] - whether or not the text is bold * @param {boolean} [options.italic= false] - whether or not the text is italic @@ -54,11 +56,14 @@ export class ButtonStim extends TextBox borderColor, borderWidth = 0, opacity, + depth, letterHeight, bold = true, italic, autoDraw, autoLog, + boxFn, + multiline } = {}, ) { @@ -77,12 +82,15 @@ export class ButtonStim extends TextBox borderColor, borderWidth, opacity, + depth, letterHeight, + multiline, bold, italic, alignment: "center", autoDraw, autoLog, + boxFn }); this.psychoJS.logger.debug("create a new Button with name: ", name); @@ -112,7 +120,7 @@ export class ButtonStim extends TextBox if (this._autoLog) { - this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`); + this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${util.toString(this)}`); } } diff --git a/src/visual/ImageStim.js b/src/visual/ImageStim.js index f043579..1b3da06 100644 --- a/src/visual/ImageStim.js +++ b/src/visual/ImageStim.js @@ -47,7 +47,7 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) * @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip * @param {boolean} [options.autoLog= false] - whether or not to log */ - constructor({ name, win, image, mask, pos, anchor, units, ori, size, color, opacity, contrast, texRes, depth, interpolate, flipHoriz, flipVert, autoDraw, autoLog } = {}) + constructor({ name, win, image, mask, pos, anchor, units, ori, size, color, opacity, contrast, texRes, depth, interpolate, flipHoriz, flipVert, aspectRatio, autoDraw, autoLog } = {}) { super({ name, win, units, ori, opacity, depth, pos, anchor, size, autoDraw, autoLog }); @@ -94,6 +94,12 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) false, this._onChange(false, false), ); + this._addAttribute( + "aspectRatio", + aspectRatio, + ImageStim.AspectRatioStrategy.VARIABLE, + this._onChange(true, true), + ); // estimate the bounding box: this._estimateBoundingBox(); @@ -309,7 +315,18 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) this._texture = new PIXI.Texture(new PIXI.BaseTexture(this._image, texOpts)); } - this._pixi = PIXI.Sprite.from(this._texture); + if (this.aspectRatio === ImageStim.AspectRatioStrategy.HORIZONTAL_TILING) + { + const [width_px, _] = util.to_px([this.size[0], 0], this.units, this.win); + this._pixi = PIXI.TilingSprite.from(this._texture, 1, 1); + this._pixi.width = width_px; + this._pixi.height = this._texture.height; + } + else + { + this._pixi = PIXI.Sprite.from(this._texture); + } + // add a mask if need be: if (typeof this._mask !== "undefined") @@ -349,8 +366,24 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) // set the scale: const displaySize = this._getDisplaySize(); const size_px = util.to_px(displaySize, this.units, this.win); - const scaleX = size_px[0] / this._texture.width; - const scaleY = size_px[1] / this._texture.height; + let scaleX = size_px[0] / this._texture.width; + let scaleY = size_px[1] / this._texture.height; + if (this.aspectRatio === ImageStim.AspectRatioStrategy.FIT_TO_WIDTH) + { + scaleY = scaleX; + } + else if (this.aspectRatio === ImageStim.AspectRatioStrategy.FIT_TO_HEIGHT) + { + scaleX = scaleY; + } + else if (this.aspectRatio === ImageStim.AspectRatioStrategy.HORIZONTAL_TILING) + { + scaleX = 1.0; + scaleY = 1.0; + } + + // note: this calls VisualStim.setAnchor, which properly sets the PixiJS anchor + // from the PsychoPy text format this.anchor = this._anchor; this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX; this._pixi.scale.y = this.flipVert ? scaleY : -scaleY; @@ -383,7 +416,47 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) displaySize = util.to_unit(textureSize, "pix", this.win, this.units); } } + else + { + if (this.aspectRatio === ImageStim.AspectRatioStrategy.FIT_TO_WIDTH) + { + // use the size of the texture, if we have access to it: + if (typeof this._texture !== "undefined" && this._texture.width > 0) + { + displaySize = [displaySize[0], displaySize[0] * this._texture.height / this._texture.width]; + } + } + else if (this.aspectRatio === ImageStim.AspectRatioStrategy.FIT_TO_HEIGHT) + { + // use the size of the texture, if we have access to it: + if (typeof this._texture !== "undefined" && this._texture.width > 0) + { + displaySize = [displaySize[1] * this._texture.width / this._texture.height, displaySize[1]]; + } + } + else if (this.aspectRatio === ImageStim.AspectRatioStrategy.HORIZONTAL_TILING) + { + // use the size of the texture, if we have access to it: + if (typeof this._texture !== "undefined" && this._texture.width > 0) + { + displaySize = [displaySize[0], this._texture.height]; + } + } + } return displaySize; } } + +/** + * ImageStim Aspect Ratio Strategy. + * + * @enum {Symbol} + * @readonly + */ +ImageStim.AspectRatioStrategy = { + FIT_TO_WIDTH: Symbol.for("FIT_TO_WIDTH"), + HORIZONTAL_TILING: Symbol.for("HORIZONTAL_TILING"), + FIT_TO_HEIGHT: Symbol.for("FIT_TO_HEIGHT"), + VARIABLE: Symbol.for("VARIABLE"), +}; diff --git a/src/visual/TextBox.js b/src/visual/TextBox.js index 4d8e2bc..ab06378 100644 --- a/src/visual/TextBox.js +++ b/src/visual/TextBox.js @@ -86,7 +86,8 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) clipMask, autoDraw, autoLog, - fitToContent + fitToContent, + boxFn } = {}, ) { @@ -202,12 +203,14 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) // and setSize called from super class would not have a proper effect this.setSize(size); + this._addAttribute("boxFn", boxFn, null); + // estimate the bounding box: this._estimateBoundingBox(); if (this._autoLog) { - this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`); + this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${util.toString(this)}`); } } @@ -481,6 +484,26 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) alignmentStyles = ["center", "center"]; } + let box; + if (this._boxFn !== null) + { + box = this._boxFn; + } + else + { + // note: box style properties eventually become PIXI.Graphics settings, so same syntax applies + box = { + fill: new Color(this._fillColor).int, + alpha: this._fillColor === undefined || this._fillColor === null ? 0 : 1, + rounded: 5, + stroke: { + color: new Color(this._borderColor).int, + width: borderWidth_px, + alpha: this._borderColor === undefined || this._borderColor === null ? 0 : 1 + } + }; + } + return { // input style properties eventually become CSS, so same syntax applies input: { @@ -504,41 +527,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) overflow: "hidden", pointerEvents: "none" }, - // box style properties eventually become PIXI.Graphics settings, so same syntax applies - box: { - fill: new Color(this._fillColor).int, - alpha: this._fillColor === undefined || this._fillColor === null ? 0 : 1, - rounded: 5, - stroke: { - color: new Color(this._borderColor).int, - width: borderWidth_px, - alpha: this._borderColor === undefined || this._borderColor === null ? 0 : 1 - }, - /*default: { - fill: new Color(this._fillColor).int, - rounded: 5, - stroke: { - color: new Color(this._borderColor).int, - width: borderWidth_px - } - }, - focused: { - fill: new Color(this._fillColor).int, - rounded: 5, - stroke: { - color: new Color(this._borderColor).int, - width: borderWidth_px - } - }, - disabled: { - fill: new Color(this._fillColor).int, - rounded: 5, - stroke: { - color: new Color(this._borderColor).int, - width: borderWidth_px - } - }*/ - }, + box }; } diff --git a/src/visual/survey/widgets/MaxDiffMatrix.js b/src/visual/survey/widgets/MaxDiffMatrix.js index d9958c5..a50c784 100644 --- a/src/visual/survey/widgets/MaxDiffMatrix.js +++ b/src/visual/survey/widgets/MaxDiffMatrix.js @@ -95,18 +95,11 @@ class MaxDiffMatrix question.setCssRoot(rootClass); question.cssClasses.mainRoot = rootClass; } - let html; - let headerCells = ""; - let subHeaderCells = ""; - let bodyCells = ""; - let bodyHTML = ""; - let cellGenerator; - let i, j; // Relying on a fact that there's always 2 columns. // This is correct according current Qualtrics design for MaxDiff matrices. // Header generation - headerCells = + let headerCells = `${question.columns[0].text} @@ -114,9 +107,10 @@ class MaxDiffMatrix ${question.columns[1].text}`; // Body generation - for (i = 0; i < question.rows.length; i++) + let bodyHTML = ""; + for (let i = 0; i < question.rows.length; i++) { - bodyCells = + const bodyCells = `