circular_maze.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. /*
  2. */
  3. #include "common/sketchbook.hpp"
  4. using namespace common;
  5. using support::wrap;
  6. constexpr float2 size(range2f x)
  7. {
  8. return x.upper() - x.lower();
  9. }
  10. [[nodiscard]]
  11. constexpr float2 rotate(float2 v, float angle)
  12. {
  13. return rotate(v, protractor<>::tau(angle));
  14. }
  15. [[nodiscard]]
  16. constexpr float distance(float2 a, float2 b)
  17. {
  18. return support::root2(quadrance(a-b));
  19. }
  20. // ugh! dealing with modulo arithmetic is harder than i thought...
  21. // could be because i chose (0,1) instead of (-1,1)?
  22. // there has got to be a better way to do this regardless.
  23. // maybe just mod/wrap at the last moment and otherwise work in normal arithmetic
  24. // hint; mod_cmp used in both of below,
  25. // fix a, consider b and (b +/- mod),
  26. // calculate diff and abs_diff and chose min by abs_diff
  27. // return both chosen abs_diff and corresponding diff
  28. [[nodiscard]]
  29. constexpr float mod_distance(float a, float b, float mod)
  30. {
  31. auto minmax = std::minmax(a,b);
  32. return std::min( minmax.second - minmax.first, minmax.first + mod - minmax.second);
  33. }
  34. [[nodiscard]]
  35. constexpr float mod_difference(float a, float b, float mod)
  36. {
  37. const auto distance = mod_distance(a,b,mod);
  38. const auto difference = b > a ? +distance : -distance;
  39. return support::abs(b-a) > distance ? -difference : +difference;
  40. }
  41. constexpr float cord_length(float slice_angle, float radius = 1.f)
  42. {
  43. // very clear - self descriptive
  44. // very simple - naive direct implementation
  45. // fast?
  46. // + distance can be replaced with quadrance
  47. return distance(
  48. rotate(float2::i(radius), slice_angle),
  49. float2::i(radius)
  50. );
  51. // // very obscure - need a picture to understand
  52. // // very complicated - need advanced calculus to implement
  53. // // slow?
  54. // return 2 * radius * sin(slice_angle/2 * tau);
  55. };
  56. constexpr range2f fit(float2 area, float2 unit)
  57. {
  58. // N dimensional =)
  59. auto scale = area/unit;
  60. auto min_dimension = support::min_element(scale) - scale.begin();
  61. auto fit_area = scale[min_dimension] * unit;
  62. auto cenering_mask = float2::one() - float2::unit(min_dimension);
  63. return range2f{float2::zero(), fit_area}
  64. + (area - fit_area)/2 * (cenering_mask);
  65. };
  66. class circular_maze
  67. {
  68. int layers = 10;
  69. float2 _screen_size;
  70. float fov;
  71. rangef fov_range;
  72. range2f bounds;
  73. float corridor_radius;
  74. float wall_width;
  75. float initial_radius;
  76. float2 center;
  77. using float_NxM = std::vector<std::vector<float>>;
  78. float_NxM walls;
  79. float_NxM paths;
  80. std::optional<float> closest_wall(int level, float angle)
  81. {
  82. auto closest = support::min_element(walls[level], [&](auto a, auto b)
  83. {
  84. return abs(a - angle) < abs(b - angle);
  85. });
  86. if(closest != walls[level].end())
  87. return *closest;
  88. return std::nullopt;
  89. }
  90. float path_edge_proximity(int level, float angle)
  91. {
  92. float proximity = std::numeric_limits<float>::infinity();
  93. const float radius = corridor_radius/tau/2/(initial_radius + level * corridor_radius);
  94. for(auto&& path : paths[level])
  95. {
  96. proximity = std::min(mod_distance(path + radius, angle, 1.f), proximity);
  97. proximity = std::min(mod_distance(path - radius, angle, 1.f), proximity);
  98. }
  99. return proximity * tau * (level*corridor_radius + initial_radius);
  100. }
  101. float wall_proximity(int level, float angle)
  102. {
  103. auto closest = closest_wall(level, angle);
  104. return closest
  105. ? mod_distance(*closest, angle, 1.f) * tau * (level * corridor_radius + initial_radius)
  106. : std::numeric_limits<float>::infinity();
  107. }
  108. float proximity(int level, float angle)
  109. {
  110. return std::min(
  111. path_edge_proximity(level,angle),
  112. wall_proximity(level,angle)
  113. );
  114. }
  115. public:
  116. float current_angle = 0;
  117. float player_level = -1;
  118. auto get_corridor_radius() const {return corridor_radius;}
  119. circular_maze(float2 screen_size) :
  120. layers(7),
  121. _screen_size(screen_size),
  122. fov(1.f/8),
  123. fov_range{-fov/2,+fov/2},
  124. bounds{fit(screen_size,{cord_length(fov),1.f})},
  125. corridor_radius( size(bounds).y() / (layers+2) ),
  126. wall_width(corridor_radius/6),
  127. initial_radius(corridor_radius * 2),
  128. center{bounds.lower()+(bounds.upper() - bounds.lower()) * float2{0.5f,1.f}}
  129. {
  130. walls.resize(layers);
  131. paths.resize(layers);
  132. for(int i = 0; i < layers * 5; ++i)
  133. {
  134. int level;
  135. float angle;
  136. int breaker = 2000;
  137. do
  138. {
  139. level = trand_int({0,layers});
  140. angle = trand_float();
  141. }
  142. while(proximity(level, angle) < corridor_radius && breaker --> 0);
  143. paths[level].push_back(angle);
  144. }
  145. for(int i = 0; i < layers*layers; ++i)
  146. {
  147. int level;
  148. float angle;
  149. int breaker = 2000;
  150. do
  151. {
  152. level = trand_int({0,layers-1});
  153. angle = trand_float();
  154. }
  155. while((proximity(level, angle) < corridor_radius ||
  156. path_edge_proximity(level + 1, angle) < corridor_radius) && breaker --> 0);
  157. walls[level].push_back(angle);
  158. }
  159. }
  160. const float2& screen_size() { return _screen_size; }
  161. std::optional<float> hit_test(float angle, float level, const float_NxM& elements)
  162. {
  163. if(level < 0 || level >= layers)
  164. return std::nullopt;
  165. for(auto&& element : elements[level])
  166. {
  167. const auto radius = level * corridor_radius + initial_radius;
  168. const auto player_position = -float2::j(radius);
  169. const auto element_position = rotate(float2::i(radius), wrap(angle + element, 1.f));
  170. if(quadrance(player_position - element_position) < corridor_radius * corridor_radius / 4)
  171. return element;
  172. }
  173. return std::nullopt;
  174. }
  175. std::optional<float> wall_hit_test(float angle)
  176. {
  177. return hit_test(angle, player_level, walls);
  178. }
  179. std::optional<float> path_hit_test(float angle, float level, float direction)
  180. {
  181. return hit_test(angle, level + (direction+1)/2, paths);
  182. }
  183. void circular_move(float velocity)
  184. {
  185. const auto radius = player_level * corridor_radius + initial_radius;
  186. const auto corridor_angle = corridor_radius/tau/radius;
  187. const auto max_angular_velocity = corridor_angle*0.8;
  188. float angular_velocity = velocity/tau/radius;
  189. if(abs(angular_velocity) > max_angular_velocity)
  190. angular_velocity = std::copysign(max_angular_velocity, angular_velocity);
  191. auto new_angle = wrap(current_angle + angular_velocity, 1.f);
  192. auto hit_wall = wall_hit_test(new_angle);
  193. if(!hit_wall)
  194. {
  195. current_angle = new_angle;
  196. }
  197. else
  198. {
  199. const auto offset = std::copysign(corridor_angle*0.51f, mod_difference(current_angle + *hit_wall, 3.f/4, 1.f));
  200. current_angle = wrap(3.f/4 - *hit_wall - offset, 1.f);
  201. }
  202. }
  203. void draw(vg::frame& frame)
  204. {
  205. frame.begin_sketch()
  206. .rectangle(rect{ frame.size })
  207. .fill(0x1d4151_rgb)
  208. ;
  209. const auto fov_range_up = fov_range - 1.f/4;
  210. {auto sketch = frame.begin_sketch();
  211. float radius = initial_radius;
  212. for(int i = 0; i < layers; ++i)
  213. {
  214. sketch.arc(center, fov_range_up * tau, radius - corridor_radius/2);
  215. radius += corridor_radius;
  216. }
  217. sketch.line_width(wall_width).outline(0xfbfbf9_rgb);
  218. }
  219. {auto sketch = frame.begin_sketch();
  220. float radius = initial_radius - corridor_radius/2;
  221. for(size_t level = 0; level < paths.size(); ++level, radius += corridor_radius)
  222. {
  223. float path_arc_angle = (corridor_radius * 0.8)/tau/radius;
  224. auto path_arc_range = range{-path_arc_angle, path_arc_angle}/2;
  225. for(size_t angle = 0; angle < paths[level].size(); ++angle)
  226. {
  227. const auto path_angle = wrap(
  228. paths[level][angle] + current_angle,
  229. 1.f);
  230. if(!(fov_range_up + 1.f).intersects(path_arc_range + path_angle))
  231. continue;
  232. // TODO: approximate arc with a polygon so that we don't need to convert to radians,
  233. // here and everywhere
  234. sketch.arc(center, (path_arc_range + path_angle) * tau, radius);
  235. }
  236. } sketch.line_width(wall_width + 3).outline(0x1d4151_rgb); }
  237. float radius = initial_radius - corridor_radius/2;
  238. for(size_t level = 0; level < walls.size(); ++level, radius += corridor_radius)
  239. {
  240. const float wall_arc_angle = wall_width/tau/radius;
  241. const auto wall_arc_range = range{-wall_arc_angle, wall_arc_angle}/2;
  242. for(size_t angle = 0; angle < walls[level].size(); ++angle)
  243. {
  244. const auto wall_angle = wrap(
  245. walls[level][angle] + current_angle,
  246. 1.f);
  247. const auto intersection = (fov_range_up + 1.f).intersection(wall_arc_range + wall_angle);
  248. if(!intersection.valid())
  249. continue;
  250. const auto visible_wall_angle = intersection.upper() - intersection.lower();
  251. const auto visible_wall_width = wall_width * visible_wall_angle/wall_arc_angle;
  252. const auto wall_anchor = intersection.lower() == (fov_range_up + 1.f).lower() ? .5f : -.5f;
  253. // TODO: welp, looks like even here, polygon would be better, to render different widths with one draw call
  254. frame.begin_sketch()
  255. .line(
  256. center + rotate(
  257. float2::i(initial_radius + corridor_radius*(float(level)-0.5f)),
  258. wall_angle + wall_anchor * (wall_arc_angle - visible_wall_angle)
  259. ),
  260. center + rotate(
  261. float2::i(initial_radius + corridor_radius*(float(level)+0.5f)),
  262. wall_angle + wall_anchor * (wall_arc_angle - visible_wall_angle)
  263. )
  264. )
  265. .line_width(visible_wall_width).outline(0xfbfbf9_rgb);
  266. }
  267. }
  268. const auto player_diameter =
  269. corridor_radius - wall_width -3;
  270. const auto player_center =
  271. center - float2::j(player_level * corridor_radius + initial_radius);
  272. frame.begin_sketch().ellipse(rect{
  273. float2::one(player_diameter),
  274. player_center,
  275. half
  276. }).fill(paint::radial_gradient(
  277. player_center,
  278. {player_diameter/2 * .3f, player_diameter/2},
  279. {rgba_vector(0x00feed'ff_rgba), rgba_vector(0x00000000_rgba)}));
  280. };
  281. void diagram(vg::frame& frame)
  282. {
  283. auto fov_range_up = fov_range - 1.f/4;
  284. auto sketch = frame.begin_sketch();
  285. float radius = initial_radius;
  286. for(int i = 0; i < layers; ++i)
  287. {
  288. sketch.arc(center, fov_range_up * tau, radius);
  289. radius += corridor_radius;
  290. }
  291. for(size_t level = 0; level < walls.size(); ++level)
  292. {
  293. for(size_t angle = 0; angle < walls[level].size(); ++angle)
  294. {
  295. const auto wall_angle = wrap(
  296. walls[level][angle] + current_angle,
  297. 1.f);
  298. if(!(fov_range_up + 1.f).contains(wall_angle))
  299. continue;
  300. sketch.ellipse(rect{
  301. float2::one(4),
  302. center +
  303. rotate(
  304. float2::i(initial_radius + corridor_radius*level),
  305. wall_angle
  306. ),
  307. half
  308. });
  309. }
  310. }
  311. for(size_t level = 0; level < paths.size(); ++level)
  312. {
  313. for(size_t angle = 0; angle < paths[level].size(); ++angle)
  314. {
  315. const auto path_angle = wrap(
  316. paths[level][angle] + current_angle,
  317. 1.f);
  318. if(!(fov_range_up + 1.f).contains(path_angle))
  319. continue;
  320. sketch.line(
  321. center + rotate(
  322. float2::i(initial_radius + corridor_radius*level),
  323. path_angle
  324. ),
  325. center + rotate(
  326. float2::i(initial_radius + corridor_radius*(float(level)-1)),
  327. path_angle
  328. )
  329. );
  330. }
  331. }
  332. sketch.ellipse(rect{
  333. float2::one(corridor_radius),
  334. center - float2::j(player_level * corridor_radius + initial_radius),
  335. half
  336. });
  337. sketch.line_width(1).outline(0x0_rgb);
  338. }
  339. } maze(float2::one(400));
  340. using radial_motion_t = movement<float, motion::quadratic_curve>;
  341. using circular_motion_t = movement<float, motion::quadratic_curve>;
  342. melody<circular_motion_t, radial_motion_t>
  343. complex_radial_motion;
  344. radial_motion_t simple_radial_motion;
  345. struct radial_movement
  346. {
  347. float path = 0;
  348. float level = 0;
  349. };
  350. std::queue<radial_movement> radial_movements;
  351. void make_radial_movement(float direction)
  352. {
  353. auto level = !empty(radial_movements) ? radial_movements.back().level : maze.player_level;
  354. auto angle = !empty(radial_movements)
  355. ? wrap(3/4.f - radial_movements.back().path, 1.f)
  356. : maze.current_angle;
  357. auto path = maze.path_hit_test(angle, level, direction);
  358. if(path)
  359. radial_movements.push({*path, level + direction});
  360. }
  361. bool diagram = false;
  362. std::optional<float2> drag = std::nullopt;
  363. std::optional<float2> jerk = std::nullopt;
  364. float2 possible_jerk = float2::zero();
  365. float circular_velocity = 0.f;
  366. void start(Program& program)
  367. {
  368. program.fullscreen = true;
  369. program.draw_once = [](auto frame)
  370. {
  371. maze = circular_maze(frame.size);
  372. };
  373. program.key_down = [](scancode code, keycode)
  374. {
  375. switch(code)
  376. {
  377. case scancode::j:
  378. case scancode::down:
  379. make_radial_movement(-1);
  380. break;
  381. case scancode::k:
  382. case scancode::up:
  383. make_radial_movement(+1);
  384. break;
  385. case scancode::d:
  386. diagram = !diagram;
  387. break;
  388. default: break;
  389. }
  390. };
  391. program.mouse_down = [](float2, auto)
  392. {
  393. drag = float2::zero();
  394. circular_velocity = 0;
  395. possible_jerk = float2::zero();
  396. };
  397. program.mouse_up = [](auto, auto)
  398. {
  399. drag = std::nullopt;
  400. jerk = std::nullopt;
  401. };
  402. program.mouse_move = [](auto, float2 motion)
  403. {
  404. if(drag)
  405. {
  406. *drag += motion;
  407. if(!jerk)
  408. {
  409. // TODO: use mouse_motion::window_normalized_motion
  410. possible_jerk += motion / (maze.get_corridor_radius()/3);
  411. if(trunc(possible_jerk) != float2::zero())
  412. jerk = signum(trunc(possible_jerk));
  413. }
  414. }
  415. };
  416. program.draw_loop = [](auto frame, auto delta)
  417. {
  418. maze.draw(frame);
  419. if(diagram)
  420. maze.diagram(frame);
  421. if(!complex_radial_motion.done())
  422. {
  423. float unwrapped_angle = maze.current_angle;
  424. auto result = complex_radial_motion.move(std::forward_as_tuple(
  425. unwrapped_angle,
  426. maze.player_level)
  427. , delta);
  428. maze.current_angle = wrap(unwrapped_angle, 1.f);
  429. if(result.done)
  430. {
  431. radial_movements.pop();
  432. circular_velocity = 0;
  433. }
  434. }
  435. else if(!simple_radial_motion.done())
  436. {
  437. auto result = simple_radial_motion.move(maze.player_level, delta);
  438. if(result.done)
  439. radial_movements.pop();
  440. }
  441. else
  442. {
  443. if(!empty(radial_movements))
  444. {
  445. auto movement = radial_movements.front();
  446. auto circular_distance = mod_difference(maze.current_angle, wrap(3/4.f - movement.path, 1.f), 1.f);
  447. auto radial_distance = movement.level - maze.player_level;
  448. auto radial_motion = radial_motion_t
  449. {
  450. abs(radial_distance) * 100ms,
  451. maze.player_level, movement.level
  452. };
  453. if(abs(circular_distance) > 0)
  454. {
  455. complex_radial_motion = melody(
  456. circular_motion_t{ abs(circular_distance) * 10s,
  457. maze.current_angle,
  458. maze.current_angle + circular_distance },
  459. radial_motion
  460. );
  461. }
  462. else
  463. simple_radial_motion = radial_motion;
  464. }
  465. if(pressed(scancode::h) || pressed(scancode::left))
  466. {
  467. maze.circular_move(1);
  468. circular_velocity = 1;
  469. }
  470. if(pressed(scancode::l) || pressed(scancode::right))
  471. {
  472. maze.circular_move(-1);
  473. circular_velocity = -1;
  474. }
  475. if(drag)
  476. {
  477. circular_velocity += drag->x();
  478. maze.circular_move(drag->x()/3);
  479. if(jerk && (*jerk * float2::j() != float2::zero()))
  480. {
  481. make_radial_movement(-jerk->y());
  482. jerk = float2::zero(); // prevents further jerks on this drag TODO: should probably add some kind of a timer on this, to eventually reset back to nullopt
  483. }
  484. drag = float2::zero();
  485. }
  486. else
  487. {
  488. maze.circular_move(circular_velocity/3);
  489. }
  490. circular_velocity *= 0.8f;
  491. }
  492. };
  493. }