usage() -> 'camera: scarpet app. ------------------- "/camera start" - Set the starting point, resetting the path "/camera add " - Add a point to the end, secs later "/camera prepend " - Prepend the start secs before "/camera clear" - Remove entire path "/camera select" - .. a point or just punch it, to select it "/camera place_player - Move player to the selected point "/camera move" - Move the selected point to players location "/camera duration " - Set new selected path duration "/camera split_point" - Split selected path in half. "/camera delete_point" - Remove current key point "/camera trim_path" - Remove all key points from selected up "/camera save_as " "/camera load " - Store and load paths from world saves /scripts folder "/camera interpolation < linear | gauss | catmull_rom | gauss >" Select interpolation between points: - catmull_rom: Catmull-Rom interpolation (default). smooth path that goes through all points. - linear: straight paths between points. - gauss: automatic smooth transitions. - gauss : custom fixed variance (in seconds) for special effects. gauss makes the smoothest path, but treating points as suggestions only "/camera repeat " - Repeat existing points configuration n-times using seconds to link path ends "/camera stretch " - Change length of the entire path from 25 -> 4x faster, to 400 -> 4x slower, "/camera transpose" - Move entire path with the start at players position. "/camera play : Run the path with the player use "sneak" to stop it prematurely run /camera hide and F1 to clear the view "/camera show": Show current path particles color of particles used is different for different players "/camera hide": Hide path display "/camera prefer_smooth_play": Eat CPU spikes and continue as usual "/camera prefer_synced_play": After CPU spikes jump to where you should be '; __config() -> { 'commands' -> { '' -> _() -> print(usage()), '' -> '_call', 'add ' -> 'add', 'prepend ' -> 'prepend', 'duration ' -> 'duration', 'save_as ' -> 'save_as', 'load ' -> 'load', 'interpolation ' -> ['interpolation', null, true], 'interpolation gauss' -> ['interpolation', 'gauss', null, true], 'interpolation gauss ' -> _(float) -> interpolation('gauss', float, true), 'repeat ' -> 'repeat', 'stretch ' -> 'stretch' }, 'arguments'->{ 'seconds'->{'type'->'float', 'min' -> 0.01, 'suggest'->[]}, 'last_delay'->{'type'->'float','min' -> 0.01, 'suggest'->[]}, 'name'->{'type'->'string','suggest'->[]}, 'interpolation'->{'type'->'term','options'->['linear','catmull_rom']}, 'factor'->{'type'->'int','min'->25,'max'->400}, 'command'->{'type'->'term','options'->[ 'start', 'clear', 'select', 'place_player', 'move', 'split_point', 'delete_point', 'trim_path', 'transpose', 'play', 'show', 'hide', 'prefer_smooth_play', 'prefer_synced_play' ]} } }; _call(command)->call(command); global_points = null; global_dimension = null; global_player = null; global_player_eye_offset = map(range(5), 0); global_showing_path = false; global_playing_path = false; global_needs_updating = false; global_selected_point = null; global_markers = null; global_particle_density = 100; global_color_a = null; global_color_b = null; global_path_precalculated = null; // starts the path with current player location start() -> _start_with( _() -> [ [_camera_position(), 0, 'sharp'] ] ); // start path with customized initial points selection _start_with(points_supplier) -> ( p = player(); global_player = str(p); global_player_eye_offset = [0, p~'eye_height',0,0,0]; global_dimension = p~'dimension'; code = abs(hash_code(str(p))-123456); global_color_a = str('dust %.1f %.1f %.1f 1',(code%10)/10,(((code/10)%10)/10), (((code/100)%10)/10) ); // 'dust 0.1 0.9 0.1 1', global_color_b = str('dust %.1f %.1f %.1f 1',(((code/1000)%10)/10),(((code/10000)%10)/10), (((code/100000)%10)/10) );// 'dust 0.6 0.6 0.6 1' global_points = call(points_supplier); global_selected_point = null; _update(); show(); print(str('Started path at %.1f %.1f %.1f', p~'x', p~'y', p~'z')); ); // gets current player controlling the path, or fails _get_player() -> ( if (!global_player, exit('No player selected')); p = player(global_player); if (p == null, exit('Player logged out')); p; ); // clears current path clear() -> ( global_points = []; global_dimension = null; global_player = null; global_selected_point = null; global_showing_path = false; global_playing_path = false; _update(); print('Path cleared'); ); // camera position for the player sticks out of their eyes _camera_position() -> (_get_player()~'location' + global_player_eye_offset); // path changed, now notify all processes that they need to update; _update() -> ( global_path_precalculated = null; global_playing_path = false; global_needs_updating = true; ); // ensures that the current execution context can modify the path _assert_can_modify_path() -> ( if (!global_points, exit('Path is not setup for current player')); if (!global_showing_path, exit('Turn on path showing to edit key points')); if(!global_player || !global_dimension || _get_player()~'dimension' != global_dimension, exit('Player not in dimension')); ); // make sure selected point is correct _assert_point_selected(validator) -> ( _assert_can_modify_path(); if (!call(validator, global_selected_point), exit('No appropriate point selected')); ); //select a custom interpolation method interpolation(method, option, verbose) -> ( _prepare_path_if_needed() -> _prepare_path_if_needed_generic(); // each supported method needs to specify its _find_position_for_point to trace the path // accepting segment number, and position in the segment // or optionally _prepare_path_if_needed, if path is inefficient to compute point by point global_interpolator = if ( method == 'linear', '_interpolator_linear', method == 'catmull_rom', '_interpolator_cr', method == 'gauss' && option == null, _(s, p) -> _interpolator_gauB(s, p, 0), method == 'gauss', variance = round(60*option); _(s, p, outer(variance)) -> _interpolator_gauB(s, p, variance); ); _update(); if(verbose, print('Interpolation changed to '+method + if(option, ' '+option, ''))); ); interpolation('catmull_rom', null, false); // adds a point to the end of the path with delay in seconds add(delay) -> ( _assert_can_modify_path(); //mode is currently unused, run_path does always sharp, gauss interpolator is always smooth // but this option could be used could be used at some point by more robust interpolators _add_path_segment(_camera_position(), round(60*delay), 'smooth', true); _update(); print(_get_path_size_string()); ); // prepends the path with a new starting point, with a segment of specified delay prepend(delay) -> ( _assert_can_modify_path(); _add_path_segment(_camera_position(), round(60*delay), 'smooth', false); _update(); print(_get_path_size_string()); ); // repeats existing points seveal times, using last section delay (seconds) to join points repeat(times, last_section_delay) -> ( if (err = _is_not_valid_for_motion(), exit(err)); positions = map(global_points, _:0); modes = map(global_points, _:(-1)); durations = map(global_points, global_points:(_i+1):1 - _:1 ); durations:(-1) = round(60*last_section_delay); loop(times, loop( length(positions), _add_path_segment(copy(positions:_), durations:_, modes:_, true) ) ); _update(); print(_get_path_size_string()); ); //stretches or shrinks current path to X percent of what it was before stretch(percentage) -> ( if (err = _is_not_valid_for_motion(), exit(err)); ratio = percentage/100; previous_path_length = global_points:(-1):1; for(global_points, _:1 = _:1*ratio ); _update(); print(str('path %s from %.2f to %.2f seconds', if(ratio<1,'shortened','extended'), previous_path_length/60, global_points:(-1):1/60 )) ); // moves current selected point to player location move() -> ( _assert_point_selected(_(p) -> p != null); new_position = _camera_position(); new_position:(-2) = __adjusted_rot( global_points:global_selected_point:0:(-2), new_position:(-2) ); global_points:global_selected_point:0 = new_position; _update(); ); // chenges duration of the current selected segment to X seconds duration(amount) -> ( _assert_point_selected(_(p) -> p); // skips nulls and 0 - starting point duration = number(amount); new_ticks = round(duration * 60); if (new_ticks < 10, return()); previous_ticks = global_points:global_selected_point:1-global_points:(global_selected_point-1):1; delta = new_ticks - previous_ticks; // adjust duration of all points after that. for (range(global_selected_point, length(global_points)), global_points:_:1 += delta; ); _update(); print(_get_path_size_string()); ); // deletes current keypoint without changing the path length delete_point() -> ( _assert_point_selected(_(p) -> p != null); if (length(global_points) < 2, clear(); return()); if (global_selected_point == 0, global_points:1:1 = 0); global_points = filter(global_points, _i != global_selected_point); if (global_selected_point >= length(global_points), global_selected_point = null); _update(); print(_get_path_size_string()); ); // splits current selected segment in half by adding a keypoint in between split_point() -> ( _assert_point_selected(_(p) -> p); // skips nulls and 0 - starting point current_time = global_points:global_selected_point:1; previous_time = global_points:(global_selected_point-1):1; segment_duration = current_time-previous_time; put( global_points, global_selected_point, [ _get_path_at(global_selected_point-1, previous_time, segment_duration/2), previous_time+segment_duration/2, global_points:global_selected_point:2 ], 'insert' ); _update(); print(_get_path_size_string()); ); // removes all points in the path from the current point trim_path() -> ( _assert_point_selected(_(p) -> p != null); global_points = slice(global_points, 0, global_selected_point); global_selected_point = null; _update(); print(_get_path_size_string()); ); // moves entire camera path keeping the angles to player position being in the starting point transpose() -> ( _assert_can_modify_path(); shift = pos(_get_player())-slice(global_points:0:0, 0, 3); shift += 0; shift += 0; shift = shift + global_player_eye_offset; for(global_points, _:0 = _:0 + shift); _update(); print(_get_path_size_string()); ); // selects either a point of certain number (starting from 1), or closest point select(num) -> ( if (!global_points, return()); if (!global_showing_path, return()); p = _get_player(); num = (num+1000*(length(global_points)+1)) % (length(global_points)+1); selected_point = if (num, num-1, _closest_point_to_center( p~'pos'+[0, p~'eye_height', 0], map(global_points, slice(_:0, 0, 3)) ) ); global_selected_point = if (global_selected_point == selected_point, null, selected_point); global_needs_updating = true; ); // player can also punch the mannequin to select/deselect it __on_player_attacks_entity(p, e) -> ( if (e~'type' == 'armor_stand' && query(e, 'has_tag', '__scarpet_marker_camera') && global_markers, for (global_markers, if(_==e, global_selected_point = if (global_selected_point == _i, null, _i); global_needs_updating = true; return(); ) ); ); ); // adds new segment to the path _add_path_segment(vector, duration, mode, append) -> ( if ( (['sharp','smooth'] ~ mode) == null, exit('use smooth or sharp point')); [v, segment_time, m] = global_points:(if(append, -1, 0)); vector:(-2) = __adjusted_rot(v:(-2), vector:(-2)); if (append, global_points += [vector, segment_time+duration, mode]; , new_points = [[vector, 0, mode]]; for (global_points, _:1 += duration; new_points += _; ); global_points = new_points; ); null; ); // adjusts current rotation so we don't spin around like crazy __adjusted_rot(previous_rot, current_rot) -> ( while( abs(previous_rot-current_rot) > 180, 1000, current_rot += if(previous_rot < current_rot, -360, 360) ); current_rot ); // returns current path size blurb _get_path_size_string() -> ( if (!_is_not_valid_for_motion(), str('%d points, %.1f secs', length(global_points), global_points:(-1):1/60); , 'Path too small to run'; ); ); // checks if the current path is valid for motion _is_not_valid_for_motion() -> ( if(!global_points, return('Path not defined yet')); if(length(global_points)<2, return('Path not complete - add more points')); if(!global_dimension || _get_player()~'dimension' != global_dimension, return('Wrong dimension')); false ); // grabs position of a player for a given segment, which segment starts at start point, and offset by index points _get_path_at(segment, start, index) -> ( v = global_path_precalculated:(start+index); if(v == null, v = call(global_interpolator, segment, index); global_path_precalculated:(start+index) = v ); v ); // squared euclidean distance between two points _distsq(vec1, vec2) -> reduce(vec1 - vec2, _a + _*_, 0); //finds index of the closest point from a list to the center point _closest_point_to_center(center, points) -> ( reduce(points, d = _distsq(_, center); if( d ( _get_path_size_string(); if (global_showing_path, return ()); global_showing_path = true; global_needs_updating= false; _create_markers() -> ( map(global_points || [], is_selected = global_selected_point != null && _i == global_selected_point; caption = if (_i == 0, '1: Start', str('%d: %.1fs', _i+1, global_points:_i:1/60); ); if (is_selected && _i > 0, caption += str(' (%.1fs current segment)', (global_points:_i:1 - global_points:(_i-1):1)/60); ); marker = create_marker(caption, _:0, 'observer'); if (is_selected, modify(marker,'effect','glowing',72000, 0, false, false); ); marker ); ); __show_path_tick() -> ( if (_is_not_valid_for_motion(), return()); _prepare_path_if_needed(); loop(global_particle_density, segment = floor(rand(length(global_points)-1)); particle_type = if ((segment+1) == global_selected_point, global_color_a, global_color_b); //'dust 0.1 0.9 0.1 1', 'dust 0.6 0.6 0.6 1'); start = global_points:segment:1; end = global_points:(segment+1):1; index = floor(rand(end-start)); [x, y, z] = slice(_get_path_at(segment, start, index), 0, 3); particle(particle_type, x, y, z, 1, 0, 0) ); null ); task( _() -> ( global_markers = _create_markers(); on_close = ( _() -> ( for(global_markers, modify(_,'remove')); global_markers = null; global_showing_path = false; )); loop(7200, if(!global_showing_path, break()); _get_player(); if (global_needs_updating, global_needs_updating = false; for(global_markers, modify(_,'remove')); global_markers = _create_markers(); ); __show_path_tick(); sleep(100, call(on_close)); ); call(on_close); )); null; ); // hides path display hide() -> ( if (global_showing_path, global_showing_path = false; ); ); // runs the player on the path global_prefer_sync = false; prefer_smooth_play() -> (global_prefer_sync = false; 'Smooth path play'); prefer_synced_play() -> (global_prefer_sync = true; 'Synchronized path play'); play() -> ( if (err = _is_not_valid_for_motion(), exit(err)); _prepare_path_if_needed(); if (!_get_player() || _get_player()~'dimension' != global_dimension, exit('No player in dimension')); task( _() -> ( if (global_playing_path, // we don't want to join_task not to lock it just in case. No need to panic here global_playing_path = false; // cancel current path rendering sleep(1500); ); showing_path = global_showing_path; hide(); sleep(1000); sound('ui.button.click', pos(_get_player()), 8, 1); // to synchro with other clips sleep(1000); // so particles can discipate global_playing_path = true; mspt = 1000 / 60; start_time = time(); very_start = start_time; point = 0; p = _get_player(); try ( loop( length(global_points)-1, segment = _; start = global_points:segment:1; end = global_points:(segment+1):1; loop(end-start, if (p~'sneaking', global_playing_path = false); if (!global_playing_path, throw()); v = _get_path_at(segment, start, _)-global_player_eye_offset; modify(p, 'location', v); point += 1; end_time = time(); sleep(); if (global_prefer_sync, should_be = very_start + mspt*point; if (end_time < should_be, sleep(should_be-end_time) ) , took = end_time - start_time; if (took < mspt, sleep(mspt-took)); start_time = time() ) ) ); ); sleep(1000); global_playing_path = false; if (showing_path, show()); )); null; ); // moves player to a selected camera position place_player() -> ( _assert_point_selected(_(p) -> p != null); modify(_get_player(), 'location', global_points:global_selected_point:0 - global_player_eye_offset); ); // prepares empty path to fit new points _prepare_path_if_needed_generic() -> ( if(!global_path_precalculated, global_path_precalculated = map(range(global_points:(-1):1), null)) ); // linear interpolator _interpolator_linear(segment, point) -> ( [va, start, mode_a] = global_points:segment; [vb, end, mode_b] = global_points:(segment+1); section = end-start; dt = point/section; dt*vb+(1-dt)*va ); // normal distribution should look like that //(1/sqrt(2*pi*d*d))*euler^(-((x-miu)^2)/(2*d*d)) // but we will be normalizing anyways, so who cares _norm_prob(x, miu, d) -> euler^(-((x-miu)^2)/(2*d*d)); //gauB interpolator _interpolator_gauB(from_index, point, deviation) -> ( components = []; path_point = global_points:from_index:1; try( for(range(from_index+1, length(global_points)), [v,ptime,mode] = global_points:_; dev = if (deviation > 0, deviation, devs = []; if (_+1 < length(global_points), devs += global_points:(_+1):1-ptime); if (_-1 >= 0, devs += ptime-global_points:(_-1):1); 0.6*reduce(devs, _a+_, 0)/length(devs) ); impact = _norm_prob(path_point+point, ptime, dev); //if(rtotal && impact < 0.000001*rtotal, throw()); // can work badly on segments with vastly diff lengths components += [v, impact]; rtotal += impact ) ); try( for(range(from_index, -1, -1), [v,ptime,mode] = global_points:_; dev = if (deviation > 0, deviation, devs = []; if (_+1 < length(global_points), devs += global_points:(_+1):1-ptime); if (_-1 >= 0, devs += ptime-global_points:(_-1):1); 0.6*reduce(devs, _a+_, 0)/length(devs) ); impact = _norm_prob(path_point+point, ptime, dev); //if(ltotal && impact < 0.000001*ltotal, throw()); components += [v, impact]; ltotal += impact ) ); total = rtotal+ltotal; reduce(components, _a+_:0*(_:1/total), [0,0,0,0,0]) ); // Catmull-Rom spline _interpolator_cr(from_index, point) -> ( total = global_points:(from_index+1):1 - global_points:from_index:1; p__1 = global_points:(if(from_index == 0, 0, from_index-1)):0; p_0 = global_points:from_index:0; p_1 = global_points:(from_index+1):0; p_2 = global_points:(if(from_index == (length(global_points)-2), -1, from_index+2)):0; r = point/total; // ratio within segment (r*((2-r)*r-1) * p__1 + (r*r*(3*r-5)+2) * p_0 + r*((4 - 3*r)*r + 1) * p_1 + (r-1)*r*r * p_2) / 2 ); // store current path in a world file save_as(file) -> ( if (!global_points, exit('No path to save')); path_nbt = nbt('{}'); for (global_points, point_nbt = nbt('{}'); point_nbt:'duration' = _:1; point_nbt:'type' = _:2; for(_:0, put(point_nbt:'pos',str('%.6fd',_),_i)); // need to print to float string //otherwise mojang will interpret 0.0d as 0i and fail to insert put(path_nbt:'points', point_nbt, _i); ); write_file(file, 'nbt', path_nbt); print('stored path as '+file); ); // loads path under the local file that load(file) -> ( path_nbt = read_file(file, 'nbt'); if (!path_nbt, exit('No path to load: '+file)); new_points = map(get(path_nbt, 'points[]'), [_:'pos[]', _:'duration', _:'type']); if (!new_points || first(new_points, length(_:0) != 5), exit('Incorrect data for :'+file); ); _start_with(_(outer(new_points)) -> new_points); print('loaded '+file); ); // when closing - shut-down visualization and playback threads __on_close() -> ( global_playing_path = false; global_showing_path = false; );