From 01aa360790fed6e43b1cbec18f6bbefc5a4210a0 Mon Sep 17 00:00:00 2001 From: VANSH3104 Date: Fri, 6 Mar 2026 10:44:48 +0530 Subject: [PATCH 1/2] Add ShapePrimitive support for arcs and ellipses --- src/core/p5.Renderer2D.js | 60 ++------------- src/shape/custom_shapes.js | 151 +++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 52 deletions(-) diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index fbe5747449..206d187f5f 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -8,7 +8,7 @@ import { MediaElement } from '../dom/p5.MediaElement'; import { RGBHDR } from '../color/creating_reading'; import FilterRenderer2D from '../image/filterRenderer2D'; import { Matrix } from '../math/p5.Matrix'; -import { PrimitiveToPath2DConverter } from '../shape/custom_shapes'; +import { PrimitiveToPath2DConverter, ArcPrimitive, EllipsePrimitive } from '../shape/custom_shapes'; import { DefaultFill, textCoreConstants } from '../type/textCore'; @@ -661,13 +661,6 @@ class Renderer2D extends Renderer { * start <= stop < start + TWO_PI */ arc(x, y, w, h, start, stop, mode) { - const ctx = this.drawingContext; - const rx = w / 2.0; - const ry = h / 2.0; - const epsilon = 0.00001; // Smallest visible angle on displays up to 4K. - let arcToDraw = 0; - const curves = []; - const centerX = x + w / 2, centerY = y + h / 2, radiusX = w / 2, @@ -681,48 +674,16 @@ class Renderer2D extends Renderer { this.clipPath.addPath(tempPath, relativeTransform); return this; } - // Determines whether to add a line to the center, which should be done - // when the mode is PIE or default; as well as when the start and end - // angles do not form a full circle. - const createPieSlice = ! ( - mode === constants.CHORD || - mode === constants.OPEN || - (stop - start) % constants.TWO_PI === 0 - ); - - // Fill curves - if (this.states.fillColor) { - ctx.beginPath(); - ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, start, stop); - if (createPieSlice) ctx.lineTo(centerX, centerY); - ctx.closePath(); - ctx.fill(); - } - // Stroke curves - if (this.states.strokeColor) { - ctx.beginPath(); - ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, start, stop); - - if (mode === constants.PIE && createPieSlice) { - // In PIE mode, stroke is added to the center and back to path, - // unless the pie forms a complete ellipse (see: createPieSlice) - ctx.lineTo(centerX, centerY); - } - - if (mode === constants.PIE || mode === constants.CHORD) { - // Stroke connects back to path begin for both PIE and CHORD - ctx.closePath(); - } - ctx.stroke(); - } + const primitive = new ArcPrimitive(x, y, w, h, start, stop, mode); + const shape = { accept(visitor) { primitive.accept(visitor); } }; + this.drawShape(shape); return this; } ellipse(args) { - const ctx = this.drawingContext; const doFill = !!this.states.fillColor, doStroke = this.states.strokeColor; const x = parseFloat(args[0]), @@ -751,15 +712,10 @@ class Renderer2D extends Renderer { this.clipPath.addPath(tempPath, relativeTransform); return this; } - ctx.beginPath(); - ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI); - ctx.closePath(); - if (doFill) { - ctx.fill(); - } - if (doStroke) { - ctx.stroke(); - } + + const primitive = new EllipsePrimitive(x, y, w, h); + const shape = { accept(visitor) { primitive.accept(visitor); } }; + this.drawShape(shape); return this; } diff --git a/src/shape/custom_shapes.js b/src/shape/custom_shapes.js index 3a09200f75..61ffe2ba4c 100644 --- a/src/shape/custom_shapes.js +++ b/src/shape/custom_shapes.js @@ -466,6 +466,77 @@ class Quad extends ShapePrimitive { } } +class ArcPrimitive extends ShapePrimitive { + #x; + #y; + #w; + #h; + #start; + #stop; + #mode; + // vertexCapacity 0 means this primitive should not accumulate normal path vertices + #vertexCapacity = 0; + + constructor(x, y, w, h, start, stop, mode) { + // ShapePrimitive requires at least one vertex; pass a placeholder + super(new Vertex({ position: new Vector(x + w / 2, y + h / 2) })); + this.#x = x; + this.#y = y; + this.#w = w; + this.#h = h; + this.#start = start; + this.#stop = stop; + this.#mode = mode; + } + + get x() { return this.#x; } + get y() { return this.#y; } + get w() { return this.#w; } + get h() { return this.#h; } + get start() { return this.#start; } + get stop() { return this.#stop; } + get mode() { return this.#mode; } + + get vertexCapacity() { + return this.#vertexCapacity; + } + + accept(visitor) { + visitor.visitArcPrimitive(this); + } +} + +class EllipsePrimitive extends ShapePrimitive { + #x; + #y; + #w; + #h; + // vertexCapacity 0 means this primitive should not accumulate normal path vertices + #vertexCapacity = 0; + + constructor(x, y, w, h) { + + super(new Vertex({ position: new Vector(x + w / 2, y + h / 2) })); + this.#x = x; + this.#y = y; + this.#w = w; + this.#h = h; + } + + get x() { return this.#x; } + get y() { return this.#y; } + get w() { return this.#w; } + get h() { return this.#h; } + + get vertexCapacity() { + return this.#vertexCapacity; + } + + accept(visitor) { + visitor.visitEllipsePrimitive(this); + } +} + // ---- TESSELLATION PRIMITIVES ---- class TriangleFan extends ShapePrimitive { @@ -1003,6 +1074,12 @@ class PrimitiveVisitor { visitArcSegment(arcSegment) { throw new Error('Method visitArcSegment() has not been implemented.'); } + visitArcPrimitive(arc) { + throw new Error('Method visitArcPrimitive() has not been implemented.'); + } + visitEllipsePrimitive(ellipse) { + throw new Error('Method visitEllipsePrimitive() has not been implemented.'); + } // isolated primitives visitPoint(point) { @@ -1151,6 +1228,34 @@ class PrimitiveToPath2DConverter extends PrimitiveVisitor { this.path.closePath(); } } + visitArcPrimitive(arc) { + const centerX = arc.x + arc.w / 2; + const centerY = arc.y + arc.h / 2; + const radiusX = arc.w / 2; + const radiusY = arc.h / 2; + + this.path.ellipse( + centerX, centerY, radiusX, radiusY, 0, arc.start, arc.stop + ); + + if (arc.mode === constants.OPEN) { + // OPEN: leave path open — arc stroke/fill is just the curve + } else if (arc.mode === constants.CHORD) { + + this.path.closePath(); + } else { + this.path.lineTo(centerX, centerY); + this.path.closePath(); + } + } + visitEllipsePrimitive(ellipse) { + const centerX = ellipse.x + ellipse.w / 2; + const centerY = ellipse.y + ellipse.h / 2; + const radiusX = ellipse.w / 2; + const radiusY = ellipse.h / 2; + + this.path.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI); + } visitQuadStrip(quadStrip) { for (let i = 0; i < quadStrip.vertices.length - 3; i += 2) { const v0 = quadStrip.vertices[i]; @@ -1277,6 +1382,50 @@ class PrimitiveToVerticesConverter extends PrimitiveVisitor { // WebGL itself interprets the vertices as a strip, no reformatting needed this.contours.push(quadStrip.vertices.slice()); } + visitArcPrimitive(arc) { + const centerX = arc.x + arc.w / 2; + const centerY = arc.y + arc.h / 2; + const radiusX = arc.w / 2; + const radiusY = arc.h / 2; + const numPoints = Math.max(3, this.curveDetail); + const verts = []; + + if (arc.mode === constants.PIE) { + verts.push(new Vertex({ position: new Vector(centerX, centerY) })); + } + + for (let i = 0; i <= numPoints; i++) { + const angle = arc.start + (arc.stop - arc.start) * (i / numPoints); + verts.push(new Vertex({ + position: new Vector( + centerX + radiusX * Math.cos(angle), + centerY + radiusY * Math.sin(angle) + ) + })); + } + + this.contours.push(verts); + } + visitEllipsePrimitive(ellipse) { + const centerX = ellipse.x + ellipse.w / 2; + const centerY = ellipse.y + ellipse.h / 2; + const radiusX = ellipse.w / 2; + const radiusY = ellipse.h / 2; + const numPoints = Math.max(3, this.curveDetail); + const verts = []; + + for (let i = 0; i <= numPoints; i++) { + const angle = (2 * Math.PI * i) / numPoints; + verts.push(new Vertex({ + position: new Vector( + centerX + radiusX * Math.cos(angle), + centerY + radiusY * Math.sin(angle) + ) + })); + } + + this.contours.push(verts); + } } class PointAtLengthGetter extends PrimitiveVisitor { @@ -2793,6 +2942,8 @@ export { Line, Triangle, Quad, + ArcPrimitive, + EllipsePrimitive, TriangleFan, TriangleStrip, QuadStrip, From bea47ea3b35b9a54833101e9ad2a8789750b0820 Mon Sep 17 00:00:00 2001 From: VANSH3104 Date: Wed, 18 Mar 2026 18:15:51 +0530 Subject: [PATCH 2/2] Fix: Initialize shape with vertex properties in arc() to match ellipse() pattern --- src/core/p5.Renderer2D.js | 61 ++++++++------------------- src/shape/custom_shapes.js | 85 +++++++++++++++++++++++++++++++------- 2 files changed, 87 insertions(+), 59 deletions(-) diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 206d187f5f..981ae3288d 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -8,7 +8,7 @@ import { MediaElement } from '../dom/p5.MediaElement'; import { RGBHDR } from '../color/creating_reading'; import FilterRenderer2D from '../image/filterRenderer2D'; import { Matrix } from '../math/p5.Matrix'; -import { PrimitiveToPath2DConverter, ArcPrimitive, EllipsePrimitive } from '../shape/custom_shapes'; +import { PrimitiveToPath2DConverter } from '../shape/custom_shapes'; import { DefaultFill, textCoreConstants } from '../type/textCore'; @@ -661,22 +661,18 @@ class Renderer2D extends Renderer { * start <= stop < start + TWO_PI */ arc(x, y, w, h, start, stop, mode) { - const centerX = x + w / 2, - centerY = y + h / 2, - radiusX = w / 2, - radiusY = h / 2; - if (this._clipping) { - const tempPath = new Path2D(); - tempPath.ellipse(centerX, centerY, radiusX, radiusY, 0, start, stop); - const currentTransform = this.drawingContext.getTransform(); - const clipBaseTransform = this._clipBaseTransform.inverse(); - const relativeTransform = clipBaseTransform.multiply(currentTransform); - this.clipPath.addPath(tempPath, relativeTransform); - return this; - } - - const primitive = new ArcPrimitive(x, y, w, h, start, stop, mode); - const shape = { accept(visitor) { primitive.accept(visitor); } }; + const shape = new p5.Shape({ position: new p5.Vector(0, 0) }); + shape.beginShape(); + shape.arcPrimitive( + x, + y, + w, + h, + start, + stop, + mode + ); + shape.endShape(); this.drawShape(shape); return this; @@ -684,39 +680,16 @@ class Renderer2D extends Renderer { } ellipse(args) { - const doFill = !!this.states.fillColor, - doStroke = this.states.strokeColor; const x = parseFloat(args[0]), y = parseFloat(args[1]), w = parseFloat(args[2]), h = parseFloat(args[3]); - if (doFill && !doStroke) { - if (this._getFill() === styleEmpty) { - return this; - } - } else if (!doFill && doStroke) { - if (this._getStroke() === styleEmpty) { - return this; - } - } - const centerX = x + w / 2, - centerY = y + h / 2, - radiusX = w / 2, - radiusY = h / 2; - if (this._clipping) { - const tempPath = new Path2D(); - tempPath.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI); - const currentTransform = this.drawingContext.getTransform(); - const clipBaseTransform = this._clipBaseTransform.inverse(); - const relativeTransform = clipBaseTransform.multiply(currentTransform); - this.clipPath.addPath(tempPath, relativeTransform); - return this; - } - const primitive = new EllipsePrimitive(x, y, w, h); - const shape = { accept(visitor) { primitive.accept(visitor); } }; + const shape = new p5.Shape({ position: new p5.Vector(0, 0) }); + shape.beginShape(); + shape.ellipsePrimitive(x,y,w,h); + shape.endShape(); this.drawShape(shape); - return this; } diff --git a/src/shape/custom_shapes.js b/src/shape/custom_shapes.js index 61ffe2ba4c..f100808803 100644 --- a/src/shape/custom_shapes.js +++ b/src/shape/custom_shapes.js @@ -474,12 +474,11 @@ class ArcPrimitive extends ShapePrimitive { #start; #stop; #mode; - // vertexCapacity 0 means this primitive should not accumulate normal path vertices - #vertexCapacity = 0; + #vertexCapacity = 2; - constructor(x, y, w, h, start, stop, mode) { + constructor(startVertex, endVertex, x, y, w, h, start, stop, mode) { // ShapePrimitive requires at least one vertex; pass a placeholder - super(new Vertex({ position: new Vector(x + w / 2, y + h / 2) })); + super(startVertex, endVertex); this.#x = x; this.#y = y; this.#w = w; @@ -511,12 +510,11 @@ class EllipsePrimitive extends ShapePrimitive { #y; #w; #h; - // vertexCapacity 0 means this primitive should not accumulate normal path vertices - #vertexCapacity = 0; + #vertexCapacity = 1; - constructor(x, y, w, h) { + constructor(centerVertex, x, y, w, h) { - super(new Vertex({ position: new Vector(x + w / 2, y + h / 2) })); + super(centerVertex); this.#x = x; this.#y = y; this.#w = w; @@ -976,6 +974,44 @@ class Shape { this.#generalVertex('arcVertex', position, textureCoordinates); } + + arcPrimitive(x,y,w,h,start,stop,mode){ + const centerX = x+w/2; + const centerY = y+h/2; + + const startVertex = this.#createVertex( + new Vector( + centerX+(w/2)*Math.cos(start), + centerY+(h/2)*Math.sin(start) + ) + ); + + const endVertex = this.#createVertex( + new Vector( + centerX+(w/2)*Math.cos(stop), + centerY+(h/2)*Math.sin(stop) + ) + ); + + const primitive = new ArcPrimitive( + startVertex, + endVertex, + x, y, w, h, + start, + stop, + mode + ); + return primitive.addToShape(this); + + } + + ellipsePrimitive(x,y,w,h){ + const centerVertex = this.#createVertex(new Vector(x+w/2,y+h/2)); + + const primitive = new EllipsePrimitive(centerVertex, x, y, w, h); + return primitive.addToShape(this); + } + beginContour(shapeKind = constants.PATH) { if (this.at(-1)?.kind === constants.EMPTY_PATH) { this.contours.pop(); @@ -1387,7 +1423,11 @@ class PrimitiveToVerticesConverter extends PrimitiveVisitor { const centerY = arc.y + arc.h / 2; const radiusX = arc.w / 2; const radiusY = arc.h / 2; - const numPoints = Math.max(3, this.curveDetail); + const avgRadius = (radiusX+radiusY)/2; + + const arcLength = avgRadius*Math.abs(arc.stop-arc.start); + + const numPoints=Math.max(3, Math.ceil(this.curveDetail*arcLength)); const verts = []; if (arc.mode === constants.PIE) { @@ -1396,12 +1436,27 @@ class PrimitiveToVerticesConverter extends PrimitiveVisitor { for (let i = 0; i <= numPoints; i++) { const angle = arc.start + (arc.stop - arc.start) * (i / numPoints); - verts.push(new Vertex({ - position: new Vector( - centerX + radiusX * Math.cos(angle), - centerY + radiusY * Math.sin(angle) - ) - })); + const startVertex=arc.vertices[0]; + const endVertex=arc.vertices[1]; + const t=i/numPoints; + const props={}; + for(const key in startVertex){ + if(key === 'position') continue; + if(typeof startVertex[key] === 'number' + && typeof endVertex[key]=== 'number'){ + props[key] = startVertex[key]*(1-t) + endVertex[key]*t; + } + else{ + props[key]=startVertex[key]; + } + } + + props.position=new Vector( + centerX+radiusX*Math.cos(angle), + centerY+radiusY*Math.sin(angle) + ); + + verts.push(new Vertex(props)); } this.contours.push(verts);