Forum Replies Created
-
AuthorPosts
-
May 28, 2021 at 5:00 pm in reply to: preventing dashed sentence spillover / centering sentences #7010
Jeremy
KeymasterI re-read your message and realized that you might want to print multiple sentences, but only one per line
Here’s a suggestion, it will split your string at every
.
,?
or!
followed by a space and print each sentence on a new, centered line:dashed = (name,sentences) => [ newText(name,"").size("100vw","auto").center().css("text-align","center").print() , ...sentences.split(/(?<=[\.!\?])[\s\t]/).map( (s,n) => [ newText(name+"-s"+n,"") .css({display:'inline-block','line-height':'2em','white-space':'nowrap'}) .print(getText(name)) , ...s.split(/[\s\t]+/).map( (w,i) => newText(name+'-s'+n+"-"+i, w.replace(/([^.,?:;!\s])/g,"<span class='DashedSentence-ospan'><span class='DashedSentence-ispan'>$1</span></span>")) .css("margin","0em 0.25em") .print(getText(name+"-s"+n)) ) ] ).flat() , newKey(name+'-start', " ").log().wait() // first keypress, to reveal first word , ...sentences.split(/(?<=[\.!\?])[\s\t]/).map( (s,n) => [ ...s.split(/[\s\t]+/).map((w,i)=>[ getText(name+'-s'+n+'-'+i).text(w) // reveal chunk , newKey('s'+n+'-'+i+"-"+w," ").log().wait() // wait for keypress , getText(name+'-s'+n+'-'+i).text(w.replace(/([^.,?:;!\s])/g,"<span class='DashedSentence-ospan'><span class='DashedSentence-ispan'>$1</span></span>")) // hide chunk ] ).flat() ] ).flat() ]
Jeremy
May 28, 2021 at 4:38 pm in reply to: preventing dashed sentence spillover / centering sentences #7009Jeremy
KeymasterHi,
Try this:
newController("DashedSentence",{s:"This is a rather long sentence but hopefully this will be long enough that it should insert linebreaks into narrow containers"}) .css("white-space","nowrap") .center() .print() .wait()
Note that since you don’t want any linebreak, your sentence might overflow horizontally from the page area if it is too long
Jeremy
Jeremy
KeymasterHi,
The MediaRecorder element will capture video unless it is explicitly restricted to audio: https://doc.pcibex.net/mediarecorder/
If there is a
newMediaRecorder
command in your script that does not explicitly specify"audio"
(ie neithernewMediaRecorder("audio")
nornewMediaRecorder("myrecorder", "audio")
) then PennController will determine that you need to capture video and accordingly require access to the participant’s webcamJeremy
Jeremy
KeymasterHi Zach,
You are calling
wait
on the Canvas element: there is nowait
command on Canvas elements. You want to call it on the Controller element instead:newImage("sb1", "training_sb_inplace.png") .size(sb_width, sb_height) , newController("ds1", "DashedSentence", {s: row.SentenceText, mode: "speeded acceptability", blankText: "+", display: "in place"}) , newCanvas("sprcanv", canv_width, canv_height) .center() .add( sb_x_left, 0, getImage("sb1")) .add(ds_x_left, ds_y , getController("ds1")) .print() , getController("ds1") .wait() , getCanvas("sprcanv") .remove()
(I called
remove
on the Canvas element, but since you meant to callwait
on the Controller element, I’m not sure which element you wanted to remove)Jeremy
Jeremy
KeymasterHi Doug,
PennController elements, and Text elements in particular, are not strings, so you cannot just concatenate them to a string with
+
. More info here: https://doc.pcibex.net/how-to-guides/using-javascript/Here’s how to achieve what you need:
newVar("link") .set(getVar("pID")) .set(v=>"<p><a href='https://pitt.co1.qualtrics.com/jfe/form/SV_9suLiT1t7sABeDg?id="+v+"' target='_blank' rel='noopener noreferrer'>Click here to complete a brief survey on Qualtrics.</a></p>") , newText("") .text(getVar("link")) .center() .print()
Jeremy
Jeremy
KeymasterHi Rafael,
The reasoning is the same here: elements are parts of trials, so when their trial ends, they cease to exist. The only exception is the Var element, which can be made
global
so you can access and manipulate its value across trialsIf you need to keep track of how much time has passed since the beginning of the experiment, you can set a javascript variable to
Date.now()
and check it on every trial, for example inside aHeader
. Here’s an example that will print a 2min timer in the top-left corner of the page, and clear the screen and print a “Timed out!” message when there’s not time left:ALLOTEDTIME = 2*60*1000 startTime = Date.now() toMinSec = v=>Math.floor((ALLOTEDTIME-v)/60000)+":"+Math.floor(((ALLOTEDTIME-v)/1000)%60) Header( newText("timer", "").print("left at 2em", "top at 2em") , newVar("time elapsed") .set(v=>Date.now()-startTime) .test.is(v=>v>ALLOTEDTIME).success( end() ) , newTimer("timeout", 500) .callback( getVar("time elapsed") .set(v=>Date.now()-startTime) .test.is(v=>v>ALLOTEDTIME) .success( clear() , newText("Time out!").print() , newButton().wait() ) .failure( getVar("time elapsed").set(toMinSec) , getText("timer").text(getVar("time elapsed")) , getTimer("timeout").start() ) ) .start() )
Jeremy
Jeremy
KeymasterHi,
The element being MediaRecorder and not Video, you need to use
.PennController-mediarecorder video
as your selectorJeremy
Jeremy
KeymasterThe counter is unfortunately not a solution: if you do not increment it as soon as a participant starts your study then you run the risk of running too many early participants in the same condition (for example, with rapid signups, it could be that the counter only increases after 40 participants have started). Even if you manage to increment the counter instantly, you end up with a number of participants who did not complete your study (maybe they decide to stop, or they encounter a fatal error, etc.)
As you can imagine, this is not specific to how Ibex handles the counter. You could imagine more sophisticated algorithms (eg. initially increment with each click on the link, and switch to conditional assignment once you start receiving submissions) but in the end you will always have multiple participants taking your study in parallel at the same time, and you will never be certain that a given participant will complete your study
This is why I suggested you handle this manually. The other reason why you don’t want to use the internal counter is that it is already used to subset your table to rows that share the same value in the Group/List column, so you would have a confounding factor here. Moreover, what you suggest (switching to the other sequence once the counter reaches 30) practically means that not all participants start at the same time, ie. one group of participants starts before the other one. If you’re ok with that, I don’t really see a problem with running 30 participants first, and another 30 participants after that
If your recruiting solution gives you that option, you could generate two links that differ in the value of a specific parameter (eg. “seqOrder”:
https://farm.pcibex.net/p/mySlug/?seqOrder=0
vshttps://farm.pcibex.net/p/mySlug/?seqOrder=1
) with your participants only allowed to click one of the two. Then you can do:if (GetURLParameter("seqOrder")==0) Sequence("intro","consent","subexp1practice",rshuffle("subexp1items","subexp1fillers"),"break","subexp2practice",rshuffle("subexp2items","subexp2fillers"),"send","ending") else Sequence("intro","consent","subexp2practice",rshuffle("subexp2items","subexp2fillers"),"break","subexp1practice",rshuffle("subexp1items","subexp1fillers"),"send","ending")
Jeremy
Jeremy
KeymasterHi Rafael,
The last command in your trial is
wait
on the Button element, so whenever the button is clicked, the trial ends, and all elements are removed from the page, so you’ll just no longer be able to see any changes to the Canvas elements after the click. You will either need to keep track of things across trials using a global Var element and testing it at the beginning of the trial to run differentadd
orprint
commands accordingly, or change your approach of your task and incorporate it within a single trialA couple tips: if you’re not
get
ting the Text elements, you don’t need to explicitly give them names (right now you’re creating two distinct elements both named “player 1” and three distinct elements all named “player 2”). Timer elements have no visual content, no it doesn’t make much sense toadd
them to a Canvas element. You could simply attach yourcallback
commands directly onto thenewTimer
command (also, you can pass three comma-separated commands tocallback
, no need to call threecallback
s in a row)Jeremy
Jeremy
KeymasterIt looks like the problem has to do with the project’s name being too long. Ironically that very problem prevents you from renaming the cloned copy of the project, because it requires creating a new project with a long name before renaming it…
I’ll fix this and update the farm soon, but in the meantime you can try renaming the original project with a shorter name, just make sure that you use fewer than 45 characters (the overall limit is 50 characters, and cloned projects automatically add “_copy” at the end, which uses 5 characters)
Jeremy
Jeremy
KeymasterHi Xinchi,
I am sorry you are experiencing issues cloning and saving your project. May I ask what the slug of your project is (the 6 characters at the end of the demonstration link) and whether you still encounter this problem with this and/or other projects?
Jeremy
Jeremy
KeymasterHi,
If you need perfectly balanced groups (30-30) you’ll have to assign participants manually. If I were you, I would run a first batch of 30 participants with
Sequence("intro","consent","subexp1practice",rshuffle("subexp1items","subexp1fillers"),"break","subexp2practice",randomize("subexp2items","subexp2fillers"),"send","ending")
, then run a second batch of 30 participants withSequence("intro","consent","subexp2practice",randomize("subexp2items","subexp2fillers"),"break","subexp1practice",randomize("subexp1items","subexp1fillers"),"send","ending")
If you want it to be random, you could simply do this, but then you won’t be certain you’ll end up with exactly 30 participants in each group:
if (Math.random() >= 0.5) Sequence("intro","consent","subexp1practice",rshuffle("subexp1items","subexp1fillers"),"break","subexp2practice",randomize("subexp2items","subexp2fillers"),"send","ending") else Sequence("intro","consent","subexp2practice",randomize("subexp2items","subexp2fillers"),"break","subexp1practice",randomize("subexp1items","subexp1fillers"),"send","ending")
NB: shouldn’t it be
rshuffle
orrandomize
everywhere? Right now you havershuffle
for exp1 in the firstSequence
command onlyJeremy
Jeremy
KeymasterHi,
sepWith
is illustrated on the documentation page forUploadRecordings
: https://doc.pcibex.net/global-commands/uploadrecordings/It’s an Ibex function that inserts trials after each trial from a set, which proves handy for regularly inserting asynchronous uploadrecordings trials.
Would you mind sharing your project’s demonstration link with me, either here or at support@pcibex.net?
Jeremy
Jeremy
KeymasterHi,
UploadRecordings
creates a trial, you cannot include it inside another trial (onlySendResults
has this exceptional behavior, which might not have been the best design decision on my end, since that seems to introduce confusion)I’m working on fixing the behavior of the automatically inserted last UploadRecordings trial. When you say that “this always happens” even when you manually include an UploadRecordings trial earlier in your
Sequence
, do you mean that you still see the default one at the end of the experiment, and that that one shows you an error message (but not the one you insert earlier)?Have you tried something along those lines?
Sequence("intro", sepWith("asyncUpload",randomize("trials")), "syncUpload", SendResults(), "thanks") UploadRecordings("syncUpload") UploadRecordings("asyncUpload", "noblock") newTrial("thanks", newText("Thank you for participating in this study.").print() , newButton().wait() // wait on this screen forever )
Jeremy
Jeremy
KeymasterHi Matthias,
Since what you need is not natively supported by Ibex’s DashedSentence controller, and is not immediately implementable in PennController from a simple string, you must resort to at least some javascript code. The snippet from my previous message defines a function that takes a string and automatically generates a series of PennController commands that reproduce certain behaviors of the native-Ibex DashedSentence controller
PennController lets you inject some native-Ibex controller inside your trial’s script, but you cannot use (bits of) PennController code to edit a native-Ibex controller. This means that you cannot simply integrate the function above (which, again, simply outputs a simple series of PennController commands) into the DashedSentence controller
I decided to exclusively rely on PennController for several reasons. First, this is a PennController support space 😉 This means that people here know at least some PennController code, but they do not necessarily know the quite advanced javascript making up the DashedSentence controller’s code. Second, PennController gives you more control over the elements in your trial (technically, you could access each chunk separately, since each is a Text element). Third, I think it’s much easier to understand. This is the output of
dashed("myDashed", "Hello world")
(for the above definition ofdashed
):newText("myDashed","").css({display:'flex','flex-direction':'row','flex-wrap':'wrap','line-height':'2em','max-width':'100%'}).print() , newText("myDashed-0", "<span class='DashedSentence-ospan'><span class='DashedSentence-ispan'>Hello</span></span>")) .css("margin","0em 0.2em") .print(getText("myDashed")) , newText("myDashed-1", "<span class='DashedSentence-ospan'><span class='DashedSentence-ispan'>world</span></span>")) .css("margin","0em 0.2em") .print(getText("myDashed")) , newKey("0-Hello"," ").log().wait(), getText("myDashed-0").text("Hello") , newKey("0-Hello"," ").log().wait(), getText("myDashed-1").text("world")
This output is reasonably simple PennController code. The tricky part is seeing how exactly the
dashed
function maps the string onto that series of commands. This part does most of the job...sentence.split(/[\s\t<>]+/)
: it splits the string at every space/tab/<
/>
character, and generates commands for each chunk. The code is further obscured by the need to handle<br>
s, which include linebreaks (rendered as content-less Text elements which occupy 100% of the available page’s width, visually resulting in linebreaks)Replacing the space/tab separator character with
*
so as to split chunks of words rather than individual words is pretty straightforward, all that’s needed is to replace the regular expression/[\s\t<>]+/
with/[*<>]+/
. Masking the previous word again when revealing the next word requires some slight rearrangement, so that the function outputs “reveal word; wait for keypress; hide word” for each chunk instead of just “wait for keypress; reveal word.” as it does now.So here’s what you get (I also made it use hyphens instead of underscores after I went back to your previous messages):
dashed = (name,sentence) => [ newText(name,"").css({display:'flex','flex-direction':'row','flex-wrap':'wrap','line-height':'2em','max-width':'100%'}).print() , ...sentence.split(/[*<>]+/).map( (w,i) => (w=="br"? newText("").css("width","100vw").print(getText(name)) : newText(name+'-'+i, w.replace(/([^.,?:;!\s])/g,'-')) .css({margin:"0em 0.2em",'font-family':'monospace',"font-size":"large"}) .print(getText(name)) )) , newKey(name+'-start', " ").log().wait() // first keypress, to reveal first chunk , ...sentence.split(/[*<>]+/).map((w,i)=>(w!="br"?[ getText(name+'-'+i).text(w) // reveal chunk , newKey(i+"-"+w," ").log().wait() // wait for keypress , getText(name+'-'+i).text(w.replace(/([^.,?:;!\s])/g,'-')) // hide chunk ]:null)) ] newTrial( dashed("myDashed", "This is a test.*This is the second,*longer part of the test!*"+ "<br>And now*this is a third part,*just to test whether*it will automatically*insert a linebreak") , getText("myDashed").remove() , newButton("Finish").print().wait() )
Here’s a live example: https://farm.pcibex.net/r/frrjaU/
Let me know if you have any questions
Jeremy
-
AuthorPosts