Click and drag images to lock in a specified area

Hello,

I am fairly new to Python and am working through a coding problem.

I am creating an experiment where participants must click and drag 22 different coloured squares to create a smooth transition between different colours (see screenshot below). The squares at each end will be fixed for reference and the squares in between will be shuffled within the row to reach the correct pattern. I have started with the code below:

for stimulus in [square2, square3, square4, etc]:
    if mouse.isPressedIn(stimulus):
        stimulus.pos = mouse.getPos()
        break

The squares will start off in random locations in the row (with the exception of the end reference squares). I want to be able to click on one square and drag it to a new location, and when it enters the area of a different square, it takes it’s place. It will get fitted in to the original square’s place and the other squares will shuffle (to the left or right depending on where the dragged square came from).

I’m thinking if there is a way to designate each square a zone in the row to start off, and then track the mouse (and therefore the dragged square) such that when the mouse/square is in a different zone, the square gets that zone. Then the other squares will be designated one spot over to the left or the right depending where the initial square came from.

Also, the code I have used has an issue where when I drag a square across other squares, it starts moving a different square. How could I adjust it so the currently dragged square is moved only?

Thanks so much in advance!

Cool, I guess this is sort a Farnsworth-Munsell equivalent.

Just to address this problem in isolation first, I think you are using code from this topic:

If you follow that thread down a little further you’ll find that person had exactly the same issue and that some more complicated code was suggested in this post to fix the issue of keeping a given stimulus as the selected one during a drag:

The issue of shuffling stimuli aside is another layer of complication. Let’s make sure you have the dynamic dragging solved first.

Yes exactly, I am creating a version of the Farnsworth-Munsell Hue test.

Thank you very much, I was able to add the code and it works perfectly. I just made the adjustment of 9 to 20 to account for the 20 stimuli I have to sort. Do you think this new code could be adapted for the shuffling function?

It can certainly be done but will be complicated I can give you some general pointers but realistically, this is code that should be developed and tested interactively, rather than something I could just type out in one go.

I’d suggest you start by associating each stimulus with its ordinal location, say counting lifting to right from 0 to 19:

stimuli = [square0, square1, square2, …] # up to 20, you already have this list

# create a dictionary that associates each stimulus with an ordinal 
# number from left to right:
stimulus_boxes = {image: i for i, image in enumerate([stimuli])}

You could also/instead construct a dictionary where you associate the stimuli with their actual x coordinates (in pixels or degrees or whatever).

Then in the 'each frame" tab, you need to monitor for when a drag had been commenced but now the mouse has been released, so the drag has ended. At that point, check if the mouse coordinates are within another stimulus. If not, zoom the stimulus back to its previous location (by looking it up in the stimulus_boxes dictionary). i.e. a drag won’t “stick” unless it lands on a valid location.

If the mouse coordinates do overlap another stimulus, look up its box number. If it is greater than the one being dragged, then cycle through all the stimuli from the one with a box number one greater than the dragged one through to the underlying one, and shift them to a box number that is one less. Conversely, if it is less, then shift the affected ones to the right. Lastly, alter the box number of the one that was dragged to match the one that was beneath it. Then cycle through all affected stimuli, shifting their locations to match their respective ordinal positions (i.e. box numbers).

Hopefully you can turn that recipe into pseudo-code and then functional code…

I’ve actually implemented something similar to this recently. The basic structure is exactly what Michael describes, though my drag and drop code is done a little differently (for reasons I won’t get into). As described above, I have a list of locations for the objects I’m working with. You can think of it as a set of “slots” that the objects can be placed into. When the mouse is released, I cycle through that list and compare the distance of the center of each “slot” from the mouse location:

minDist = 2 # Maximum possible distance [because I was using Norm units]
closest = 0 # index of closest location
for loc in range(0, len(newFlowLocs)):
    tmpDist = self._distanceCalc(finPos, newFlowLocs[loc])
    if tmpDist < minDist:
        closest = deepcopy(loc)
        minDist = tmpDist

So “newFlowLocs” is the list of “slots”. The distance calculation is just a simple little utility function:

 def _distanceCalc(self, pos1, pos2):
        """
        A simple function for computing distance between two points

        :param pos1: X,Y location 1
        :type pos1: list
        :param pos2: X,Y location 2
        :type pos2: list
        :return: Distance
        :rtype: float
        """
        x1 = pos1[0]
        x2 = pos2[0]
        y1 = pos1[1]
        y2 = pos2[1]
        dist = sqrt((x1-x2)**2 + (y1-y2)**2)
        return dist

This is just one way of implementing of some of @Michael’s suggestions, but it gets you the key thing you need to update the positions of every object in the dictionary, which is the closest one when the mouse is released.

Good luck!

1 Like

I apologize for not posting this sooner - current situation has really thrown a wrench in things. Here is code that was done by David. It works beautifully on line. It doesn’t lock pieces in place, but it does do a very nice drag and drop. I’ll do the python first then JS.

Note: This is David Bridge’s Code, so I have cited him as the author of the code on my experiment.

In Begin Experiment Tab:

def createPiece(piece, pos, name):
    return visual.ImageStim(win, image=piece.image, name=name, size=piece.size, pos=pos)

def drawPicked(picked):
    for each in picked:
        each.draw()

def movePicked(picked, mouse, grabbed):
    if grabbed is not None and mouse.isPressedIn(grabbed):
        grabbed.pos = mouse.getPos()
        return grabbed
    else:
        for piece in picked:
            if mouse.isPressedIn(piece) and grabbed is None:
                return piece



