Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 144 additions & 55 deletions cpp/ActiveStrokeRenderer.cpp
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
#include "ActiveStrokeRenderer.h"
#include "PathRenderer.h"
#include <algorithm>
#include <cmath>
#include <include/core/SkImageInfo.h>

namespace nativedrawing {

ActiveStrokeRenderer::ActiveStrokeRenderer(int width, int height, PathRenderer* pathRenderer)
: pathRenderer_(pathRenderer)
, logicalWidth_(width)
, logicalHeight_(height)
, lastRenderedInputIndex_(0)
, hasLastEdge_(false)
, lastHalfWidth_(-1.0f) {
, hasLastEdge_(false) {

SkImageInfo info = SkImageInfo::MakeN32Premul(width, height);
activeStrokeSurface_ = SkSurfaces::Raster(info);
Expand All @@ -17,25 +20,91 @@ ActiveStrokeRenderer::ActiveStrokeRenderer(int width, int height, PathRenderer*
}
}

void ActiveStrokeRenderer::reset() {
if (activeStrokeSurface_) {
activeStrokeSurface_->getCanvas()->clear(SK_ColorTRANSPARENT);
}
void ActiveStrokeRenderer::resetIncrementalState() {
cachedActiveSnapshot_ = nullptr;
lastRenderedInputIndex_ = 0;
overlapBuffer_.clear();
hasLastEdge_ = false;
lastLeftEdge_ = SkPoint::Make(0, 0);
lastRightEdge_ = SkPoint::Make(0, 0);
lastHalfWidth_ = -1.0f; // Will use default baseWidth/2
}

void ActiveStrokeRenderer::reset() {
if (activeStrokeSurface_) {
activeStrokeSurface_->getCanvas()->clear(SK_ColorTRANSPARENT);
}
resetIncrementalState();
}

void ActiveStrokeRenderer::ensureViewportAlignment(const ActiveStrokeViewport& viewport) {
const float requestedScale =
std::clamp(viewport.scale, kMinimumRenderScale, kMaximumRenderScale);

// The surface is fixed at logical dimensions, so the covered region at
// `scale` magnification is logical/scale. Cap the scale at the highest
// magnification whose viewport still fits; inconsistent viewport data
// (e.g. a consumer that sets renderScale without viewport ratios)
// degrades to the identity mapping instead of an anchored surface that
// misses where the user is drawing.
const float viewWidth = viewport.width > 0.0f
? viewport.width
: static_cast<float>(logicalWidth_);
const float viewHeight = viewport.height > 0.0f
? viewport.height
: static_cast<float>(logicalHeight_);
const float fitScale = std::min(
static_cast<float>(logicalWidth_) / std::max(1.0f, viewWidth),
static_cast<float>(logicalHeight_) / std::max(1.0f, viewHeight)
);
const float nextScale = std::max(kMinimumRenderScale, std::min(requestedScale, fitScale));

float nextOriginX = 0.0f;
float nextOriginY = 0.0f;
if (nextScale > kIdentityRenderScaleThreshold) {
const float coveredWidth = static_cast<float>(logicalWidth_) / nextScale;
const float coveredHeight = static_cast<float>(logicalHeight_) / nextScale;
nextOriginX = std::clamp(
viewport.left, 0.0f, static_cast<float>(logicalWidth_) - coveredWidth);
nextOriginY = std::clamp(
viewport.top, 0.0f, static_cast<float>(logicalHeight_) - coveredHeight);
}

if (activeStrokeSurface_
&& std::fabs(nextScale - viewportScale_) < 0.01f
&& std::fabs(nextOriginX - viewportOriginX_) < 0.5f
&& std::fabs(nextOriginY - viewportOriginY_) < 0.5f) {
return;
}

viewportScale_ = nextScale;
viewportOriginX_ = nextOriginX;
viewportOriginY_ = nextOriginY;

if (!activeStrokeSurface_) {
SkImageInfo info = SkImageInfo::MakeN32Premul(logicalWidth_, logicalHeight_);
activeStrokeSurface_ = SkSurfaces::Raster(info);
}
if (activeStrokeSurface_) {
activeStrokeSurface_->getCanvas()->clear(SK_ColorTRANSPARENT);
}
// Anchoring changed mid-stroke: drop incremental progress so the next
// renderIncremental call re-renders every point into the fresh surface.
resetIncrementalState();
}

void ActiveStrokeRenderer::applySurfaceTransform(SkCanvas* surfaceCanvas) const {
surfaceCanvas->scale(viewportScale_, viewportScale_);
surfaceCanvas->translate(-viewportOriginX_, -viewportOriginY_);
}

void ActiveStrokeRenderer::renderIncremental(
SkCanvas* canvas,
const std::vector<Point>& points,
const SkPaint& paint,
const std::string& toolType
const std::string& toolType,
const ActiveStrokeViewport& viewport
) {
ensureViewportAlignment(viewport);
if (!activeStrokeSurface_ || points.size() < 2) return;

// Calligraphy: full redraw each frame for clean rendering
Expand Down Expand Up @@ -70,28 +139,25 @@ void ActiveStrokeRenderer::renderIncremental(
bool isFirstSegment = !hasLastEdge_;

IncrementalResult result;
if (toolType == "crayon") {
result = pathRenderer_->drawCrayonPathIncremental(
surfaceCanvas, segment, paint,
lastLeftEdge_, lastRightEdge_, isFirstSegment);
// Draw start cap on first segment
if (isFirstSegment) {
pathRenderer_->drawCrayonStartCap(surfaceCanvas, segment, paint);
}
} else if (toolType == "calligraphy") {
result = pathRenderer_->drawCalligraphyPathIncremental(
surfaceCanvas, segment, paint,
lastLeftEdge_, lastRightEdge_, isFirstSegment,
lastHalfWidth_);
lastHalfWidth_ = result.lastHalfWidth;
// Calligraphy has tapered ends, no caps needed
} else {
result = pathRenderer_->drawVariableWidthPathIncremental(
surfaceCanvas, segment, paint,
lastLeftEdge_, lastRightEdge_, isFirstSegment);
// Draw start cap on first segment
if (isFirstSegment) {
pathRenderer_->drawVariableWidthStartCap(surfaceCanvas, segment, paint);
{
SkAutoCanvasRestore acr(surfaceCanvas, true);
applySurfaceTransform(surfaceCanvas);
if (toolType == "crayon") {
result = pathRenderer_->drawCrayonPathIncremental(
surfaceCanvas, segment, paint,
lastLeftEdge_, lastRightEdge_, isFirstSegment);
// Draw start cap on first segment
if (isFirstSegment) {
pathRenderer_->drawCrayonStartCap(surfaceCanvas, segment, paint);
}
} else {
result = pathRenderer_->drawVariableWidthPathIncremental(
surfaceCanvas, segment, paint,
lastLeftEdge_, lastRightEdge_, isFirstSegment);
// Draw start cap on first segment
if (isFirstSegment) {
pathRenderer_->drawVariableWidthStartCap(surfaceCanvas, segment, paint);
}
}
}

Expand All @@ -112,9 +178,7 @@ void ActiveStrokeRenderer::renderIncremental(
}

// Draw cached portion to output canvas
if (cachedActiveSnapshot_) {
canvas->drawImage(cachedActiveSnapshot_, 0, 0);
}
drawSnapshot(canvas);

// Draw "tail" - recent points not yet finalized
if (points.size() > lastRenderedInputIndex_) {
Expand All @@ -133,10 +197,6 @@ void ActiveStrokeRenderer::renderIncremental(
// drawCrayonPathTail already draws end cap
pathRenderer_->drawCrayonPathTail(canvas, tail, paint,
lastLeftEdge_, lastRightEdge_, hasLastEdge_);
} else if (toolType == "calligraphy") {
// Calligraphy has tapered ends, no caps needed
pathRenderer_->drawCalligraphyPathTail(canvas, tail, paint,
lastLeftEdge_, lastRightEdge_, hasLastEdge_, lastHalfWidth_);
} else {
pathRenderer_->drawVariableWidthPathTail(canvas, tail, paint,
lastLeftEdge_, lastRightEdge_, hasLastEdge_);
Expand Down Expand Up @@ -168,27 +228,56 @@ void ActiveStrokeRenderer::renderFinalTail(

SkCanvas* surfaceCanvas = activeStrokeSurface_->getCanvas();

if (toolType == "crayon") {
pathRenderer_->drawCrayonPathIncremental(
surfaceCanvas, finalTail, paint,
lastLeftEdge_, lastRightEdge_, !hasLastEdge_);
// Only draw end cap - start cap was already drawn during incremental rendering
pathRenderer_->drawCrayonEndCap(surfaceCanvas, points, paint);
} else if (toolType == "calligraphy") {
pathRenderer_->drawCalligraphyPathIncremental(
surfaceCanvas, finalTail, paint,
lastLeftEdge_, lastRightEdge_, !hasLastEdge_,
lastHalfWidth_);
// Calligraphy has tapered ends, no caps needed
} else {
pathRenderer_->drawVariableWidthPathIncremental(
surfaceCanvas, finalTail, paint,
lastLeftEdge_, lastRightEdge_, !hasLastEdge_);
// Only draw end cap - start cap was already drawn during incremental rendering
pathRenderer_->drawVariableWidthEndCap(surfaceCanvas, points, paint);
{
SkAutoCanvasRestore acr(surfaceCanvas, true);
applySurfaceTransform(surfaceCanvas);
if (toolType == "crayon") {
pathRenderer_->drawCrayonPathIncremental(
surfaceCanvas, finalTail, paint,
lastLeftEdge_, lastRightEdge_, !hasLastEdge_);
// Only draw end cap - start cap was already drawn during incremental rendering
pathRenderer_->drawCrayonEndCap(surfaceCanvas, points, paint);
} else if (toolType == "calligraphy") {
// Calligraphy renders the whole stroke here (its live preview is a
// direct full redraw), so there is no carried half-width; -1 selects
// the default baseWidth/2. Tapered ends, no caps needed.
pathRenderer_->drawCalligraphyPathIncremental(
surfaceCanvas, finalTail, paint,
lastLeftEdge_, lastRightEdge_, !hasLastEdge_,
-1.0f);
} else {
pathRenderer_->drawVariableWidthPathIncremental(
surfaceCanvas, finalTail, paint,
lastLeftEdge_, lastRightEdge_, !hasLastEdge_);
// Only draw end cap - start cap was already drawn during incremental rendering
pathRenderer_->drawVariableWidthEndCap(surfaceCanvas, points, paint);
}
}

cachedActiveSnapshot_ = activeStrokeSurface_->makeImageSnapshot();
}

void ActiveStrokeRenderer::drawSnapshot(SkCanvas* canvas) const {
if (!canvas || !cachedActiveSnapshot_) {
return;
}

if (viewportScale_ <= kIdentityRenderScaleThreshold) {
canvas->drawImage(cachedActiveSnapshot_, 0, 0);
return;
}

const SkRect dst = SkRect::MakeXYWH(
viewportOriginX_,
viewportOriginY_,
static_cast<float>(logicalWidth_) / viewportScale_,
static_cast<float>(logicalHeight_) / viewportScale_
);
canvas->drawImageRect(
cachedActiveSnapshot_,
dst,
SkSamplingOptions(SkFilterMode::kLinear)
);
}

} // namespace nativedrawing
45 changes: 40 additions & 5 deletions cpp/ActiveStrokeRenderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,29 @@ namespace nativedrawing {

class PathRenderer;

/**
* Viewport the active stroke is being drawn in, in logical (document)
* coordinates. When scale > 1 the preview surface is anchored to this
* region so the magnified stroke stays crisp without growing the surface.
*/
struct ActiveStrokeViewport {
float scale = 1.0f;
float left = 0.0f;
float top = 0.0f;
float width = 0.0f; // <= 0 means the full canvas is visible
float height = 0.0f; // <= 0 means the full canvas is visible
};

/**
* ActiveStrokeRenderer - Handles incremental O(1) rendering of active strokes
*
* Extracted from SkiaDrawingEngine for maintainability.
* Implements surface caching and incremental rendering to maintain 60-120fps
* during stroke input, regardless of stroke complexity.
*
* The surface is always allocated at the engine's logical dimensions. When
* the viewport is zoomed, the surface covers only the visible region at
* magnification instead of the whole canvas, keeping memory constant.
*/
class ActiveStrokeRenderer {
public:
Expand All @@ -32,16 +49,18 @@ class ActiveStrokeRenderer {
/**
* Render the active stroke incrementally to the output canvas
*
* @param canvas Output canvas to draw to
* @param canvas Output canvas to draw to (already scaled to the viewport)
* @param points Current stroke points
* @param paint Paint to use for rendering
* @param toolType Current tool type (pen, crayon, etc.)
* @param viewport Visible region and zoom the stroke is drawn in
*/
void renderIncremental(
SkCanvas* canvas,
const std::vector<Point>& points,
const SkPaint& paint,
const std::string& toolType
const std::string& toolType,
const ActiveStrokeViewport& viewport
);

/**
Expand All @@ -55,9 +74,15 @@ class ActiveStrokeRenderer {
);

/**
* Get the cached active stroke image for compositing
* Draw the cached active stroke onto a canvas in logical coordinates.
*/
sk_sp<SkImage> getSnapshot() const { return cachedActiveSnapshot_; }
void drawSnapshot(SkCanvas* canvas) const;

/**
* Effective magnification the preview surface is currently anchored at.
* 1.0 means the surface maps 1:1 onto the full canvas.
*/
float viewportScale() const { return viewportScale_; }

/**
* Get the last rendered input index
Expand All @@ -66,6 +91,11 @@ class ActiveStrokeRenderer {

private:
PathRenderer* pathRenderer_;
int logicalWidth_;
int logicalHeight_;
float viewportScale_ = 1.0f;
float viewportOriginX_ = 0.0f;
float viewportOriginY_ = 0.0f;

// Active stroke surface for incremental rendering
sk_sp<SkSurface> activeStrokeSurface_;
Expand All @@ -76,7 +106,12 @@ class ActiveStrokeRenderer {
std::vector<Point> overlapBuffer_;
SkPoint lastLeftEdge_, lastRightEdge_;
bool hasLastEdge_;
float lastHalfWidth_; // For calligraphy width continuity

void ensureViewportAlignment(const ActiveStrokeViewport& viewport);
// Maps logical coordinates onto the viewport-anchored surface. Callers
// wrap the call in SkAutoCanvasRestore.
void applySurfaceTransform(SkCanvas* surfaceCanvas) const;
void resetIncrementalState();

static constexpr size_t OVERLAP = 2; // Spline overlap for Catmull-Rom
};
Expand Down
18 changes: 0 additions & 18 deletions cpp/DrawingHistory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,6 @@ void commitStrokeDelta(
}
}

void appendPixelEraseCircleToDelta(
std::vector<StrokeDelta::PixelEraseEntry>& pendingPixelEraseEntries,
size_t strokeIndex,
const EraserCircle& circle
) {
for (auto& entry : pendingPixelEraseEntries) {
if (entry.strokeIndex == strokeIndex) {
entry.addedCircles.push_back(circle);
return;
}
}

StrokeDelta::PixelEraseEntry entry;
entry.strokeIndex = strokeIndex;
entry.addedCircles.push_back(circle);
pendingPixelEraseEntries.push_back(std::move(entry));
}

void applyStrokeDelta(
const StrokeDelta& delta,
std::vector<Stroke>& strokes,
Expand Down
6 changes: 0 additions & 6 deletions cpp/DrawingHistory.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,6 @@ void commitStrokeDelta(
size_t maxHistoryEntries
);

void appendPixelEraseCircleToDelta(
std::vector<StrokeDelta::PixelEraseEntry>& pendingPixelEraseEntries,
size_t strokeIndex,
const EraserCircle& circle
);

void applyStrokeDelta(
const StrokeDelta& delta,
std::vector<Stroke>& strokes,
Expand Down
Loading
Loading