Example #1
0
    def trial_prep(self):
        # Grab locations (and their cardinal labels) for each T & D
        self.T_prime_loc = list(self.prime_locs[self.prime_target])
        self.D_prime_loc = list(self.prime_locs[self.prime_distractor])
        self.T_probe_loc = list(self.probe_locs[self.probe_target])
        self.D_probe_loc = list(self.probe_locs[self.probe_distractor])

        # Grab distance between each item pair
        self.T_prime_to_T_probe = line_segment_len(self.T_prime_loc[0],
                                                   self.T_probe_loc[0])
        self.T_prime_to_D_probe = line_segment_len(self.T_prime_loc[0],
                                                   self.D_probe_loc[0])
        self.D_prime_to_T_probe = line_segment_len(self.D_prime_loc[0],
                                                   self.T_probe_loc[0])
        self.D_prime_to_D_probe = line_segment_len(self.D_prime_loc[0],
                                                   self.D_probe_loc[0])

        # Once locations selected, determine which trial type this trial would fall under.
        self.trial_type = self.determine_trial_type()

        # Hide mouse cursor throughout trial
        hide_mouse_cursor()

        # Present fixation & start trial
        self.present_fixation()
Example #2
0
    def __generate_location__(self):
        if self.final_disc:
            n_back = self.exp.disc_locations[self.exp.n_back_index]
            penultimate = self.exp.disc_locations[-1]
            angle = self.exp.angle
            amplitude = int(
                line_segment_len(n_back.x_y_pos, penultimate.x_y_pos))
            self.rotation = angle_between(penultimate.x_y_pos, n_back.x_y_pos)
            if self.using_secondary_pos:
                self.secondary_angle = angle - 180
                if self.secondary_angle < 0:
                    self.secondary_angle = angle + 180
                self.secondary_x_y_pos = point_pos(self.origin, amplitude,
                                                   self.secondary_angle,
                                                   self.rotation)
        else:
            amplitude = randrange(self.exp.min_amplitude,
                                  self.exp.max_amplitude)
            angle = randrange(0, 360)
        self.x_y_pos = point_pos(self.origin, amplitude, angle, self.rotation)
        # ensure disc is inside drawable bounds; if penultimate saccade, ensure all final saccade angles are possible
        self.__margin_check()
        self.__penultimate_viability_check__()

        # assign generation output
        self.angle = angle
        self.amplitude = amplitude
        self.__add_eyelink_boundary__()
Example #3
0
def linear_transitions(start, end, velocity, fps=60):
	"""Generates transition points along a given line for animating at a constant velocity.
	"""

	duration = line_segment_len(start, end) / float(velocity)
	steps = int(duration / (1000.0 / fps))
	transitions = [t / float(steps - 1) for t in range(steps)]

	return transitions
Example #4
0
    def path_length(self):
        """float: The full length of the figure in pixels.
		"""
        length = 0
        for curve, points in self.raw_segments:
            if curve:
                p1, p2, ctrl = points
                length += bezier_length(p1, ctrl, p2)
            else:
                p1, p2 = points
                length += line_segment_len(p1, p2)

        return length
Example #5
0
def linear_transitions_by_dist(start, end, dist_per_frame, offset=0):
	"""Generates transition points along a given line for animating at a constant velocity,
	moving at a constant distance (in pixels) per frame.

	Unlike the regular linear_transitions function, this does not guarantee that the endpoint
	of the curve (transition = 1.0) is included in the returned list, opting instead to match
	the provided speed (dist_per_frame) as closely as possible. Additionally, a starting offset
	can be specified defining the distance along the curve that the first transition should be.
	"""

	dist = float(line_segment_len(start, end))
	frames = int(round((dist - offset) / float(dist_per_frame), 8))
	transitions = [(offset + f * dist_per_frame) / dist for f in range(frames + 1)]

	return transitions
Example #6
0
 def __penultimate_viability_check__(self):
     if not self.penultimate_disc:
         return
     d_xy = line_segment_len(
         self.x_y_pos,
         self.exp.disc_locations[self.exp.n_back_index].x_y_pos)
     disc_diam = self.exp.search_disc_proto.surface_width
     if d_xy - disc_diam < disc_diam:
         raise ValueError("Penultimate target too close to n-back target.")
     theta = angle_between(
         self.x_y_pos,
         self.exp.disc_locations[self.exp.n_back_index].x_y_pos)
     for a in range(0, 360, 60):
         self.__margin_check(point_pos(self.x_y_pos, d_xy, a + theta))
     self.exp.search_disc_proto.fill = self.exp.penultimate_disc_color
     self.penultimate_disc = True