def createGrid(rows, size, pos, names):
    inc = (size/rows)
    rowStart = pos[0] - size/2
    colStart = pos[1] + size/2
    row, col = rowStart  + inc/2, colStart - inc/2
    counter = 0
    
    grid = []
    for i in range(rows):
        for j in range(rows):
            grid.append(visual.Rect(win, name=names[counter], units='height', size = [size/rows, size/rows], pos= [row,col], lineColor= [-1.000,0.184,0.184]))
            row += inc
            counter += 1
        col -= inc
        row = rowStart + inc/2
    return grid

def drawGrid(grid):
    for i in grid:
        i.draw()

def checkAnswer(grid, pieces):
    # Get names of pieces that were picked
    picNames = [pic.image for pic in pieces]
    correctPieces = []
    for cell in grid:
        # Check if piece has been picked
        if cell.name in picNames:
            for name in range(0, len(picNames)):
                if cell.name == picNames[name]:
                    if cell.contains(pieces[name].pos):
                        correctPieces.append(True)
                        break # Piece found, go to next cell
        else:
            return False  # Correct piece not picked
    return len(correctPieces) == len(grid)
    

In Begin Routine Tab:

continueRoutine = True
pieces = [p1a, p2a, p3a, p4a, p5a, p6a]
answers = [a1,a2,a3,a4,a5,a6,a7,a8,a9]
picked = []
newPiece = None
movingPiece = None
grid = createGrid(nRows1, size, polygon.pos, answers)
polygon.setFillColor(None)

In Each Frame Tab:

for piece in pieces:
    if mouse.isPressedIn(piece) and newPiece == None:
        newPiece = createPiece(piece, mouse.getPos(), piece.image)
        picked.append(newPiece)
        
    
if newPiece is not None and mouse.getPressed()[0] == 0:
    newPiece = None

movingPiece = movePicked(picked, mouse, movingPiece)
drawGrid(grid)
drawPicked(picked)

JS in Begin Experiment Tab:

createPiece = function(piece, pos, name){
  return new visual.ImageStim({win : psychoJS.window,
                                image: piece.image, 
                                name: name,
                                size: piece.size, 
                                pos: pos})
}

drawPicked = function(picked, draw) {
  if (picked.length > 0) {
    for(let each of picked) {
      each.autoDraw = draw;
    }
  }
}

movePicked = function(picked, mouse, grabbed) {
  if (grabbed != 'undefined' &&  mouse.getPressed()[0] === 1) {
    grabbed.pos = mouse.getPos();
    return grabbed
  } else {
      for (let piece of picked) {
        if (piece.contains(mouse) &&  mouse.getPressed()[0] === 1 && grabbed === 'undefined'){
          piece.pos = mouse.getPos();
          return piece;
        }
      }
   return 'undefined'
  }
}

createGrid = function(rows, size, pos, names) {
    var inc = (size/rows);
    var rowStart = pos[0] - size/2;
    var colStart = pos[1] + size/2;
    var row = rowStart  + inc/2;
    var col = colStart - inc/2;
    var counter = 0;
    var grid = [];
    
    for (let i = 0; i < rows; i++) {
        for (let j = 0; j < rows; j++) {
            grid.push(new visual.Rect({win : psychoJS.window,
                                        name: names[counter], 
                                        units: 'height',
                                        lineColor: new util.Color([0,0,0]),
                                        size: [size/rows, size/rows], 
                                        pos: [row,col]}))
            row += inc
            counter += 1
        }
        col -= inc
        row = rowStart + inc/2
    }
    return grid
}


drawGrid = function(grid, draw) {
    for (let i of grid) {
        i.autoDraw = draw;
    }
}

checkAnswer = function(grid, pieces) {
    var picNames = pieces.map((pic) => pic.name)
    var correctPieces = []
    for (let cell of grid) {
        if (picNames.includes(cell.name)) {
            for (let name = 0; name < picNames.length; name++) {
                if (cell.name === picNames[name]) {
                    if (cell.contains(pieces[name])) {
                        correctPieces.push(true)
                        break
                    }
                }
            }
        } else {
            return false
        }
    }
    return correctPieces.length === grid.length
} 

picNameDict = {p1a: "pcetl.png",
               p2a: "pcetr.png", 
               p3a: "pcered.png",
               p4a: "pcebl.png",
               p5a: "pcebr.png",
               p6a: "pcewte.png",
               p1b: "pcetl.png",
               p2b: "pcetr.png", 
               p3b: "pcered.png",
               p4b: "pcebl.png",
               p5b: "pcebr.png",
               p6b: "pcewte.png",
               }

In Begin Routine Tab:

pieces = [p1a, p2a, p3a, p4a, p5a, p6a]
answers = [a1,a2,a3,a4,a5,a6,a7,a8,a9]
picked = []
newPiece = 'undefined'
movingPiece = 'undefined'
grid = createGrid(nRows1, size, polygon.pos, answers)

In Each Frame Tab:

for (let piece of pieces) {
    if (piece.contains(mouse) && mouse.getPressed()[0] === 1 && newPiece === 'undefined') {
        newPiece = createPiece(piece, mouse.getPos(), picNameDict[piece.name])
        picked.push(newPiece)
    }
}
        
    
if (newPiece !== 'undefined' && mouse.getPressed()[0] === 0) {
    newPiece = 'undefined'
}

movingPiece = movePicked(picked, mouse, movingPiece)
drawGrid(grid, true)
drawPicked(picked, true)

if (t > .5 && nextButton.contains(mouse) && mouse.getPressed()[0] === 1) {
  continueRoutine = false
}

In End Routine Tab:

piece = drawPicked(picked, false, piece)
drawGrid(grid, false)
nextButton.setAutoDraw(false)

Good Luck!