@@ -3180,7 +3180,7 @@ it contains.
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.
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 @@
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
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.
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
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 @@
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 @@
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
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
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
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 @@
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.
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 @@
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
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 @@
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
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.
@@ -7154,7 +7154,7 @@ but does not change the actual rating returned by the slider.
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 @@
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.
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 @@
@@ -4338,7 +4338,7 @@
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().
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.
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
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 @@
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.
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
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
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 = {
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 = {
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
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
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 = {
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 = {
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
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) =>
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 = {
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 = {
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 = {
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 = {
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 = {
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
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.
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 @@
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 @@
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); }
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 @@
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
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
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 @@
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 @@
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)
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,
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
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 @@
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 = {
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
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
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
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
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
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 = {
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
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
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 = {
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) =>
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
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)
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
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 = {
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 = {
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
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
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 = {
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
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)
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
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
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
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 = {
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 = {
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([
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([
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)
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";