Example #7
0
    def within(self, p):
        """Determines whether a given point is within the boundary.

		Args:
			p (:obj:`Tuple` or :obj:`List`): The (x, y) coordinates of the point to
				check against the boundary.
		
		Returns:
			bool: True if the point falls within the boundary, otherwise False.
		
		Raises:
			ValueError: If the given point is not a valid set of (x, y) coordinates.
		
		"""
        if not valid_coords(p):
            raise ValueError(
                "The given value must be a valid set of (x, y) coordinates.")

        return line_segment_len(p, self.center) <= self.radius
Example #8
0
    def __generate_linear_segment(self, p1, p2, prev_seg=None):

        if prev_seg and prev_seg[0] == False:
            p_prev = prev_seg[1][0]
            #  if the angle is too acute, try to shift p2 a way from prev_seg until it's ok
            seg_angle = acute_angle(p1, p_prev, p2)
            if seg_angle < self.min_lin_ang_width:
                print(seg_angle, self.min_lin_ang_width, p1, p_prev, p2)
                p2 = list(p2)

                a_p1_prev = angle_between(p1, p_prev)
                a_prev_p2 = angle_between(p_prev, p2, a_p1_prev)
                len_prev_p2 = line_segment_len(p_prev, p2)
                p2 = point_pos(p_prev, len_prev_p2 + 1, a_prev_p2, a_p1_prev)
                if p2[0] < 0 or p2[0] > P.screen_x or p2[1] < 0 or p2[
                        1] > P.screen_y:
                    raise TrialException(
                        "No appropriate angle can be generated.")
                else:
                    return p2

        return [False, (p1, p2)]
Example #9
0
    def segments_to_frames(self, segments, duration, fps=60):
        """Converts linear/bezier segments comprising a shape into a list of (x, y) pixel
		coordinates representing the frames of the shape animation at the given velocity.

		Args:
			segments (list): A list of linear and/or bezier segments generated by the
				__generate_linear_segment and __generate_curved_segment functions.
			duration (float): The duration the tracing motion in milliseconds.
			fps (float, optional): The frame rate at which to render the frames.
		"""

        total_frames = int(round(duration / (1000.0 / fps)))
        dist_per_frame = self.path_length / total_frames

        offset = 0
        fig_frames = []
        for curve, points in segments:

            if curve:
                start, end, ctrl = points
                dist = bezier_length(start, ctrl, end)
                transitions = bezier_transitions_by_dist(
                    start, ctrl, end, dist_per_frame, offset)
                fig_frames += bezier_interpolation(start, end, ctrl,
                                                   transitions)
            else:
                start, end = points
                dist = line_segment_len(start, end)
                transitions = linear_transitions_by_dist(
                    start, end, dist_per_frame, offset)
                fig_frames += linear_interpolation(start, end, transitions)

            frames = len(transitions) - 1
            offset = (frames + 1) * dist_per_frame - (dist - offset)

        return fig_frames
