circular_maze.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  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 initial_radius;
  75. float2 center;
  76. using float_NxN = std::vector<std::vector<float>>;
  77. float_NxN walls;
  78. float_NxN paths;
  79. std::optional<float> closest_wall(int level, float angle)
  80. {
  81. auto closest = support::min_element(walls[level], [&](auto a, auto b)
  82. {
  83. return abs(a - angle) < abs(b - angle);
  84. });
  85. if(closest != walls[level].end())
  86. return *closest;
  87. return std::nullopt;
  88. }
  89. float path_edge_proximity(int level, float angle)
  90. {
  91. float proximity = std::numeric_limits<float>::infinity();
  92. const float radius = corridor_radius/tau/2/(initial_radius + level * corridor_radius);
  93. for(auto&& path : paths[level])
  94. {
  95. proximity = std::min(mod_distance(path + radius, angle, 1.f), proximity);
  96. proximity = std::min(mod_distance(path - radius, angle, 1.f), proximity);
  97. }
  98. return proximity * tau * (level*corridor_radius + initial_radius);
  99. }
  100. float wall_proximity(int level, float angle)
  101. {
  102. auto closest = closest_wall(level, angle);
  103. return closest
  104. ? mod_distance(*closest, angle, 1.f) * tau * (level * corridor_radius + initial_radius)
  105. : std::numeric_limits<float>::infinity();
  106. }
  107. float proximity(int level, float angle)
  108. {
  109. return std::min(
  110. path_edge_proximity(level,angle),
  111. wall_proximity(level,angle)
  112. );
  113. }
  114. public:
  115. float current_angle = 0;
  116. float player_level = -1;
  117. circular_maze(float2 screen_size) :
  118. layers(7),
  119. _screen_size(screen_size),
  120. fov(1.f/8),
  121. fov_range{-fov/2,+fov/2},
  122. bounds{fit(screen_size,{cord_length(fov),1.f})},
  123. corridor_radius( size(bounds).y() / (layers+2) ),
  124. initial_radius(corridor_radius * 2),
  125. center{bounds.lower()+(bounds.upper() - bounds.lower()) * float2{0.5f,1.f}}
  126. {
  127. walls.resize(layers);
  128. paths.resize(layers);
  129. for(int i = 0; i < layers * 5; ++i)
  130. {
  131. int level;
  132. float angle;
  133. int breaker = 2000;
  134. do
  135. {
  136. level = trand_int({0,layers});
  137. angle = trand_float();
  138. }
  139. while(proximity(level, angle) < corridor_radius && breaker --> 0);
  140. paths[level].push_back(angle);
  141. }
  142. for(int i = 0; i < layers*layers; ++i)
  143. {
  144. int level;
  145. float angle;
  146. int breaker = 2000;
  147. do
  148. {
  149. level = trand_int({0,layers-1});
  150. angle = trand_float();
  151. }
  152. while((proximity(level, angle) < corridor_radius ||
  153. path_edge_proximity(level + 1, angle) < corridor_radius) && breaker --> 0);
  154. walls[level].push_back(angle);
  155. }
  156. }
  157. const float2& screen_size() { return _screen_size; }
  158. float hit_test(float angle, float level, const float_NxN& elements)
  159. {
  160. if(level < 0 || level >= layers)
  161. return -1;
  162. for(auto&& element : elements[level])
  163. {
  164. const auto radius = level * corridor_radius + initial_radius;
  165. const auto player_position = -float2::j(radius);
  166. const auto element_position = rotate(float2::i(radius), wrap(angle + element, 1.f));
  167. if(quadrance(player_position - element_position) < corridor_radius * corridor_radius / 4)
  168. return element;
  169. }
  170. return -1;
  171. }
  172. bool wall_hit_test(float angle)
  173. {
  174. return -1 != hit_test(angle, player_level, walls);
  175. }
  176. float path_hit_test_up(float angle)
  177. {
  178. return hit_test(angle, player_level + 1, paths);
  179. }
  180. float path_hit_test_up(float angle, float level)
  181. {
  182. return hit_test(angle, level + 1, paths);
  183. }
  184. float path_hit_test_down(float angle)
  185. {
  186. return hit_test(angle, player_level, paths);
  187. }
  188. float path_hit_test_down(float angle, float level)
  189. {
  190. return hit_test(angle, level, paths);
  191. }
  192. void draw(vg::frame& frame)
  193. {
  194. frame.begin_sketch()
  195. .rectangle(rect{ frame.size })
  196. .fill(0x1d4151_rgb)
  197. ;
  198. auto fov_range_up = fov_range - 1.f/4;
  199. constexpr float wall_wdith = 6;
  200. {auto sketch = frame.begin_sketch();
  201. float radius = initial_radius;
  202. for(int i = 0; i < layers; ++i)
  203. {
  204. sketch.arc(center, fov_range_up * tau, radius - corridor_radius/2);
  205. radius += corridor_radius;
  206. }
  207. sketch.line_width(wall_wdith).outline(0xfbfbf9_rgb);
  208. }
  209. {auto sketch = frame.begin_sketch();
  210. float radius = initial_radius;
  211. for(size_t level = 0; level < paths.size(); ++level)
  212. {
  213. float path_arc_angle = corridor_radius/tau/radius;
  214. auto path_arc_range = range{-path_arc_angle, path_arc_angle}/2;
  215. for(size_t angle = 0; angle < paths[level].size(); ++angle)
  216. {
  217. const auto path_angle = wrap(
  218. paths[level][angle] + current_angle,
  219. 1.f);
  220. if(!(fov_range_up + 1.f).intersects(path_arc_range + path_angle))
  221. continue;
  222. sketch.arc(center, (path_arc_range + path_angle) * tau, radius - corridor_radius/2);
  223. }
  224. radius += corridor_radius;
  225. } sketch.line_width(wall_wdith + 3).outline(0x1d4151_rgb); }
  226. {auto sketch = frame.begin_sketch();
  227. for(size_t level = 0; level < walls.size(); ++level)
  228. {
  229. for(size_t angle = 0; angle < walls[level].size(); ++angle)
  230. {
  231. const auto wall_angle = wrap(
  232. walls[level][angle] + current_angle,
  233. 1.f);
  234. if(!(fov_range_up + 1.f).contains(wall_angle))
  235. continue;
  236. sketch.line(
  237. center + rotate(
  238. float2::i(initial_radius + corridor_radius*(float(level)-0.5f)),
  239. wall_angle
  240. ),
  241. center + rotate(
  242. float2::i(initial_radius + corridor_radius*(float(level)+0.5f)),
  243. wall_angle
  244. )
  245. );
  246. }
  247. } sketch.line_width(wall_wdith).outline(0xfbfbf9_rgb); }
  248. frame.begin_sketch().ellipse(rect{
  249. float2::one(corridor_radius - wall_wdith -3),
  250. center - float2::j(player_level * corridor_radius + initial_radius),
  251. half
  252. }).fill(0x00feed_rgb);
  253. };
  254. void diagram(vg::frame& frame)
  255. {
  256. auto fov_range_up = fov_range - 1.f/4;
  257. auto sketch = frame.begin_sketch();
  258. float radius = initial_radius;
  259. for(int i = 0; i < layers; ++i)
  260. {
  261. sketch.arc(center, fov_range_up * tau, radius);
  262. radius += corridor_radius;
  263. }
  264. for(size_t level = 0; level < walls.size(); ++level)
  265. {
  266. for(size_t angle = 0; angle < walls[level].size(); ++angle)
  267. {
  268. const auto wall_angle = wrap(
  269. walls[level][angle] + current_angle,
  270. 1.f);
  271. if(!(fov_range_up + 1.f).contains(wall_angle))
  272. continue;
  273. sketch.ellipse(rect{
  274. float2::one(4),
  275. center +
  276. rotate(
  277. float2::i(initial_radius + corridor_radius*level),
  278. wall_angle
  279. ),
  280. half
  281. });
  282. }
  283. }
  284. for(size_t level = 0; level < paths.size(); ++level)
  285. {
  286. for(size_t angle = 0; angle < paths[level].size(); ++angle)
  287. {
  288. const auto path_angle = wrap(
  289. paths[level][angle] + current_angle,
  290. 1.f);
  291. if(!(fov_range_up + 1.f).contains(path_angle))
  292. continue;
  293. sketch.line(
  294. center + rotate(
  295. float2::i(initial_radius + corridor_radius*level),
  296. path_angle
  297. ),
  298. center + rotate(
  299. float2::i(initial_radius + corridor_radius*(float(level)-1)),
  300. path_angle
  301. )
  302. );
  303. }
  304. }
  305. sketch.ellipse(rect{
  306. float2::one(corridor_radius),
  307. center - float2::j(player_level * corridor_radius + initial_radius),
  308. half
  309. });
  310. sketch.line_width(1).outline(0x0_rgb);
  311. }
  312. } maze(float2::one(400));
  313. using radial_motion_t = motion<float, quadratic_curve>;
  314. using circular_motion_t = motion<float, quadratic_curve>;
  315. symphony<circular_motion_t, radial_motion_t>
  316. radial_motion;
  317. float target_player_level = maze.player_level;
  318. float upway = -1;
  319. float downway = -1;
  320. bool diagram = false;
  321. void start(Program& program)
  322. {
  323. program.size = int2(maze.screen_size());
  324. program.key_down = [](scancode code, keycode)
  325. {
  326. switch(code)
  327. {
  328. case scancode::j:
  329. downway = maze.path_hit_test_down(maze.current_angle, target_player_level);
  330. if(downway > 0)
  331. target_player_level -= 1;
  332. break;
  333. case scancode::k:
  334. upway = maze.path_hit_test_up(maze.current_angle, target_player_level);
  335. if(upway > 0)
  336. target_player_level += 1;
  337. break;
  338. case scancode::d:
  339. diagram = !diagram;
  340. break;
  341. default: break;
  342. }
  343. };
  344. // program.mouse_down = [](float2 position, auto)
  345. // {
  346. // };
  347. //
  348. // program.mouse_up = [](auto, auto)
  349. // {
  350. // };
  351. //
  352. // program.mouse_move = [](auto, float2 motion)
  353. // {
  354. // };
  355. program.draw_loop = [](auto frame, auto delta)
  356. {
  357. maze.draw(frame);
  358. if(diagram)
  359. maze.diagram(frame);
  360. if(!radial_motion.done())
  361. {
  362. float unwrapped_angle = maze.current_angle;
  363. radial_motion.move(std::forward_as_tuple(
  364. unwrapped_angle,
  365. maze.player_level)
  366. , delta);
  367. maze.current_angle = wrap(unwrapped_angle, 1.f);
  368. }
  369. else
  370. {
  371. if(target_player_level != maze.player_level)
  372. {
  373. auto way = target_player_level > maze.player_level ? upway : downway;
  374. auto circular_distance = mod_difference(maze.current_angle, wrap(3/4.f - way, 1.f), 1.f);
  375. auto radial_distance = target_player_level - maze.player_level;
  376. radial_motion = symphony(
  377. circular_motion_t{
  378. maze.current_angle,
  379. maze.current_angle + circular_distance,
  380. abs(circular_distance) * 10s},
  381. radial_motion_t{
  382. maze.player_level,
  383. target_player_level,
  384. abs(radial_distance) * 100ms}
  385. );
  386. }
  387. if(pressed(scancode::h))
  388. {
  389. auto new_angle = wrap(maze.current_angle + 0.001f, 1.f);
  390. if(!maze.wall_hit_test(new_angle))
  391. maze.current_angle = new_angle;
  392. }
  393. if(pressed(scancode::l))
  394. {
  395. auto new_angle = wrap(maze.current_angle - 0.001f, 1.f);
  396. if(!maze.wall_hit_test(new_angle))
  397. maze.current_angle = new_angle;
  398. }
  399. }
  400. };
  401. }