no_trig_rotation.cpp 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. #include "common/sketchbook.hpp"
  2. // you know what projection is right? you better do cause i sure don't
  3. constexpr
  4. float2 project(float2 x, float2 surface)
  5. {
  6. return surface * surface(x)
  7. / surface.magnitude(); // can skip this, then angles will also scale (like complex numbers)
  8. }
  9. // rejection is the other part
  10. // projection + rejection = original vector
  11. constexpr
  12. float2 reject(float2 x, float2 surface)
  13. {
  14. return x - project(x,surface);
  15. }
  16. // reflection simply negates the rejection
  17. constexpr
  18. float2 reflect(float2 x, float2 surface)
  19. {
  20. return project(x,surface) - reject(x,surface);
  21. }
  22. // rotation is just two reflections
  23. constexpr
  24. float2 rotate(float2 x, float2 half_angle)
  25. {
  26. return reflect( reflect(x, float2::i()), half_angle );
  27. }
  28. constexpr
  29. float2 normalize(float2 x)
  30. {
  31. // return x / x.quadrance(); // fun!
  32. return x / support::root2(x.quadrance());
  33. }
  34. // the tricky part is specifying angles as vectors, so here are some approximations that can help
  35. template <size_t Exponent = 3, typename Value = float>
  36. class protractor
  37. {
  38. public:
  39. using vector = geom::vector<Value,2>;
  40. // one approach is to approximate the circle with a regular polygon
  41. // and then linearly interpolate on that polygon to get angles
  42. //
  43. // we actually just need to approximate a half circle, since
  44. // our rotation doubles angles
  45. // we'll generate the half circle polygon
  46. // by starting with a flat(tau/2) angle and repeatedly bisecting it
  47. // the number of section will always be a power of two
  48. // since bisection is essentially multiplying them by two
  49. // so our number of iteration/precision parameter is the exponent
  50. using array = std::array<vector, (size_t(1)<<Exponent) + 1>;
  51. static constexpr array circle = []()
  52. {
  53. using support::halfway;
  54. array points{};
  55. // end points of half polygon/circle
  56. points.front() = vector::i();
  57. points.back() = -vector::i();
  58. auto bisect = [](auto begin, auto end,
  59. auto& self) // ugly recursive lambda trick -_-
  60. {
  61. // can't squeeze a new value between two adjacent elements,
  62. // so the array must be full already
  63. if(end - begin <= 2)
  64. return;
  65. // otherwise
  66. // we're going to put the new bisector in the middle of provided range
  67. auto middle = halfway(begin, end);
  68. // the bisector itself lies halfway between the two points,
  69. // since both are on circle.
  70. // need to normalize it to stay on the circle for subsequent bisections
  71. *(middle) = normalize(halfway(*begin, *(end - 1)));
  72. // do the same for the two bisected parts
  73. self(begin, middle + 1,
  74. self);
  75. self(middle, end,
  76. self);
  77. };
  78. if(Exponent > 0)
  79. {
  80. // have to handle the case of first bisection separately,
  81. // since can't normalize vector 0 :/
  82. auto middle = halfway(points.begin(), points.end());
  83. *middle = vector::j();
  84. bisect(points.begin(), middle + 1,
  85. bisect);
  86. bisect(middle, points.end(),
  87. bisect);
  88. }
  89. return points;
  90. }();
  91. // fraction of full circle
  92. static constexpr vector tau(Value factor)
  93. {
  94. assert(Value{0} <= factor && factor < Value{1});
  95. Value index = factor * (protractor::circle.size() - 1);
  96. int whole = index;
  97. Value fraction = index - whole;
  98. return way(circle[whole], circle[whole+1], fraction);
  99. // linear interpolation more or less works thanks to normalizing/dividing projection
  100. // otherwise this method is no good
  101. }
  102. // full circle
  103. static constexpr vector tau()
  104. {
  105. return -vector::i();
  106. }
  107. // small angle in radians
  108. static constexpr vector small_radian(Value value)
  109. {
  110. return {support::root2(Value{1} - value*value), value};
  111. // cause sine of small angle is pretty much equal to that angle
  112. }
  113. // any angle in radians, not constexpr -_-
  114. static vector radian(Value angle)
  115. {
  116. // using trigonometric function, very optional
  117. return {std::cos(angle), std::sin(angle)};
  118. }
  119. // decides for you weather to use tau() or small_radian()
  120. // is not necessarily smart
  121. static vector angle(Value tau_factor)
  122. {
  123. const static auto tau = std::acos(-1) * 2;
  124. if(tau_factor < (Value{0.5f}/(circle.size()-1))/10 )
  125. return small_radian(tau_factor * tau);
  126. else
  127. return protractor::tau(tau_factor);
  128. }
  129. };
  130. float2 angle = float2::one(1.f);
  131. float regular_angle = 0.f;
  132. void start(Program& program)
  133. {
  134. program.draw_loop = [&](auto frame, auto delta)
  135. {
  136. frame.begin_sketch()
  137. .rectangle(rect{frame.size})
  138. .fill(rgb::white(0))
  139. ;
  140. constexpr auto half = float2::one(0.5);
  141. // the outline of expected path (circle)
  142. frame.begin_sketch()
  143. .ellipse(anchored_rect2f{
  144. float2::one(300), frame.size/2, half
  145. })
  146. .outline(0xff00ff_rgb)
  147. ;
  148. // draw rotated point (should follow the circle)
  149. frame.begin_sketch()
  150. .ellipse(anchored_rect2f{
  151. float2::one(10),
  152. frame.size/2 + rotate(float2::j(150), angle),
  153. float2::one(0.5f)
  154. })
  155. .fill(0xff00ff_rgb);
  156. ;
  157. // draw the angle itself and a unit circle
  158. frame.begin_sketch()
  159. .line(frame.size/2, frame.size/2 + angle * 50)
  160. .ellipse(anchored_rect2f{
  161. float2::one(100), frame.size/2, half
  162. })
  163. .outline(0xffff00_rgb)
  164. ;
  165. // draw the protractor polygon
  166. // smaller radius than unit circle just to not overlap
  167. { auto sketch = frame.begin_sketch();
  168. for(size_t i = 1; i < protractor<>::circle.size(); ++i)
  169. {
  170. sketch.line(frame.size/2 + protractor<>::circle[i-1]*45, frame.size/2 + protractor<>::circle[i] * 45);
  171. }
  172. sketch.outline(0x00ffff_rgb);
  173. }
  174. // ijkl just free-form move the endpoint of the angle
  175. if(pressed(scancode::j))
  176. angle.x() -= 1.f * delta.count();
  177. if(pressed(scancode::l))
  178. angle.x() += 1.f * delta.count();
  179. if(pressed(scancode::k))
  180. angle.y() += 1.f * delta.count();
  181. if(pressed(scancode::i))
  182. angle.y() -= 1.f * delta.count();
  183. // left and right set the angle using the elaborate polygon-lerp scheme
  184. auto move_regular_angle = [&](float direction)
  185. {
  186. regular_angle += direction * delta.count() * 0.1f;
  187. regular_angle = support::wrap(regular_angle,1.f);
  188. angle = protractor<>::tau(regular_angle);
  189. };
  190. if(pressed(scancode::right))
  191. move_regular_angle(1);
  192. if(pressed(scancode::left))
  193. move_regular_angle(-1);
  194. // a and d use small radian approximation
  195. // we rotate the angle itself by another small angle
  196. if(pressed(scancode::a))
  197. angle = rotate(angle, protractor<>::small_radian(delta.count() * 0.1f));
  198. if(pressed(scancode::d))
  199. angle = rotate(angle, protractor<>::small_radian(delta.count() * -0.1f));
  200. };
  201. }