This adds support for dash patterns to Path::stroke_to_fill().
This is used in PDFs, <canvas>, and <svg>.
The implementation is based on the <canvas> spec. It seems to do
the right thing for PDF files too.
(This commit only adds the feature to LibGfx. Future commits will
hook this up for PDF, <canvas>, and <svg>.)
This makes several calling sites shorter, and makes it easier
to add a miter limit with less plumbing in the future.
This also exposed a bug in AffineDisplayListPlayerCPU.cpp: We weren't
passing path linecap style on. I added a FIXME for that for now.
No intended behavior change.
Similar to the first version of butt line joins, bevels are done
using the pen used for round joins and caps, we just don't add
any vertices except the last one on the pen. We can make this nicer
later on.
Previously, when stroking closed paths, we'd create a single
path where the inner stroke and the outer stroke were connected
with round linecaps. Since we currently only support round line joins,
this looked ok.
Now, we don't draw caps when stroking closed paths, but instead
generate a closed inner path and an independent closed outer path.
This has the advantage that it produces slightly less geometry, and
it's a prerequisite for implementing non-round line joins.
No visual difference.
Instead of a single loop that adds either a pen vertex or a path
vertex each round, there is now an outer loop for adding path vertices,
and an inner loop that adds all pen vertices needed at that vertex.
No behavior change.
PDF 1.7 spec, p231, 4.4.2 Path-Painting Operators, Stroking:
"If a subpath is degenerate (consists of a single-point closed path or
of two or more points at the same coordinates), the S operator paints it
only if round line caps have been specified, producing a filled circle
centered at the single point. If butt or projecting square line caps
have been specified, S produces no output, because the orientation of
the caps would be indeterminate. (This rule applies only to zero-length
subpaths of the path being stroked, and not to zero-length dashes in a
dash pattern. In the latter case, the line caps are always painted,
since their orientation is determined by the direction of the underlying
path.) A single-point open subpath (specified by a trailing m operator)
produces no output."
In practice, Chrome, Firefox, and Preview all also draw a square
for square line endings (Preview a square rotated 45 degrees). Chrome
even draws something weird-looking for butt caps. (Acrobat only draws
the round cap, per spec.)
https://html.spec.whatwg.org/multipage/canvas.html#trace-a-path sounds
like zero-lengths paths should be ignored for canvas, but in practice
Chrome and Firefox do draw them. (Safari doesn't.)
We don't do linecaps in SVGs yet, but
https://www.w3.org/TR/SVG/paths.html#ZeroLengthSegments says:
"As mentioned in Stroke Properties, linecaps must be painted for
zero-length subpaths when stroke-linecap has a value of round or
square."
With this commit, we now draw round and square linecaps for
zero-lengths paths, which is what's apparently desired most of the
time. Maybe we can add a setting to pick different behavior for
PDF (only draw round caps), canvas (don't draw caps on zero-length
paths), and SVG (draw round and square caps) in the future.
We now put butt line cap vertices in the correct position,
vertical to line direction. We still pretend we're walking on the
pen polygon though, so it's probably possible to make this produce
weird-looking output by making the line cap line segments very
short. In practice, for butt line caps of real-world paths,
it's a big improvement though.
Instead of one dense loop, there are now one call for the outer
stroke, one for the first cap, one for the inner stroke, and one
for the second cap.
This will make it easier to do butt caps correctly, and to add
support for square caps.
It also makes it easier to not add any caps at all for closed
paths.
Maybe it also helps for adding non-round joins eventually.
In particular:
* `shape_idx` now starts at 1, since we now start with the
stroke part, not the cap part, and explicitly call `close()`
to connect the second cap with the first stroke
* Having explicit cap building code means that the convolution
loop is kind-of duplicated for round caps
* We now need to remove duplicate points, else the explicit
cap drawing gets confused. This is the only non-behavior-preserving
part of this commit, and it's a progression for lines that have
two identical points at the end of an open path (this would previously
not correctly draw a round join)
* Similarly, there's no explicit rejection of empty paths
The ChunkIterator now limits a chunk to using only one font (before, it
was possible to have a chunk with >1 font, when `unicode-range` CSS
property is used).
This change allows us to reduce some complexity in the text shaping and
painting code and makes us compatible with the APIs in Skia and
HarfBuzz.
(cherry picked from commit 7181c3f2ea5fba73e77d98acbf9e46640b4a9015,
minorly amended to fix conflicts caused by:
* Our VectorFont not being renamed to Typeface
* Us cherry-picking https://github.com/LadybirdBrowser/ladybird/pull/502
first
* Us still having bitmap fonts, and hence needing glyph_spacing()
Also amended for:
* AffineDisplayListPlayerCPU changes
* Removing pure virtuals for glyph_id_for_code_point and
glyph_id_for_code_point in Font.h again since we still have BitmapFont
which can't implement them
* Updating more Painter methods that we still had
(Painter::draw_glyph_or_emoji(), Painter::draw_text_run())
)
A stroked path that has the same start and end point looks different
depending on if it's closed or not: If it's closed, the start/end point
is drawn as a line join; if it's not closed, the start/end point is
drawn as a cap.
(It has an effect only for stroked paths, but not for filled paths.)
So make Path remember if it's closed or not by adding a ClosePath
segment type.
This matches the canvas, pdf, svg specs.
(TinyVG doesn't have strokes yet.)
This fixes the apparent rendering regression from #25040 / #25044
(which just made an existing bug visible).
(We should probably make an inner and an outer path when stroking a
closed path instead of just giving closed paths a round cap. When
filled, both look identical, but the current approach produces more
geometry. For now, this is good enough.)
This is done by walking half the pen at the two line ends instead
of using each pen vertex (like it happens for round caps).
This won't be completely vertical to the stroke direction, but it
is kind of close, and it's very easy to implement. We'll improve
this later :^)
On macOS, when going from the right end of a horizontal line
to the left, and when slope_now is pi, and the current
range.end is 0 (i.e. current_angle = pi, target_angle = 0),
clockwise() would set target_angle to 2 * pi, and
`target_angle - current_angle` (ie 2 * pi - pi) would end up
being ever so sligthly more than `pi` (handwavingly due to
floating point in accuracies), and we'd go in the wrong direction.
Add an explicit check for that, and a reftest that catches this.
No observed behavior change on linux, but the reftest only passes
on macOS with the code change.
This is much more useful than the previous format, as you can now just
paste the path into a site like https://svg-path-visualizer.netlify.app/
to debug issues.
Rather than make path segments virtual and refcounted let's store
`Gfx::Path`s as a list of `FloatPoints` and a separate list of commands.
This reduces the size of paths, for example, a `MoveTo` goes from 24
bytes to 9 bytes (one point + a single byte command), and removes a
layer of indirection when accessing segments. A nice little bonus is
transforming a path can now be done by applying the transform to all
points in the path (without looking at the commands).
Alongside this there's been a few minor API changes:
- `path.segments()` has been removed
* All current uses could be replaced by a new `path.is_empty()` API
* There's also now an iterator for looping over `Gfx::Path` segments
- `path.add_path(other_path)` has been removed
* This was a duplicate of `path.append_path(other_path)`
- `path.ensure_subpath(point)` has been removed
* Had one use and is equivalent to an `is_empty()` check + `move_to()`
- `path.close()` and `path.close_all_subpaths()` assume an implicit
`moveto 0,0` if there's no `moveto` at the start of a path (for
consistency with `path.segmentize_path()`).
Only the last point could change behaviour (though in LibWeb/SVGs all
paths start with a `moveto` as per the spec, it's only possible to
construct a path without a starting `moveto` via LibGfx APIs).
This commit un-deprecates DeprecatedString, and repurposes it as a byte
string.
As the null state has already been removed, there are no other
particularly hairy blockers in repurposing this type as a byte string
(what it _really_ is).
This commit is auto-generated:
$ xs=$(ack -l \bDeprecatedString\b\|deprecated_string AK Userland \
Meta Ports Ladybird Tests Kernel)
$ perl -pie 's/\bDeprecatedString\b/ByteString/g;
s/deprecated_string/byte_string/g' $xs
$ clang-format --style=file -i \
$(git diff --name-only | grep \.cpp\|\.h)
$ gn format $(git ls-files '*.gn' '*.gni')
This change separates a part of the `draw_text_run()` function, which
is responsible for calculating the positions for glyphs that need to be
painted, into a separate function called `get_glyph_run()`.
It is a part of the preparation for text run painting using OpenGL,
where we can't immediately blit glyph bitmaps but instead need to
prepare a sequence of quads for them in advance.
This updates fonts so rather than rastering directly to a bitmap, you
can extract paths for glyphs. This is then used to implement a
Gfx::Path::text("some text", font) API, that if given a vector font
appends the path of the text to your Gfx::Path. This then allows
arbitrary manipulation of the text (rotation, skewing, etc), paving the
way for Word Art in Serenity.
Unlike all other primitives elliptical arcs are non-trivial to
manipulate, it's tricky to correctly apply a Gfx::AffineTransform to
them. Prior to this change, Path::copy_transformed() was still
incorrectly applying transforms such as flips and skews to arcs.
This patch very closely approximates arcs with cubic beziers (I can not
visually spot any differences), which can then be easily and correctly
transformed in all cases.
Most of the maths here was taken from:
https://mortoray.com/rendering-an-svg-elliptical-arc-as-bezier-curves/
(which came from https://www.joecridge.me/content/pdf/bezier-arcs.pdf,
now a dead link).
These are stored as floats internally and other parameters are
FloatPoints, so taking doubles is a little inconsistent. Doubles are
needed for the endpoint -> center parametrization conversion, but that
does not need exposing in the API.
This function generates a new path, which can be filled to rasterize
a stroke of the original path (at whatever thickness you like). It
does this by convolving a circular pen with the path, so right now
only supports round line caps.
Since filled paths now have good antialiasing, doing this results in
good stroked paths for "free". It also (for free) fixes stroked lines
with an opacity < 1, nice line joins, and is possible to fill with a
paint style (e.g. a gradient or an image).
Algorithm from: https://keithp.com/~keithp/talks/cairo2003.pdf
Remove SplitLineSegment and replace it with a FloatLine, nobody was
interested in its extra fields anymore. Also, remove the sorting of
the split segments, this really should not have been done here
anyway, and is not required by the rasterizer anymore. Keeping the
segments in stroke order will also make it possible to generate
stroked path geometry (in future).
Since close_all_subpaths() appends while iterating, the vector can
end up being resized and the iterator invalidated. Previously, this
led to a crash/UAF in some cases.
This class had slightly confusing semantics and the added weirdness
doesn't seem worth it just so we can say "." instead of "->" when
iterating over a vector of NNRPs.
This patch replaces NonnullRefPtrVector<T> with Vector<NNRP<T>>.