Skip to content

Commit effb09d

Browse files
committed
Improved AwtRenderingBackend arc() now forwards to ellipse() as suggested by the spec.
Improved AwtRenderingBackend ellipse() angle conversation from canvas to AWT. CanvasRenderingContext2D fill() supports the winding rule parameter.
1 parent 60bf030 commit effb09d

6 files changed

Lines changed: 270 additions & 52 deletions

File tree

src/changes/changes.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@
88

99
<body>
1010
<release version="5.3.0" date="July xx, 2026" description="Bugfixes">
11+
<action type="update" dev="rbri">
12+
Improved AwtRenderingBackend arc() now forwards to ellipse() as suggested by the spec.
13+
</action>
14+
<action type="update" dev="rbri">
15+
Improved AwtRenderingBackend ellipse() angle conversation from canvas to AWT.
16+
</action>
17+
<action type="add" dev="rbri">
18+
CanvasRenderingContext2D fill() supports the winding rule parameter.
19+
</action>
1120
<action type="update" dev="rbri">
1221
AwtRenderingBackend more robust clip handling in the SaveState.
1322
</action>

src/main/java/org/htmlunit/javascript/host/canvas/CanvasRenderingContext2D.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -473,10 +473,48 @@ public void ellipse(final double x, final double y,
473473

474474
/**
475475
* Fills the shape.
476+
* @param context the context
477+
* @param scope the scope
478+
* @param thisObj this object
479+
* @param args the arguments
480+
* @param function the function
476481
*/
477482
@JsxFunction
478-
public void fill() {
479-
getRenderingBackend().fill();
483+
public static void fill(final Context context, final VarScope scope,
484+
final Scriptable thisObj, final Object[] args, final Function function) {
485+
if (!(thisObj instanceof CanvasRenderingContext2D renderingCtx)) {
486+
throw JavaScriptEngine.reportRuntimeError(
487+
"CanvasRenderingContext2D.fill() failed - this is not a CanvasRenderingContext2D");
488+
}
489+
490+
// Determine which argument (if any) is the fill rule string.
491+
// Signature 1: fill(optional CanvasFillRule fillRule)
492+
// Signature 2: fill(Path2D path, optional CanvasFillRule fillRule)
493+
String fillRuleStr = null;
494+
if (args.length > 1) {
495+
fillRuleStr = JavaScriptEngine.toString(args[1]);
496+
}
497+
else if (args.length > 0) {
498+
fillRuleStr = JavaScriptEngine.toString(args[0]);
499+
}
500+
501+
RenderingBackend.WindingRule windingRule = RenderingBackend.WindingRule.NON_ZERO;
502+
if (fillRuleStr != null) {
503+
if ("evenodd".equals(fillRuleStr)) {
504+
windingRule = RenderingBackend.WindingRule.EVEN_ODD;
505+
}
506+
else if (!"nonzero".equals(fillRuleStr)) {
507+
// Per spec: unrecognised values are ignored entirely.
508+
// Since fill() has no persistent fill-rule state, we just
509+
// use the default nonzero — which is already set above.
510+
// Log it for debugging purposes.
511+
if (LOG.isWarnEnabled()) {
512+
LOG.warn("fill() called with unrecognised fillRule: '" + fillRuleStr + "', using 'nonzero'.");
513+
}
514+
}
515+
}
516+
517+
renderingCtx.getRenderingBackend().fill(windingRule);
480518
}
481519

482520
/**

src/main/java/org/htmlunit/platform/canvas/rendering/AwtRenderingBackend.java

Lines changed: 37 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -339,24 +339,35 @@ public void ellipse(final double x, final double y,
339339
LOG.debug("[" + id_ + "] ellipse()");
340340
}
341341

342-
final Point2D p = transformation_.transform(new Point2D.Double(x, y), null);
343-
final double startX = p.getX() + radiusX * Math.cos(rotation) * Math.cos(-startAngle)
344-
- radiusY * Math.sin(rotation) * Math.sin(-startAngle);
345-
final double startY = p.getY() + radiusX * Math.sin(rotation) * Math.cos(-startAngle)
346-
+ radiusY * Math.cos(rotation) * Math.sin(-startAngle);
347-
final double startAngleDegree = 360 - (startAngle * 180 / Math.PI);
348-
final double endAngleDegree = 360 - (endAngle * 180 / Math.PI);
349-
350-
double extendAngle = startAngleDegree - endAngleDegree;
351-
extendAngle = Math.min(360, Math.abs(extendAngle));
352-
if (anticlockwise && extendAngle < 360) {
353-
extendAngle = extendAngle - 360;
342+
if (startAngle == endAngle) {
343+
return;
354344
}
355345

346+
final Point2D p = transformation_.transform(new Point2D.Double(x, y), null);
356347
final AffineTransform transformation = new AffineTransform();
357348
transformation.rotate(rotation, p.getX(), p.getY());
358-
final Arc2D arc = new Arc2D.Double(p.getX() - radiusX, p.getY() - radiusY, radiusX * 2, radiusY * 2,
359-
startAngleDegree, extendAngle * -1, Arc2D.OPEN);
349+
350+
double startAngleDegree = Math.toDegrees(startAngle);
351+
double endAngleDegree = Math.toDegrees(endAngle);
352+
353+
double extendAngle = endAngleDegree - startAngleDegree;
354+
final Arc2D arc;
355+
if (Math.abs(extendAngle) >= 360) {
356+
arc = new Arc2D.Double(p.getX() - radiusX, p.getY() - radiusY, radiusX * 2, radiusY * 2,
357+
0, 360, Arc2D.OPEN);
358+
}
359+
else {
360+
startAngleDegree = reverseAngle(startAngleDegree);
361+
endAngleDegree = reverseAngle(endAngleDegree);
362+
363+
extendAngle = endAngleDegree - startAngleDegree;
364+
if (!anticlockwise) {
365+
extendAngle = -reverseAngle(extendAngle);
366+
}
367+
368+
arc = new Arc2D.Double(p.getX() - radiusX, p.getY() - radiusY, radiusX * 2, radiusY * 2,
369+
startAngleDegree, extendAngle, Arc2D.OPEN);
370+
}
360371

361372
// connect=true only if there is already a current point (implicit lineTo behaviour);
362373
// connect=false when the subpath is new so we don't get a spurious line from (0,0)
@@ -401,33 +412,11 @@ public void arc(final double x, final double y, final double radius, final doubl
401412
LOG.debug("[" + id_ + "] arc()");
402413
}
403414

404-
final Point2D p = transformation_.transform(new Point2D.Double(x, y), null);
405-
final double startX = p.getX() + radius * Math.cos(-startAngle);
406-
final double startY = p.getY() + radius * Math.sin(-startAngle);
407-
final double startAngleDegree = 360 - (startAngle * 180 / Math.PI);
408-
final double endAngleDegree = 360 - (endAngle * 180 / Math.PI);
409-
410-
double extendAngle = startAngleDegree - endAngleDegree;
411-
extendAngle = Math.min(360, Math.abs(extendAngle));
412-
if (anticlockwise && extendAngle < 360) {
413-
extendAngle = extendAngle - 360;
414-
}
415-
final Arc2D arc = new Arc2D.Double(p.getX() - radius, p.getY() - radius, radius * 2, radius * 2,
416-
startAngleDegree, extendAngle * -1, Arc2D.OPEN);
415+
ellipse(x, y, radius, radius, 0, startAngle, endAngle, anticlockwise);
416+
}
417417

418-
// connect=true only if there is already a current point (implicit lineTo behaviour);
419-
// connect=false when the subpath is new so we don't get a spurious line from (0,0)
420-
// or from the moveTo seed point to the arc's own geometric start.
421-
final boolean hasCurrentPoint;
422-
if (subPaths_.isEmpty()) {
423-
final Path2D subPath = new Path2D.Double();
424-
subPaths_.add(subPath);
425-
hasCurrentPoint = false;
426-
}
427-
else {
428-
hasCurrentPoint = subPaths_.get(subPaths_.size() - 1).getCurrentPoint() != null;
429-
}
430-
getCurrentSubPath().append(arc, hasCurrentPoint);
418+
private static double reverseAngle(final double degrees) {
419+
return ((-degrees % 360) + 360) % 360;
431420
}
432421

433422
/**
@@ -592,13 +581,19 @@ public String encodeToString(final String type) throws IOException {
592581
* {@inheritDoc}
593582
*/
594583
@Override
595-
public void fill() {
584+
public void fill(final RenderingBackend.WindingRule windingRule) {
596585
if (LOG.isDebugEnabled()) {
597-
LOG.debug("[" + id_ + "] fill()");
586+
LOG.debug("[" + id_ + "] fill("
587+
+ (windingRule == RenderingBackend.WindingRule.EVEN_ODD ? "evenOdd" : "nonZero") + ")");
598588
}
599589

590+
final int awtRule = (windingRule == RenderingBackend.WindingRule.EVEN_ODD)
591+
? Path2D.WIND_EVEN_ODD
592+
: Path2D.WIND_NON_ZERO;
593+
600594
graphics2D_.setColor(fillColor_);
601595
for (final Path2D path2d : subPaths_) {
596+
path2d.setWindingRule(awtRule);
602597
graphics2D_.fill(path2d);
603598
}
604599
}

src/main/java/org/htmlunit/platform/canvas/rendering/NoOpRenderingBackend.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ public String encodeToString(final String type) throws IOException {
142142
* {@inheritDoc}
143143
*/
144144
@Override
145-
public void fill() {
145+
public void fill(final WindingRule windingRule) {
146146
if (LOG.isDebugEnabled()) {
147147
LOG.debug("[" + id_ + "] fill()");
148148
}

src/main/java/org/htmlunit/platform/canvas/rendering/RenderingBackend.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,9 @@ void ellipse(double x, double y,
127127

128128
/**
129129
* Fills the current or given path with the current fillStyle.
130+
* @param windingRule the {@link WindingRule}
130131
*/
131-
void fill();
132+
void fill(RenderingBackend.WindingRule windingRule);
132133

133134
/**
134135
* Paints the specified rectangular area.

0 commit comments

Comments
 (0)