diff --git a/build/android/jni/Android.mk b/build/android/jni/Android.mk index 6d21544bf..72b0daab6 100644 --- a/build/android/jni/Android.mk +++ b/build/android/jni/Android.mk @@ -178,6 +178,7 @@ LOCAL_SRC_FILES := \ jni/src/filesys.cpp \ jni/src/genericobject.cpp \ jni/src/gettext.cpp \ + jni/src/gui/guiAnimatedImage.cpp \ jni/src/gui/guiBackgroundImage.cpp \ jni/src/gui/guiBox.cpp \ jni/src/gui/guiButton.cpp \ diff --git a/doc/lua_api.txt b/doc/lua_api.txt index 75a083bdd..fe5b1a626 100644 --- a/doc/lua_api.txt +++ b/doc/lua_api.txt @@ -2133,6 +2133,15 @@ Elements * Show an image +### `animated_image[,;,;:,]` + +* Show an animated image. The image is drawn like a "vertical_frames" tile + animation (See Tile animation definition), but uses a frame count/duration + for simplicity +* `` is the image to use +* `` is the number of frames animating the image +* `` is in milliseconds + ### `item_image[,;,;]` * Show an inventory image of registered item/node @@ -2580,6 +2589,8 @@ Some types may inherit styles from parent types. ### Valid Properties +* animated_image + * noclip - boolean, set to true to allow the element to exceed formspec bounds. * box * noclip - boolean, set to true to allow the element to exceed formspec bounds. * Default to false in formspec_version version 3 or higher diff --git a/games/minimal/mods/test/formspec.lua b/games/minimal/mods/test/formspec.lua index 67aad3b20..50c506899 100644 --- a/games/minimal/mods/test/formspec.lua +++ b/games/minimal/mods/test/formspec.lua @@ -12,6 +12,7 @@ local clip_fs = [[ style_type[dropdown;noclip=%c] style_type[scrollbar;noclip=%c] style_type[table;noclip=%c] + style_type[animated_image;noclip=%c] label[0,0;A clipping test] button[0,1;3,0.8;x;A clipping test] @@ -25,6 +26,7 @@ local clip_fs = [[ scrollbar[0,9;3,0.8;horizontal;x9;3] tablecolumns[text;text] table[0,10;3,1;x10;one,two,three,four;1] + animated_image[0,11;3,1;test_animation.png:4,100] ]] @@ -119,8 +121,8 @@ local style_fs = [[ local pages = { [[ + formspec_version[3] size[12,12] - real_coordinates[true] image_button[0,0;1,1;logo.png;;1x1] image_button[1,0;2,2;logo.png;;2x2] button[0,2;1,1;;1x1] @@ -157,7 +159,7 @@ local pages = { tabheader[6.5,0;6,0.65;name;Tab 1,Tab 2,Tab 3,Secrets;1;false;false] ]], - "size[12,12]real_coordinates[true]" .. + "formspec_version[3]size[12,12]" .. ("label[0.375,0.375;Styled - %s %s]"):format( color("#F00", "red text"), color("#77FF00CC", "green text")) .. @@ -170,17 +172,27 @@ local pages = { style_fs:gsub("one_", "two_"):gsub("style%[[^%]]+%]", ""):gsub("style_type%[[^%]]+%]", "") .. "container_end[]", - "size[12,12]real_coordinates[true]" .. + "formspec_version[3]size[12,13]" .. "label[0.1,0.5;Clip]" .. "container[-2.5,1]" .. clip_fs:gsub("%%c", "false") .. "container_end[]" .. "label[11,0.5;Noclip]" .. "container[11.5,1]" .. clip_fs:gsub("%%c", "true") .. "container_end[]", + + [[ + formspec_version[3] + size[12,12] + animated_image[0.5,0.5;1,1;test_animation.png:4,100] + animated_image[1.75,0.5;1,1;test_animation.png:100,100] + animated_image[0.5,1.75;1,1;test_animation.jpg:4,100] + animated_image[3,0.5;5,2;test_animation.png:4,100] + animated_image[3,2.75;5,2;test_animation.jpg:4,100] + ]] } local function show_test_formspec(pname, page_id) page_id = page_id or 2 - local fs = pages[page_id] .. "tabheader[0,0;6,0.65;maintabs;Real Coord,Styles,Noclip;" .. page_id .. ";false;false]" + local fs = pages[page_id] .. "tabheader[0,0;6,0.65;maintabs;Real Coord,Styles,Noclip,MiscEle;" .. page_id .. ";false;false]" minetest.show_formspec(pname, "test:formspec", fs) end diff --git a/games/minimal/mods/test/textures/test_animation.jpg b/games/minimal/mods/test/textures/test_animation.jpg new file mode 100644 index 000000000..c4f125d5a Binary files /dev/null and b/games/minimal/mods/test/textures/test_animation.jpg differ diff --git a/games/minimal/mods/test/textures/test_animation.png b/games/minimal/mods/test/textures/test_animation.png new file mode 100644 index 000000000..b58715bf8 Binary files /dev/null and b/games/minimal/mods/test/textures/test_animation.png differ diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index a9df7848d..110a00595 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -1,4 +1,5 @@ set(gui_SRCS + ${CMAKE_CURRENT_SOURCE_DIR}/guiAnimatedImage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/guiBackgroundImage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/guiBox.cpp ${CMAKE_CURRENT_SOURCE_DIR}/guiButton.cpp diff --git a/src/gui/guiAnimatedImage.cpp b/src/gui/guiAnimatedImage.cpp new file mode 100644 index 000000000..822304087 --- /dev/null +++ b/src/gui/guiAnimatedImage.cpp @@ -0,0 +1,83 @@ +#include "guiAnimatedImage.h" + +#include "client/guiscalingfilter.h" +#include "client/tile.h" // ITextureSource +#include "log.h" +#include "porting.h" +#include + +GUIAnimatedImage::GUIAnimatedImage(gui::IGUIEnvironment *env, gui::IGUIElement *parent, + s32 id, const core::rect &rectangle, const std::string &name, + ISimpleTextureSource *tsrc) : + gui::IGUIElement(gui::EGUIET_ELEMENT, env, parent, id, rectangle), + m_name(name), m_tsrc(tsrc), m_texture(nullptr), m_global_time(0), + m_frame_idx(0), m_frame_count(1), m_frame_duration(1), m_frame_time(0) +{ + // Expected format: "texture_name:frame_count,frame_duration" + // If this format is not met, the string will be loaded as a normal texture + + std::string::size_type colon_position = name.find(':', 0); + std::string::size_type comma_position = name.find(',', 0); + + if (comma_position != std::string::npos && + colon_position != std::string::npos && + comma_position < name.size()) { + m_texture = m_tsrc->getTexture(name.substr(0, colon_position)); + + m_frame_count = std::max(stoi(name.substr( + colon_position + 1, comma_position - colon_position - 1)), 1); + + m_frame_duration = std::max(stoi(name.substr(comma_position + 1)), 1); + } else { + // Leave the count/duration and display a static image + m_texture = m_tsrc->getTexture(name); + errorstream << "animated_image[]: Invalid texture format " << name << + ". Expected format: texture_name:frame_count,frame_duration" << std::endl; + } + + if (m_texture != nullptr) { + core::dimension2d size = m_texture->getOriginalSize(); + if (size.Height < (u64)m_frame_count) { + m_frame_count = size.Height; + } + } else { + // No need to step an animation if we have nothing to draw + m_frame_count = 1; + } +} + +void GUIAnimatedImage::draw() +{ + // Render the current frame + if (m_texture != nullptr) { + video::IVideoDriver *driver = Environment->getVideoDriver(); + + const video::SColor color(255, 255, 255, 255); + const video::SColor colors[] = {color, color, color, color}; + + core::dimension2d size = m_texture->getOriginalSize(); + size.Height /= m_frame_count; + + draw2DImageFilterScaled( driver, m_texture, AbsoluteRect, + core::rect(core::position2d(0, size.Height * m_frame_idx), size), + NoClip ? nullptr : &AbsoluteClippingRect, colors, true); + } + + // Step the animation + if (m_frame_count > 1) { + // Determine the delta time to step + u64 new_global_time = porting::getTimeMs(); + if (m_global_time > 0) + m_frame_time += new_global_time - m_global_time; + + m_global_time = new_global_time; + + // Advance by the number of elapsed frames, looping if necessary + m_frame_idx += u32(m_frame_time / m_frame_duration); + m_frame_idx %= m_frame_count; + + // If 1 or more frames have elapsed, reset the frame time counter with + // the remainder + m_frame_time %= m_frame_duration; + } +} diff --git a/src/gui/guiAnimatedImage.h b/src/gui/guiAnimatedImage.h new file mode 100644 index 000000000..8fb2977f2 --- /dev/null +++ b/src/gui/guiAnimatedImage.h @@ -0,0 +1,26 @@ +#pragma once + +#include "irrlichttypes_extrabloated.h" +#include "util/string.h" + +class ISimpleTextureSource; + +class GUIAnimatedImage : public gui::IGUIElement { +public: + GUIAnimatedImage(gui::IGUIEnvironment *env, gui::IGUIElement *parent, s32 id, + const core::rect &rectangle, const std::string &name, + ISimpleTextureSource *tsrc); + + virtual void draw() override; + +private: + std::string m_name; + ISimpleTextureSource *m_tsrc; + + video::ITexture *m_texture; + u64 m_global_time; + s32 m_frame_idx; + s32 m_frame_count; + u64 m_frame_duration; + u64 m_frame_time; +}; diff --git a/src/gui/guiFormSpecMenu.cpp b/src/gui/guiFormSpecMenu.cpp index 98f4368f4..3d473550c 100644 --- a/src/gui/guiFormSpecMenu.cpp +++ b/src/gui/guiFormSpecMenu.cpp @@ -55,6 +55,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "util/string.h" // for parseColorString() #include "irrlicht_changes/static_text.h" #include "client/guiscalingfilter.h" +#include "guiAnimatedImage.h" #include "guiBackgroundImage.h" #include "guiBox.h" #include "guiButton.h" @@ -779,6 +780,58 @@ void GUIFormSpecMenu::parseImage(parserData* data, const std::string &element) errorstream<< "Invalid image element(" << parts.size() << "): '" << element << "'" << std::endl; } +void GUIFormSpecMenu::parseAnimatedImage(parserData *data, const std::string &element) +{ + std::vector parts = split(element, ';'); + + if (parts.size() != 3 && + !(parts.size() > 3 && m_formspec_version > FORMSPEC_API_VERSION)) { + errorstream << "Invalid animated image element(" << parts.size() + << "): '" << element << "'" << std::endl; + return; + } + + std::vector v_pos = split(parts[0], ','); + std::vector v_geom = split(parts[1], ','); + std::string name = unescape_string(parts[2]); + + MY_CHECKPOS("animated_image", 0); + MY_CHECKGEOM("animated_image", 1); + + v2s32 pos; + v2s32 geom; + + if (data->real_coordinates) { + pos = getRealCoordinateBasePos(v_pos); + geom = getRealCoordinateGeometry(v_geom); + } else { + pos = getElementBasePos(&v_pos); + geom.X = stof(v_geom[0]) * (float)imgsize.X; + geom.Y = stof(v_geom[1]) * (float)imgsize.Y; + } + + if (!data->explicit_size) + warningstream << "invalid use of animated_image without a size[] element" << std::endl; + + FieldSpec spec( + "", + L"", + L"", + 258 + m_fields.size() + ); + + core::rect rect = core::rect(pos, pos + geom); + + gui::IGUIElement *e = new GUIAnimatedImage(Environment, this, spec.fid, + rect, name, m_tsrc); + + auto style = getStyleForElement("animated_image", spec.fname); + e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false)); + e->drop(); + + m_fields.push_back(spec); +} + void GUIFormSpecMenu::parseItemImage(parserData* data, const std::string &element) { std::vector parts = split(element,';'); @@ -2500,6 +2553,11 @@ void GUIFormSpecMenu::parseElement(parserData* data, const std::string &element) return; } + if (type == "animated_image") { + parseAnimatedImage(data, description); + return; + } + if (type == "item_image") { parseItemImage(data, description); return; diff --git a/src/gui/guiFormSpecMenu.h b/src/gui/guiFormSpecMenu.h index 67be4268a..7c52336c9 100644 --- a/src/gui/guiFormSpecMenu.h +++ b/src/gui/guiFormSpecMenu.h @@ -38,6 +38,7 @@ class InventoryManager; class ISimpleTextureSource; class Client; class GUIScrollBar; +class TexturePool; typedef enum { f_Button, @@ -388,6 +389,7 @@ private: void parseListRing(parserData* data, const std::string &element); void parseCheckbox(parserData* data, const std::string &element); void parseImage(parserData* data, const std::string &element); + void parseAnimatedImage(parserData *data, const std::string &element); void parseItemImage(parserData* data, const std::string &element); void parseButton(parserData* data, const std::string &element, const std::string &typ); diff --git a/util/travis/clang-format-whitelist.txt b/util/travis/clang-format-whitelist.txt index a2559194a..05b4a96c4 100644 --- a/util/travis/clang-format-whitelist.txt +++ b/util/travis/clang-format-whitelist.txt @@ -155,6 +155,8 @@ src/genericobject.cpp src/genericobject.h src/gettext.cpp src/gettext.h +src/gui/guiAnimatedImage.cpp +src/gui/guiAnimatedImage.h src/gui/guiBackgroundImage.cpp src/gui/guiBackgroundImage.h src/gui/guiBox.cpp