648 lines
23 KiB
C++

/******************************************************************************
* Copyright (c) 2018(-2023) STMicroelectronics.
* All rights reserved.
*
* This file is part of the TouchGFX 4.21.2 distribution.
*
* This software is licensed under terms that can be found in the LICENSE file in
* the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
*******************************************************************************/
#include <math.h>
#include <touchgfx/widgets/canvas/CWRVectorRenderer.hpp>
namespace touchgfx
{
void CWRVectorRenderer::setup(const Widget& renderer, const Rect& drawingArea)
{
drawArea = drawingArea;
proxyWidget.setPosition(renderer);
proxyWidget.setParent(renderer.getParent());
// Clear transformation matrix
matrix.reset();
// Alpha reset
alpha = 255;
colorAlpha = 255;
// Stroke reset
strokeWidth = 1.0f;
strokeLineJoin = VG_STROKE_LINEJOIN_MITER;
strokeLineCap = VG_STROKE_LINECAP_BUTT;
}
void CWRVectorRenderer::tearDown()
{
// Clear drawing area to avoid drawing any paths until next setup
drawArea = Rect();
// Wait for the painter to finish
if (proxyWidget.getPainter())
{
proxyWidget.getPainter()->tearDown();
}
}
void CWRVectorRenderer::drawPath(const uint8_t* cmds, uint32_t nCmds, const float* points, uint32_t nPoints, const float* /* bbox */)
{
// Draw the path. Try reduced areas until success
Rect area = drawArea;
if (area.isEmpty())
{
return;
}
const int16_t bottom = area.bottom();
while (area.y < bottom)
{
while (!drawPathArea(cmds, nCmds, points, nPoints, area))
{
if (area.height == 1)
{
// Failed on a single line
break;
}
area.height = (area.height + 1) >> 1; // Cannot become 0 as (2+1)>>1=1
}
area.y += area.height;
if (area.bottom() > bottom)
{
area.height = bottom - area.y;
}
}
}
bool CWRVectorRenderer::drawPathArea(const uint8_t* cmds, uint32_t nCmds, const float* points, uint32_t nPoints, const Rect& area) const
{
assert(cmds);
assert(points);
if (drawMode != STROKE)
{
return drawFill(cmds, nCmds, points, nPoints, area);
}
return drawStroke(cmds, nCmds, points, nPoints, area);
}
bool CWRVectorRenderer::drawFill(const uint8_t* cmds, uint32_t nCmds, const float* points, uint32_t nPoints, const Rect& area) const
{
(void)nPoints;
uint32_t cmdInx = 0;
uint32_t pointInx = 0;
Canvas canvas(&proxyWidget, area);
canvas.setFillingRule((drawMode == FILL_EVEN_ODD) ? Rasterizer::FILL_EVEN_ODD : Rasterizer::FILL_NON_ZERO);
float positionX = 0.0f;
float positionY = 0.0f;
Matrix3x3::Point transformedPosition = matrix.affineTransform(positionX, positionY);
while (cmdInx < nCmds)
{
switch (static_cast<VectorPrimitives>(cmds[cmdInx]))
{
case VECTOR_PRIM_CLOSE:
assert(pointInx <= nPoints);
canvas.close();
break;
case VECTOR_PRIM_MOVE:
assert(pointInx + 2 <= nPoints);
positionX = points[pointInx];
positionY = points[pointInx + 1];
transformedPosition = matrix.affineTransform(positionX, positionY);
canvas.moveTo(transformedPosition.x, transformedPosition.y);
pointInx += 2;
break;
case VECTOR_PRIM_LINE:
assert(pointInx + 1 <= nPoints);
positionX = points[pointInx];
positionY = points[pointInx + 1];
transformedPosition = matrix.affineTransform(positionX, positionY);
canvas.lineTo(transformedPosition.x, transformedPosition.y);
pointInx += 2;
break;
case VECTOR_PRIM_HLINE:
assert(pointInx + 1 <= nPoints);
positionX = points[pointInx];
transformedPosition = matrix.affineTransform(positionX, positionY);
canvas.lineTo(transformedPosition.x, transformedPosition.y);
pointInx += 1;
break;
case VECTOR_PRIM_VLINE:
assert(pointInx + 1 <= nPoints);
positionY = points[pointInx];
transformedPosition = matrix.affineTransform(positionX, positionY);
canvas.lineTo(transformedPosition.x, transformedPosition.y);
pointInx += 1;
break;
case VECTOR_PRIM_BEZIER_QUAD:
assert(pointInx + 4 <= nPoints);
positionX = points[pointInx + 2];
positionY = points[pointInx + 3];
{
const Matrix3x3::Point p1 = matrix.affineTransform(points[pointInx], points[pointInx + 1]);
const Matrix3x3::Point p2 = matrix.affineTransform(positionX, positionY);
canvas.quadraticBezierTo(transformedPosition.x, transformedPosition.y, p1.x, p1.y, p2.x, p2.y);
transformedPosition = p2;
}
pointInx += 4;
break;
case VECTOR_PRIM_BEZIER_CUBIC:
assert(pointInx + 6 <= nPoints);
positionX = points[pointInx + 4];
positionY = points[pointInx + 5];
{
const Matrix3x3::Point p1 = matrix.affineTransform(points[pointInx], points[pointInx + 1]);
const Matrix3x3::Point p2 = matrix.affineTransform(points[pointInx + 2], points[pointInx + 3]);
const Matrix3x3::Point p3 = matrix.affineTransform(positionX, positionY);
canvas.cubicBezierTo(transformedPosition.x, transformedPosition.y, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
transformedPosition = p3;
}
pointInx += 6;
break;
}
cmdInx++;
}
return canvas.render(LCD::div255(colorAlpha * alpha));
}
bool CWRVectorRenderer::drawStroke(const uint8_t* cmds, uint32_t nCmds, const float* points, uint32_t nPoints, const Rect& area) const
{
(void)nPoints;
uint32_t cmdInx = 0;
uint32_t pointInx = 0;
if (strokeWidth <= 0.0f)
{
return true;
}
StrokeCanvas canvas(&proxyWidget, area, matrix);
canvas.setStroke(strokeWidth, strokeMiterLimit, strokeLineJoin, strokeLineCap, LCD::div255(colorAlpha * alpha));
float positionX = 0.0f;
float positionY = 0.0f;
float initialShapeX = 0.0f;
float initialShapeY = 0.0f;
uint32_t cmdInxPathStart = 0;
while (cmdInx < nCmds)
{
assert(static_cast<VectorPrimitives>(cmds[cmdInx]) == VECTOR_PRIM_CLOSE || pointInx < nPoints);
switch (static_cast<VectorPrimitives>(cmds[cmdInx]))
{
case VECTOR_PRIM_CLOSE:
positionX = initialShapeX;
positionY = initialShapeY;
if (cmdInxPathStart < cmdInx) // A single "CLOSE" on it's own does nothing
{
// In case we are not back at the beginning, draw a line to that point
canvas.strokeLineTo(positionX, positionY);
// Check if it a zero-length closed line and draw a double linecap
if (!canvas.strokeDrawDoubleLineCap())
{
// The line has some extend - close the outside
canvas.strokeClose(positionX, positionY);
// Move back to the start
canvas.strokeMoveTo(positionX, positionY);
// Draw inside backwards without linecaps(false)
drawStrokeBackwards(cmdInxPathStart, cmdInx + 1, pointInx, cmds, points, false, canvas);
// Close the inside of the stroke
canvas.strokeClose(positionX, positionY);
}
}
// Next path starts after the CLOSE command
cmdInxPathStart = cmdInx + 1;
break;
case VECTOR_PRIM_MOVE:
assert(pointInx + 2 <= nPoints);
if (cmdInxPathStart < cmdInx) // More than a singe "MOVE"
{
// Check if it a zero-length line and draw a double linecap
if (!canvas.strokeDrawDoubleLineCap())
{
// The line has some extend, follow the commands backwards and draw linecaps(true)
drawStrokeBackwards(cmdInxPathStart, cmdInx, pointInx, cmds, points, true, canvas);
}
}
cmdInxPathStart = cmdInx;
initialShapeX = positionX = points[pointInx];
initialShapeY = positionY = points[pointInx + 1];
canvas.strokeMoveTo(positionX, positionY);
pointInx += 2;
break;
case VECTOR_PRIM_LINE:
assert(pointInx + 2 <= nPoints);
positionX = points[pointInx];
positionY = points[pointInx + 1];
canvas.strokeLineTo(positionX, positionY);
pointInx += 2;
break;
case VECTOR_PRIM_HLINE:
assert(pointInx + 1 <= nPoints);
positionX = points[pointInx];
canvas.strokeLineTo(positionX, positionY);
pointInx += 1;
break;
case VECTOR_PRIM_VLINE:
assert(pointInx + 1 <= nPoints);
positionY = points[pointInx];
canvas.strokeLineTo(positionX, positionY);
pointInx += 1;
break;
case VECTOR_PRIM_BEZIER_QUAD:
assert(pointInx + 4 <= nPoints);
canvas.strokeBezierQuad(positionX, positionY, points[pointInx], points[pointInx + 1], points[pointInx + 2], points[pointInx + 3]);
positionX = points[pointInx + 2];
positionY = points[pointInx + 3];
pointInx += 4;
break;
case VECTOR_PRIM_BEZIER_CUBIC:
assert(pointInx + 6 <= nPoints);
canvas.strokeBezierCubic(positionX, positionY, points[pointInx], points[pointInx + 1], points[pointInx + 2], points[pointInx + 3], points[pointInx + 4], points[pointInx + 5]);
positionX = points[pointInx + 4];
positionY = points[pointInx + 5];
pointInx += 6;
break;
}
cmdInx++;
}
if (cmdInxPathStart < cmdInx) // More than a singe "MOVE"
{
// Check if it a zero-length line and draw a double linecap
if (!canvas.strokeDrawDoubleLineCap())
{
// The line has some extend, follow the commands backwards and draw linecaps(true)
drawStrokeBackwards(cmdInxPathStart, cmdInx, pointInx, cmds, points, true, canvas);
}
}
return canvas.strokeRender();
}
void CWRVectorRenderer::drawStrokeBackwards(uint32_t cmdInxPathStart, uint32_t cmdInx, uint32_t pointInx, const uint8_t* cmds, const float* points, bool drawLineCaps, StrokeCanvas& canvas) const
{
float positionX = 0.0f;
float positionY = 0.0f;
canvas.lineCapInsteadOfLineJoin(drawLineCaps);
while (cmdInx > cmdInxPathStart)
{
assert(cmdInx > 0);
cmdInx--;
switch (static_cast<VectorPrimitives>(cmds[cmdInx]))
{
case VECTOR_PRIM_CLOSE:
getPreviousDestination(positionX, positionY, cmdInx, pointInx, cmds, points);
canvas.strokeLineTo(positionX, positionY);
break;
case VECTOR_PRIM_MOVE:
assert(cmdInx == cmdInxPathStart);
break;
case VECTOR_PRIM_LINE:
assert(pointInx >= 2);
pointInx -= 2;
getPreviousDestination(positionX, positionY, cmdInx, pointInx, cmds, points);
canvas.strokeLineTo(positionX, positionY);
break;
case VECTOR_PRIM_HLINE:
case VECTOR_PRIM_VLINE:
assert(pointInx >= 1);
pointInx -= 1;
getPreviousDestination(positionX, positionY, cmdInx, pointInx, cmds, points);
canvas.strokeLineTo(positionX, positionY);
break;
case VECTOR_PRIM_BEZIER_QUAD:
assert(pointInx >= 4);
pointInx -= 4;
getPreviousDestination(positionX, positionY, cmdInx, pointInx, cmds, points);
canvas.strokeBezierQuad(points[pointInx + 2], points[pointInx + 3],
points[pointInx], points[pointInx + 1],
positionX, positionY);
break;
case VECTOR_PRIM_BEZIER_CUBIC:
assert(pointInx >= 6);
pointInx -= 6;
getPreviousDestination(positionX, positionY, cmdInx, pointInx, cmds, points);
canvas.strokeBezierCubic(points[pointInx + 4], points[pointInx + 5],
points[pointInx + 2], points[pointInx + 3],
points[pointInx], points[pointInx + 1],
positionX, positionY);
break;
}
}
if (drawLineCaps)
{
canvas.drawFinalLineCap();
}
}
void CWRVectorRenderer::getPreviousDestination(float& positionX, float& positionY, uint32_t cmdInx, uint32_t pointInx, const uint8_t* cmds, const float* points) const
{
bool foundX = false;
bool foundY = false;
while (cmdInx > 0)
{
cmdInx--;
switch (static_cast<VectorPrimitives>(cmds[cmdInx]))
{
case VECTOR_PRIM_CLOSE:
break;
case VECTOR_PRIM_MOVE:
case VECTOR_PRIM_LINE:
case VECTOR_PRIM_BEZIER_QUAD:
case VECTOR_PRIM_BEZIER_CUBIC:
assert(pointInx >= 2);
pointInx -= 2;
if (!foundX)
{
positionX = points[pointInx];
}
if (!foundY)
{
positionY = points[pointInx + 1];
}
return;
case VECTOR_PRIM_HLINE:
assert(pointInx >= 1);
pointInx -= 1;
if (!foundX)
{
positionX = points[pointInx];
}
if (foundY)
{
return;
}
foundX = true;
break;
case VECTOR_PRIM_VLINE:
assert(pointInx >= 1);
pointInx -= 1;
if (!foundY)
{
positionY = points[pointInx];
}
if (foundX)
{
return;
}
foundY = true;
break;
}
}
if (!foundX)
{
positionX = 0.0f;
}
if (!foundY)
{
positionY = 0.0f;
}
}
void CWRVectorRenderer::setStrokeWidth(float w)
{
strokeWidth = w;
}
void CWRVectorRenderer::setColor(colortype c)
{
// Save color alpha for rendering
colorAlpha = c >> 24;
getColorPainterColor().setColor(c);
proxyWidget.setPainter(getColorPainter());
}
void CWRVectorRenderer::setAlpha(uint8_t a)
{
alpha = a;
}
void CWRVectorRenderer::setLinearGradient(float x0, float y0, float x1, float y1,
uint32_t stops,
const float* /* stopPositions */,
const uint32_t* stopColors,
float width,
float height,
bool isSolid,
const uint32_t* palette)
{
// If 'x1' = 'x2' and 'y1' = 'y2', then the area to be painted shall be painted
// as a single color using the color and opacity of the last gradient stop.
if ((x0 - x1) == 0.0f && (y0 - y1) == 0.0f)
{
setColor(stopColors[stops - 1]);
return;
}
// Make colorAlpha neutral
colorAlpha = 255;
AbstractPainterLinearGradient& linearPainter = getLinearPainter();
linearPainter.setGradientEndPoints(x0, y0, x1, y1, width, height, matrix);
assert(palette && "A gradient palette is required by CWRVectorRenderer");
linearPainter.setGradientTexture(palette, isSolid);
proxyWidget.setPainter(linearPainter);
}
void CWRVectorRenderer::setRadialGradient(float /* cx */, float /* cy */, float /* radius */,
uint32_t /* stops */,
const float* /* stopPositions */,
const colortype* /* stopColors */)
{
assert(!"CWRVectorRenderer does not support radial gradient!");
}
void CWRVectorRenderer::setTransformationMatrix(const Matrix3x3& m)
{
matrix = m;
}
void CWRVectorRenderer::StrokeCanvas::strokeMoveTo(float x, float y)
{
lastX = x;
lastY = y;
firstLineTo = true;
noLineHasBeenDrawn = true;
}
void CWRVectorRenderer::StrokeCanvas::strokeLineTo(float x, float y)
{
float dx = x - lastX;
float dy = y - lastY;
float len = sqrtf(dx * dx + dy * dy);
if (len > 0.0f)
{
noLineHasBeenDrawn = false;
len /= strokeWidthHalf;
const float thisStrokeStartX = lastX + dy / len;
const float thisStrokeStartY = lastY - dx / len;
const float thisStrokeEndX = x + dy / len;
const float thisStrokeEndY = y - dx / len;
if (firstLineTo)
{
initialStrokeStartX = thisStrokeStartX;
initialStrokeStartY = thisStrokeStartY;
initialStrokeEndX = thisStrokeEndX;
initialStrokeEndY = thisStrokeEndY;
moveToWithTransform(thisStrokeStartX, thisStrokeStartY);
firstLineTo = false;
}
else if (drawLineCapInsteadOfLineJoin)
{
strokeDrawLineCap(lastStrokeEndX, lastStrokeEndY, thisStrokeStartX, thisStrokeStartY);
drawLineCapInsteadOfLineJoin = false;
}
else
{
strokeDrawLineJoin(lastX, lastY, lastStrokeStartX, lastStrokeStartY,
lastStrokeEndX, lastStrokeEndY, thisStrokeStartX, thisStrokeStartY, thisStrokeEndX, thisStrokeEndY);
}
lastStrokeStartX = thisStrokeStartX;
lastStrokeStartY = thisStrokeStartY;
lastStrokeEndX = thisStrokeEndX;
lastStrokeEndY = thisStrokeEndY;
lastX = x;
lastY = y;
}
}
bool CWRVectorRenderer::StrokeCanvas::strokeClose(float x, float y)
{
if (strokeDrawDoubleLineCap())
{
return false; // Created two line-caps, stop drawing, we are done
}
strokeDrawLineJoin(x, y, lastStrokeStartX, lastStrokeStartY,
lastStrokeEndX, lastStrokeEndY, initialStrokeStartX, initialStrokeStartY, initialStrokeEndX, initialStrokeEndY);
return true; // Closed as with a line-join, continue drawing on inside
}
bool CWRVectorRenderer::StrokeCanvas::strokeDrawDoubleLineCap()
{
if (noLineHasBeenDrawn)
{
const float y1 = lastY - strokeWidthHalf;
const float y2 = lastY + strokeWidthHalf;
moveToWithTransform(lastX, y1);
strokeDrawLineCap(lastX, y1, lastX, y2);
strokeDrawLineCap(lastX, y2, lastX, y1);
return true;
}
return false;
}
void CWRVectorRenderer::StrokeCanvas::lineCapInsteadOfLineJoin(bool drawLineCaps)
{
drawLineCapInsteadOfLineJoin = drawLineCaps;
}
void CWRVectorRenderer::StrokeCanvas::drawFinalLineCap()
{
strokeDrawLineCap(lastStrokeEndX, lastStrokeEndY, initialStrokeStartX, initialStrokeStartY);
}
void CWRVectorRenderer::StrokeCanvas::strokeDrawLineJoin(float x, float y, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4)
{
if (strokeLineJoin == VG_STROKE_LINEJOIN_ROUND)
{
lineToWithTransform(x2, y2);
// Same formula as used below for MITER
if ((x1 - x2) * (y3 - y4) >= (y1 - y2) * (x3 - x4))
{
strokeDrawLineJoinRound(x, y, x2, y2, x3, y3, 1);
}
lineToWithTransform(x3, y3);
return;
}
if (strokeLineJoin == VG_STROKE_LINEJOIN_MITER)
{
// https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line
const float divisor = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
// If divisor>0, we are on the "outside" of the angle
// If divisor<0, we are on the "inside" of the angle
if (divisor != 0.0f)
{
const float miterX = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / divisor;
const float miterY = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / divisor;
const float distSquared = (x - miterX) * (x - miterX) + (y - miterY) * (y - miterY);
if (distSquared <= miterLimitSquared)
{
lineToWithTransform(miterX, miterY);
return;
}
}
}
// VG_STROKE_LINEJOIN_BEVEL (and fallback for VG_STROKE_LINEJOIN_MITER)
lineToWithTransform(x2, y2);
lineToWithTransform(x3, y3);
}
void CWRVectorRenderer::StrokeCanvas::strokeDrawLineJoinRound(float x, float y, float x2, float y2, float x3, float y3, int level)
{
assert(level >= 1 && level <= 3);
float midX = (x2 + x3) / 2.0f;
float midY = (y2 + y3) / 2.0f;
const float midLen = sqrtf((x - midX) * (x - midX) + (y - midY) * (y - midY));
if (midLen == 0.0f)
{
const float dx = (x3 - x2) / 2.0f;
const float dy = (y3 - y2) / 2.0f;
midX = x2 + dx + dy;
midY = y2 + dy - dx;
}
else
{
midX = x + (midX - x) * strokeWidthHalf / midLen;
midY = y + (midY - y) * strokeWidthHalf / midLen;
}
if (level < 3)
{
strokeDrawLineJoinRound(x, y, x2, y2, midX, midY, level + 1);
lineToWithTransform(midX, midY);
strokeDrawLineJoinRound(x, y, midX, midY, x3, y3, level + 1);
}
else
{
lineToWithTransform(midX, midY);
}
}
void CWRVectorRenderer::StrokeCanvas::strokeDrawLineCap(float x2, float y2, float x3, float y3)
{
if (strokeLineCap == VG_STROKE_LINECAP_BUTT)
{
lineToWithTransform(x2, y2);
lineToWithTransform(x3, y3);
return;
}
const float dx = (x3 - x2) / 2.0f;
const float dy = (y3 - y2) / 2.0f;
// dx,dy is from (x2,y2) to the midpoint between (x2,y2) and (x3,y3)
// +dy,-dx is orthogonal and points in the direction of where the cap should be drawn
if (strokeLineCap == VG_STROKE_LINECAP_SQUARE)
{
lineToWithTransform(x2 + dy, y2 - dx);
lineToWithTransform(x3 + dy, y3 - dx);
return;
}
// VG_STROKE_LINECAP_ROUND:
// Using 3^2 + 4^2 = 5^2 as well as 7^2 + 24^2 = 25^2
lineToWithTransform(x2, y2);
lineToWithTransform(x2 + dx * 0.04f + dy * 0.28f, y2 + dy * 0.04f - dx * 0.28f);
lineToWithTransform(x2 + dx * 0.2f + dy * 0.6f, y2 + dy * 0.2f - dx * 0.6f);
lineToWithTransform(x2 + dx * 0.4f + dy * 0.8f, y2 + dy * 0.4f - dx * 0.8f);
lineToWithTransform(x2 + dx * 0.72f + dy * 0.96f, y2 + dy * 0.72f - dx * 0.96f);
lineToWithTransform(x2 + dx + dy, y2 + dy - dx);
lineToWithTransform(x2 + dx * 1.28f + dy * 0.96f, y2 + dy * 1.28f - dx * 0.96f);
lineToWithTransform(x2 + dx * 1.6f + dy * 0.8f, y2 + dy * 1.6f - dx * 0.8f);
lineToWithTransform(x2 + dx * 1.8f + dy * 0.6f, y2 + dy * 1.8f - dx * 0.6f);
lineToWithTransform(x2 + dx * 1.96f + dy * 0.28f, y2 + dy * 1.96f - dx * 0.28f);
lineToWithTransform(x3, y3);
}
} // namespace touchgfx