Dynamic control of hundreds TextStim to create a layout experiment

Hi everyone,

I’m creating an experiment that requires participants to change the layout (including line spacing, word spacing, and font size) of a Chinese article using sliders.

I have not yet found functions for directly changing the word spacing, so I simply use TextStim and for loops to present and change the position of each Chinese character individually. It works OK when I have around 10 characters, but when I have more characters (let’s say a hundred), the program becomes super slow and the responses of sliders severely lag.

It could be the for loop that slows down everything, but since I’m kinda new to Psychopy/Python, I have not yet figured out how to have it done more elegantly. I’m now checking the ElementArray.py which allows user to present multiple objects at the same time. Yet I don’t know if it can be used for presenting words or characters.

May I know if any of you got advices for speeding up the program so that the stimuli are presented smoothly? Thank you very much in advance.

I’m using version 3.6.8 on Mac.
Here is my code:

# Initialize components for Routine "Trials"

sentence='最基本的眼動研究是觀察'
def split(assay):
    return [char for char in assay]
wordstr=split(sentence)

wordlist=[None]*len(wordstr)
for index in range(len(wordstr)):
    wordlist[index] = visual.TextStim(win=win,
        text=wordstr[index],
        font='Songti SC',
        units='pix', pos=((-250+50*index), 0), height=50.0, wrapWidth=None, ori=0.0, 
        color='black', colorSpace='rgb', opacity=None, 
        languageStyle='LTR',
        depth=-1.0);

# *wordlist* updates
for j in range(len(wordlist)):
    if wordlist[j].status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
    # keep track of start time/frame for later
        wordlist[j].frameNStart = frameN  # exact frame index
        wordlist[j].tStart = t  # local t and not account for scr refresh
        wordlist[j].tStartRefresh = tThisFlipGlobal  # on global time
        win.timeOnFlip(wordlist[j], 'tStartRefresh')  # time at next scr refresh
        wordlist[j].setAutoDraw(True)
    if wordlist[j].status == STARTED:  # only update if drawing
        if sli_WS.value is None: # sli_WS is the slider control word spacing
            condsWS=0
        else:
            condsWS=sli_WS.value
                    
        if sli_LD.value is None: # sli_LD is the slider control line spacing
            condsLD=100
        else:
            condsLD=sli_LD.value
                        
        if sli_FS.value is None: # sli_FS is the slider control font size
            condsFS=26
        else:
            condsFS=sli_FS.value
                    
        lineCountH=(j+2)%5 # telling the position of the character within a line
        lineCountV=(j+2)//5 # telling the position of the line within an article
                    
        PosX=-516+condsFS/2+lineCountH*condsFS+condsWS*condsFS*lineCountH
        PosY=276-(condsFS/2)-(lineCountV*condsFS*((100+condsLD)/100))
        wordlist[j].setPos((PosX, PosY))
        wordlist[j].setHeight(sli_FS.value or 26)
                   

The main way to make your code more efficient is to save the current value of the slider and only change the spacings if it changes.

@wakecarter Thanks for your help!

Do you mean that the current loop is calculated once on every flame, and I should rewrite my code to make the loop only calculate after receive the value of slider and then flip?

Could you explain more?
It would be great that if you show some example code. :blush:

Thank you very much!!!

How about

if condsWS != oldWS or condsLD != oldLD or condsFS != old FS:
        lineCountH=(j+2)%5 # telling the position of the character within a line
        lineCountV=(j+2)//5 # telling the position of the line within an article
                    
        PosX=-516+condsFS/2+lineCountH*condsFS+condsWS*condsFS*lineCountH
        PosY=276-(condsFS/2)-(lineCountV*condsFS*((100+condsLD)/100))
        wordlist[j].setPos((PosX, PosY))
        wordlist[j].setHeight(sli_FS.value or 26)
        oldWS = condsWS
        oldLD = condsLD
        oldFS = condsFS

Set all three “old” variables to 0 in Begin Routine.