Example #10
0
    def __render_frames__(self):
        total_frames = 0
        asset_frames = []
        num_static_directives = 0
        img_drctvs = []
        try:
            # strip out audio track if there is one, first
            for d in self.directives:

                for key in ['start', 'end']:
                    if key in d.keys() and is_string(d[key]):
                        eval_statement = re.match(
                            re.compile(u"^EVAL:[ ]*(.*)$"), d[key])
                        d[key] = eval(eval_statement.group(1))

                try:
                    asset = self.assets[d.asset].contents
                except KeyError:
                    e_msg = "Asset '{0}' not found in KeyFrame.assets.".format(
                        d.asset)
                    raise KeyError(e_msg)

                if self.assets[d.asset].is_audio:
                    if self.audio_track is not None:
                        raise RuntimeError(
                            "Only one audio track per key frame can be set.")
                    else:
                        self.audio_track = self.assets[d.asset].contents
                        self.audio_start_time = d.start * 0.001
                else:
                    # Scale pixel values from 1920x1080 to current screen resolution
                    d.start = scale(d.start, (1920, 1080))
                    d.end = scale(d.end, (1920, 1080))
                    if "control" in d.keys():
                        d.control = scale(d.control, (1920, 1080))
                    img_drctvs.append(d)
                    if d.start == d.end:
                        num_static_directives += 1

            if len(img_drctvs) == num_static_directives:
                self.asset_frames = [[(self.assets[d.asset].contents, d.start,
                                       d.registration) for d in img_drctvs]]
                return

            for d in img_drctvs:

                for key in ['start', 'end']:
                    if is_string(d[key]):
                        eval_statement = re.match(
                            re.compile(u"^EVAL:[ ]*(.*)$"), d[key])
                        d[key] = eval(eval_statement.group(1))

                asset = self.assets[d.asset].contents
                if d.start == d.end:
                    asset_frames.append([(asset, d.start, d.registration)])
                    continue

                frames = []
                if "control" in d.keys():  # if bezier curve
                    bounds = bezier_bounds(d.start, d.control, d.end)
                    if not all([self.screen_bounds.within(p) for p in bounds]):
                        txt = "KeyFrame {0} does not fit in drawable area and will not be rendered."
                        cso("<red>\tWarning: {0}</red>".format(
                            txt.format(self.label)))
                        continue
                    fps = P.refresh_rate
                    path_len = bezier_length(d.start, d.control, d.end)
                    vel = path_len / (self.duration * 1000.0)
                    transitions = bezier_transitions(d.start, d.control, d.end,
                                                     vel, fps)
                    raw_frames = bezier_interpolation(d.start, d.end,
                                                      d.control, transitions)
                else:  # if not a bezier curve, it's aline
                    try:
                        vel = line_segment_len(
                            d.start, d.end) / (self.duration * 1000.0)
                    except TypeError:
                        raise ValueError(
                            "Image assets require their 'start' and 'end' attributes to be an x,y pair."
                        )
                    transitions = linear_transitions(d.start,
                                                     d.end,
                                                     vel,
                                                     fps=P.refresh_rate)
                    raw_frames = linear_interpolation(d.start, d.end,
                                                      transitions)

                for p in raw_frames:
                    frames.append([asset, p, d.registration])
                if len(frames) > total_frames:
                    total_frames = len(frames)
                asset_frames.append(frames)

            for frame_set in asset_frames:
                while len(frame_set) < total_frames:
                    frame_set.append(frame_set[-1])

            self.asset_frames = []
            if total_frames > 1:
                for i in range(0, total_frames):
                    self.asset_frames.append([n[i] for n in asset_frames])
            else:
                self.asset_frames = asset_frames

        except (IndexError, AttributeError, TypeError) as e:
            err = (
                "An error occurred when rendering this frame."
                "This is usually do an unexpected return from an 'EVAL:' entry in the JSON script."
                "The error occurred in keyframe {0} and the last attempted directive was:"
            )
            print(err.format(self.label))
            print("\nThe original error was:\n")
            traceback.print_exception(*sys.exc_info())
            raise e
