/* +-------------------------------------------------------+ ____/ \____ /| Open source game framework licensed to freely use, | \ / / | copy, and modify, created for dank.game | +--\ ^__^ /--+ | | | ~/ \~ | | Download at https://open.shampoo.ooo/shampoo/spacebox | | ~~~~~~~~~~~~ | +-------------------------------------------------------+ | SPACE ~~~~~ | / | ~~~~~~~ BOX |/ +-------------*/ #pragma once #include #include #include "Model.hpp" #include "Switch.hpp" #include "math.hpp" namespace sb { /*! * A Pad is an object containing an sb::Plane which can be clicked to launch an arbitrary user function. It can be sized and placed * by setting its translation and scale values. * * Each instance: * * - Has an sb::Plane or derivative (either a custom one provided, or a default constructed one automatically) * - Has an arbitrary response function for when pressed * - Can collide with a point (for example, mouse click) * * Example: * * glm::vec3 w = glm::mat3({{1, 0, 0}, {0, 1, 0}, {-0.6739, -0.74, 1}}) * glm::mat3({{.1, 0, 0}, {0, .1 * (460.0 / 768.0), 0}, \ * {0, 0, 1}}) * glm::vec3({-1, -1, 1}); * std::cout << w << std::endl << glm::translate(glm::vec3{-0.6739, -0.74, 0}) * * glm::scale(glm::vec3{.1, .1 * (460.0 / 768.0), 1}) * glm::vec4{-1, -1, 0, 1} << std::endl; * Pad p {background.current(), {-0.6739f, -0.74f}, 0.1f, get_display().window_box().aspect(), std::function()}; * const std::vector& p_position = *p.attributes("position"); * glm::vec4 final_position = p.transformation() * glm::vec4{p_position[2].x, p_position[2].y, 0, 1}; * std::cout << p.transformation() << std::endl << final_position << std::endl; * assert(final_position == glm::vec4({w.x, w.y, 0, 1})); */ template class Pad { private: using Reaction = std::function; sb::Switch connection; sb::Plane _plane; Box _box; int texture_index = 0; bool _enabled = true, _visible = true; public: /*! * Construct a Pad object with a default constructed sb::Plane. * * @see Pad(sb::Plane, const glm::vec2&, float, float, Reaction, float) */ Pad(const glm::vec2& translation = {0.0f, 0.0f}, float scale = 1.0f, float ratio = 1.0f, Reaction on_state_change = Reaction(), float rotation = 0.0f) : Pad(sb::Plane(), translation, scale, ratio, on_state_change, rotation) {} /*! * Construct a pad object from an sb::Plane, a translation amount, a scale factor, and a reaction function. The translation is * relative to (0.0, 0.0), and the scale is relative to the plane object. * * The reaction function must accept a boolean as its first argument, which will be given the state of the pad object's switch. * * The plane object will be copied into the pad, so any edits must be made before constructing the pad, unless there is a Pad * function that applies the edit, such as Pad::scale(float, float). * * @param plane plane or plane derivative to represent the pad visually * @param translation x, y amount to translate the position * @param scale amount to scale both x and y * @param ratio ratio to adjust scale of x and y * @param on_state_change reaction function which accepts a boolean as its first argument * @param rotation angle in radians to rotate the pad */ Pad(const sb::Plane& plane, glm::vec2 translation = {0.0f, 0.0f}, float scale = 1.0f, float ratio = 1.0f, Reaction on_state_change = Reaction(), float rotation = 0.0f) : _plane(plane) { _box.size({2.0f, 2.0f}, true); if (translation != glm::vec2{0.0f, 0.0f}) { this->translate(translation); } if (scale != 1.0f || ratio != 1.0f) { this->scale(scale, ratio); } if (rotation) { this->rotate(rotation); } this->on_state_change(on_state_change); } /*! * Assign a new plane to the pad. The existing plane will be replaced. * * @param plane plane or plane derivative to represent the pad visually */ void plane(const sb::Plane& plane) { _plane = plane; } /*! * @return Constant reference to the pad's plane object */ const sb::Plane& plane() const { return _plane; } /*! * @return Constant reference to the pad's box object */ const sb::Box& box() const { return _box; } /*! * Rotate the pad around its center by 90 degrees. If a count is given, rotate by 90 degrees that many times, so for example, * a count of 3 will be a 270 degree rotation. If the count is negative, rotate -90 degrees. * * @param count number of 90 degree rotations to make, or use a negative count to rotate -90 degrees */ void rotate(int count = 1) { _plane.transform(glm::rotate(count * glm::half_pi(), glm::vec3{0.0f, 0.0f, 1.0f})); } /*! * Scale using a factor and ratio that will transform the pad in the X and Y dimensions. The ratio determines how much each axis * is scaled. If the ratio is above one, the X-axis's scale will be divided by the ratio. If the ratio is below one, the Y-axis's * scale will be multiplied by the aspect ratio. If the aspect ratio of the window is given, this will force the pad to display as * a square, and the ratio will be relative to the shorter axis. * * The collision box will be scaled by the same factors. * * @param factor amount to scale in both x and y directions * @param ratio amount to adjust scaling, set to the window aspect ratio to make the pad appear square */ const glm::mat4& scale(float factor, float ratio = 1.0f) { glm::vec3 scale { factor, factor, 1.0f }; if (ratio > 1.0f) { scale.x /= ratio; } else if (ratio < 1.0f) { scale.y *= ratio; } _box.size({2.0f, 2.0f}, true); _box.scale({scale.x, scale.y}, true); return _plane.scale(scale); } /*! * Set the pad's location in the X and Y dimension using a 2D vector. The collision box will be moved by the same translation. * * @param translation x, y distance to translate the pad */ const glm::mat4& translate(const glm::vec2& translation) { _box.center({0.0f, 0.0f}); _box.move(translation); return _plane.translate({translation.x, translation.y, 0.0f}); } /*! * Set the function that will run when a pad object is clicked. * * This example always keeps the state false and prints "Hello, World!" whenever the pad is clicked, * * start_button.on_state_change([&](bool state, int count){ * if (state) { * std::ostringstream message; * message << "Hello, " << state << " World! " << count; * sb::Log::log(message); * start_button.press(1); * }}); * * @param on_state_change reaction function which accepts a boolean as its first argument */ void on_state_change(Reaction reaction) { connection.on_state_change(reaction); } /*! * Returns true if the point at given NDC coordinates collides with the pad's collision box. This only works properly if the * Pad is flat in the z-dimension. * * @param position x, y NDC coordinates to check for collision * @return true if the point is inside Pad::box, false otherwise */ bool collide(const glm::vec2& position, const glm::mat4& view, const glm::mat4& projection) const { /* Corners of the box */ std::vector corners; /* Transform each of the corners into NDC coordinates */ for (const glm::vec2& vertex : {_box.sw(), _box.nw(), _box.ne(), _box.se()}) { corners.push_back(sb::world_to_ndc(vertex, projection * view)); } /* Create a new box using the NDC bottom-left corner */ glm::vec2 control = corners[0]; float width = glm::distance(corners[3], corners[0]); float height = glm::distance(corners[1], corners[0]); sb::Box transformed {control, {width, height}}; /* Collide the NDC box with the caller's coordinates */ return transformed.collide(position); } /*! * Set the transformation uniform, bind a texture if any are attached, and draw the vertices. Build the full transformation matrix * by combining the Pad object's transformation with the supplied projection and view matrices, and pass it to the shader. * * @param transformation_uniform transformation uniform ID * @param view the view matrix for transforming from world space to camera space * @param projection projection matrix for transforming from camera space to clip space * @param texture_flag_uniform uniform ID for boolean enabling or disabling texture display */ void draw(GLuint transformation_uniform, glm::mat4 view, glm::mat4 projection, std::optional texture_flag_uniform = std::nullopt) { if (_visible) { if (!_plane.textures().empty()) { if (texture_flag_uniform.has_value()) { glUniform1i(texture_flag_uniform.value(), true); } /* Determine texture index by checking the state of the pad and the amount of available textures. If there is more * than 1 texture, the texture will correspond with the state. */ if (connection && _plane.textures().size() > 1) { texture_index = 1; } else { texture_index = 0; } _plane.texture(texture_index).bind(); } else if (texture_flag_uniform.has_value()) { glUniform1i(texture_flag_uniform.value(), false); } glm::mat4 mvp = projection * view * _plane.transformation(); glUniformMatrix4fv(transformation_uniform, 1, GL_FALSE, &mvp[0][0]); _plane.enable(); glDrawArrays(GL_TRIANGLES, 0, _plane.attributes("position")->count()); _plane.disable(); } } /*! * Run the reaction function. * * @param args Arguments to pass to the reaction function * @exception std::runtime_error if the pad is currently disabled * @return Result of the reaction function if it returns a value, or void otherwise */ ReturnType press(Arguments... args) { if (!_enabled) { throw std::runtime_error( "The pad cannot be pressed because it is currently disabled. Please check Pad::enabled before calling Pad::press."); } else { return connection.flip(args...); } } /*! * @return the state of the pad */ bool pressed() const { return connection; } /*! * This will not trigger the callback if any is set. To do that, use Pad::press(Arguments...). * * @param state true or false to set the pad state */ void state(bool pressed) { connection = pressed; } /*! * @return size in bytes of the pad object's plane object */ std::size_t size() const { return _plane.size(); } /*! * By default, a pad object is enabled to accept input presses. If the pad is disabled, however, the press function will not * be able to be used and will throw an exception. This function can be used both to check the state and to set the state. * * @param state Set to false to disable, true to enable, or omit to check the current state * @return True if button is set to enabled */ bool enabled(std::optional state = std::nullopt) { if (state.has_value()) { _enabled = state.value(); } return _enabled; } /*! * @return the button's enabled state */ bool enabled() const { return _enabled; } /*! * Use this function to prevent the draw function from running, which will prevent the pad object from being rendered. Note * that this does not disable input. To do that, use Pad::enabled(bool) * * @param state Set to false to prevent the pad from being drawn, set to true to re-enable drawing */ bool visible(bool state) { _visible = state; return _visible; } /*! * @return the pad's visibility state */ bool visible() const { return _visible; } }; }