#include "Brush.h" #include #include "Assets.h" #include #include "RenderingContext/StrokeRenderingContext.h" #include "Input/PointerStream.h" #include #include #include #include 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 ibispaint static std::array 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& 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& 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& flat, std::list& 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 Brush::CalculateStampList(std::list& inputChangeList) { const double subdivideTolerancePixels = 0.05; std::list 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 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 flat; FinalizeSegment(P0, P1, P2, P3, flat, subdivideTolerancePixels); StampAlong(flat, list, cursor, Size, spacing); finalizedSegments++; } } } if (eventData.actDirection == PointerDirection::Up) { IsDrawing = false; } } return list; }