From 82c0e2527f187066ee45e2cd008a75fb38d2f8ac Mon Sep 17 00:00:00 2001 From: Yggdrasil75 Date: Fri, 7 Nov 2025 13:03:36 -0500 Subject: [PATCH] can be improved. --- main.cpp | 107 ++++++++++++++-- simtools/sim2.hpp | 265 +++++++++++++++++++++++++++++++++++++++ simtools/sim22.hpp | 18 +++ util/noise2.hpp | 300 +++++++++++++++++++++++++++++++++++++++++++++ web/index.html | 13 +- web/script.js | 134 +++++++++++++++++--- web/style.css | 11 +- 7 files changed, 818 insertions(+), 30 deletions(-) create mode 100644 simtools/sim2.hpp create mode 100644 simtools/sim22.hpp create mode 100644 util/noise2.hpp diff --git a/main.cpp b/main.cpp index 8a5dccd..fb142eb 100644 --- a/main.cpp +++ b/main.cpp @@ -5,6 +5,7 @@ #include "util/bmpwriter.hpp" #include "util/jxlwriter.hpp" #include "util/timing_decorator.hpp" +#include "simtools/sim2.hpp" // Function to convert hex color string to Vec4 Vec4 hexToVec4(const std::string& hex) { @@ -70,6 +71,22 @@ bool generateGradientImage(const std::string& filename, int width = 512, int hei return JXLWriter::saveJXL(filename, imageData, width, height); } +// Generate terrain simulation image +bool generateTerrainImage(const std::string& filename, int width = 512, int height = 512) { + TIME_FUNCTION; + + static Sim2 sim(width, height); + + // Randomize seed for variety + sim.randomizeSeed(); + + // Render to RGB image + std::vector imageData = sim.renderToRGB(width, height); + + // Save as JXL + return JXLWriter::saveJXL(filename, imageData, width, height); +} + // Add this function to get timing stats as JSON std::string getTimingStatsJSON() { @@ -107,6 +124,7 @@ int main(int argc, char* argv[]) { // Check command line arguments int port = 8080; std::string webRoot = "web"; + std::string mode = "gradient"; // Default mode for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; @@ -118,22 +136,36 @@ int main(int argc, char* argv[]) { if (i + 1 < argc) { webRoot = argv[++i]; } + } else if (arg == "-2d") { + mode = "terrain"; + } else if (arg == "-all") { + mode = "all"; } else if (arg == "--help" || arg == "-h") { std::cout << "Usage: " << argv[0] << " [options]" << std::endl; std::cout << "Options:" << std::endl; std::cout << " -p, --port PORT Set server port (default: 8080)" << std::endl; std::cout << " -w, --webroot DIR Set web root directory (default: web)" << std::endl; + std::cout << " -2d Display 2D terrain simulation" << std::endl; + std::cout << " -all Allow switching between gradient and terrain" << std::endl; std::cout << " -h, --help Show this help message" << std::endl; return 0; } } - // Generate gradient image before starting server - std::cout << "Generating gradient image..." << std::endl; - if (generateGradientImage(webRoot + "/output/gradient.jxl")) { - std::cout << "Gradient image generated successfully" << std::endl; + // Generate initial image based on mode + std::cout << "Generating " << mode << " image..." << std::endl; + bool success = false; + + if (mode == "terrain") { + success = generateTerrainImage(webRoot + "/output/display.jxl"); } else { - std::cerr << "Failed to generate gradient image" << std::endl; + success = generateGradientImage(webRoot + "/output/display.jxl"); + } + + if (success) { + std::cout << mode << " image generated successfully" << std::endl; + } else { + std::cerr << "Failed to generate " << mode << " image" << std::endl; return 1; } @@ -144,18 +176,67 @@ int main(int argc, char* argv[]) { if (method == "GET") { return std::make_pair(200, getTimingStatsJSON()); } - //return std::make_pair(405, "{\"error\":\"Method Not Allowed\"}"); + return std::make_pair(405, std::basic_string("{\"error\":\"Method Not Allowed\"}")); }); // Add clear stats endpoint server.addRoute("/api/clear-stats", [](const std::string& method, const std::string& body) { if (method == "POST") { FunctionTimer::clearStats(); - return std::make_pair(200, "{\"status\":\"success\"}"); + return std::make_pair(200, std::basic_string("{\"status\":\"success\"}")); } - return std::make_pair(405, "{\"error\":\"Method Not Allowed\"}"); + return std::make_pair(405, std::basic_string("{\"error\":\"Method Not Allowed\"}")); }); + // Add mode switching endpoint for -all mode + if (mode == "all") { + server.addRoute("/api/switch-mode", [webRoot](const std::string& method, const std::string& body) { + if (method == "POST") { + static bool currentModeGradient = true; + + bool success = false; + if (currentModeGradient) { + success = generateTerrainImage(webRoot + "/output/display.jxl"); + } else { + success = generateGradientImage(webRoot + "/output/display.jxl"); + } + + if (success) { + currentModeGradient = !currentModeGradient; + std::string newMode = currentModeGradient ? "gradient" : "terrain"; + return std::make_pair(200, std::basic_string("{\"status\":\"success\", \"mode\":\"" + newMode + "\"}")); + } else { + return std::make_pair(500, std::basic_string("{\"error\":\"Failed to generate image\"}")); + } + } + return std::make_pair(405, std::basic_string("{\"error\":\"Method Not Allowed\"}")); + }); + + server.addRoute("/api/current-mode", [](const std::string& method, const std::string& body) { + if (method == "GET") { + static bool currentModeGradient = true; + std::string mode = currentModeGradient ? "gradient" : "terrain"; + return std::make_pair(200, std::basic_string("{\"mode\":\"" + mode + "\"}")); + } + return std::make_pair(405, std::basic_string("{\"error\":\"Method Not Allowed\"}")); + }); + } + + // Add refresh endpoint for terrain mode (fast regeneration) + if (mode == "terrain" || mode == "all") { + server.addRoute("/api/refresh-terrain", [webRoot](const std::string& method, const std::string& body) { + if (method == "POST") { + bool success = generateTerrainImage(webRoot + "/output/display.jxl"); + if (success) { + return std::make_pair(200, std::basic_string("{\"status\":\"success\"}")); + } else { + return std::make_pair(500, std::basic_string("{\"error\":\"Failed to generate terrain\"}")); + } + } + return std::make_pair(405, std::basic_string("{\"error\":\"Method Not Allowed\"}")); + }); + } + if (!server.start()) { std::cerr << "Failed to start server on port " << port << std::endl; return 1; @@ -163,7 +244,17 @@ int main(int argc, char* argv[]) { std::cout << "Server running on http://localhost:" << port << std::endl; std::cout << "Web root: " << webRoot << std::endl; + std::cout << "Mode: " << mode << std::endl; std::cout << "Timing stats available at /api/timing-stats" << std::endl; + + if (mode == "all") { + std::cout << "Mode switching available at /api/switch-mode" << std::endl; + } + + if (mode == "terrain") { + std::cout << "Fast terrain refresh available at /api/refresh-terrain" << std::endl; + } + std::cout << "Press Ctrl+C to stop the server" << std::endl; server.handleRequests(); diff --git a/simtools/sim2.hpp b/simtools/sim2.hpp new file mode 100644 index 0000000..7b1dc4f --- /dev/null +++ b/simtools/sim2.hpp @@ -0,0 +1,265 @@ +#ifndef SIM2_HPP +#define SIM2_HPP + +#include "../util/noise2.hpp" +#include "../util/grid2.hpp" +#include "../util/vec2.hpp" +#include "../util/vec4.hpp" +#include "../util/timing_decorator.hpp" +#include +#include +#include + +class Sim2 { +private: + std::unique_ptr noiseGenerator; + Grid2 terrainGrid; + int gridWidth; + int gridHeight; + + // Terrain generation parameters + float scale; + int octaves; + float persistence; + float lacunarity; + uint32_t seed; + Vec2 offset; + + // Terrain modification parameters + float elevationMultiplier; + float waterLevel; + Vec4 landColor; + Vec4 waterColor; + +public: + Sim2(int width = 512, int height = 512, uint32_t seed = 12345) + : gridWidth(width), gridHeight(height), scale(4.0f), octaves(4), + persistence(0.5f), lacunarity(2.0f), seed(seed), offset(0, 0), + elevationMultiplier(1.0f), waterLevel(0.3f), + landColor(0.2f, 0.8f, 0.2f, 1.0f), // Green + waterColor(0.2f, 0.3f, 0.8f, 1.0f) // Blue + { + noiseGenerator = std::make_unique(seed); + generateTerrain(); + } + + // Generate initial terrain + void generateTerrain() { + TIME_FUNCTION; + terrainGrid = noiseGenerator->generateTerrainNoise( + gridWidth, gridHeight, scale, octaves, persistence, seed, offset); + + applyTerrainColors(); + } + + // Regenerate terrain with current parameters + void regenerate() { + generateTerrain(); + } + + // Basic parameter modifications + void setScale(float newScale) { + scale = std::max(0.1f, newScale); + generateTerrain(); + } + + void setOctaves(int newOctaves) { + octaves = std::max(1, newOctaves); + generateTerrain(); + } + + void setPersistence(float newPersistence) { + persistence = std::clamp(newPersistence, 0.0f, 1.0f); + generateTerrain(); + } + + void setLacunarity(float newLacunarity) { + lacunarity = std::max(1.0f, newLacunarity); + generateTerrain(); + } + + void setSeed(uint32_t newSeed) { + seed = newSeed; + noiseGenerator->setSeed(seed); + generateTerrain(); + } + + void setOffset(const Vec2& newOffset) { + offset = newOffset; + generateTerrain(); + } + + void setElevationMultiplier(float multiplier) { + elevationMultiplier = std::max(0.0f, multiplier); + applyElevationModification(); + } + + void setWaterLevel(float level) { + waterLevel = std::clamp(level, 0.0f, 1.0f); + applyTerrainColors(); + } + + void setLandColor(const Vec4& color) { + landColor = color; + applyTerrainColors(); + } + + void setWaterColor(const Vec4& color) { + waterColor = color; + applyTerrainColors(); + } + + // Get current parameters + float getScale() const { return scale; } + int getOctaves() const { return octaves; } + float getPersistence() const { return persistence; } + float getLacunarity() const { return lacunarity; } + uint32_t getSeed() const { return seed; } + Vec2 getOffset() const { return offset; } + float getElevationMultiplier() const { return elevationMultiplier; } + float getWaterLevel() const { return waterLevel; } + Vec4 getLandColor() const { return landColor; } + Vec4 getWaterColor() const { return waterColor; } + + // Get the terrain grid + const Grid2& getTerrainGrid() const { + return terrainGrid; + } + + // Get terrain dimensions + int getWidth() const { return gridWidth; } + int getHeight() const { return gridHeight; } + + // Get elevation at specific coordinates + float getElevation(int x, int y) const { + if (x < 0 || x >= gridWidth || y < 0 || y >= gridHeight) { + return 0.0f; + } + return terrainGrid.colors[y * gridWidth + x].x; // Elevation stored in red channel + } + + // Render to RGB image + std::vector renderToRGB(int width, int height, + const Vec4& backgroundColor = Vec4(0, 0, 0, 1)) const { + return terrainGrid.renderToRGB(width, height, backgroundColor); + } + + // Render to RGBA image + std::vector renderToRGBA(int width, int height, + const Vec4& backgroundColor = Vec4(0, 0, 0, 1)) const { + return terrainGrid.renderToRGBA(width, height, backgroundColor); + } + + // Export terrain data as heightmap (grayscale) + Grid2 exportHeightmap() const { + Grid2 heightmap(gridWidth * gridHeight); + + for (int y = 0; y < gridHeight; y++) { + for (int x = 0; x < gridWidth; x++) { + int index = y * gridWidth + x; + float elevation = terrainGrid.colors[index].x; + heightmap.positions[index] = Vec2(x, y); + heightmap.colors[index] = Vec4(elevation, elevation, elevation, 1.0f); + } + } + + return heightmap; + } + + // Generate random seed and regenerate + void randomizeSeed() { + std::random_device rd; + setSeed(rd()); + } + + // Reset all parameters to default + void reset() { + scale = 4.0f; + octaves = 4; + persistence = 0.5f; + lacunarity = 2.0f; + elevationMultiplier = 1.0f; + waterLevel = 0.3f; + landColor = Vec4(0.2f, 0.8f, 0.2f, 1.0f); + waterColor = Vec4(0.2f, 0.3f, 0.8f, 1.0f); + generateTerrain(); + } + + // Get terrain statistics + struct TerrainStats { + float minElevation; + float maxElevation; + float averageElevation; + float landPercentage; + int landArea; + int waterArea; + }; + + TerrainStats getTerrainStats() const { + TerrainStats stats = {1.0f, 0.0f, 0.0f, 0.0f, 0, 0}; + float totalElevation = 0.0f; + + for (const auto& color : terrainGrid.colors) { + float elevation = color.x; + stats.minElevation = std::min(stats.minElevation, elevation); + stats.maxElevation = std::max(stats.maxElevation, elevation); + totalElevation += elevation; + + if (elevation > waterLevel) { + stats.landArea++; + } else { + stats.waterArea++; + } + } + + stats.averageElevation = totalElevation / terrainGrid.colors.size(); + stats.landPercentage = static_cast(stats.landArea) / + (stats.landArea + stats.waterArea) * 100.0f; + + return stats; + } + +private: + void applyTerrainColors() { + for (int y = 0; y < gridHeight; y++) { + for (int x = 0; x < gridWidth; x++) { + int index = y * gridWidth + x; + float elevation = terrainGrid.colors[index].x; + + // Apply water level and color based on elevation + if (elevation <= waterLevel) { + // Water - optionally add depth variation + float depth = (waterLevel - elevation) / waterLevel; + Vec4 water = waterColor * (0.7f + 0.3f * depth); + water.w = 1.0f; // Ensure full alpha + terrainGrid.colors[index] = water; + } else { + // Land - optionally add elevation-based color variation + float height = (elevation - waterLevel) / (1.0f - waterLevel); + Vec4 land = landColor * (0.8f + 0.2f * height); + land.w = 1.0f; // Ensure full alpha + terrainGrid.colors[index] = land; + } + } + } + } + + void applyElevationModification() { + for (int y = 0; y < gridHeight; y++) { + for (int x = 0; x < gridWidth; x++) { + int index = y * gridWidth + x; + float originalElevation = terrainGrid.colors[index].x; + + // Apply elevation multiplier with clamping + float newElevation = std::clamp(originalElevation * elevationMultiplier, 0.0f, 1.0f); + terrainGrid.colors[index].x = newElevation; + terrainGrid.colors[index].y = newElevation; // Keep grayscale for heightmap + terrainGrid.colors[index].z = newElevation; + } + } + + applyTerrainColors(); + } +}; + +#endif \ No newline at end of file diff --git a/simtools/sim22.hpp b/simtools/sim22.hpp new file mode 100644 index 0000000..613a8a0 --- /dev/null +++ b/simtools/sim22.hpp @@ -0,0 +1,18 @@ +#ifndef SIM2_HPP +#define SIM2_HPP + +#include "../util/noise2.hpp" +#include "../util/grid2.hpp" +#include "../util/vec2.hpp" +#include "../util/vec4.hpp" +#include +#include +#include + +class Sim2 { +private: + Noise2 noise; + Grid2 terrainGrid; +} + +#endif \ No newline at end of file diff --git a/util/noise2.hpp b/util/noise2.hpp new file mode 100644 index 0000000..3558020 --- /dev/null +++ b/util/noise2.hpp @@ -0,0 +1,300 @@ +#ifndef NOISE2_HPP +#define NOISE2_HPP + +#include "grid2.hpp" +#include +#include +#include +#include + +struct Grad { float x, y; }; +std::array gradients; + +class Noise2 { +private: + std::mt19937 rng; + std::uniform_real_distribution dist; +public: + Noise2(uint32_t seed = 0) : rng(seed), dist(0.0f, 1.0f) {} + + // Set random seed + void setSeed(uint32_t seed) { + rng.seed(seed); + } + + // Generate simple value noise + float valueNoise(float x, float y, int octaves = 1, float persistence = 0.5f, float lacunarity = 2.0f) { + float total = 0.0f; + float frequency = 1.0f; + float amplitude = 1.0f; + float maxValue = 0.0f; + + for (int i = 0; i < octaves; i++) { + total += rawNoise(x * frequency, y * frequency) * amplitude; + maxValue += amplitude; + amplitude *= persistence; + frequency *= lacunarity; + } + + return total / maxValue; + } + + // Generate Perlin-like noise + float perlinNoise(float x, float y, int octaves = 1, float persistence = 0.5f, float lacunarity = 2.0f) { + float total = 0.0f; + float frequency = 1.0f; + float amplitude = 1.0f; + float maxValue = 0.0f; + + for (int i = 0; i < octaves; i++) { + total += improvedNoise(x * frequency, y * frequency) * amplitude; + maxValue += amplitude; + amplitude *= persistence; + frequency *= lacunarity; + } + + return (total / maxValue + 1.0f) * 0.5f; // Normalize to [0,1] + } + + // Generate a grayscale noise grid + Grid2 generateGrayNoise(int width, int height, + float scale = 1.0f, + int octaves = 1, + float persistence = 0.5f, + uint32_t seed = 0, + const Vec2& offset = Vec2(0, 0)) { + if (seed != 0) setSeed(seed); + + Grid2 grid(width * height); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + float nx = (x + offset.x) / width * scale; + float ny = (y + offset.y) / height * scale; + + float noiseValue = perlinNoise(nx, ny, octaves, persistence); + + // Convert to position and grayscale color + Vec2 position(x, y); + Vec4 color(noiseValue, noiseValue, noiseValue, 1.0f); + + grid.positions[y * width + x] = position; + grid.colors[y * width + x] = color; + } + } + + return grid; + } + + float pascalTri(const float& a, const float& b) { + TIME_FUNCTION; + int result = 1; + for (int i = 0; i < b; ++i){ + result *= (a - 1) / (i + 1); + } + return result; + } + + float genSmooth(int N, float x) { + TIME_FUNCTION; + x = clamp(x, 0, 1); + float result = 0; + for (int n = 0; n <= N; ++n){ + result += pascalTri(-N - 1, n) * pascalTri(2 * N + 1, N-1) * pow(x, N + n + 1); + } + return result; + } + + float inverse_smoothstep(float x) { + TIME_FUNCTION; + return 0.5 - sin(asin(1.0 - 2.0 * x) / 3.0); + } + + // Generate multi-layered RGBA noise + Grid2 generateRGBANoise(int width, int height, + const Vec4& scale = Vec4(1.0f, 1.0f, 1.0f, 1.0f), + const Vec4& octaves = Vec4(1.0f, 1.0f, 1.0f, 1.0f), + const Vec4& persistence = Vec4(0.5f, 0.5f, 0.5f, 0.5f), + uint32_t seed = 0, + const Vec2& offset = Vec2(0, 0)) { + if (seed != 0) setSeed(seed); + + Grid2 grid(width * height); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + float nx = (x + offset.x) / width; + float ny = (y + offset.y) / height; + + // Generate separate noise for each channel + float r = perlinNoise(nx * scale.x, ny * scale.x, + static_cast(octaves.x), persistence.x); + float g = perlinNoise(nx * scale.y, ny * scale.y, + static_cast(octaves.y), persistence.y); + float b = perlinNoise(nx * scale.z, ny * scale.z, + static_cast(octaves.z), persistence.z); + float a = perlinNoise(nx * scale.w, ny * scale.w, + static_cast(octaves.w), persistence.w); + + Vec2 position(x, y); + Vec4 color(r, g, b, a); + + grid.positions[y * width + x] = position; + grid.colors[y * width + x] = color; + } + } + + return grid; + } + + // Generate terrain-like noise (useful for heightmaps) + Grid2 generateTerrainNoise(int width, int height, + float scale = 1.0f, + int octaves = 4, + float persistence = 0.5f, + uint32_t seed = 0, + const Vec2& offset = Vec2(0, 0)) { + if (seed != 0) setSeed(seed); + + Grid2 grid(width * height); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + float nx = (x + offset.x) / width * scale; + float ny = (y + offset.y) / height * scale; + + // Use multiple octaves for more natural terrain + float heightValue = perlinNoise(nx, ny, octaves, persistence); + + // Apply some curve to make it more terrain-like + heightValue = std::pow(heightValue, 1.5f); + + Vec2 position(x, y); + Vec4 color(heightValue, heightValue, heightValue, 1.0f); + + grid.positions[y * width + x] = position; + grid.colors[y * width + x] = color; + } + } + + return grid; + } + + // Generate cloud-like noise + Grid2 generateCloudNoise(int width, int height, + float scale = 2.0f, + int octaves = 3, + float persistence = 0.7f, + uint32_t seed = 0, + const Vec2& offset = Vec2(0, 0)) { + auto grid = generateGrayNoise(width, height, scale, octaves, persistence, seed, offset); + + // Apply soft threshold for cloud effect + for (auto& color : grid.colors) { + float value = color.x; // Assuming grayscale in red channel + // Soft threshold: values below 0.3 become 0, above 0.7 become 1, smooth in between + if (value < 0.3f) value = 0.0f; + else if (value > 0.7f) value = 1.0f; + else value = (value - 0.3f) / 0.4f; // Linear interpolation + + color = Vec4(value, value, value, 1.0f); + } + + return grid; + } + +private: + // Raw noise function (simple hash-based) + float rawNoise(float x, float y) { + // Simple hash function for deterministic noise + int xi = static_cast(std::floor(x)); + int yi = static_cast(std::floor(y)); + + // Use the RNG to generate consistent noise based on grid position + rng.seed(xi * 1619 + yi * 31337); + return dist(rng); + } + + // Improved noise function (Perlin-like) + float improvedNoise(float x, float y) { + // Integer part + int xi = static_cast(std::floor(x)); + int yi = static_cast(std::floor(y)); + + // Fractional part + float xf = x - xi; + float yf = y - yi; + + // Smooth interpolation + float u = fade(xf); + float v = fade(yf); + + // Gradient noise from corners + float n00 = gradNoise(xi, yi, xf, yf); + float n01 = gradNoise(xi, yi + 1, xf, yf - 1); + float n10 = gradNoise(xi + 1, yi, xf - 1, yf); + float n11 = gradNoise(xi + 1, yi + 1, xf - 1, yf - 1); + + // Bilinear interpolation + float x1 = lerp(n00, n10, u); + float x2 = lerp(n01, n11, u); + return lerp(x1, x2, v); + } + + // Fade function for smooth interpolation + float fade(float t) { + return t * t * t * (t * (t * 6 - 15) + 10); + } + + // Linear interpolation + float lerp(float a, float b, float t) { + return a + t * (b - a); + } + + float clamp(float x, float lowerlimit = 0.0f, float upperlimit = 1.0f) { + TIME_FUNCTION; + if (x < lowerlimit) return lowerlimit; + if (x > upperlimit) return upperlimit; + return x; + } + + // float grad(const int& hash, const float& b, const float& c, const float& d) { + // TIME_FUNCTION; + // int h = hash & 15; + // float u = (h < 8) ? c : b; + // float v = (h < 4) ? b : ((h == 12 || h == 14) ? c : d); + // return (((h & 1) == 0) ? u : -u) + (((h & 2) == 0) ? v : -v); + // } + float gradNoise(int xi, int yi, float xf, float yf) { + // Generate deterministic "random" unit vector using hash + int hash = (xi * 1619 + yi * 31337); + + // Use hash to generate angle in fixed steps (faster than trig) + float angle = (hash & 255) * (2.0f * 3.14159265f / 256.0f); + + // Or even faster: use gradient table with 8 or 16 precomputed directions + int gradIndex = hash & 7; // 8 directions + static constexpr std::array grads = { + {1,0}, {0.707f,0.707f}, {0,1}, {-0.707f,0.707f}, + {-1,0}, {-0.707f,-0.707f}, {0,-1}, {0.707f,-0.707f} + }; + + return xf * grads[gradIndex].x + yf * grads[gradIndex].y; + } + + // Gradient noise function + float slowGradNoise(int xi, int yi, float xf, float yf) { + // Generate consistent random gradient from integer coordinates + rng.seed(xi * 1619 + yi * 31337); + float angle = dist(rng) * 2.0f * 3.14159265f; + + // Gradient vector + float gx = std::cos(angle); + float gy = std::sin(angle); + + // Dot product + return xf * gx + yf * gy; + } +}; + +#endif \ No newline at end of file diff --git a/web/index.html b/web/index.html index b6a6a6b..5bda12f 100644 --- a/web/index.html +++ b/web/index.html @@ -1,22 +1,25 @@ - Dynamic Gradient Generator + Generator
-

Dynamic Gradient Generator

+

Generator

+ +
Current Mode: Loading...
- - + + +
- Dynamic Gradient + Dynamic Image