litehtml/containers/test/canvas_ity.hpp

3628 lines
144 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// canvas_ity v1.00 -- ISC license
// Copyright (c) 2022 Andrew Kensler
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// ======== ABOUT ========
//
// This is a tiny, single-header C++ library for rasterizing immediate-mode
// 2D vector graphics, closely modeled on the basic W3C (not WHATWG) HTML5 2D
// canvas specification (https://www.w3.org/TR/2015/REC-2dcontext-20151119/).
//
// The priorities for this library are high-quality rendering, ease of use,
// and compact size. Speed is important too, but secondary to the other
// priorities. Notably, this library takes an opinionated approach and
// does not provide options for trading off quality for speed.
//
// Despite its small size, it supports nearly everything listed in the W3C
// HTML5 2D canvas specification, except for hit regions and getting certain
// properties. The main differences lie in the surface-level API to make this
// easier for C++ use, while the underlying implementation is carefully based
// on the specification. In particular, stroke, fill, gradient, pattern,
// image, and font styles are specified slightly differently (avoiding strings
// and auxiliary classes). Nonetheless, the goal is that this library could
// produce a conforming HTML5 2D canvas implementation if wrapped in a thin
// layer of JavaScript bindings. See the accompanying C++ automated test
// suite and its HTML5 port for a mapping between the APIs and a comparison
// of this library's rendering output against browser canvas implementations.
// ======== FEATURES ========
//
// High-quality rendering:
//
// - Trapezoidal area antialiasing provides very smooth antialiasing, even
// when lines are nearly horizontal or vertical.
// - Gamma-correct blending, interpolation, and resampling are used
// throughout. It linearizes all colors and premultiplies alpha on
// input and converts back to unpremultiplied sRGB on output. This
// reduces muddiness on many gradients (e.g., red to green), makes line
// thicknesses more perceptually uniform, and avoids dark fringes when
// interpolating opacity.
// - Bicubic convolution resampling is used whenever it needs to resample a
// pattern or image. This smoothly interpolates with less blockiness when
// magnifying, and antialiases well when minifying. It can simultaneously
// magnify and minify along different axes.
// - Ordered dithering is used on output. This reduces banding on subtle
// gradients while still being compression-friendly.
// - High curvature is handled carefully in line joins. Thick lines are
// drawn correctly as though tracing with a wide pen nib, even where
// the lines curve sharply. (Simpler curve offsetting approaches tend
// to show bite-like artifacts in these cases.)
//
// Ease of use:
//
// - Provided as a single-header library with no dependencies beside the
// standard C++ library. There is nothing to link, and it even includes
// built-in binary parsing for TrueType font (TTF) files. It is pure CPU
// code and does not require a GPU or GPU context.
// - Has extensive Doxygen-style documentation comments for the public API.
// - Compiles cleanly at moderately high warning levels on most compilers.
// - Shares no internal pointers, nor holds any external pointers. Newcomers
// to C++ can have fun drawing with this library without worrying so much
// about resource lifetimes or mutability.
// - Uses no static or global variables. Threads may safely work with
// different canvas instances concurrently without locking.
// - Allocates no dynamic memory after reaching the high-water mark. Except
// for the pixel buffer, flat std::vector instances embedded in the canvas
// instance handle all dynamic memory. This reduces fragmentation and
// makes it easy to change the code to reserve memory up front or even
// to use statically allocated vectors.
// - Works with exceptions and RTTI disabled.
// - Intentionally uses a plain C++03 style to make it as widely portable
// as possible, easier to understand, and (with indexing preferred over
// pointer arithmetic) easier to port natively to other languages. The
// accompanying test suite may also help with porting.
//
// Compact size:
//
// - The source code for the entire library consists of a bit over 2300 lines
// (not counting comments or blanks), each no longer than 78 columns.
// Alternately measured, it has fewer than 1300 semicolons.
// - The object code for the library can be less than 36 KiB on x86-64 with
// appropriate compiler settings for size.
// - Due to the library's small size, the accompanying automated test suite
// achieves 100% line coverage of the library in gcov and llvm-cov.
// ======== LIMITATIONS ========
//
// - Trapezoidal antialiasing overestimates coverage where paths self-
// intersect within a single pixel. Where inner joins are visible, this
// can lead to a "grittier" appearance due to the extra windings used.
// - Clipping uses an antialiased sparse pixel mask rather than geometrically
// intersecting paths. Therefore, it is not subpixel-accurate.
// - Text rendering is extremely basic and mainly for convenience. It only
// supports left-to-right text, and does not do any hinting, kerning,
// ligatures, text shaping, or text layout. If you require any of those,
// consider using another library to provide those and feed the results
// to this library as either placed glyphs or raw paths.
// - TRUETYPE FONT PARSING IS NOT SECURE! It does some basic validity
// checking, but should only be used with known-good or sanitized fonts.
// - Parameter checking does not test for non-finite floating-point values.
// - Rendering is single-threaded, not explicitly vectorized, and not GPU-
// accelerated. It also copies data to avoid ownership issues. If you
// need the speed, you are better off using a more fully-featured library.
// - The library does no input or output on its own. Instead, you must
// provide it with buffers to copy into or out of.
// ======== USAGE ========
//
// This is a single-header library. You may freely include it in any of
// your source files to declare the canvas_ity namespace and its members.
// However, to get the implementation, you must
// #define CANVAS_ITY_IMPLEMENTATION
// in exactly one C++ file before including this header.
//
// Then, construct an instance of the canvas_ity::canvas class with the pixel
// dimensions that you want and draw into it using any of the various drawing
// functions. You can then use the get_image_data() function to retrieve the
// currently drawn image at any time.
//
// See each of the public member function and data member (i.e., method
// and field) declarations for the full API documentation. Also see the
// accompanying C++ automated test suite for examples of the usage of each
// public member, and the test suite's HTML5 port for how these map to the
// HTML5 canvas API.
#ifndef CANVAS_ITY_HPP
#define CANVAS_ITY_HPP
#include <cstddef>
#include <vector>
#include <optional>
namespace canvas_ity
{
// Public API enums
enum composite_operation {
source_in = 1, source_copy, source_out, destination_in,
destination_atop = 7,
// confusing name, 'lighter' should be called 'plus'
// https://www.w3.org/TR/compositing-1/#porterduffcompositingoperators_plus
// RED_out = ALPHA_src * RED_src + ALPHA_dst * RED_dst
// ALPHA_out = ALPHA_src + ALPHA_dst
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
// "Where both shapes overlap, the color is determined by adding color values."
lighter = 10,
destination_over, destination_out,
source_atop, source_over, exclusive_or };
enum cap_style { butt, square, circle };
enum join_style { miter, bevel, rounded };
enum brush_type { fill_style, stroke_style };
enum repetition_style { repeat, repeat_x, repeat_y, no_repeat };
enum align_style { leftward, rightward, center, start = 0, ending };
enum baseline_style {
alphabetic, top, middle, bottom, hanging, ideographic = 3 };
// Implementation details
struct xy { float x, y; xy(); xy( float, float ); };
struct rgba { float r, g, b, a; rgba(); rgba( float, float, float, float ); };
struct affine_matrix { float a, b, c, d, e, f; };
struct paint_brush { enum types { color, pattern, linear, radial, css_radial, conic } type;
std::vector< rgba > colors; std::vector< float > stops; std::vector< std::optional<float> > hints;
xy start, end; float start_radius, end_radius; xy css_radius; float angle;
int width, height; repetition_style repetition; };
struct font_face { std::vector< unsigned char > data;
int cmap, glyf, head, hhea, hmtx, loca, maxp, os_2;
float scale; };
struct subpath_data { size_t count; bool closed; };
struct bezier_path { std::vector< xy > points;
std::vector< subpath_data > subpaths; };
struct line_path { std::vector< xy > points;
std::vector< subpath_data > subpaths; };
struct pixel_run { unsigned short x, y; float delta; };
typedef std::vector< pixel_run > pixel_runs;
class canvas
{
public:
// ======== LIFECYCLE ========
/// @brief Construct a new canvas.
///
/// It will begin with all pixels set to transparent black. Initially,
/// the visible coordinates will run from (0, 0) in the upper-left to
/// (width, height) in the lower-right and with pixel centers offset
/// (0.5, 0.5) from the integer grid, though all this may be changed
/// by transforms. The sizes must be between 1 and 32768, inclusive.
///
/// @param width horizontal size of the new canvas in pixels
/// @param height vertical size of the new canvas in pixels
///
canvas(
int width,
int height );
canvas(
int width,
int height,
rgba color);
canvas() : canvas(0, 0) {}
/// @brief Destroy the canvas and release all associated memory.
///
~canvas();
// ======== TRANSFORMS ========
/// @brief Scale the current transform.
///
/// Scaling may be non-uniform if the x and y scaling factors are
/// different. When a scale factor is less than one, things will be
/// shrunk in that direction. When a scale factor is greater than
/// one, they will grow bigger. Negative scaling factors will flip or
/// mirror it in that direction. The scaling factors must be non-zero.
/// If either is zero, most drawing operations will do nothing.
///
/// @param x horizontal scaling factor
/// @param y vertical scaling factor
///
void scale(
float x,
float y );
/// @brief Rotate the current transform.
///
/// The rotation is applied clockwise in a direction around the origin.
///
/// Tip: to rotate around another point, first translate that point to
/// the origin, then do the rotation, and then translate back.
///
/// @param angle clockwise angle in radians
///
void rotate(
float angle );
/// @brief Translate the current transform.
///
/// By default, positive x values shift that many pixels to the right,
/// while negative y values shift left, positive y values shift up, and
/// negative y values shift down.
///
/// @param x amount to shift horizontally
/// @param y amount to shift vertically
///
void translate(
float x,
float y );
/// @brief Add an arbitrary transform to the current transform.
///
/// This takes six values for the upper two rows of a homogenous 3x3
/// matrix (i.e., {{a, c, e}, {b, d, f}, {0.0, 0.0, 1.0}}) describing an
/// arbitrary affine transform and appends it to the current transform.
/// The values can represent any affine transform such as scaling,
/// rotation, translation, or skew, or any composition of affine
/// transforms. The matrix must be invertible. If it is not, most
/// drawing operations will do nothing.
///
/// @param a horizontal scaling factor (m11)
/// @param b vertical skewing (m12)
/// @param c horizontal skewing (m21)
/// @param d vertical scaling factor (m22)
/// @param e horizontal translation (m31)
/// @param f vertical translation (m32)
///
void transform(
float a,
float b,
float c,
float d,
float e,
float f );
/// @brief Replace the current transform.
///
/// This takes six values for the upper two rows of a homogenous 3x3
/// matrix (i.e., {{a, c, e}, {b, d, f}, {0.0, 0.0, 1.0}}) describing
/// an arbitrary affine transform and replaces the current transform
/// with it. The values can represent any affine transform such as
/// scaling, rotation, translation, or skew, or any composition of
/// affine transforms. The matrix must be invertible. If it is not,
/// most drawing operations will do nothing.
///
/// Tip: to reset the current transform back to the default, use
/// 1.0, 0.0, 0.0, 1.0, 0.0, 0.0.
///
/// @param a horizontal scaling factor (m11)
/// @param b vertical skewing (m12)
/// @param c horizontal skewing (m21)
/// @param d vertical scaling factor (m22)
/// @param e horizontal translation (m31)
/// @param f vertical translation (m32)
///
void set_transform(
float a,
float b,
float c,
float d,
float e,
float f );
// ======== COMPOSITING ========
/// @brief Set the degree of opacity applied to all drawing operations.
///
/// If an operation already uses a transparent color, this can make it
/// yet more transparent. It must be in the range from 0.0 for fully
/// transparent to 1.0 for fully opaque, inclusive. If it is not, this
/// does nothing. Defaults to 1.0 (opaque).
///
/// @param alpha degree of opacity applied to all drawing operations
///
void set_global_alpha(
float alpha );
/// @brief Compositing operation for blending new drawing and old pixels.
///
/// The source_copy, source_in, source_out, destination_atop, and
/// destination_in operations may clear parts of the canvas outside the
/// new drawing but within the clip region. Defaults to source_over.
///
/// source_atop: Show new over old where old is opaque.
/// source_copy: Replace old with new.
/// source_in: Replace old with new where old was opaque.
/// source_out: Replace old with new where old was transparent.
/// source_over: Show new over old.
/// destination_atop: Show old over new where new is opaque.
/// destination_in: Clear old where new is transparent.
/// destination_out: Clear old where new is opaque.
/// destination_over: Show new under old.
/// exclusive_or: Show new and old but clear where both are opaque.
/// lighter: Sum the new with the old.
///
composite_operation global_composite_operation;
// ======== SHADOWS ========
/// @brief Set the color and opacity of the shadow.
///
/// Most drawing operations can optionally draw a blurred drop shadow
/// before doing the main drawing. The shadow is modulated by the opacity
/// of the drawing and will be blended into the existing pixels subject to
/// the compositing settings and clipping region. Shadows will only be
/// drawn if the shadow color has any opacity and the shadow is either
/// offset or blurred. The color and opacity values will be clamped to
/// the 0.0 to 1.0 range, inclusive. Defaults to 0.0, 0.0, 0.0, 0.0
/// (transparent black).
///
/// @param red sRGB red component of the shadow color
/// @param green sRGB green component of the shadow color
/// @param blue sRGB blue component of the shadow color
/// @param alpha opacity of the shadow (not premultiplied)
///
void set_shadow_color(
float red,
float green,
float blue,
float alpha );
/// @brief Horizontal offset of the shadow in pixels.
///
/// Negative shifts left, positive shifts right. This is not affected
/// by the current transform. Defaults to 0.0 (no offset).
///
float shadow_offset_x;
/// @brief Vertical offset of the shadow in pixels.
///
/// Negative shifts up, positive shifts down. This is not affected by
/// the current transform. Defaults to 0.0 (no offset).
///
float shadow_offset_y;
/// @brief Set the level of Gaussian blurring on the shadow.
///
/// Zero produces no blur, while larger values will blur the shadow
/// more strongly. This is not affected by the current transform.
/// Must be non-negative. If it is not, this does nothing. Defaults to
/// 0.0 (no blur).
///
/// @param level the level of Gaussian blurring on the shadow
///
void set_shadow_blur(
float level );
// ======== LINE STYLES ========
/// @brief Set the width of the lines when stroking.
///
/// Initially this is measured in pixels, though the current transform
/// at the time of drawing can affect this. Must be positive. If it
/// is not, this does nothing. Defaults to 1.0.
///
/// @param width width of the lines when stroking
///
void set_line_width(
float width );
/// @brief Cap style for the ends of open subpaths and dash segments.
///
/// The actual shape may be affected by the current transform at the time
/// of drawing. Only affects stroking. Defaults to butt.
///
/// butt: Use a flat cap flush to the end of the line.
/// square: Use a half-square cap that extends past the end of the line.
/// circle: Use a semicircular cap.
///
cap_style line_cap;
/// @brief Join style for connecting lines within the paths.
///
/// The actual shape may be affected by the current transform at the time
/// of drawing. Only affects stroking. Defaults to miter.
///
/// miter: Continue the ends until they intersect, if within miter limit.
/// bevel: Connect the ends with a flat triangle.
/// round: Join the ends with a circular arc.
///
join_style line_join;
/// @brief Set the limit on maximum pointiness allowed for miter joins.
///
/// If the distance from the point where the lines intersect to the
/// point where the outside edges of the join intersect exceeds this
/// ratio relative to the line width, then a bevel join will be used
/// instead, and the miter will be lopped off. Larger values allow
/// pointier miters. Only affects stroking and only when the line join
/// style is miter. Must be positive. If it is not, this does nothing.
/// Defaults to 10.0.
///
/// @param limit the limit on maximum pointiness allowed for miter joins
///
void set_miter_limit(
float limit );
/// @brief Offset where each subpath starts the dash pattern.
///
/// Changing this shifts the location of the dashes along the path and
/// animating it will produce a marching ants effect. Only affects
/// stroking and only when a dash pattern is set. May be negative or
/// exceed the length of the dash pattern, in which case it will wrap.
/// Defaults to 0.0.
///
float line_dash_offset;
/// @brief Set or clear the line dash pattern.
///
/// Takes an array with entries alternately giving the lengths of dash
/// and gap segments. All must be non-negative; if any are not, this
/// does nothing. These will be used to draw with dashed lines when
/// stroking, with each subpath starting at the length along the dash
/// pattern indicated by the line dash offset. Initially these lengths
/// are measured in pixels, though the current transform at the time of
/// drawing can affect this. The count must be non-negative. If it is
/// odd, the array will be appended to itself to make an even count. If
/// it is zero, or the pointer is null, the dash pattern will be cleared
/// and strokes will be drawn as solid lines. The array is copied and
/// it is safe to change or destroy it after this call. Defaults to
/// solid lines.
///
/// @param segments pointer to array for dash pattern
/// @param count number of entries in the array
///
void set_line_dash(
float const *segments,
int count );
// ======== FILL AND STROKE STYLES ========
/// @brief Set filling or stroking to use a constant color and opacity.
///
/// The color and opacity values will be clamped to the 0.0 to 1.0 range,
/// inclusive. Filling and stroking defaults a constant color with 0.0,
/// 0.0, 0.0, 1.0 (opaque black).
///
/// @param type whether to set the fill_style or stroke_style
/// @param red sRGB red component of the painting color
/// @param green sRGB green component of the painting color
/// @param blue sRGB blue component of the painting color
/// @param alpha opacity to paint with (not premultiplied)
///
void set_color(
brush_type type,
float red,
float green,
float blue,
float alpha );
void set_color(
brush_type type,
rgba c)
{
set_color(type, c.r, c.g, c.b, c.a);
}
/// @brief Set filling or stroking to use a linear gradient.
///
/// Positions the start and end points of the gradient and clears all
/// color stops to reset the gradient to transparent black. Color stops
/// can then be added again. When drawing, pixels will be painted with
/// the color of the gradient at the nearest point on the line segment
/// between the start and end points. This is affected by the current
/// transform at the time of drawing.
///
/// @param type whether to set the fill_style or stroke_style
/// @param start_x horizontal coordinate of the start of the gradient
/// @param start_y vertical coordinate of the start of the gradient
/// @param end_x horizontal coordinate of the end of the gradient
/// @param end_y vertical coordinate of the end of the gradient
///
void set_linear_gradient(
brush_type type,
float start_x,
float start_y,
float end_x,
float end_y );
/// @brief Set filling or stroking to use a radial gradient.
///
/// Positions the start and end circles of the gradient and clears all
/// color stops to reset the gradient to transparent black. Color stops
/// can then be added again. When drawing, pixels will be painted as
/// though the starting circle moved and changed size linearly to match
/// the ending circle, while sweeping through the colors of the gradient.
/// Pixels not touched by the moving circle will be left transparent
/// black. The radial gradient is affected by the current transform
/// at the time of drawing. The radii must be non-negative.
///
/// @param type whether to set the fill_style or stroke_style
/// @param start_x horizontal starting coordinate of the circle
/// @param start_y vertical starting coordinate of the circle
/// @param start_radius starting radius of the circle
/// @param end_x horizontal ending coordinate of the circle
/// @param end_y vertical ending coordinate of the circle
/// @param end_radius ending radius of the circle
///
void set_radial_gradient(
brush_type type,
float start_x,
float start_y,
float start_radius,
float end_x,
float end_y,
float end_radius );
void set_css_radial_gradient(
brush_type type,
float x,
float y,
float radius_x,
float radius_y);
void set_conic_gradient(
brush_type type,
float x,
float y,
float angle);
/// @brief Add a color stop to a linear or radial gradient.
///
/// Each color stop has an offset which defines its position from 0.0 at
/// the start of the gradient to 1.0 at the end. Colors and opacity are
/// linearly interpolated along the gradient between adjacent pairs of
/// stops without premultiplying the alpha. If more than one stop is
/// added for a given offset, the first one added is considered closest
/// to 0.0 and the last is closest to 1.0. If no stop is at offset 0.0
/// or 1.0, the stops with the closest offsets will be extended. If no
/// stops are added, the gradient will be fully transparent black. Set a
/// new linear or radial gradient to clear all the stops and redefine the
/// gradient colors. Color stops may be added to a gradient at any time.
/// The color and opacity values will be clamped to the 0.0 to 1.0 range,
/// inclusive. The offset must be in the 0.0 to 1.0 range, inclusive.
/// If it is not, or if chosen style type is not currently set to a
/// gradient, this does nothing.
///
/// @param type whether to add to the fill_style or stroke_style
/// @param offset position of the color stop along the gradient
/// @param red sRGB red component of the color stop
/// @param green sRGB green component of the color stop
/// @param blue sRGB blue component of the color stop
/// @param alpha opacity of the color stop (not premultiplied)
///
void add_color_stop(
brush_type type,
float offset,
float red,
float green,
float blue,
float alpha,
std::optional<float> hint = {} );
/// @brief Set filling or stroking to draw with an image pattern.
///
/// Initially, pixels in the pattern correspond exactly to pixels on the
/// canvas, with the pattern starting in the upper left. The pattern
/// is affected by the current transform at the time of drawing, and
/// the pattern will be resampled as needed (with the filtering always
/// wrapping regardless of the repetition setting). The pattern can be
/// repeated either horizontally, vertically, both, or neither, relative
/// to the source image. If the pattern is not repeated, then beyond it
/// will be considered transparent black. The pattern image, which should
/// be in top to bottom rows of contiguous pixels from left to right,
/// is copied and it is safe to change or destroy it after this call.
/// The width and height must both be positive. If either are not, or
/// the image pointer is null, this does nothing.
///
/// Tip: to use a small piece of a larger image, reduce the width and
/// height, and offset the image pointer while keeping the stride.
///
/// @param type whether to set the fill_style or stroke_style
/// @param image pointer to unpremultiplied sRGB RGBA8 image data
/// @param width width of the pattern image in pixels
/// @param height height of the pattern image in pixels
/// @param stride number of bytes between the start of each image row
/// @param repetition repeat, repeat_x, repeat_y, or no_repeat
///
void set_pattern(
brush_type type,
unsigned char const *image,
int width,
int height,
int stride,
repetition_style repetition );
// ======== BUILDING PATHS ========
/// @brief Reset the current path.
///
/// The current path and all subpaths will be cleared after this, and a
/// new path can be built.
///
void begin_path();
/// @brief Create a new subpath.
///
/// The given point will become the first point of the new subpath and
/// is subject to the current transform at the time this is called.
///
/// @param x horizontal coordinate of the new first point
/// @param y vertical coordinate of the new first point
///
void move_to(
float x,
float y );
/// @brief Close the current subpath.
///
/// Adds a straight line from the end of the current subpath back to its
/// first point and marks the subpath as closed so that this line will
/// join with the beginning of the path at this point. A new, empty
/// subpath will be started beginning with the same first point. If the
/// current path is empty, this does nothing.
///
void close_path();
/// @brief Extend the current subpath with a straight line.
///
/// The line will go from the current end point (if the current path is
/// not empty) to the given point, which will become the new end point.
/// Its position is affected by the current transform at the time that
/// this is called. If the current path was empty, this is equivalent
/// to just a move.
///
/// @param x horizontal coordinate of the new end point
/// @param y vertical coordinate of the new end point
///
void line_to(
float x,
float y );
/// @brief Extend the current subpath with a quadratic Bezier curve.
///
/// The curve will go from the current end point (or the control point
/// if the current path is empty) to the given point, which will become
/// the new end point. The curve will be tangent toward the control
/// point at both ends. The current transform at the time that this
/// is called will affect both points passed in.
///
/// Tip: to make multiple curves join smoothly, ensure that each new end
/// point is on a line between the adjacent control points.
///
/// @param control_x horizontal coordinate of the control point
/// @param control_y vertical coordinate of the control point
/// @param x horizontal coordinate of the new end point
/// @param y vertical coordinate of the new end point
///
void quadratic_curve_to(
float control_x,
float control_y,
float x,
float y );
/// @brief Extend the current subpath with a cubic Bezier curve.
///
/// The curve will go from the current end point (or the first control
/// point if the current path is empty) to the given point, which will
/// become the new end point. The curve will be tangent toward the first
/// control point at the beginning and tangent toward the second control
/// point at the end. The current transform at the time that this is
/// called will affect all points passed in.
///
/// Tip: to make multiple curves join smoothly, ensure that each new end
/// point is on a line between the adjacent control points.
///
/// @param control_1_x horizontal coordinate of the first control point
/// @param control_1_y vertical coordinate of the first control point
/// @param control_2_x horizontal coordinate of the second control point
/// @param control_2_y vertical coordinate of the second control point
/// @param x horizontal coordinate of the new end point
/// @param y vertical coordinate of the new end point
///
void bezier_curve_to(
float control_1_x,
float control_1_y,
float control_2_x,
float control_2_y,
float x,
float y );
/// @brief Extend the current subpath with an arc tangent to two lines.
///
/// The arc is from the circle with the given radius tangent to both
/// the line from the current end point to the vertex, and to the line
/// from the vertex to the given point. A straight line will be added
/// from the current end point to the first tangent point (unless the
/// current path is empty), then the shortest arc from the first to the
/// second tangent points will be added. The second tangent point will
/// become the new end point. If the radius is large, these tangent
/// points may fall outside the line segments. The current transform
/// at the time that this is called will affect the passed in points
/// and the arc. If the current path was empty, this is equivalent to
/// a move. If the arc would be degenerate, it is equivalent to a line
/// to the vertex point. The radius must be non-negative. If it is not,
/// or if the current transform is not invertible, this does nothing.
///
/// Tip: to draw a polygon with rounded corners, call this once for each
/// vertex and pass the midpoint of the adjacent edge as the second
/// point; this works especially well for rounded boxes.
///
/// @param vertex_x horizontal coordinate where the tangent lines meet
/// @param vertex_y vertical coordinate where the tangent lines meet
/// @param x a horizontal coordinate on the second tangent line
/// @param y a vertical coordinate on the second tangent line
/// @param radius radius of the circle containing the arc
///
void arc_to(
float vertex_x,
float vertex_y,
float x,
float y,
float radius );
/// @brief Extend the current subpath with an arc between two angles.
///
/// The arc is from the circle centered at the given point and with the
/// given radius. A straight line will be added from the current end
/// point to the starting point of the arc (unless the current path is
/// empty), then the arc along the circle from the starting angle to the
/// ending angle in the given direction will be added. If they are more
/// than two pi radians apart in the given direction, the arc will stop
/// after one full circle. The point at the ending angle will become
/// the new end point of the path. Initially, the angles are clockwise
/// relative to the x-axis. The current transform at the time that
/// this is called will affect the passed in point, angles, and arc.
/// The radius must be non-negative.
///
/// @param x horizontal coordinate of the circle center
/// @param y vertical coordinate of the circle center
/// @param radius radius of the circle containing the arc
/// @param start_angle radians clockwise from x-axis to begin
/// @param end_angle radians clockwise from x-axis to end
/// @param counter_clockwise true if the arc turns counter-clockwise
///
void arc(
float x,
float y,
float radius,
float start_angle,
float end_angle,
bool counter_clockwise = false );
/// @brief Add a closed subpath in the shape of a rectangle.
///
/// The rectangle has one corner at the given point and then goes in the
/// direction along the width before going in the direction of the height
/// towards the opposite corner. The current transform at the time that
/// this is called will affect the given point and rectangle. The width
/// and/or the height may be negative or zero, and this can affect the
/// winding direction.
///
/// @param x horizontal coordinate of a rectangle corner
/// @param y vertical coordinate of a rectangle corner
/// @param width width of the rectangle
/// @param height height of the rectangle
///
void rectangle(
float x,
float y,
float width,
float height );
void polygon(std::vector<xy> points);
// ======== DRAWING PATHS ========
/// @brief Draw the interior of the current path using the fill style.
///
/// Interior pixels are determined by the non-zero winding rule, with
/// all open subpaths implicitly closed by a straight line beforehand.
/// If shadows have been enabled by setting the shadow color with any
/// opacity and either offsetting or blurring the shadows, then the
/// shadows of the filled areas will be drawn first, followed by the
/// filled areas themselves. Both will be blended into the canvas
/// according to the global alpha, the global compositing operation,
/// and the clip region. If the fill style is a gradient or a pattern,
/// it will be affected by the current transform. The current path is
/// left unchanged by filling; begin a new path to clear it. If the
/// current transform is not invertible, this does nothing.
///
void fill();
/// @brief Draw the edges of the current path using the stroke style.
///
/// Edges of the path will be expanded into strokes according to the
/// current dash pattern, dash offset, line width, line join style
/// (and possibly miter limit), line cap, and transform. If shadows
/// have been enabled by setting the shadow color with any opacity and
/// either offsetting or blurring the shadows, then the shadow will be
/// drawn for the stroked lines first, then the stroked lines themselves.
/// Both will be blended into the canvas according to the global alpha,
/// the global compositing operation, and the clip region. If the stroke
/// style is a gradient or a pattern, it will be affected by the current
/// transform. The current path is left unchanged by stroking; begin a
/// new path to clear it. If the current transform is not invertible,
/// this does nothing.
///
/// Tip: to draw with a calligraphy-like angled brush effect, add a
/// non-uniform scale transform just before stroking.
///
void stroke();
/// @brief Restrict the clip region by the current path.
///
/// Intersects the current clip region with the interior of the current
/// path (the region that would be filled), and replaces the current
/// clip region with this intersection. Subsequent calls to clip can
/// only reduce this further. When filling or stroking, only pixels
/// within the current clip region will change. The current path is
/// left unchanged by updating the clip region; begin a new path to
/// clear it. Defaults to the entire canvas.
///
/// Tip: to be able to reset the current clip region, save the canvas
/// state first before clipping then restore the state to reset it.
///
void clip();
/// @brief Tests whether a point is in or on the current path.
///
/// Interior areas are determined by the non-zero winding rule, with
/// all open subpaths treated as implicitly closed by a straight line
/// beforehand. Points exactly on the boundary are also considered
/// inside. The point to test is interpreted without being affected by
/// the current transform, nor is the clip region considered. The current
/// path is left unchanged by this test.
///
/// @param x horizontal coordinate of the point to test
/// @param y vertical coordinate of the point to test
/// @return true if the point is in or on the current path
///
bool is_point_in_path(
float x,
float y );
// ======== DRAWING RECTANGLES ========
/// @brief Clear a rectangular area back to transparent black.
///
/// The clip region may limit the area cleared. The current path is not
/// affected by this clearing. The current transform at the time that
/// this is called will affect the given point and rectangle. The width
/// and/or the height may be negative or zero. If either is zero, or the
/// current transform is not invertible, this does nothing.
///
/// @param x horizontal coordinate of a rectangle corner
/// @param y vertical coordinate of a rectangle corner
/// @param width width of the rectangle
/// @param height height of the rectangle
///
void clear_rectangle(
float x,
float y,
float width,
float height );
/// @brief Fill a rectangular area.
///
/// This behaves as though the current path were reset to a single
/// rectangle and then filled as usual. However, the current path is
/// not actually changed. The current transform at the time that this is
/// called will affect the given point and rectangle. The width and/or
/// the height may be negative but not zero. If either is zero, or the
/// current transform is not invertible, this does nothing.
///
/// @param x horizontal coordinate of a rectangle corner
/// @param y vertical coordinate of a rectangle corner
/// @param width width of the rectangle
/// @param height height of the rectangle
///
void fill_rectangle(
float x,
float y,
float width,
float height );
/// @brief Stroke a rectangular area.
///
/// This behaves as though the current path were reset to a single
/// rectangle and then stroked as usual. However, the current path is
/// not actually changed. The current transform at the time that this
/// is called will affect the given point and rectangle. The width
/// and/or the height may be negative or zero. If both are zero, or
/// the current transform is not invertible, this does nothing. If only
/// one is zero, this behaves as though it strokes a single horizontal or
/// vertical line.
///
/// @param x horizontal coordinate of a rectangle corner
/// @param y vertical coordinate of a rectangle corner
/// @param width width of the rectangle
/// @param height height of the rectangle
///
void stroke_rectangle(
float x,
float y,
float width,
float height );
// ======== DRAWING TEXT ========
/// @brief Horizontal position of the text relative to the anchor point.
///
/// When drawing text, the positioning of the text relative to the anchor
/// point includes the side bearings of the first and last glyphs.
/// Defaults to leftward.
///
/// leftward: Draw the text's left edge at the anchor point.
/// rightward: Draw the text's right edge at the anchor point.
/// center: Draw the text's horizontal center at the anchor point.
/// start: This is a synonym for leftward.
/// ending: This is a synonym for rightward.
///
align_style text_align;
/// @brief Vertical position of the text relative to the anchor point.
///
/// Defaults to alphabetic.
///
/// alphabetic: Draw with the alphabetic baseline at the anchor point.
/// top: Draw the top of the em box at the anchor point.
/// middle: Draw the exact middle of the em box at the anchor point.
/// bottom: Draw the bottom of the em box at the anchor point.
/// hanging: Draw 60% of an em over the baseline at the anchor point.
/// ideographic: This is a synonym for bottom.
///
baseline_style text_baseline;
/// @brief Set the font to use for text drawing.
///
/// The font must be a TrueType font (TTF) file which has been loaded or
/// mapped into memory. Following some basic validation, the relevant
/// sections of the font file contents are copied, and it is safe to
/// change or destroy after this call. As an optimization, calling this
/// with either a null pointer or zero for the number of bytes will allow
/// for changing the size of the previous font without recopying from
/// the file. Note that the font parsing is not meant to be secure;
/// only use this with trusted TTF files!
///
/// @param font pointer to the contents of a TrueType font (TTF) file
/// @param bytes number of bytes in the font file
/// @param size size in pixels per em to draw at
/// @return true if the font was set successfully
///
bool set_font(
unsigned char const *font,
int bytes,
float size );
void get_font_metrics(int& ascent, int& descent, int& height, int& x_height);
/// @brief Draw a line of text by filling its outline.
///
/// This behaves as though the current path were reset to the outline
/// of the given text in the current font and size, positioned relative
/// to the given anchor point according to the current alignment and
/// baseline, and then filled as usual. However, the current path is
/// not actually changed. The current transform at the time that this
/// is called will affect the given anchor point and the text outline.
/// However, the comparison to the maximum width in pixels and the
/// condensing if needed is done before applying the current transform.
/// The maximum width (if given) must be positive. If it is not, or
/// the text pointer is null, or the font has not been set yet, or the
/// last setting of it was unsuccessful, or the current transform is not
/// invertible, this does nothing.
///
/// @param text null-terminated UTF-8 string of text to fill
/// @param x horizontal coordinate of the anchor point
/// @param y vertical coordinate of the anchor point
/// @param maximum_width horizontal width to condense wider text to
///
void fill_text(
char const *text,
float x,
float y,
float maximum_width = 1.0e30f );
/// @brief Draw a line of text by stroking its outline.
///
/// This behaves as though the current path were reset to the outline
/// of the given text in the current font and size, positioned relative
/// to the given anchor point according to the current alignment and
/// baseline, and then stroked as usual. However, the current path is
/// not actually changed. The current transform at the time that this
/// is called will affect the given anchor point and the text outline.
/// However, the comparison to the maximum width in pixels and the
/// condensing if needed is done before applying the current transform.
/// The maximum width (if given) must be positive. If it is not, or
/// the text pointer is null, or the font has not been set yet, or the
/// last setting of it was unsuccessful, or the current transform is not
/// invertible, this does nothing.
///
/// @param text null-terminated UTF-8 string of text to stroke
/// @param x horizontal coordinate of the anchor point
/// @param y vertical coordinate of the anchor point
/// @param maximum_width horizontal width to condense wider text to
///
void stroke_text(
char const *text,
float x,
float y,
float maximum_width = 1.0e30f );
/// @brief Measure the width in pixels of a line of text.
///
/// The measured width is the advance width, which includes the side
/// bearings of the first and last glyphs. However, text as drawn may
/// go outside this (e.g., due to glyphs that spill beyond their nominal
/// widths or stroked text with wide lines). Measurements ignore the
/// current transform. If the text pointer is null, or the font has
/// not been set yet, or the last setting of it was unsuccessful, this
/// returns zero.
///
/// @param text null-terminated UTF-8 string of text to measure
/// @return width of the text in pixels
///
float measure_text(
char const *text );
// ======== DRAWING IMAGES ========
/// @brief Draw an image onto the canvas.
///
/// The position of the rectangle that the image is drawn to is affected
/// by the current transform at the time of drawing, and the image will
/// be resampled as needed (with the filtering always clamping to the
/// edges of the image). The drawing is also affected by the shadow,
/// global alpha, global compositing operation settings, and by the
/// clip region. The current path is not affected by drawing an image.
/// The image data, which should be in top to bottom rows of contiguous
/// pixels from left to right, is not retained and it is safe to change
/// or destroy it after this call. The width and height must both be
/// positive and the width and/or the height to scale to may be negative
/// but not zero. Otherwise, or if the image pointer is null, or the
/// current transform is not invertible, this does nothing.
///
/// Tip: to use a small piece of a larger image, reduce the width and
/// height, and offset the image pointer while keeping the stride.
///
/// @param image pointer to unpremultiplied sRGB RGBA8 image data
/// @param width width of the image in pixels
/// @param height height of the image in pixels
/// @param stride number of bytes between the start of each image row
/// @param x horizontal coordinate to draw the corner at
/// @param y vertical coordinate to draw the corner at
/// @param to_width width to scale the image to
/// @param to_height height to scale the image to
///
void draw_image(
unsigned char const *image,
int width,
int height,
int stride,
float x,
float y,
float to_width,
float to_height );
// ======== PIXEL MANIPULATION ========
/// @brief Fetch a rectangle of pixels from the canvas to an image.
///
/// This call is akin to a direct pixel-for-pixel copy from the canvas
/// buffer without resampling. The position and size of the canvas
/// rectangle is not affected by the current transform. The image data
/// is copied into, from top to bottom in rows of contiguous pixels from
/// left to right, and it is safe to change or destroy it after this call.
/// The requested rectangle may safely extend outside the bounds of the
/// canvas; these pixels will be set to transparent black. The width
/// and height must be positive. If not, or if the image pointer is
/// null, this does nothing.
///
/// Tip: to copy into a section of a larger image, reduce the width and
/// height, and offset the image pointer while keeping the stride.
/// Tip: use this to get the rendered image from the canvas after drawing.
///
/// @param image pointer to unpremultiplied sRGB RGBA8 image data
/// @param width width of the image in pixels
/// @param height height of the image in pixels
/// @param stride number of bytes between the start of each image row
/// @param x horizontal coordinate of upper-left pixel to fetch
/// @param y vertical coordinate of upper-left pixel to fetch
///
void get_image_data(
unsigned char *image,
int width,
int height,
int stride,
int x,
int y );
/// @brief Replace a rectangle of pixels on the canvas with an image.
///
/// This call is akin to a direct pixel-for-pixel copy into the canvas
/// buffer without resampling. The position and size of the canvas
/// rectangle is not affected by the current transform. Nor is the
/// result affected by the current settings for the global alpha, global
/// compositing operation, shadows, or the clip region. The image data,
/// which should be in top to bottom rows of contiguous pixels from left
/// to right, is copied from and it is safe to change or destroy it after
/// this call. The width and height must be positive. If not, or if the
/// image pointer is null, this does nothing.
///
/// Tip: to copy from a section of a larger image, reduce the width and
/// height, and offset the image pointer while keeping the stride.
/// Tip: use this to prepopulate the canvas before drawing.
///
/// @param image pointer to unpremultiplied sRGB RGBA8 image data
/// @param width width of the image in pixels
/// @param height height of the image in pixels
/// @param stride number of bytes between the start of each image row
/// @param x horizontal coordinate of upper-left pixel to replace
/// @param y vertical coordinate of upper-left pixel to replace
///
void put_image_data(
unsigned char const *image,
int width,
int height,
int stride,
int x,
int y );
int width() { return size_x; }
int height() { return size_y; }
// ======== CANVAS STATE ========
/// @brief Save the current state as though to a stack.
///
/// The full state of the canvas is saved, except for the pixels in the
/// canvas buffer, and the current path.
///
/// Tip: to be able to reset the current clip region, save the canvas
/// state first before clipping then restore the state to reset it.
///
void save();
/// @brief Restore a previously saved state as though from a stack.
///
/// This does not affect the pixels in the canvas buffer or the current
/// path. If the stack of canvas states is empty, this does nothing.
///
void restore();
private:
int size_x;
int size_y;
affine_matrix forward;
affine_matrix inverse;
float global_alpha;
rgba shadow_color;
float shadow_blur;
std::vector< float > shadow;
float line_width;
float miter_limit;
std::vector< float > line_dash;
paint_brush fill_brush;
paint_brush stroke_brush;
paint_brush image_brush;
bezier_path path;
line_path lines;
line_path scratch;
pixel_runs runs;
pixel_runs mask;
font_face face;
rgba *bitmap;
canvas *saves;
canvas( canvas const & );
canvas &operator=( canvas const & );
void add_tessellation( xy, xy, xy, xy, float, int );
void add_bezier( xy, xy, xy, xy, float );
void path_to_lines( bool );
void add_glyph( int, float );
int character_to_glyph( char const *, int & );
void text_to_lines( char const *, xy, float, bool );
void dash_lines();
void add_half_stroke( size_t, size_t, bool );
void stroke_lines();
void add_runs( xy, xy );
void lines_to_runs( xy, int );
rgba paint_pixel( xy, paint_brush const & );
void render_shadow( paint_brush const & );
void render_main( paint_brush const & );
};
}
#endif // CANVAS_ITY_HPP
// ======== IMPLEMENTATION ========
//
// The general internal data flow (albeit not control flow!) for rendering
// a shape onto the canvas is as follows:
//
// 1. Construct a set of polybeziers representing the current path via the
// public path building API (move_to, line_to, bezier_curve_to, etc.).
// 2. Adaptively tessellate the polybeziers into polylines (path_to_lines).
// 3. Maybe break the polylines into shorter polylines according to the dash
// pattern (dash_lines).
// 4. Maybe stroke expand the polylines into new polylines that can be filled
// to show the lines with width, joins, and caps (stroke_lines).
// 5. Scan-convert the polylines into a sparse representation of fractional
// pixel coverage (lines_to_runs).
// 6. Maybe paint the covered pixel span alphas "offscreen", blur, color,
// and blend them onto the canvas where not clipped (render_shadow).
// 7. Paint the covered pixel spans and blend them onto the canvas where not
// clipped (render_main).
#ifdef CANVAS_ITY_IMPLEMENTATION
#define LINEARIZE_RGB 2
#include <algorithm>
#include <cmath>
#include <numeric>
namespace canvas_ity
{
// 2D vector math operations
const float pi = 3.14159265f;
xy::xy() : x( 0.0f ), y( 0.0f ) {}
xy::xy( float new_x, float new_y ) : x( new_x ), y( new_y ) {}
static xy &operator+=( xy &left, xy right ) {
left.x += right.x; left.y += right.y; return left; }
static xy &operator-=( xy &left, xy right ) {
left.x -= right.x; left.y -= right.y; return left; }
static xy &operator*=( xy &left, float right ) {
left.x *= right; left.y *= right; return left; }
static xy const operator+( xy left, xy right ) {
return left += right; }
static xy const operator-( xy left, xy right ) {
return left -= right; }
static xy const operator*( float left, xy right ) {
return right *= left; }
static xy const operator*( affine_matrix const &left, xy right ) {
return xy( left.a * right.x + left.c * right.y + left.e,
left.b * right.x + left.d * right.y + left.f ); }
static float dot( xy left, xy right ) {
return left.x * right.x + left.y * right.y; }
static float length( xy that ) {
return sqrtf( dot( that, that ) ); }
static float direction( xy that ) {
return atan2f( that.y, that.x ); }
static xy const normalized( xy that ) {
return 1.0f / std::max( 1.0e-6f, length( that ) ) * that; }
static xy const perpendicular( xy that ) {
return xy( -that.y, that.x ); }
static xy const lerp( xy from, xy to, float ratio ) {
return from + ratio * ( to - from ); }
// ensure 0 <= angle < 360
float normalize_angle(float angle) {
return fmodf(angle, 360) + (angle < 0 ? 360 : 0);
}
// RGBA color operations
rgba::rgba() : r( 0.0f ), g( 0.0f ), b( 0.0f ), a( 0.0f ) {}
rgba::rgba( float new_r, float new_g, float new_b, float new_a )
: r( new_r ), g( new_g ), b( new_b ), a( new_a ) {}
static rgba &operator+=( rgba &left, rgba right ) {
left.r += right.r; left.g += right.g; left.b += right.b;
left.a += right.a; return left; }
//static rgba &operator-=( rgba &left, rgba right ) {
// left.r -= right.r; left.g -= right.g; left.b -= right.b;
// left.a -= right.a; return left; }
static rgba &operator*=( rgba &left, float right ) {
left.r *= right; left.g *= right; left.b *= right;
left.a *= right; return left; }
static rgba const operator+( rgba left, rgba right ) {
return left += right; }
//static rgba const operator-( rgba left, rgba right ) {
// return left -= right; }
static rgba const operator*( float left, rgba right ) {
return right *= left; }
#if (CANVAS_ITY_IMPLEMENTATION+0) & LINEARIZE_RGB
static float linearized( float value ) {
return value < 0.04045f ? value / 12.92f :
powf( ( value + 0.055f ) / 1.055f, 2.4f ); }
static float delinearized( float value ) {
return value < 0.0031308f ? 12.92f * value :
1.055f * powf( value, 1.0f / 2.4f ) - 0.055f; }
#else
static float linearized(float value) { return value; }
static float delinearized(float value) { return value; }
#endif
static rgba const linearized( rgba that ) {
return rgba( linearized( that.r ), linearized( that.g ),
linearized( that.b ), that.a ); }
static rgba const premultiplied( rgba that ) {
return rgba( that.r * that.a, that.g * that.a,
that.b * that.a, that.a ); }
static rgba const delinearized( rgba that ) {
return rgba( delinearized( that.r ), delinearized( that.g ),
delinearized( that.b ), that.a ); }
static rgba const unpremultiplied( rgba that ) {
static float const threshold = 1.0f / 8160.0f;
return that.a < threshold ? rgba( 0.0f, 0.0f, 0.0f, 0.0f ) :
rgba( 1.0f / that.a * that.r, 1.0f / that.a * that.g,
1.0f / that.a * that.b, that.a ); }
static rgba const clamped( rgba that ) {
return rgba( std::min( std::max( that.r, 0.0f ), 1.0f ),
std::min( std::max( that.g, 0.0f ), 1.0f ),
std::min( std::max( that.b, 0.0f ), 1.0f ),
std::min( std::max( that.a, 0.0f ), 1.0f ) ); }
// Helpers for TTF file parsing
static int unsigned_8( std::vector< unsigned char > &data, int index ) {
return data[ static_cast< size_t >( index ) ]; }
static int signed_8( std::vector< unsigned char > &data, int index ) {
size_t place = static_cast< size_t >( index );
return static_cast< signed char >( data[ place ] ); }
static int unsigned_16( std::vector< unsigned char > &data, int index ) {
size_t place = static_cast< size_t >( index );
return data[ place ] << 8 | data[ place + 1 ]; }
static int signed_16( std::vector< unsigned char > &data, int index ) {
size_t place = static_cast< size_t >( index );
return static_cast< short >( data[ place ] << 8 | data[ place + 1 ] ); }
static int signed_32( std::vector< unsigned char > &data, int index ) {
size_t place = static_cast< size_t >( index );
return ( data[ place + 0 ] << 24 | data[ place + 1 ] << 16 |
data[ place + 2 ] << 8 | data[ place + 3 ] << 0 ); }
// Tessellate (at low-level) a cubic Bezier curve and add it to the polyline
// data. This recursively splits the curve until two criteria are met
// (subject to a hard recursion depth limit). First, the control points
// must not be farther from the line between the endpoints than the tolerance.
// By the Bezier convex hull property, this ensures that the distance between
// the true curve and the polyline approximation will be no more than the
// tolerance. Secondly, it takes the cosine of an angular turn limit; the
// curve will be split until it turns less than this amount. This is used
// for stroking, with the angular limit chosen such that the sagita of an arc
// with that angle and a half-stroke radius will be equal to the tolerance.
// This keeps expanded strokes approximately within tolerance. Note that
// in the base case, it adds the control points as well as the end points.
// This way, stroke expansion infers the correct tangents from the ends of
// the polylines.
//
void canvas::add_tessellation(
xy point_1,
xy control_1,
xy control_2,
xy point_2,
float angular,
int limit )
{
static float const tolerance = 0.125f;
float flatness = tolerance * tolerance;
xy edge_1 = control_1 - point_1;
xy edge_2 = control_2 - control_1;
xy edge_3 = point_2 - control_2;
xy segment = point_2 - point_1;
float squared_1 = dot( edge_1, edge_1 );
float squared_2 = dot( edge_2, edge_2 );
float squared_3 = dot( edge_3, edge_3 );
static float const epsilon = 1.0e-4f;
float length_squared = std::max( epsilon, dot( segment, segment ) );
float projection_1 = dot( edge_1, segment ) / length_squared;
float projection_2 = dot( edge_3, segment ) / length_squared;
float clamped_1 = std::min( std::max( projection_1, 0.0f ), 1.0f );
float clamped_2 = std::min( std::max( projection_2, 0.0f ), 1.0f );
xy to_line_1 = point_1 + clamped_1 * segment - control_1;
xy to_line_2 = point_2 - clamped_2 * segment - control_2;
float cosine = 1.0f;
if ( angular > -1.0f )
{
if ( squared_1 * squared_3 != 0.0f )
cosine = dot( edge_1, edge_3 ) / sqrtf( squared_1 * squared_3 );
else if ( squared_1 * squared_2 != 0.0f )
cosine = dot( edge_1, edge_2 ) / sqrtf( squared_1 * squared_2 );
else if ( squared_2 * squared_3 != 0.0f )
cosine = dot( edge_2, edge_3 ) / sqrtf( squared_2 * squared_3 );
}
if ( ( dot( to_line_1, to_line_1 ) <= flatness &&
dot( to_line_2, to_line_2 ) <= flatness &&
cosine >= angular ) ||
!limit )
{
if ( angular > -1.0f && squared_1 != 0.0f )
lines.points.push_back( control_1 );
if ( angular > -1.0f && squared_2 != 0.0f )
lines.points.push_back( control_2 );
if ( angular == -1.0f || squared_3 != 0.0f )
lines.points.push_back( point_2 );
return;
}
xy left_1 = lerp( point_1, control_1, 0.5f );
xy middle = lerp( control_1, control_2, 0.5f );
xy right_2 = lerp( control_2, point_2, 0.5f );
xy left_2 = lerp( left_1, middle, 0.5f );
xy right_1 = lerp( middle, right_2, 0.5f );
xy split = lerp( left_2, right_1, 0.5f );
add_tessellation( point_1, left_1, left_2, split, angular, limit - 1 );
add_tessellation( split, right_1, right_2, point_2, angular, limit - 1 );
}
// Tessellate (at high-level) a cubic Bezier curve and add it to the polyline
// data. This solves both for the extreme in curvature and for the horizontal
// and vertical extrema. It then splits the curve into segments at these
// points and passes them off to the lower-level recursive tessellation.
// This preconditioning means the polyline exactly includes any cusps or
// ends of tight loops without the flatness test needing to locate it via
// bisection, and the angular limit test can use simple dot products without
// fear of curves turning more than 90 degrees.
//
void canvas::add_bezier(
xy point_1,
xy control_1,
xy control_2,
xy point_2,
float angular )
{
xy edge_1 = control_1 - point_1;
xy edge_2 = control_2 - control_1;
xy edge_3 = point_2 - control_2;
if ( dot( edge_1, edge_1 ) == 0.0f &&
dot( edge_3, edge_3 ) == 0.0f )
{
lines.points.push_back( point_2 );
return;
}
float at[ 7 ] = { 0.0f, 1.0f };
int cuts = 2;
xy extrema_a = -9.0f * edge_2 + 3.0f * ( point_2 - point_1 );
xy extrema_b = 6.0f * ( point_1 + control_2 ) - 12.0f * control_1;
xy extrema_c = 3.0f * edge_1;
static float const epsilon = 1.0e-4f;
if ( fabsf( extrema_a.x ) > epsilon )
{
float discriminant =
extrema_b.x * extrema_b.x - 4.0f * extrema_a.x * extrema_c.x;
if ( discriminant >= 0.0f )
{
float sign = extrema_b.x > 0.0f ? 1.0f : -1.0f;
float term = -extrema_b.x - sign * sqrtf( discriminant );
float extremum_1 = term / ( 2.0f * extrema_a.x );
at[ cuts++ ] = extremum_1;
at[ cuts++ ] = extrema_c.x / ( extrema_a.x * extremum_1 );
}
}
else if ( fabsf( extrema_b.x ) > epsilon )
at[ cuts++ ] = -extrema_c.x / extrema_b.x;
if ( fabsf( extrema_a.y ) > epsilon )
{
float discriminant =
extrema_b.y * extrema_b.y - 4.0f * extrema_a.y * extrema_c.y;
if ( discriminant >= 0.0f )
{
float sign = extrema_b.y > 0.0f ? 1.0f : -1.0f;
float term = -extrema_b.y - sign * sqrtf( discriminant );
float extremum_1 = term / ( 2.0f * extrema_a.y );
at[ cuts++ ] = extremum_1;
at[ cuts++ ] = extrema_c.y / ( extrema_a.y * extremum_1 );
}
}
else if ( fabsf( extrema_b.y ) > epsilon )
at[ cuts++ ] = -extrema_c.y / extrema_b.y;
float determinant_1 = dot( perpendicular( edge_1 ), edge_2 );
float determinant_2 = dot( perpendicular( edge_1 ), edge_3 );
float determinant_3 = dot( perpendicular( edge_2 ), edge_3 );
float curve_a = determinant_1 - determinant_2 + determinant_3;
float curve_b = -2.0f * determinant_1 + determinant_2;
if ( fabsf( curve_a ) > epsilon &&
fabsf( curve_b ) > epsilon )
at[ cuts++ ] = -0.5f * curve_b / curve_a;
for ( int index = 1; index < cuts; ++index )
{
float value = at[ index ];
int sorted = index - 1;
for ( ; 0 <= sorted && value < at[ sorted ]; --sorted )
at[ sorted + 1 ] = at[ sorted ];
at[ sorted + 1 ] = value;
}
xy split_point_1 = point_1;
for ( int index = 0; index < cuts - 1; ++index )
{
if ( !( 0.0f <= at[ index ] && at[ index + 1 ] <= 1.0f &&
at[ index ] != at[ index + 1 ] ) )
continue;
float ratio = at[ index ] / at[ index + 1 ];
xy partial_1 = lerp( point_1, control_1, at[ index + 1 ] );
xy partial_2 = lerp( control_1, control_2, at[ index + 1 ] );
xy partial_3 = lerp( control_2, point_2, at[ index + 1 ] );
xy partial_4 = lerp( partial_1, partial_2, at[ index + 1 ] );
xy partial_5 = lerp( partial_2, partial_3, at[ index + 1 ] );
xy partial_6 = lerp( partial_1, partial_4, ratio );
xy split_point_2 = lerp( partial_4, partial_5, at[ index + 1 ] );
xy split_control_2 = lerp( partial_4, split_point_2, ratio );
xy split_control_1 = lerp( partial_6, split_control_2, ratio );
add_tessellation( split_point_1, split_control_1,
split_control_2, split_point_2,
angular, 20 );
split_point_1 = split_point_2;
}
}
// Convert the current path to a set of polylines. This walks over the
// complete set of subpaths in the current path (stored as sets of cubic
// Beziers) and converts each Bezier curve segment to a polyline while
// preserving information about where subpaths begin and end and whether
// they are closed or open. This replaces the previous polyline data.
//
void canvas::path_to_lines(
bool stroking )
{
static float const tolerance = 0.125f;
float ratio = tolerance / std::max( 0.5f * line_width, tolerance );
float angular = stroking ? ( ratio - 2.0f ) * ratio * 2.0f + 1.0f : -1.0f;
lines.points.clear();
lines.subpaths.clear();
size_t index = 0;
size_t ending = 0;
for ( size_t subpath = 0; subpath < path.subpaths.size(); ++subpath )
{
ending += path.subpaths[ subpath ].count;
size_t first = lines.points.size();
xy point_1 = path.points[ index++ ];
lines.points.push_back( point_1 );
for ( ; index < ending; index += 3 )
{
xy control_1 = path.points[ index + 0 ];
xy control_2 = path.points[ index + 1 ];
xy point_2 = path.points[ index + 2 ];
add_bezier( point_1, control_1, control_2, point_2, angular );
point_1 = point_2;
}
subpath_data entry = {
lines.points.size() - first,
path.subpaths[ subpath ].closed };
lines.subpaths.push_back( entry );
}
}
// Add a text glyph directly to the polylines. Given a glyph index, this
// parses the data for that glyph directly from the TTF glyph data table and
// immediately tessellates it to a set of a polyline subpaths which it adds
// to any subpaths already present. It uses the current transform matrix to
// transform from the glyph's vertices in font units to the proper size and
// position on the canvas.
//
void canvas::add_glyph(
int glyph,
float angular )
{
int loc_format = unsigned_16( face.data, face.head + 50 );
int offset = face.glyf + ( loc_format ?
signed_32( face.data, face.loca + glyph * 4 ) :
unsigned_16( face.data, face.loca + glyph * 2 ) * 2 );
int next = face.glyf + ( loc_format ?
signed_32( face.data, face.loca + glyph * 4 + 4 ) :
unsigned_16( face.data, face.loca + glyph * 2 + 2 ) * 2 );
if ( offset == next )
return;
int contours = signed_16( face.data, offset );
if ( contours < 0 )
{
offset += 10;
for ( ; ; )
{
int flags = unsigned_16( face.data, offset );
int component = unsigned_16( face.data, offset + 2 );
if ( !( flags & 2 ) )
return; // Matching points are not supported
float e = static_cast< float >( flags & 1 ?
signed_16( face.data, offset + 4 ) :
signed_8( face.data, offset + 4 ) );
float f = static_cast< float >( flags & 1 ?
signed_16( face.data, offset + 6 ) :
signed_8( face.data, offset + 5 ) );
offset += flags & 1 ? 8 : 6;
float a = flags & 200 ? static_cast< float >(
signed_16( face.data, offset ) ) / 16384.0f : 1.0f;
float b = flags & 128 ? static_cast< float >(
signed_16( face.data, offset + 2 ) ) / 16384.0f : 0.0f;
float c = flags & 128 ? static_cast< float >(
signed_16( face.data, offset + 4 ) ) / 16384.0f : 0.0f;
float d = flags & 8 ? a :
flags & 64 ? static_cast< float >(
signed_16( face.data, offset + 2 ) ) / 16384.0f :
flags & 128 ? static_cast< float >(
signed_16( face.data, offset + 6 ) ) / 16384.0f :
1.0f;
offset += flags & 8 ? 2 : flags & 64 ? 4 : flags & 128 ? 8 : 0;
affine_matrix saved_forward = forward;
affine_matrix saved_inverse = inverse;
transform( a, b, c, d, e, f );
add_glyph( component, angular );
forward = saved_forward;
inverse = saved_inverse;
if ( !( flags & 32 ) )
return;
}
}
int hmetrics = unsigned_16( face.data, face.hhea + 34 );
int left_side_bearing = glyph < hmetrics ?
signed_16( face.data, face.hmtx + glyph * 4 + 2 ) :
signed_16( face.data, face.hmtx + hmetrics * 2 + glyph * 2 );
int x_min = signed_16( face.data, offset + 2 );
int points = unsigned_16( face.data, offset + 8 + contours * 2 ) + 1;
int instructions = unsigned_16( face.data, offset + 10 + contours * 2 );
int flags_array = offset + 12 + contours * 2 + instructions;
int flags_size = 0;
int x_size = 0;
for ( int index = 0; index < points; )
{
int flags = unsigned_8( face.data, flags_array + flags_size++ );
int repeated = flags & 8 ?
unsigned_8( face.data, flags_array + flags_size++ ) + 1 : 1;
x_size += repeated * ( flags & 2 ? 1 : flags & 16 ? 0 : 2 );
index += repeated;
}
int x_array = flags_array + flags_size;
int y_array = x_array + x_size;
int x = left_side_bearing - x_min;
int y = 0;
int flags = 0;
int repeated = 0;
int index = 0;
for ( int contour = 0; contour < contours; ++contour )
{
int beginning = index;
int ending = unsigned_16( face.data, offset + 10 + contour * 2 );
xy begin_point = xy( 0.0f, 0.0f );
bool begin_on = false;
xy end_point = xy( 0.0f, 0.0f );
bool end_on = false;
size_t first = lines.points.size();
for ( ; index <= ending; ++index )
{
if ( repeated )
--repeated;
else
{
flags = unsigned_8( face.data, flags_array++ );
if ( flags & 8 )
repeated = unsigned_8( face.data, flags_array++ );
}
if ( flags & 2 )
x += ( unsigned_8( face.data, x_array ) *
( flags & 16 ? 1 : -1 ) );
else if ( !( flags & 16 ) )
x += signed_16( face.data, x_array );
if ( flags & 4 )
y += ( unsigned_8( face.data, y_array ) *
( flags & 32 ? 1 : -1 ) );
else if ( !( flags & 32 ) )
y += signed_16( face.data, y_array );
x_array += flags & 2 ? 1 : flags & 16 ? 0 : 2;
y_array += flags & 4 ? 1 : flags & 32 ? 0 : 2;
xy point = forward * xy( static_cast< float >( x ),
static_cast< float >( y ) );
int on_curve = flags & 1;
if ( index == beginning )
{
begin_point = point;
begin_on = on_curve;
if ( on_curve )
lines.points.push_back( point );
}
else
{
xy point_2 = on_curve ? point :
lerp( end_point, point, 0.5f );
if ( lines.points.size() == first ||
( end_on && on_curve ) )
lines.points.push_back( point_2 );
else if ( !end_on || on_curve )
{
xy point_1 = lines.points.back();
xy control_1 = lerp( point_1, end_point, 2.0f / 3.0f );
xy control_2 = lerp( point_2, end_point, 2.0f / 3.0f );
add_bezier( point_1, control_1, control_2, point_2,
angular );
}
}
end_point = point;
end_on = on_curve;
}
if ( begin_on ^ end_on )
{
xy point_1 = lines.points.back();
xy point_2 = lines.points[ first ];
xy control = end_on ? begin_point : end_point;
xy control_1 = lerp( point_1, control, 2.0f / 3.0f );
xy control_2 = lerp( point_2, control, 2.0f / 3.0f );
add_bezier( point_1, control_1, control_2, point_2, angular );
}
else if ( !begin_on && !end_on )
{
xy point_1 = lines.points.back();
xy split = lerp( begin_point, end_point, 0.5f );
xy point_2 = lines.points[ first ];
xy left_1 = lerp( point_1, end_point, 2.0f / 3.0f );
xy left_2 = lerp( split, end_point, 2.0f / 3.0f );
xy right_1 = lerp( split, begin_point, 2.0f / 3.0f );
xy right_2 = lerp( point_2, begin_point, 2.0f / 3.0f );
add_bezier( point_1, left_1, left_2, split, angular );
add_bezier( split, right_1, right_2, point_2, angular );
}
lines.points.push_back( lines.points[ first ] );
subpath_data entry = { lines.points.size() - first, true };
lines.subpaths.push_back( entry );
}
}
// Decode the next codepoint from a null-terminated UTF-8 string to its glyph
// index within the font. The index to the next codepoint in the string
// is advanced accordingly. It checks for valid UTF-8 encoding, but not
// for valid unicode codepoints. Where it finds an invalid encoding, it
// decodes it as the Unicode replacement character (U+FFFD) and advances to
// the invalid byte, per Unicode recommendation. It also replaces low-ASCII
// whitespace characters with regular spaces. After decoding the codepoint,
// it looks up the corresponding glyph index from the current font's character
// map table, returning a glyph index of 0 for the .notdef character (i.e.,
// "tofu") if the font lacks a glyph for that codepoint.
//
int canvas::character_to_glyph(
char const *text,
int &index )
{
int bytes = ( ( text[ index ] & 0x80 ) == 0x00 ? 1 :
( text[ index ] & 0xe0 ) == 0xc0 ? 2 :
( text[ index ] & 0xf0 ) == 0xe0 ? 3 :
( text[ index ] & 0xf8 ) == 0xf0 ? 4 :
0 );
int const masks[] = { 0x0, 0x7f, 0x1f, 0x0f, 0x07 };
int codepoint = bytes ? text[ index ] & masks[ bytes ] : 0xfffd;
++index;
while ( --bytes > 0 )
if ( ( text[ index ] & 0xc0 ) == 0x80 )
codepoint = codepoint << 6 | ( text[ index++ ] & 0x3f );
else
{
codepoint = 0xfffd;
break;
}
if ( codepoint == '\t' || codepoint == '\v' || codepoint == '\f' ||
codepoint == '\r' || codepoint == '\n' )
codepoint = ' ';
int tables = unsigned_16( face.data, face.cmap + 2 );
int format_12 = 0;
int format_4 = 0;
int format_0 = 0;
for ( int table = 0; table < tables; ++table )
{
int platform = unsigned_16( face.data, face.cmap + table * 8 + 4 );
int encoding = unsigned_16( face.data, face.cmap + table * 8 + 6 );
int offset = signed_32( face.data, face.cmap + table * 8 + 8 );
int format = unsigned_16( face.data, face.cmap + offset );
if ( platform == 3 && encoding == 10 && format == 12 )
format_12 = face.cmap + offset;
else if ( platform == 3 && encoding == 1 && format == 4 )
format_4 = face.cmap + offset;
else if ( format == 0 )
format_0 = face.cmap + offset;
}
if ( format_12 )
{
int groups = signed_32( face.data, format_12 + 12 );
for ( int group = 0; group < groups; ++group )
{
int start = signed_32( face.data, format_12 + 16 + group * 12 );
int end = signed_32( face.data, format_12 + 20 + group * 12 );
int glyph = signed_32( face.data, format_12 + 24 + group * 12 );
if ( start <= codepoint && codepoint <= end )
return codepoint - start + glyph;
}
}
else if ( format_4 )
{
int segments = unsigned_16( face.data, format_4 + 6 );
int end_array = format_4 + 14;
int start_array = end_array + 2 + segments;
int delta_array = start_array + segments;
int range_array = delta_array + segments;
for ( int segment = 0; segment < segments; segment += 2 )
{
int start = unsigned_16( face.data, start_array + segment );
int end = unsigned_16( face.data, end_array + segment );
int delta = signed_16( face.data, delta_array + segment );
int range = unsigned_16( face.data, range_array + segment );
if ( start <= codepoint && codepoint <= end )
return range ?
unsigned_16( face.data, range_array + segment +
( codepoint - start ) * 2 + range ) :
( codepoint + delta ) & 0xffff;
}
}
else if ( format_0 && 0 <= codepoint && codepoint < 256 )
return unsigned_8( face.data, format_0 + 6 + codepoint );
return 0;
}
void canvas::get_font_metrics(int& ascent, int& descent, int& height, int& x_height)
{
if (face.data.empty()) return;
// https://www.w3.org/TR/css-inline/#ascent-descent
// "It is recommended that implementations that use OpenType or TrueType fonts use the metrics sTypoAscender and sTypoDescender from the fonts OS/2 table"
float sTypoAscender = (float)signed_16(face.data, face.os_2 + 68);
float sTypoDescender = (float)signed_16(face.data, face.os_2 + 70);
// Some fonts, eg. Inconsolata, don't have the sxHeight field (it is defined in the second version of OS/2 table).
// If it is absent (yMax - yMin) for 'x' from glyf table can be used.
int os2ver = unsigned_16(face.data, face.os_2);
float sxHeight = os2ver >= 2 ? (float)signed_16(face.data, face.os_2 + 86) : 0;
ascent = (int)ceil(sTypoAscender * face.scale);
descent = (int)ceil(-sTypoDescender * face.scale);
// https://www.w3.org/TR/css-inline/#font-line-gap
// https://www.w3.org/TR/css-inline/#inline-height
// In several fonts I examined, including Inconsolata and csstest-ascii.ttf from w3.org,
// both OS/2 sTypoLineGap and hhea lineGap are either 0 or very small (I used https://fontdrop.info/ viewer).
// cairo container sets height to ascent + descent, litehtml uses this value as a normal line height and for baseline calculations.
height = (int)ceil((sTypoAscender - sTypoDescender) * face.scale);
x_height = (int)ceil(sxHeight * face.scale);
}
// Convert a text string to a set of polylines. This works out the placement
// of the text string relative to the anchor position. Then it walks through
// the string, sizing and placing each character by temporarily changing the
// current transform matrix to map from font units to canvas pixel coordinates
// before adding the glyph to the polylines. This replaces the previous
// polyline data.
//
void canvas::text_to_lines(
char const *text,
xy position,
float maximum_width,
bool stroking )
{
static float const tolerance = 0.125f;
float ratio = tolerance / std::max( 0.5f * line_width, tolerance );
float angular = stroking ? ( ratio - 2.0f ) * ratio * 2.0f + 1.0f : -1.0f;
lines.points.clear();
lines.subpaths.clear();
if ( face.data.empty() || !text || maximum_width <= 0.0f )
return;
float width = maximum_width == 1.0e30f && text_align == leftward ? 0.0f :
measure_text( text );
float reduction = maximum_width / std::max( maximum_width, width );
if ( text_align == rightward )
position.x -= width * reduction;
else if ( text_align == center )
position.x -= 0.5f * width * reduction;
xy scaling = face.scale * xy( reduction, 1.0f );
float units_per_em = static_cast< float >(
unsigned_16( face.data, face.head + 18 ) );
float ascender = static_cast< float >(
signed_16( face.data, face.os_2 + 68 ) );
float descender = static_cast< float >(
signed_16( face.data, face.os_2 + 70 ) );
if ( text_baseline == top )
position.y += ascender * face.scale;
else if ( text_baseline == middle )
position.y += ( ascender - descender ) * 0.5f * face.scale;
else if ( text_baseline == bottom )
position.y += descender * face.scale;
else if ( text_baseline == hanging )
position.y += 0.6f * face.scale * units_per_em;
affine_matrix saved_forward = forward;
affine_matrix saved_inverse = inverse;
int hmetrics = unsigned_16( face.data, face.hhea + 34 );
int place = 0;
for ( int index = 0; text[ index ]; )
{
int glyph = character_to_glyph( text, index );
forward = saved_forward;
transform( scaling.x, 0.0f, 0.0f, -scaling.y,
position.x + static_cast< float >( place ) * scaling.x,
position.y );
add_glyph( glyph, angular );
int entry = std::min( glyph, hmetrics - 1 );
place += unsigned_16( face.data, face.hmtx + entry * 4 );
}
forward = saved_forward;
inverse = saved_inverse;
}
// Break the polylines into smaller pieces according to the dash settings.
// This walks along the polyline subpaths and dash pattern together, emitting
// new points via lerping where dash segments begin and end. Each dash
// segment becomes a new open subpath in the polyline. Some care is to
// taken to handle two special cases of closed subpaths. First, those that
// are completely within the first dash segment should be emitted as-is and
// remain closed. Secondly, those that start and end within a dash should
// have the two dashes merged together so that the lines join. This replaces
// the previous polyline data.
//
void canvas::dash_lines()
{
if ( line_dash.empty() )
return;
lines.points.swap( scratch.points );
lines.points.clear();
lines.subpaths.swap( scratch.subpaths );
lines.subpaths.clear();
float total = std::accumulate( line_dash.begin(), line_dash.end(), 0.0f );
float offset = fmodf( line_dash_offset, total );
if ( offset < 0.0f )
offset += total;
size_t start = 0;
while ( offset >= line_dash[ start ] )
{
offset -= line_dash[ start ];
start = start + 1 < line_dash.size() ? start + 1 : 0;
}
size_t ending = 0;
for ( size_t subpath = 0; subpath < scratch.subpaths.size(); ++subpath )
{
size_t index = ending;
ending += scratch.subpaths[ subpath ].count;
size_t first = lines.points.size();
size_t segment = start;
bool emit = ~start & 1;
size_t merge_point = lines.points.size();
size_t merge_subpath = lines.subpaths.size();
bool merge_emit = emit;
float next = line_dash[ start ] - offset;
for ( ; index + 1 < ending; ++index )
{
xy from = scratch.points[ index ];
xy to = scratch.points[ index + 1 ];
if ( emit )
lines.points.push_back( from );
float line = length( inverse * to - inverse * from );
while ( next < line )
{
lines.points.push_back( lerp( from, to, next / line ) );
if ( emit )
{
subpath_data entry = {
lines.points.size() - first, false };
lines.subpaths.push_back( entry );
first = lines.points.size();
}
segment = segment + 1 < line_dash.size() ? segment + 1 : 0;
emit = !emit;
next += line_dash[ segment ];
}
next -= line;
}
if ( emit )
{
lines.points.push_back( scratch.points[ index ] );
subpath_data entry = { lines.points.size() - first, false };
lines.subpaths.push_back( entry );
if ( scratch.subpaths[ subpath ].closed && merge_emit )
{
if ( lines.subpaths.size() == merge_subpath + 1 )
lines.subpaths.back().closed = true;
else
{
size_t count = lines.subpaths.back().count;
std::rotate( ( lines.points.begin() +
static_cast< ptrdiff_t >( merge_point ) ),
( lines.points.end() -
static_cast< ptrdiff_t >( count ) ),
lines.points.end() );
lines.subpaths[ merge_subpath ].count += count;
lines.subpaths.pop_back();
}
}
}
}
}
// Trace along a series of points from a subpath in the scratch polylines
// and add new points to the main polylines with the stroke expansion on
// one side. Calling this again with the ends reversed adds the other
// half of the stroke. If the original subpath was closed, each pass
// adds a complete closed loop, with one adding the inside and one adding
// the outside. The two will wind in opposite directions. If the original
// subpath was open, each pass ends with one of the line caps and the two
// passes together form a single closed loop. In either case, this handles
// adding line joins, including inner joins. Care is taken to fill tight
// inside turns correctly by adding additional windings. See Figure 10 of
// "Converting Stroked Primitives to Filled Primitives" by Diego Nehab, for
// the inspiration for these extra windings.
//
void canvas::add_half_stroke(
size_t beginning,
size_t ending,
bool closed )
{
float half = line_width * 0.5f;
float ratio = miter_limit * miter_limit * half * half;
xy in_direction = xy( 0.0f, 0.0f );
float in_length = 0.0f;
xy point = inverse * scratch.points[ beginning ];
size_t finish = beginning;
size_t index = beginning;
do
{
xy next = inverse * scratch.points[ index ];
xy out_direction = normalized( next - point );
float out_length = length( next - point );
static float const epsilon = 1.0e-4f;
if ( in_length != 0.0f && out_length >= epsilon )
{
if ( closed && finish == beginning )
finish = index;
xy side_in = point + half * perpendicular( in_direction );
xy side_out = point + half * perpendicular( out_direction );
float turn = dot( perpendicular( in_direction ), out_direction );
if ( fabsf( turn ) < epsilon )
turn = 0.0f;
xy offset = turn == 0.0f ? xy( 0.0f, 0.0f ) :
half / turn * ( out_direction - in_direction );
bool tight = ( dot( offset, in_direction ) < -in_length &&
dot( offset, out_direction ) > out_length );
if ( turn > 0.0f && tight )
{
std::swap( side_in, side_out );
std::swap( in_direction, out_direction );
lines.points.push_back( forward * side_out );
lines.points.push_back( forward * point );
lines.points.push_back( forward * side_in );
}
if ( ( turn > 0.0f && !tight ) ||
( turn != 0.0f && line_join == miter &&
dot( offset, offset ) <= ratio ) )
lines.points.push_back( forward * ( point + offset ) );
else if ( line_join == rounded )
{
float cosine = dot( in_direction, out_direction );
float angle = acosf(
std::min( std::max( cosine, -1.0f ), 1.0f ) );
float alpha = 4.0f / 3.0f * tanf( 0.25f * angle );
lines.points.push_back( forward * side_in );
add_bezier(
forward * side_in,
forward * ( side_in + alpha * half * in_direction ),
forward * ( side_out - alpha * half * out_direction ),
forward * side_out,
-1.0f );
}
else
{
lines.points.push_back( forward * side_in );
lines.points.push_back( forward * side_out );
}
if ( turn > 0.0f && tight )
{
lines.points.push_back( forward * side_out );
lines.points.push_back( forward * point );
lines.points.push_back( forward * side_in );
std::swap( in_direction, out_direction );
}
}
if ( out_length >= epsilon )
{
in_direction = out_direction;
in_length = out_length;
point = next;
}
index = ( index == ending ? beginning :
ending > beginning ? index + 1 :
index - 1 );
} while ( index != finish );
if ( closed || in_length == 0.0f )
return;
xy ahead = half * in_direction;
xy side = perpendicular( ahead );
if ( line_cap == butt )
{
lines.points.push_back( forward * ( point + side ) );
lines.points.push_back( forward * ( point - side ) );
}
else if ( line_cap == square )
{
lines.points.push_back( forward * ( point + ahead + side ) );
lines.points.push_back( forward * ( point + ahead - side ) );
}
else if ( line_cap == circle )
{
static float const alpha = 0.55228475f; // 4/3*tan(pi/8)
lines.points.push_back( forward * ( point + side ) );
add_bezier( forward * ( point + side ),
forward * ( point + side + alpha * ahead ),
forward * ( point + ahead + alpha * side ),
forward * ( point + ahead ),
-1.0f );
add_bezier( forward * ( point + ahead ),
forward * ( point + ahead - alpha * side ),
forward * ( point - side + alpha * ahead ),
forward * ( point - side ),
-1.0f );
}
}
// Perform stroke expansion on the polylines. After first breaking them up
// according to the dash pattern (if any), it then moves the polyline data to
// the scratch space. Each subpath is then traced both forwards and backwards
// to add the points for a half stroke, which together create the points for
// one (if the original subpath was open) or two (if it was closed) new closed
// subpaths which, when filled, will draw the stroked lines. While the lower
// level code this calls only adds the points of the half strokes, this
// adds subpath information for the strokes. This replaces the previous
// polyline data.
//
void canvas::stroke_lines()
{
if ( forward.a * forward.d - forward.b * forward.c == 0.0f )
return;
dash_lines();
lines.points.swap( scratch.points );
lines.points.clear();
lines.subpaths.swap( scratch.subpaths );
lines.subpaths.clear();
size_t ending = 0;
for ( size_t subpath = 0; subpath < scratch.subpaths.size(); ++subpath )
{
size_t beginning = ending;
ending += scratch.subpaths[ subpath ].count;
if ( ending - beginning < 2 )
continue;
size_t first = lines.points.size();
add_half_stroke( beginning, ending - 1,
scratch.subpaths[ subpath ].closed );
if ( scratch.subpaths[ subpath ].closed )
{
subpath_data entry = { lines.points.size() - first, true };
lines.subpaths.push_back( entry );
first = lines.points.size();
}
add_half_stroke( ending - 1, beginning,
scratch.subpaths[ subpath ].closed );
subpath_data entry = { lines.points.size() - first, true };
lines.subpaths.push_back( entry );
}
}
// Scan-convert a single polyline segment. This walks along the pixels that
// the segment touches in left-to-right order, using signed trapezoidal area
// to accumulate a list of changes in signed coverage at each visible pixel
// when processing them from left to right. Each run of horizontal pixels
// ends with one final update to the right of the last pixel to bring the
// total up to date. Note that this does not clip to the screen boundary.
//
void canvas::add_runs(
xy from,
xy to )
{
static float const epsilon = 2.0e-5f;
if ( fabsf( to.y - from.y ) < epsilon)
return;
float sign = to.y > from.y ? 1.0f : -1.0f;
if ( from.x > to.x )
std::swap( from, to );
xy now = from;
xy pixel = xy( floorf( now.x ), floorf( now.y ) );
xy corner = pixel + xy( 1.0f, to.y > from.y ? 1.0f : 0.0f );
xy slope = xy( ( to.x - from.x ) / ( to.y - from.y ),
( to.y - from.y ) / ( to.x - from.x ) );
xy next_x = ( to.x - from.x < epsilon ) ? to :
xy( corner.x, now.y + ( corner.x - now.x ) * slope.y );
xy next_y = xy( now.x + ( corner.y - now.y ) * slope.x, corner.y );
if ( ( from.y < to.y && to.y < next_y.y ) ||
( from.y > to.y && to.y > next_y.y ) )
next_y = to;
float y_step = to.y > from.y ? 1.0f : -1.0f;
do
{
float carry = 0.0f;
while ( next_x.x < next_y.x )
{
float strip = std::min( std::max( ( next_x.y - now.y ) * y_step,
0.0f ), 1.0f );
float mid = ( next_x.x + now.x ) * 0.5f;
float area = ( mid - pixel.x ) * strip;
pixel_run piece = { static_cast< unsigned short >( pixel.x ),
static_cast< unsigned short >( pixel.y ),
( carry + strip - area ) * sign };
runs.push_back( piece );
carry = area;
now = next_x;
next_x.x += 1.0f;
next_x.y = ( next_x.x - from.x ) * slope.y + from.y;
pixel.x += 1.0f;
}
float strip = std::min( std::max( ( next_y.y - now.y ) * y_step,
0.0f ), 1.0f );
float mid = ( next_y.x + now.x ) * 0.5f;
float area = ( mid - pixel.x ) * strip;
pixel_run piece_1 = { static_cast< unsigned short >( pixel.x ),
static_cast< unsigned short >( pixel.y ),
( carry + strip - area ) * sign };
pixel_run piece_2 = { static_cast< unsigned short >( pixel.x + 1.0f ),
static_cast< unsigned short >( pixel.y ),
area * sign };
runs.push_back( piece_1 );
runs.push_back( piece_2 );
now = next_y;
next_y.y += y_step;
next_y.x = ( next_y.y - from.y ) * slope.x + from.x;
pixel.y += y_step;
if ( ( from.y < to.y && to.y < next_y.y ) ||
( from.y > to.y && to.y > next_y.y ) )
next_y = to;
} while ( now.y != to.y );
}
static bool operator<(
pixel_run left,
pixel_run right )
{
return ( left.y < right.y ? true :
left.y > right.y ? false :
left.x < right.x ? true :
left.x > right.x ? false :
fabsf( left.delta ) < fabsf( right.delta ) );
}
// Scan-convert the polylines to prepare them for antialiased rendering.
// For each of the polyline loops, it first clips them to the screen.
// See "Reentrant Polygon Clipping" by Sutherland and Hodgman for details.
// Then it walks the polyline loop and scan-converts each line segment to
// produce a list of changes in signed pixel coverage when processed in
// left-to-right, top-to-bottom order. The list of changes is then sorted
// into that order, and multiple changes to the same pixel are coalesced
// by summation. The result is a sparse, run-length encoded description
// of the coverage of each pixel to be drawn.
//
void canvas::lines_to_runs(
xy offset,
int padding )
{
runs.clear();
float width = static_cast< float >( size_x + padding );
float height = static_cast< float >( size_y + padding );
size_t ending = 0;
for ( size_t subpath = 0; subpath < lines.subpaths.size(); ++subpath )
{
size_t beginning = ending;
ending += lines.subpaths[ subpath ].count;
scratch.points.clear();
for ( size_t index = beginning; index < ending; ++index )
scratch.points.push_back( offset + lines.points[ index ] );
for ( int edge = 0; edge < 4; ++edge )
{
xy normal = xy( edge == 0 ? 1.0f : edge == 2 ? -1.0f : 0.0f,
edge == 1 ? 1.0f : edge == 3 ? -1.0f : 0.0f );
float place = edge == 2 ? width : edge == 3 ? height : 0.0f;
size_t first = scratch.points.size();
for ( size_t index = 0; index < first; ++index )
{
xy from = scratch.points[ ( index ? index : first ) - 1 ];
xy to = scratch.points[ index ];
float from_side = dot( from, normal ) + place;
float to_side = dot( to, normal ) + place;
if ( from_side * to_side < 0.0f )
scratch.points.push_back( lerp( from, to,
from_side / ( from_side - to_side ) ) );
if ( to_side >= 0.0f )
scratch.points.push_back( to );
}
scratch.points.erase(
scratch.points.begin(),
scratch.points.begin() + static_cast< ptrdiff_t >( first ) );
}
size_t last = scratch.points.size();
for ( size_t index = 0; index < last; ++index )
{
xy from = scratch.points[ ( index ? index : last ) - 1 ];
xy to = scratch.points[ index ];
add_runs( xy( std::min( std::max( from.x, 0.0f ), width ),
std::min( std::max( from.y, 0.0f ), height ) ),
xy( std::min( std::max( to.x, 0.0f ), width ),
std::min( std::max( to.y, 0.0f ), height ) ) );
}
}
if ( runs.empty() )
return;
std::sort( runs.begin(), runs.end() );
size_t to = 0;
for ( size_t from = 1; from < runs.size(); ++from )
if ( runs[ from ].x == runs[ to ].x &&
runs[ from ].y == runs[ to ].y )
runs[ to ].delta += runs[ from ].delta;
else if ( runs[ from ].delta != 0.0f )
runs[ ++to ] = runs[ from ];
runs.erase( runs.begin() + static_cast< ptrdiff_t >( to ) + 1,
runs.end() );
}
// Paint a pixel according to its point location and a paint style to produce
// a premultiplied, linearized RGBA color. This handles all supported paint
// styles: solid colors, linear gradients, radial gradients, and patterns.
// For gradients and patterns, it takes into account the current transform.
// Patterns are resampled using a separable bicubic convolution filter,
// with edges handled according to the wrap mode. See "Cubic Convolution
// Interpolation for Digital Image Processing" by Keys. This filter is best
// known for magnification, but also works well for antialiased minification,
// since it's actually a Catmull-Rom spline approximation of Lanczos-2.
//
rgba canvas::paint_pixel(
xy point,
paint_brush const &brush )
{
if ( brush.colors.empty() )
return rgba( 0.0f, 0.0f, 0.0f, 0.0f );
if ( brush.type == paint_brush::color )
return brush.colors.front();
point = inverse * point;
if ( brush.type == paint_brush::pattern )
{
float width = static_cast< float >( brush.width );
float height = static_cast< float >( brush.height );
if ( ( ( brush.repetition & 2 ) &&
( point.x < 0.0f || width <= point.x ) ) ||
( ( brush.repetition & 1 ) &&
( point.y < 0.0f || height <= point.y ) ) )
return rgba( 0.0f, 0.0f, 0.0f, 0.0f );
float scale_x = fabsf( inverse.a ) + fabsf( inverse.c );
float scale_y = fabsf( inverse.b ) + fabsf( inverse.d );
scale_x = std::max( 1.0f, std::min( scale_x, width * 0.25f ) );
scale_y = std::max( 1.0f, std::min( scale_y, height * 0.25f ) );
float reciprocal_x = 1.0f / scale_x;
float reciprocal_y = 1.0f / scale_y;
point -= xy( 0.5f, 0.5f );
int left = static_cast< int >( ceilf( point.x - scale_x * 2.0f ) );
int top = static_cast< int >( ceilf( point.y - scale_y * 2.0f ) );
int right = static_cast< int >( ceilf( point.x + scale_x * 2.0f ) );
int bottom = static_cast< int >( ceilf( point.y + scale_y * 2.0f ) );
rgba total_color = rgba( 0.0f, 0.0f, 0.0f, 0.0f );
float total_weight = 0.0f;
for ( int pattern_y = top; pattern_y < bottom; ++pattern_y )
{
float y = fabsf( reciprocal_y *
( static_cast< float >( pattern_y ) - point.y ) );
float weight_y = ( y < 1.0f ?
( 1.5f * y - 2.5f ) * y * y + 1.0f :
( ( -0.5f * y + 2.5f ) * y - 4.0f ) * y + 2.0f );
int wrapped_y = pattern_y % brush.height;
if ( wrapped_y < 0 )
wrapped_y += brush.height;
if ( &brush == &image_brush )
wrapped_y = std::min( std::max( pattern_y, 0 ),
brush.height - 1 );
for ( int pattern_x = left; pattern_x < right; ++pattern_x )
{
float x = fabsf( reciprocal_x *
( static_cast< float >( pattern_x ) - point.x ) );
float weight_x = ( x < 1.0f ?
( 1.5f * x - 2.5f ) * x * x + 1.0f :
( ( -0.5f * x + 2.5f ) * x - 4.0f ) * x + 2.0f );
int wrapped_x = pattern_x % brush.width;
if ( wrapped_x < 0 )
wrapped_x += brush.width;
if ( &brush == &image_brush )
wrapped_x = std::min( std::max( pattern_x, 0 ),
brush.width - 1 );
float weight = weight_x * weight_y;
size_t index = static_cast< size_t >(
wrapped_y * brush.width + wrapped_x );
total_color += weight * brush.colors[ index ];
total_weight += weight;
}
}
return ( 1.0f / total_weight ) * total_color;
}
float offset = 0;
xy relative = point - brush.start;
xy line = brush.end - brush.start;
float gradient = dot( relative, line );
float span = dot( line, line );
if ( brush.type == paint_brush::linear )
{
if ( span == 0.0f )
return rgba( 0.0f, 0.0f, 0.0f, 0.0f );
offset = gradient / span;
}
else if (brush.type == paint_brush::radial)
{
float initial = brush.start_radius;
float change = brush.end_radius - initial;
float a = span - change * change;
float b = -2.0f * ( gradient + initial * change );
float c = dot( relative, relative ) - initial * initial;
float discriminant = b * b - 4.0f * a * c;
if ( discriminant < 0.0f ||
( span == 0.0f && change == 0.0f ) )
return rgba( 0.0f, 0.0f, 0.0f, 0.0f );
float root = sqrtf( discriminant );
float reciprocal = 1.0f / ( 2.0f * a );
float offset_1 = ( -b - root ) * reciprocal;
float offset_2 = ( -b + root ) * reciprocal;
float radius_1 = initial + change * offset_1;
float radius_2 = initial + change * offset_2;
if ( radius_2 >= 0.0f )
offset = offset_2;
else if ( radius_1 >= 0.0f )
offset = offset_1;
else
return rgba( 0.0f, 0.0f, 0.0f, 0.0f );
}
else if (brush.type == paint_brush::css_radial)
{
if (brush.css_radius.x == 0 || brush.css_radius.y == 0)
offset = 1;
else
{
xy rel = {relative.x / brush.css_radius.x, relative.y / brush.css_radius.y};
offset = length(rel);
}
}
else if (brush.type == paint_brush::conic)
{
float angle = 90 + direction(relative) * 180 / pi;
offset = normalize_angle(angle - brush.angle) / 360;
}
size_t index = static_cast< size_t >(
std::upper_bound( brush.stops.begin(), brush.stops.end(), offset ) -
brush.stops.begin() );
if ( index == 0 )
return premultiplied( brush.colors.front() );
if ( index == brush.stops.size() )
return premultiplied( brush.colors.back() );
struct { rgba color; float stop; std::optional<float> hint; }
A = { brush.colors[index-1], brush.stops[index-1], brush.hints[index-1] },
B = { brush.colors[index], brush.stops[index], {} };
// https://drafts.csswg.org/css-images-4/#coloring-gradient-line
// 1. Determine the location of the transition hint as a percentage of the distance between the two color stops, denoted as a number between 0 and 1
float H = !A.hint ? .5f : (*A.hint - A.stop) / (B.stop - A.stop);
// 2. For any given point between the two color stops, determine the points location as a percentage of the distance between the two color stops, in the same way as the previous step.
float P = (offset - A.stop) / (B.stop - A.stop);
// 3. Let C, the color weighting at that point, be equal to P^logH(.5).
float C = pow(P, log(.5f) / log(H));
// 4. The color at that point is then a linear blend between the colors of the two color stops, blending (1 - C) of the first stop and C of the second stop.
return premultiplied((1 - C) * A.color + C * B.color);
}
// Render the shadow of the polylines into the pixel buffer if needed. After
// computing the border as the maximum distance that one pixel can affect
// another via the blur, it scan-converts the lines to runs with the shadow
// offset and that extra amount of border padding. Then it bounds the scan
// converted shape, pads this with border, clips that to the extended canvas
// size, and rasterizes to fill a working area with the alpha. Next, a fast
// approximation of a Gaussian blur is done using three passes of box blurs
// each in the rows and columns. Note that these box blurs have a small extra
// weight on the tails to allow for fractional widths. See "Theoretical
// Foundations of Gaussian Convolution by Extended Box Filtering" by Gwosdek
// et al. for details. Finally, it colors the blurred alpha image with
// the shadow color and blends this into the pixel buffer according to the
// compositing settings and clip mask. Note that it does not bother clearing
// outside the area of the alpha image when the compositing settings require
// clearing; that will be done on the subsequent main rendering pass.
//
void canvas::render_shadow(
paint_brush const &brush )
{
if ( shadow_color.a == 0.0f || ( shadow_blur == 0.0f &&
shadow_offset_x == 0.0f &&
shadow_offset_y == 0.0f ) )
return;
float sigma_squared = 0.25f * shadow_blur * shadow_blur;
size_t radius = static_cast< size_t >(
0.5f * sqrtf( 4.0f * sigma_squared + 1.0f ) - 0.5f );
int border = 3 * ( static_cast< int >( radius ) + 1 );
xy offset = xy( static_cast< float >( border ) + shadow_offset_x,
static_cast< float >( border ) + shadow_offset_y );
lines_to_runs( offset, 2 * border );
int left = size_x + 2 * border;
int right = 0;
int top = size_y + 2 * border;
int bottom = 0;
for ( size_t index = 0; index < runs.size(); ++index )
{
left = std::min( left, static_cast< int >( runs[ index ].x ) );
right = std::max( right, static_cast< int >( runs[ index ].x ) );
top = std::min( top, static_cast< int >( runs[ index ].y ) );
bottom = std::max( bottom, static_cast< int >( runs[ index ].y ) );
}
left = std::max( left - border, 0 );
right = std::min( right + border, size_x + 2 * border ) + 1;
top = std::max( top - border, 0 );
bottom = std::min( bottom + border, size_y + 2 * border );
size_t width = static_cast< size_t >( std::max( right - left, 0 ) );
size_t height = static_cast< size_t >( std::max( bottom - top, 0 ) );
size_t working = width * height;
shadow.clear();
shadow.resize( working + std::max( width, height ) );
static float const threshold = 1.0f / 8160.0f;
{
int x = -1;
int y = -1;
float sum = 0.0f;
for ( size_t index = 0; index < runs.size(); ++index )
{
pixel_run next = runs[ index ];
float coverage = std::min( fabsf( sum ), 1.0f );
int to = next.y == y ? next.x : x + 1;
if ( coverage >= threshold )
for ( ; x < to; ++x )
shadow[ static_cast< size_t >( y - top ) * width +
static_cast< size_t >( x - left ) ] = coverage *
paint_pixel( xy( static_cast< float >( x ) + 0.5f,
static_cast< float >( y ) + 0.5f ) -
offset, brush ).a;
if ( next.y != y )
sum = 0.0f;
x = next.x;
y = next.y;
sum += next.delta;
}
}
float alpha = static_cast< float >( 2 * radius + 1 ) *
( static_cast< float >( radius * ( radius + 1 ) ) - sigma_squared ) /
( 2.0f * sigma_squared -
static_cast< float >( 6 * ( radius + 1 ) * ( radius + 1 ) ) );
float divisor = 2.0f * ( alpha + static_cast< float >( radius ) ) + 1.0f;
float weight_1 = alpha / divisor;
float weight_2 = ( 1.0f - alpha ) / divisor;
for ( size_t y = 0; y < height; ++y )
for ( int pass = 0; pass < 3; ++pass )
{
for ( size_t x = 0; x < width; ++x )
shadow[ working + x ] = shadow[ y * width + x ];
float running = weight_1 * shadow[ working + radius + 1 ];
for ( size_t x = 0; x <= radius; ++x )
running += ( weight_1 + weight_2 ) * shadow[ working + x ];
shadow[ y * width ] = running;
for ( size_t x = 1; x < width; ++x )
{
if ( x >= radius + 1 )
running -= weight_2 * shadow[ working + x - radius - 1 ];
if ( x >= radius + 2 )
running -= weight_1 * shadow[ working + x - radius - 2 ];
if ( x + radius < width )
running += weight_2 * shadow[ working + x + radius ];
if ( x + radius + 1 < width )
running += weight_1 * shadow[ working + x + radius + 1 ];
shadow[ y * width + x ] = running;
}
}
for ( size_t x = 0; x < width; ++x )
for ( int pass = 0; pass < 3; ++pass )
{
for ( size_t y = 0; y < height; ++y )
shadow[ working + y ] = shadow[ y * width + x ];
float running = weight_1 * shadow[ working + radius + 1 ];
for ( size_t y = 0; y <= radius; ++y )
running += ( weight_1 + weight_2 ) * shadow[ working + y ];
shadow[ x ] = running;
for ( size_t y = 1; y < height; ++y )
{
if ( y >= radius + 1 )
running -= weight_2 * shadow[ working + y - radius - 1 ];
if ( y >= radius + 2 )
running -= weight_1 * shadow[ working + y - radius - 2 ];
if ( y + radius < height )
running += weight_2 * shadow[ working + y + radius ];
if ( y + radius + 1 < height )
running += weight_1 * shadow[ working + y + radius + 1 ];
shadow[ y * width + x ] = running;
}
}
int operation = global_composite_operation;
int x = -1;
int y = -1;
float sum = 0.0f;
for ( size_t index = 0; index < mask.size(); ++index )
{
pixel_run next = mask[ index ];
float visibility = std::min( fabsf( sum ), 1.0f );
int to = std::min( next.y == y ? next.x : x + 1, right - border );
if ( visibility >= threshold &&
top <= y + border && y + border < bottom )
for ( ; x < to; ++x )
{
rgba &back = bitmap[ y * size_x + x ];
rgba fore = global_alpha *
shadow[
static_cast< size_t >( y + border - top ) * width +
static_cast< size_t >( x + border - left ) ] *
shadow_color;
float mix_fore = operation & 1 ? back.a : 0.0f;
if ( operation & 2 )
mix_fore = 1.0f - mix_fore;
float mix_back = operation & 4 ? fore.a : 0.0f;
if ( operation & 8 )
mix_back = 1.0f - mix_back;
rgba blend = mix_fore * fore + mix_back * back;
blend.a = std::min( blend.a, 1.0f );
back = visibility * blend + ( 1.0f - visibility ) * back;
}
if ( next.y != y )
sum = 0.0f;
x = std::max( static_cast< int >( next.x ), left - border );
y = next.y;
sum += next.delta;
}
}
// Render the polylines into the pixel buffer. It scan-converts the lines
// to runs which represent changes to the signed fractional coverage when
// read from left-to-right, top-to-bottom. It scans through these to
// determine spans of pixels that need to be drawn, paints those pixels
// according to the brush, and then blends them into the buffer according
// to the current compositing settings. This is slightly more complicated
// because it interleaves this with a simultaneous scan through a similar
// set of runs representing the current clip mask to determine which pixels
// it can composite into. Note that shadows are always drawn first.
//
void canvas::render_main(
paint_brush const &brush )
{
if ( forward.a * forward.d - forward.b * forward.c == 0.0f )
return;
render_shadow( brush );
lines_to_runs( xy( 0.0f, 0.0f ), 0 );
int operation = global_composite_operation;
int x = -1;
int y = -1;
float path_sum = 0.0f;
float clip_sum = 0.0f;
size_t path_index = 0;
size_t clip_index = 0;
while ( clip_index < mask.size() )
{
bool which = ( path_index < runs.size() &&
runs[ path_index ] < mask[ clip_index ] );
pixel_run next = which ? runs[ path_index ] : mask[ clip_index ];
float coverage = std::min( fabsf( path_sum ), 1.0f );
float visibility = std::min( fabsf( clip_sum ), 1.0f );
int to = next.y == y ? next.x : x + 1;
static float const threshold = 1.0f / 8160.0f;
if ( ( coverage >= threshold || ~operation & 8 ) &&
visibility >= threshold )
for ( ; x < to; ++x )
{
rgba &back = bitmap[ y * size_x + x ];
rgba fore = coverage * global_alpha *
paint_pixel( xy( static_cast< float >( x ) + 0.5f,
static_cast< float >( y ) + 0.5f ),
brush );
float mix_fore = operation & 1 ? back.a : 0.0f;
if ( operation & 2 )
mix_fore = 1.0f - mix_fore;
float mix_back = operation & 4 ? fore.a : 0.0f;
if ( operation & 8 )
mix_back = 1.0f - mix_back;
rgba blend = mix_fore * fore + mix_back * back;
blend.a = std::min( blend.a, 1.0f );
back = visibility * blend + ( 1.0f - visibility ) * back;
}
x = next.x;
if ( next.y != y )
{
y = next.y;
path_sum = 0.0f;
clip_sum = 0.0f;
}
if ( which )
path_sum += runs[ path_index++ ].delta;
else
clip_sum += mask[ clip_index++ ].delta;
}
}
canvas::canvas(
int width,
int height )
: global_composite_operation( source_over ),
shadow_offset_x( 0.0f ),
shadow_offset_y( 0.0f ),
line_cap( butt ),
line_join( miter ),
line_dash_offset( 0.0f ),
text_align( start ),
text_baseline( alphabetic ),
size_x( width ),
size_y( height ),
global_alpha( 1.0f ),
shadow_blur( 0.0f ),
line_width( 1.0f ),
miter_limit( 10.0f ),
fill_brush(),
stroke_brush(),
image_brush(),
face(),
bitmap( new rgba[ width * height ] ),
saves( 0 )
{
affine_matrix identity = { 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f };
forward = identity;
inverse = identity;
set_color( fill_style, 0.0f, 0.0f, 0.0f, 1.0f );
set_color( stroke_style, 0.0f, 0.0f, 0.0f, 1.0f );
for ( unsigned short y = 0; y < size_y; ++y )
{
pixel_run piece_1 = { 0, y, 1.0f };
pixel_run piece_2 = { static_cast< unsigned short >( size_x ), y,
-1.0f };
mask.push_back( piece_1 );
mask.push_back( piece_2 );
}
}
canvas::canvas(int width, int height, rgba c) : canvas(width, height)
{
save();
set_color(fill_style, c.r, c.g, c.b, c.a);
fill_rectangle(0, 0, (float)width, (float)height);
restore();
}
canvas::~canvas()
{
delete[] bitmap;
while ( canvas *head = saves )
{
saves = head->saves;
head->saves = 0;
delete head;
}
}
void canvas::scale(
float x,
float y )
{
transform( x, 0.0f, 0.0f, y, 0.0f, 0.0f );
}
void canvas::rotate(
float angle )
{
float cosine = cosf( angle );
float sine = sinf( angle );
transform( cosine, sine, -sine, cosine, 0.0f, 0.0f );
}
void canvas::translate(
float x,
float y )
{
transform( 1.0f, 0.0f, 0.0f, 1.0f, x, y );
}
void canvas::transform(
float a,
float b,
float c,
float d,
float e,
float f )
{
set_transform( forward.a * a + forward.c * b,
forward.b * a + forward.d * b,
forward.a * c + forward.c * d,
forward.b * c + forward.d * d,
forward.a * e + forward.c * f + forward.e,
forward.b * e + forward.d * f + forward.f );
}
void canvas::set_transform(
float a,
float b,
float c,
float d,
float e,
float f )
{
float determinant = a * d - b * c;
float scaling = determinant != 0.0f ? 1.0f / determinant : 0.0f;
affine_matrix new_forward = { a, b, c, d, e, f };
affine_matrix new_inverse = {
scaling * d, scaling * -b, scaling * -c, scaling * a,
scaling * ( c * f - d * e ), scaling * ( b * e - a * f ) };
forward = new_forward;
inverse = new_inverse;
}
void canvas::set_global_alpha(
float alpha )
{
if ( 0.0f <= alpha && alpha <= 1.0f )
global_alpha = alpha;
}
void canvas::set_shadow_color(
float red,
float green,
float blue,
float alpha )
{
shadow_color = premultiplied( linearized( clamped(
rgba( red, green, blue, alpha ) ) ) );
}
void canvas::set_shadow_blur(
float level )
{
if ( 0.0f <= level )
shadow_blur = level;
}
void canvas::set_line_width(
float width )
{
if ( 0.0f < width )
line_width = width;
}
void canvas::set_miter_limit(
float limit )
{
if ( 0.0f < limit )
miter_limit = limit;
}
void canvas::set_line_dash(
float const *segments,
int count )
{
for ( int index = 0; index < count; ++index )
if ( segments && segments[ index ] < 0.0f )
return;
line_dash.clear();
if ( !segments )
return;
for ( int index = 0; index < count; ++index )
line_dash.push_back( segments[ index ] );
if ( count & 1 )
for ( int index = 0; index < count; ++index )
line_dash.push_back( segments[ index ] );
}
void canvas::set_color(
brush_type type,
float red,
float green,
float blue,
float alpha )
{
paint_brush &brush = type == fill_style ? fill_brush : stroke_brush;
brush.type = paint_brush::color;
brush.colors.clear();
brush.colors.push_back( premultiplied( linearized( clamped(
rgba( red, green, blue, alpha ) ) ) ) );
}
void canvas::set_linear_gradient(
brush_type type,
float start_x,
float start_y,
float end_x,
float end_y )
{
paint_brush &brush = type == fill_style ? fill_brush : stroke_brush;
brush.type = paint_brush::linear;
brush.colors.clear();
brush.stops.clear();
brush.hints.clear();
brush.start = xy( start_x, start_y );
brush.end = xy( end_x, end_y );
}
void canvas::set_radial_gradient(
brush_type type,
float start_x,
float start_y,
float start_radius,
float end_x,
float end_y,
float end_radius )
{
if ( start_radius < 0.0f || end_radius < 0.0f )
return;
paint_brush &brush = type == fill_style ? fill_brush : stroke_brush;
brush.type = paint_brush::radial;
brush.colors.clear();
brush.stops.clear();
brush.hints.clear();
brush.start = xy( start_x, start_y );
brush.end = xy( end_x, end_y );
brush.start_radius = start_radius;
brush.end_radius = end_radius;
}
void canvas::set_css_radial_gradient(
brush_type type,
float x,
float y,
float radius_x,
float radius_y)
{
if (radius_x < 0.0f || radius_y < 0.0f)
return;
paint_brush& brush = type == fill_style ? fill_brush : stroke_brush;
brush.type = paint_brush::css_radial;
brush.colors.clear();
brush.stops.clear();
brush.hints.clear();
brush.start = {x, y};
brush.css_radius = {radius_x, radius_y};
}
void canvas::set_conic_gradient(
brush_type type,
float x,
float y,
float angle)
{
paint_brush& brush = type == fill_style ? fill_brush : stroke_brush;
brush.type = paint_brush::conic;
brush.colors = {};
brush.stops = {};
brush.hints = {};
brush.start = {x, y};
brush.angle = angle;
}
void canvas::add_color_stop(
brush_type type,
float offset,
float red,
float green,
float blue,
float alpha,
std::optional<float> hint )
{
paint_brush &brush = type == fill_style ? fill_brush : stroke_brush;
if ( ( brush.type != paint_brush::linear &&
brush.type != paint_brush::radial &&
brush.type != paint_brush::css_radial &&
brush.type != paint_brush::conic ) ||
offset < 0.0f || 1.0f < offset )
return;
ptrdiff_t index = std::upper_bound(
brush.stops.begin(), brush.stops.end(), offset ) -
brush.stops.begin();
rgba color = linearized( clamped( rgba( red, green, blue, alpha ) ) );
brush.colors.insert( brush.colors.begin() + index, color );
brush.stops.insert( brush.stops.begin() + index, offset );
brush.hints.insert( brush.hints.begin() + index, hint );
}
void canvas::set_pattern(
brush_type type,
unsigned char const *image,
int width,
int height,
int stride,
repetition_style repetition )
{
if ( !image || width <= 0 || height <= 0 )
return;
paint_brush &brush = type == fill_style ? fill_brush : stroke_brush;
brush.type = paint_brush::pattern;
brush.colors.clear();
for ( int y = 0; y < height; ++y )
for ( int x = 0; x < width; ++x )
{
int index = y * stride + x * 4;
rgba color = rgba(
image[ index + 0 ] / 255.0f, image[ index + 1 ] / 255.0f,
image[ index + 2 ] / 255.0f, image[ index + 3 ] / 255.0f );
brush.colors.push_back( premultiplied( linearized( color ) ) );
}
brush.width = width;
brush.height = height;
brush.repetition = repetition;
}
void canvas::begin_path()
{
path.points.clear();
path.subpaths.clear();
}
void canvas::move_to(
float x,
float y )
{
if ( !path.subpaths.empty() && path.subpaths.back().count == 1 )
{
path.points.back() = forward * xy( x, y );
return;
}
subpath_data subpath = { 1, false };
path.points.push_back( forward * xy( x, y ) );
path.subpaths.push_back( subpath );
}
void canvas::close_path()
{
if ( path.subpaths.empty() )
return;
xy first = path.points[ path.points.size() - path.subpaths.back().count ];
affine_matrix saved_forward = forward;
affine_matrix identity = { 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f };
forward = identity;
line_to( first.x, first.y );
path.subpaths.back().closed = true;
move_to( first.x, first.y );
forward = saved_forward;
}
void canvas::line_to(
float x,
float y )
{
if ( path.subpaths.empty() )
{
move_to( x, y );
return;
}
xy point_1 = path.points.back();
xy point_2 = forward * xy( x, y );
if ( dot( point_2 - point_1, point_2 - point_1 ) == 0.0f )
return;
path.points.push_back( point_1 );
path.points.push_back( point_2 );
path.points.push_back( point_2 );
path.subpaths.back().count += 3;
}
void canvas::quadratic_curve_to(
float control_x,
float control_y,
float x,
float y )
{
if ( path.subpaths.empty() )
move_to( control_x, control_y );
xy point_1 = path.points.back();
xy control = forward * xy( control_x, control_y );
xy point_2 = forward * xy( x, y );
xy control_1 = lerp( point_1, control, 2.0f / 3.0f );
xy control_2 = lerp( point_2, control, 2.0f / 3.0f );
path.points.push_back( control_1 );
path.points.push_back( control_2 );
path.points.push_back( point_2 );
path.subpaths.back().count += 3;
}
void canvas::bezier_curve_to(
float control_1_x,
float control_1_y,
float control_2_x,
float control_2_y,
float x,
float y )
{
if ( path.subpaths.empty() )
move_to( control_1_x, control_1_y );
xy control_1 = forward * xy( control_1_x, control_1_y );
xy control_2 = forward * xy( control_2_x, control_2_y );
xy point_2 = forward * xy( x, y );
path.points.push_back( control_1 );
path.points.push_back( control_2 );
path.points.push_back( point_2 );
path.subpaths.back().count += 3;
}
void canvas::arc_to(
float vertex_x,
float vertex_y,
float x,
float y,
float radius )
{
if ( radius < 0.0f ||
forward.a * forward.d - forward.b * forward.c == 0.0f )
return;
if ( path.subpaths.empty() )
move_to( vertex_x, vertex_y );
xy point_1 = inverse * path.points.back();
xy vertex = xy( vertex_x, vertex_y );
xy point_2 = xy( x, y );
xy edge_1 = normalized( point_1 - vertex );
xy edge_2 = normalized( point_2 - vertex );
float sine = fabsf( dot( perpendicular( edge_1 ), edge_2 ) );
static float const epsilon = 1.0e-4f;
if ( sine < epsilon )
{
line_to( vertex_x, vertex_y );
return;
}
xy offset = radius / sine * ( edge_1 + edge_2 );
xy center = vertex + offset;
float angle_1 = direction( dot( offset, edge_1 ) * edge_1 - offset );
float angle_2 = direction( dot( offset, edge_2 ) * edge_2 - offset );
bool reverse = static_cast< int >(
floorf( ( angle_2 - angle_1 ) / pi ) ) & 1;
arc( center.x, center.y, radius, angle_1, angle_2, reverse );
}
void canvas::arc(
float x,
float y,
float radius,
float start_angle,
float end_angle,
bool counter_clockwise )
{
if ( radius < 0.0f )
return;
static float const tau = 2 * pi;
float winding = counter_clockwise ? -1.0f : 1.0f;
float from = fmodf( start_angle, tau );
float span = fmodf( end_angle, tau ) - from;
if ( ( end_angle - start_angle ) * winding >= tau )
span = tau * winding;
else if ( span * winding < 0.0f )
span += tau * winding;
xy centered_1 = radius * xy( cosf( from ), sinf( from ) );
line_to( x + centered_1.x, y + centered_1.y );
if ( span == 0.0f )
return;
int steps = static_cast< int >(
std::max( 1.0f, roundf( 16.0f / tau * span * winding ) ) );
float segment = span / static_cast< float >( steps );
float alpha = 4.0f / 3.0f * tanf( 0.25f * segment );
for ( int step = 0; step < steps; ++step )
{
float angle = from + static_cast< float >( step + 1 ) * segment;
xy centered_2 = radius * xy( cosf( angle ), sinf( angle ) );
xy point_1 = xy( x, y ) + centered_1;
xy point_2 = xy( x, y ) + centered_2;
xy control_1 = point_1 + alpha * perpendicular( centered_1 );
xy control_2 = point_2 - alpha * perpendicular( centered_2 );
bezier_curve_to( control_1.x, control_1.y,
control_2.x, control_2.y,
point_2.x, point_2.y );
centered_1 = centered_2;
}
}
void canvas::rectangle(
float x,
float y,
float width,
float height )
{
move_to( x, y );
line_to( x + width, y );
line_to( x + width, y + height );
line_to( x, y + height );
close_path();
}
void canvas::polygon(std::vector<xy> points)
{
move_to(points[0].x, points[0].y);
for (auto pt : points)
line_to(pt.x, pt.y);
close_path();
}
void canvas::fill()
{
path_to_lines( false );
render_main( fill_brush );
}
void canvas::stroke()
{
path_to_lines( true );
stroke_lines();
render_main( stroke_brush );
}
void canvas::clip()
{
path_to_lines( false );
lines_to_runs( xy( 0.0f, 0.0f ), 0 );
size_t part = runs.size();
runs.insert( runs.end(), mask.begin(), mask.end() );
mask.clear();
int y = -1;
float last = 0.0f;
float sum_1 = 0.0f;
float sum_2 = 0.0f;
size_t index_1 = 0;
size_t index_2 = part;
while ( index_1 < part && index_2 < runs.size() )
{
bool which = runs[ index_1 ] < runs[ index_2 ];
pixel_run next = which ? runs[ index_1 ] : runs[ index_2 ];
if ( next.y != y )
{
y = next.y;
last = 0.0f;
sum_1 = 0.0f;
sum_2 = 0.0f;
}
if ( which )
sum_1 += runs[ index_1++ ].delta;
else
sum_2 += runs[ index_2++ ].delta;
float visibility = ( std::min( fabsf( sum_1 ), 1.0f ) *
std::min( fabsf( sum_2 ), 1.0f ) );
if ( visibility == last )
continue;
if ( !mask.empty() &&
mask.back().x == next.x && mask.back().y == next.y )
mask.back().delta += visibility - last;
else
{
pixel_run piece = { next.x, next.y, visibility - last };
mask.push_back( piece );
}
last = visibility;
}
}
bool canvas::is_point_in_path(
float x,
float y )
{
path_to_lines( false );
int winding = 0;
size_t subpath = 0;
size_t beginning = 0;
size_t ending = 0;
for ( size_t index = 0; index < lines.points.size(); ++index )
{
while ( index >= ending )
{
beginning = ending;
ending += lines.subpaths[ subpath++ ].count;
}
xy from = lines.points[ index ];
xy to = lines.points[ index + 1 < ending ? index + 1 : beginning ];
if ( ( from.y < y && y <= to.y ) || ( to.y < y && y <= from.y ) )
{
float side = dot( perpendicular( to - from ), xy( x, y ) - from );
if ( side == 0.0f )
return true;
winding += side > 0.0f ? 1 : -1;
}
else if ( from.y == y && y == to.y &&
( ( from.x <= x && x <= to.x ) ||
( to.x <= x && x <= from.x ) ) )
return true;
}
return winding;
}
void canvas::clear_rectangle(
float x,
float y,
float width,
float height )
{
composite_operation saved_operation = global_composite_operation;
float saved_global_alpha = global_alpha;
float saved_alpha = shadow_color.a;
paint_brush::types saved_type = fill_brush.type;
global_composite_operation = destination_out;
global_alpha = 1.0f;
shadow_color.a = 0.0f;
fill_brush.type = paint_brush::color;
fill_rectangle( x, y, width, height );
fill_brush.type = saved_type;
shadow_color.a = saved_alpha;
global_alpha = saved_global_alpha;
global_composite_operation = saved_operation;
}
void canvas::fill_rectangle(
float x,
float y,
float width,
float height )
{
if ( width == 0.0f || height == 0.0f )
return;
lines.points.clear();
lines.subpaths.clear();
lines.points.push_back( forward * xy( x, y ) );
lines.points.push_back( forward * xy( x + width, y ) );
lines.points.push_back( forward * xy( x + width, y + height ) );
lines.points.push_back( forward * xy( x, y + height ) );
subpath_data entry = { 4, true };
lines.subpaths.push_back( entry );
render_main( fill_brush );
}
void canvas::stroke_rectangle(
float x,
float y,
float width,
float height )
{
if ( width == 0.0f && height == 0.0f )
return;
lines.points.clear();
lines.subpaths.clear();
if ( width == 0.0f || height == 0.0f )
{
lines.points.push_back( forward * xy( x, y ) );
lines.points.push_back( forward * xy( x + width, y + height ) );
subpath_data entry = { 2, false };
lines.subpaths.push_back( entry );
}
else
{
lines.points.push_back( forward * xy( x, y ) );
lines.points.push_back( forward * xy( x + width, y ) );
lines.points.push_back( forward * xy( x + width, y + height ) );
lines.points.push_back( forward * xy( x, y + height ) );
lines.points.push_back( forward * xy( x, y ) );
subpath_data entry = { 5, true };
lines.subpaths.push_back( entry );
}
stroke_lines();
render_main( stroke_brush );
}
bool canvas::set_font(
unsigned char const *font,
int bytes,
float size )
{
if ( font && bytes )
{
face.data.clear();
face.cmap = 0;
face.glyf = 0;
face.head = 0;
face.hhea = 0;
face.hmtx = 0;
face.loca = 0;
face.maxp = 0;
face.os_2 = 0;
if ( bytes < 6 )
return false;
int version = ( font[ 0 ] << 24 | font[ 1 ] << 16 |
font[ 2 ] << 8 | font[ 3 ] << 0 );
int tables = font[ 4 ] << 8 | font[ 5 ];
if ( ( version != 0x00010000 && version != 0x74727565 ) ||
bytes < tables * 16 + 12 )
return false;
face.data.insert( face.data.end(), font, font + tables * 16 + 12 );
for ( int index = 0; index < tables; ++index )
{
int tag = signed_32( face.data, index * 16 + 12 );
int offset = signed_32( face.data, index * 16 + 20 );
int span = signed_32( face.data, index * 16 + 24 );
if ( bytes < offset + span )
{
face.data.clear();
return false;
}
int place = static_cast< int >( face.data.size() );
if ( tag == 0x636d6170 )
face.cmap = place;
else if ( tag == 0x676c7966 )
face.glyf = place;
else if ( tag == 0x68656164 )
face.head = place;
else if ( tag == 0x68686561 )
face.hhea = place;
else if ( tag == 0x686d7478 )
face.hmtx = place;
else if ( tag == 0x6c6f6361 )
face.loca = place;
else if ( tag == 0x6d617870 )
face.maxp = place;
else if ( tag == 0x4f532f32 )
face.os_2 = place;
else
continue;
face.data.insert(
face.data.end(), font + offset, font + offset + span );
}
if ( !face.cmap || !face.glyf || !face.head || !face.hhea ||
!face.hmtx || !face.loca || !face.maxp || !face.os_2 )
{
face.data.clear();
return false;
}
}
if ( face.data.empty() )
return false;
int units_per_em = unsigned_16( face.data, face.head + 18 );
face.scale = size / static_cast< float >( units_per_em );
return true;
}
void canvas::fill_text(
char const *text,
float x,
float y,
float maximum_width )
{
text_to_lines( text, xy( x, y ), maximum_width, false );
render_main( fill_brush );
}
void canvas::stroke_text(
char const *text,
float x,
float y,
float maximum_width )
{
text_to_lines( text, xy( x, y ), maximum_width, true );
stroke_lines();
render_main( stroke_brush );
}
float canvas::measure_text(
char const *text )
{
if ( face.data.empty() || !text )
return 0.0f;
int hmetrics = unsigned_16( face.data, face.hhea + 34 );
int width = 0;
for ( int index = 0; text[ index ]; )
{
int glyph = character_to_glyph( text, index );
int entry = std::min( glyph, hmetrics - 1 );
width += unsigned_16( face.data, face.hmtx + entry * 4 );
}
return static_cast< float >( width ) * face.scale;
}
void canvas::draw_image(
unsigned char const *image,
int width,
int height,
int stride,
float x,
float y,
float to_width,
float to_height )
{
if ( !image || width <= 0 || height <= 0 ||
to_width == 0.0f || to_height == 0.0f )
return;
std::swap( fill_brush, image_brush );
set_pattern( fill_style, image, width, height, stride, repeat );
std::swap( fill_brush, image_brush );
lines.points.clear();
lines.subpaths.clear();
lines.points.push_back( forward * xy( x, y ) );
lines.points.push_back( forward * xy( x + to_width, y ) );
lines.points.push_back( forward * xy( x + to_width, y + to_height ) );
lines.points.push_back( forward * xy( x, y + to_height ) );
subpath_data entry = { 4, true };
lines.subpaths.push_back( entry );
affine_matrix saved_forward = forward;
affine_matrix saved_inverse = inverse;
translate( x + std::min( 0.0f, to_width ),
y + std::min( 0.0f, to_height ) );
scale( fabsf( to_width ) / static_cast< float >( width ),
fabsf( to_height ) / static_cast< float >( height ) );
render_main( image_brush );
forward = saved_forward;
inverse = saved_inverse;
}
void canvas::get_image_data(
unsigned char *image,
int width,
int height,
int stride,
int x,
int y )
{
if ( !image )
return;
static float const bayer[][ 4 ] = {
{ 0.03125f, 0.53125f, 0.15625f, 0.65625f },
{ 0.78125f, 0.28125f, 0.90625f, 0.40625f },
{ 0.21875f, 0.71875f, 0.09375f, 0.59375f },
{ 0.96875f, 0.46875f, 0.84375f, 0.34375f } };
for ( int image_y = 0; image_y < height; ++image_y )
for ( int image_x = 0; image_x < width; ++image_x )
{
int index = image_y * stride + image_x * 4;
int canvas_x = x + image_x;
int canvas_y = y + image_y;
rgba color = rgba( 0.0f, 0.0f, 0.0f, 0.0f );
if ( 0 <= canvas_x && canvas_x < size_x &&
0 <= canvas_y && canvas_y < size_y )
color = bitmap[ canvas_y * size_x + canvas_x ];
float threshold = bayer[ canvas_y & 3 ][ canvas_x & 3 ];
color = rgba( threshold, threshold, threshold, threshold ) +
255.0f * delinearized( clamped( unpremultiplied( color ) ) );
image[ index + 0 ] = static_cast< unsigned char >( color.r );
image[ index + 1 ] = static_cast< unsigned char >( color.g );
image[ index + 2 ] = static_cast< unsigned char >( color.b );
image[ index + 3 ] = static_cast< unsigned char >( color.a );
}
}
void canvas::put_image_data(
unsigned char const *image,
int width,
int height,
int stride,
int x,
int y )
{
if ( !image )
return;
for ( int image_y = 0; image_y < height; ++image_y )
for ( int image_x = 0; image_x < width; ++image_x )
{
int index = image_y * stride + image_x * 4;
int canvas_x = x + image_x;
int canvas_y = y + image_y;
if ( canvas_x < 0 || size_x <= canvas_x ||
canvas_y < 0 || size_y <= canvas_y )
continue;
rgba color = rgba(
image[ index + 0 ] / 255.0f, image[ index + 1 ] / 255.0f,
image[ index + 2 ] / 255.0f, image[ index + 3 ] / 255.0f );
bitmap[ canvas_y * size_x + canvas_x ] =
premultiplied( linearized( color ) );
}
}
void canvas::save()
{
canvas *state = new canvas( 0, 0 );
state->global_composite_operation = global_composite_operation;
state->shadow_offset_x = shadow_offset_x;
state->shadow_offset_y = shadow_offset_y;
state->line_cap = line_cap;
state->line_join = line_join;
state->line_dash_offset = line_dash_offset;
state->text_align = text_align;
state->text_baseline = text_baseline;
state->forward = forward;
state->inverse = inverse;
state->global_alpha = global_alpha;
state->shadow_color = shadow_color;
state->shadow_blur = shadow_blur;
state->line_width = line_width;
state->miter_limit = miter_limit;
state->line_dash = line_dash;
state->fill_brush = fill_brush;
state->stroke_brush = stroke_brush;
state->mask = mask;
state->face = face;
state->saves = saves;
saves = state;
}
void canvas::restore()
{
if ( !saves )
return;
canvas *state = saves;
global_composite_operation = state->global_composite_operation;
shadow_offset_x = state->shadow_offset_x;
shadow_offset_y = state->shadow_offset_y;
line_cap = state->line_cap;
line_join = state->line_join;
line_dash_offset = state->line_dash_offset;
text_align = state->text_align;
text_baseline = state->text_baseline;
forward = state->forward;
inverse = state->inverse;
global_alpha = state->global_alpha;
shadow_color = state->shadow_color;
shadow_blur = state->shadow_blur;
line_width = state->line_width;
miter_limit = state->miter_limit;
line_dash = state->line_dash;
fill_brush = state->fill_brush;
stroke_brush = state->stroke_brush;
mask = state->mask;
face = state->face;
saves = state->saves;
state->saves = 0;
delete state;
}
}
#endif // CANVAS_ITY_IMPLEMENTATION