Example #11
0
    def __generate_curved_segment(self, p1, p2):

        # Single letters here mean:	r = rotation, c = control, p = point, a = angle, v = vector
        # NOTE: plenty of weirdness in this code that means figures not generated exactly as
        # specified by the controls in params.py, but since this is the way TraceLab has worked up
        # until now I'm not going to fix any of it for fear of inconsistency.

        # reference p is the closer of p1, p2 to the bottom-right screen corner (???) for the
        # purposes of determining direction and angle (NOTE: original comment said screen center,
        # so this is probably unexpected behaviour)
        p_ref = p1
        if line_segment_len(p1, P.screen_x_y) > line_segment_len(
                p2, P.screen_x_y):
            p_ref = p2

        # gets the radial rotation between p1 and p2
        r = angle_between(p_ref, p2 if p_ref == p1 else p1)

        # decides radial direction from p1->p2, clockwise or counter, from which curve will extend
        c_spin = choice([True, False])
        if P.verbose_mode and self.allow_verbosity:
            print("p_ref: {0}, r: {1}, c_spin: {2}".format(p_ref, r, c_spin))

        # find linear distance between p1 and p2
        d_p1p2 = line_segment_len(p1, p2)
        if P.verbose_mode and self.allow_verbosity:
            print("seg_line_len: {0}, p1: {1}, p2: {2}".format(
                d_p1p2, str(p1), str(p2)))

        # next lines decide location of the perpendicular extension from control point and p1->p2,
        # ensuring shift not always away from p_ref
        c_base_shift = uniform(P.peak_shift[0], P.peak_shift[1]) * d_p1p2
        c_base_amp = c_base_shift if choice([1, 0]) else d_p1p2 - c_base_shift
        p_c_base = point_pos(p_ref, c_base_amp, r)
        if P.verbose_mode and self.allow_verbosity:
            print("c_base_amp: {0}, p_c_base: {1}".format(
                c_base_amp, p_c_base))

        # the closer of p1, p2 to p_c_base will be p_c_ref when determining p_c_min
        if c_base_amp > 0.5 * d_p1p2:
            p_c_ref = p2 if p_ref == p1 else p1
        else:
            p_c_ref = p2 if p_ref == p2 else p1

        # choose an angle, deviating from 90° by some random value, for p_c_ref -> p_c_base -> p_c
        sheer = uniform(P.curve_sheer[0], P.curve_sheer[1]) * 90
        a_c = 90 + sheer if choice([0, 1]) else 90 - sheer
        if P.verbose_mode and self.allow_verbosity:
            txt = "curve_sheer: {3}, c_angle_max: {0}, c_angle_min: {1}, c_angle: {2}"
            print(
                txt.format(P.curve_sheer[0] * 90, P.curve_sheer[1] * 90, a_c,
                           sheer))

        # get the range of x,y values for p_c
        v_c_base = [p_c_base, a_c, r, c_spin]  # ie. p_c_base as origin
        v_c_min = [p_c_ref, self.curve_min_slope, r,
                   not c_spin]  # ie. p_c_ref as origin
        v_c_max = [p_c_ref, self.curve_max_slope, r,
                   not c_spin]  # ie. p_c_ref as origin
        p_c_min = [int(i) for i in linear_intersection(v_c_min, v_c_base)]
        p_c_max = [int(i) for i in linear_intersection(v_c_max, v_c_base)]
        if P.verbose_mode and self.allow_verbosity:
            txt = "v_c_min: {0}, v_c_max: {1}, p_c_min: {2}, p_c_max: {3}"
            print(txt.format(v_c_min, v_c_max, p_c_min, p_c_max))

        # choose an initial p_c; no guarantee x, y values in p_c_min are less than p_c_max
        min_x, max_x = sorted([p_c_min[0], p_c_max[0]])
        min_y, max_y = sorted([p_c_min[1], p_c_max[1]])
        p_c_x = min_x if min_x == max_x else randrange(min_x, max_x)
        p_c_y = min_y if min_y == max_y else randrange(min_y, max_y)
        p_c = (p_c_x, p_c_y)

        # Make sure the generated bezier curve doesn't go off the screen, adjusting if necessary
        v_c_b_len = line_segment_len(p_c_ref, p_c)
        v_c_b_increment = -1
        cmx, cmy = (P.curve_margin_h, P.curve_margin_v)
        screen_bounds = RectangleBoundary(' ', (cmx, cmy),
                                          (P.screen_x - cmx, P.screen_y - cmy))
        prev_err = 0
        failures = 0
        segment = False
        while not segment:

            bounds = bezier_bounds(p1, p_c, p2)
            if all([screen_bounds.within(p) for p in bounds]):
                segment = True

            else:
                # After initial pass, check if bezier adjustment making bounds closer
                # or further from being fully on-screen. If getting worse, change direction
                # of the adjustment.
                failures += 1
                err_x1, err_x2 = (cmx - bounds[0][0],
                                  bounds[1][0] - (P.screen_x - cmx))
                err_y1, err_y2 = (cmy - bounds[0][1],
                                  bounds[1][1] - (P.screen_y - cmy))
                err = max(err_x1, err_x2, err_y1, err_y2)

                if failures > 2:
                    if err > prev_err:
                        if v_c_b_increment < 0:
                            v_c_b_increment = 1
                            v_c_b_len += 1
                        else:
                            raise RuntimeError(
                                "Unable to adjust curve to fit on screen.")
                    elif failures == 3:
                        # If error getting smaller after initial shift and decrement, use rate of
                        # decrease in boundary error per decrease in v_c_b_len to estimate what the
                        # v_c_b_len for for an error of 0 should be and jump to that.
                        v_c_b_len += v_c_b_increment * int(err /
                                                           (prev_err - err))
                        v_c_b_len -= v_c_b_increment

                # NOTE: this very likely doesn't work as intended; completely changes curve
                # instead of adjusting to fit within screen
                v_c_b_len += v_c_b_increment
                p_c = point_pos(p_c_base,
                                v_c_b_len,
                                a_c,
                                r,
                                c_spin,
                                return_int=False)
                prev_err = err

            if (P.verbose_mode and self.allow_verbosity):
                msg = "Curve {0}, ".format(
                    "succeeded" if segment else "failed")
                pts = "p1:{0}, p2: {1}, p_c: {2}, v_c_b_len: {3}".format(
                    p1, p2, p_c, v_c_b_len)
                print(msg + pts)
                if not segment:
                    print("Curve bounds: p1 = {0}, p2 = {1}".format(
                        bounds[0], bounds[1]))

        return [True, (p1, p2, p_c)]