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()
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__()
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
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
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
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
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
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)]
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
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
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)]