Something went wrong. Try again.
art software for the rest of us
drawing paint image sdl art
Something went wrong. Try again.
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275#include "Brush.h"#include <SDL3_image/SDL_image.h>
#include "Assets.h"#include <fstream>#include "RenderingContext/StrokeRenderingContext.h"#include "Input/PointerStream.h"#include <algorithm>#include <array>#include <vector>#include <cmath>
void Brush::LoadStampTexture(){ std::string path = Assets::Path("stamps/" + stampPath); if (!Stamp) { Stamp = new Texture("stamps/" + stampPath); } if (!Stamp) { // fallback since project now contains untracked stamps for compatibility testing SDL_GPUTextureCreateInfo createInfo = {}; createInfo.width = 64; createInfo.height = 64; createInfo.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM; createInfo.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER; createInfo.layer_count_or_depth = 1; createInfo.num_levels = 1; Stamp = new Texture(createInfo); Stamp->Clear({ 0.0f, 0.0f, 0.0f, 0.0f }); } // TODO: Clean up improperly formed stamps (fix inconsistent color)}
uint32_t Brush::getHash(){ return (uint32_t)(Size * 1000.0f + Flow * 1000.0f + Opacity * 1000.0f);}
Brush::Brush(std::string fullPath){ this->path = fullPath; std::ifstream file(fullPath); brushData = json::parse(file);
Size = brushData["save"]["size"]; spacing = brushData["shape"]["spacing"]; stampPath = brushData["shape"]["stamp"]; Name = brushData["name"]; Flow = brushData["shape"].contains("stampOpacity") ? (float)brushData["shape"]["stampOpacity"] : 1.0f; Opacity = brushData["save"].contains("opacity") ? (float)brushData["save"]["opacity"] : 1.0f;}
Brush::~Brush(){ delete Stamp; delete Preview;}
void Brush::GeneratePreview(){
if (!IsPreviewDirty()) { return; } // hash is different here
if (Preview) { delete Preview; }
int width = 320; int height = 64;
float initialColor[4] = { 0.0f, 0.0f, 0.0f, 1.0f }; SDL_FRect _boundingBox = { 0.0f, 0.0f, -1.0f, -1.0f };
LoadStampTexture(); initialColor[3] = Flow; StrokeRenderingContext context = StrokeRenderingContext(this, width, height, initialColor, &Size); PointerStream preveiwStream = PointerStream::CreatePreviewStream(width, height); auto inputs = preveiwStream.PullRawInputs(); context.SubmitInputs(inputs); inputs.clear(); Texture* strokeTexture = context.RenderQueued(&_boundingBox);
SDL_GPUTextureCreateInfo createInfo = {}; createInfo.width = width; createInfo.height = height; createInfo.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM; createInfo.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER; createInfo.layer_count_or_depth = 1; createInfo.num_levels = 1; createInfo.sample_count = SDL_GPU_SAMPLECOUNT_1; Preview = new Texture(createInfo); Preview->Clear({ 0.0f, 0.0f, 0.0f, 0.0f });
context.RenderResultOnto(Preview, false);
delete strokeTexture; // RenderResultOnto uses strokeTexture internally
hash = getHash();
}
bool Brush::IsPreviewDirty(){ return hash != getHash();}
#define TINY_DISTANCE 1e-6#define MAX_FLATTEN_DEPTH 24#define FLAT_BEZIER_TOLERANCE 16#define SAFE_SPACING 0.05
static double StampDistance(const BrushStamp& a, const BrushStamp& b) { double dx = a.X - b.X; double dy = a.Y - b.Y; return std::sqrt(dx * dx + dy * dy);}
static BrushStamp Reflect(const BrushStamp& a, const BrushStamp& b) { BrushStamp result = { .X = 2 * b.X - a.X, .Y = 2 * b.Y - a.Y, .Size = std::clamp(b.Size, 0.0, 1.0), .Rotation = 0.0 }; return result;}
static double CentripetalKnotDelta(const BrushStamp& a, const BrushStamp& b) { return std::sqrt(std::max(StampDistance(a, b), TINY_DISTANCE));}
// TODO: Implement better smoothing method using basis splines, this works for now but it won't produce results comparable to ibispaintstatic std::array<BrushStamp, 4> CatmullRomToBezier(const BrushStamp& P0, const BrushStamp& P1, const BrushStamp& P2, const BrushStamp& P3) { double t1 = CentripetalKnotDelta(P0, P1); double t2 = t1 + CentripetalKnotDelta(P1, P2); double t3 = t2 + CentripetalKnotDelta(P2, P3); double d1 = t1, d2 = t2 - t1, d3 = t3 - t2; double dt02 = t2, dt13 = t3 - t1; BrushStamp m1 = { .X = ((P1.X - P0.X) / d1 - (P2.X - P0.X) / dt02 + (P2.X - P1.X) / d2) * d2, .Y = ((P1.Y - P0.Y) / d1 - (P2.Y - P0.Y) / dt02 + (P2.Y - P1.Y) / d2) * d2, .Size = ((P1.Size - P0.Size) / d1 - (P2.Size - P0.Size) / dt02 + (P2.Size - P1.Size) / d2) * d2, .Rotation = 0.0 }; BrushStamp m2 = { .X = ((P2.X - P1.X) / d2 - (P3.X - P1.X) / dt13 + (P3.X - P2.X) / d3) * d2, .Y = ((P2.Y - P1.Y) / d2 - (P3.Y - P1.Y) / dt13 + (P3.Y - P2.Y) / d3) * d2, .Size = ((P2.Size - P1.Size) / d2 - (P3.Size - P1.Size) / dt13 + (P3.Size - P2.Size) / d3) * d2, .Rotation = 0.0 }; BrushStamp B0 = P1; BrushStamp B1 = {.X = P1.X + m1.X / 3.0, .Y = P1.Y + m1.Y / 3.0, .Size = P1.Size + m1.Size / 3.0, .Rotation = 0.0}; BrushStamp B2 = {.X = P2.X - m2.X / 3.0, .Y = P2.Y - m2.Y / 3.0, .Size = P2.Size - m2.Size / 3.0, .Rotation = 0.0}; BrushStamp B3 = P2; return {B0, B1, B2, B3};}
static bool IsFlat(const BrushStamp& B0, const BrushStamp& B1, const BrushStamp& B2, const BrushStamp& B3, double tolerance) { double ux = 3 * B1.X - 2 * B0.X - B3.X; double uy = 3 * B1.Y - 2 * B0.Y - B3.Y; double vx = 3 * B2.X - 2 * B3.X - B0.X; double vy = 3 * B2.Y - 2 * B3.Y - B0.Y; double ex = std::max(ux * ux, vx * vx); double ey = std::max(uy * uy, vy * vy); return (ex + ey) <= FLAT_BEZIER_TOLERANCE * tolerance * tolerance;}
static void FlattenBezier(const BrushStamp& B0, const BrushStamp& B1, const BrushStamp& B2, const BrushStamp& B3, std::vector<BrushStamp>& out, int depth, double tolerance) { // eventually returns if (depth >= MAX_FLATTEN_DEPTH || IsFlat(B0, B1, B2, B3, tolerance)) { out.push_back(B3); return; } // inbetween points BrushStamp B01 = {(B0.X + B1.X) / 2, (B0.Y + B1.Y) / 2, (B0.Size + B1.Size) / 2, 0.0}; BrushStamp B12 = {(B1.X + B2.X) / 2, (B1.Y + B2.Y) / 2, (B1.Size + B2.Size) / 2, 0.0}; BrushStamp B23 = {(B2.X + B3.X) / 2, (B2.Y + B3.Y) / 2, (B2.Size + B3.Size) / 2, 0.0}; BrushStamp B012 = {(B01.X + B12.X) / 2, (B01.Y + B12.Y) / 2, (B01.Size + B12.Size) / 2, 0.0}; BrushStamp B123 = {(B12.X + B23.X) / 2, (B12.Y + B23.Y) / 2, (B12.Size + B23.Size) / 2, 0.0}; BrushStamp B0123 = {(B012.X + B123.X) / 2, (B012.Y + B123.Y) / 2, (B012.Size + B123.Size) / 2, 0.0}; // recurse FlattenBezier(B0, B01, B012, B0123, out, depth + 1, tolerance); FlattenBezier(B0123, B123, B23, B3, out, depth + 1, tolerance);}
static void FinalizeSegment(const BrushStamp& P0, const BrushStamp& P1, const BrushStamp& P2, const BrushStamp& P3, std::vector<BrushStamp>& out, double tolerance) { auto bez = CatmullRomToBezier(P0, P1, P2, P3); FlattenBezier(bez[0], bez[1], bez[2], bez[3], out, 0, tolerance);}
static void StampAlong(const std::vector<BrushStamp>& flat, std::list<BrushStamp>& out, BrushStamp& cursor, double brushSize, double spacing) { for (const auto& item : flat) { double distance = StampDistance(cursor, item); double threshold = brushSize * spacing * std::max(item.Size, SAFE_SPACING); while (distance > threshold) { double t = threshold / distance; cursor.X += (item.X - cursor.X) * t; cursor.Y += (item.Y - cursor.Y) * t; cursor.Size = item.Size; out.push_back({cursor.X, cursor.Y, brushSize * cursor.Size, 0.0}); distance = StampDistance(cursor, item); threshold = brushSize * spacing * std::max(item.Size, SAFE_SPACING); } }}
std::list<BrushStamp> Brush::CalculateStampList(std::list<PointerEvent>& inputChangeList) { const double subdivideTolerancePixels = 0.05; std::list<BrushStamp> list;
while (!inputChangeList.empty()) { PointerEvent eventData = inputChangeList.front(); inputChangeList.pop_front();
if (eventData.actDirection == PointerDirection::Down && !IsDrawing) { IsDrawing = true; strokePoints.clear(); finalizedSegments = 0; strokePoints.push_back({eventData.canvasX, eventData.canvasY, eventData.pressure, 0.0}); cursor = {eventData.canvasX, eventData.canvasY, eventData.pressure, 0.0}; list.push_back({eventData.canvasX, eventData.canvasY, Size * eventData.pressure, 0.0}); }
if (IsDrawing) { BrushStamp point = {eventData.canvasX, eventData.canvasY, eventData.pressure, 0.0}; if (StampDistance(strokePoints.back(), point) > TINY_DISTANCE) { strokePoints.push_back(point); }
size_t pointsCount = strokePoints.size(); while (finalizedSegments <= (int)pointsCount - 3) { int segmentIndex = finalizedSegments; BrushStamp P0 = (segmentIndex - 1 >= 0) ? strokePoints[segmentIndex - 1] : Reflect(strokePoints[0], strokePoints[1]); BrushStamp P1 = strokePoints[segmentIndex]; BrushStamp P2 = strokePoints[segmentIndex + 1]; BrushStamp P3 = strokePoints[segmentIndex + 2]; std::vector<BrushStamp> flat; FinalizeSegment(P0, P1, P2, P3, flat, subdivideTolerancePixels); StampAlong(flat, list, cursor, Size, spacing); finalizedSegments++; }
if (eventData.actDirection == PointerDirection::Up) { // flush remaining tail while (finalizedSegments < (int)pointsCount - 1) { int segmentIndex = finalizedSegments; BrushStamp P0 = (segmentIndex - 1 >= 0) ? strokePoints[segmentIndex - 1] : Reflect(strokePoints[0], strokePoints[1]); BrushStamp P1 = strokePoints[segmentIndex]; BrushStamp P2 = strokePoints[segmentIndex + 1]; BrushStamp P3 = (segmentIndex + 2 < (int)pointsCount) ? strokePoints[segmentIndex + 2] : Reflect(strokePoints[pointsCount - 2], strokePoints[pointsCount - 1]); std::vector<BrushStamp> flat; FinalizeSegment(P0, P1, P2, P3, flat, subdivideTolerancePixels); StampAlong(flat, list, cursor, Size, spacing); finalizedSegments++; } } }
if (eventData.actDirection == PointerDirection::Up) { IsDrawing = false; } } return list;}