diff --git a/package-lock.json b/package-lock.json index 23fe1225bb..320846519d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -102,6 +102,7 @@ "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -2936,6 +2937,7 @@ "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -3049,6 +3051,7 @@ "integrity": "sha512-vOTpLduLkZXePLxHiHsBLp98mHGnl8RptV4YAO3HfKO5UHjDvySGbxKtpYfy8Sx5+WKcgc45qNreJJRVM3L6mw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.19.2" } @@ -3344,6 +3347,7 @@ "integrity": "sha512-OWVvEJThRgxlNMYNVLEK/9qVkpRcLvyuKLngIV3Hob01P56NjPHprVBYn+rx4xAJudbM9yrCrywPIEuA3Xyo8A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@testing-library/dom": "^10.4.0", "@testing-library/user-event": "^14.5.2", @@ -3779,6 +3783,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4276,6 +4281,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -5800,6 +5806,7 @@ "integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -11659,6 +11666,7 @@ "integrity": "sha512-+4C/cgJ9w6sudisA0nZz0+O7lTP9a3CzNLsoDwaRumM8QHwghUsu6tqHXiTmNUp/rqNiM14++7dkzHDyCRs0Jg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.6" }, @@ -12761,6 +12769,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13237,6 +13246,7 @@ "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -13333,6 +13343,7 @@ "integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "2.1.8", "@vitest/mocker": "2.1.8", @@ -13482,6 +13493,7 @@ "integrity": "sha512-zpGCn+pb63w9ZltCzIeGfEUSLLFwEsr0N4R25BdDFlBPQ5467VugPdSw/hWGTwgx7BzeKSdUgbKHqZMxb77nbQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "^20.11.30", "@types/sinonjs__fake-timers": "^8.1.5", diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index b9a7e12d00..d128d17cc4 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -8,7 +8,8 @@ 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, Vertex, Ellipse, Arc } from '../shape/custom_shapes'; +import { Vector } from '../math/p5.Vector'; const styleEmpty = 'rgba(0,0,0,0)'; @@ -651,7 +652,7 @@ class Renderer2D extends Renderer { * start <= stop < start + TWO_PI */ arc(x, y, w, h, start, stop, mode) { - const ctx = this.clipPa || this.drawingContext; + const ctx = this.clipPath || this.drawingContext; const rx = w / 2.0; const ry = h / 2.0; const epsilon = 0.00001; // Smallest visible angle on displays up to 4K. @@ -666,38 +667,33 @@ class Renderer2D extends Renderer { // 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 = ! ( + const createPieSlice = !( mode === constants.CHORD || mode === constants.OPEN || (stop - start) % constants.TWO_PI === 0 ); - // Fill curves - if (this.states.fillColor) { - if (!this._clipping) ctx.beginPath(); - ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, start, stop); - if (createPieSlice) ctx.lineTo(centerX, centerY); - ctx.closePath(); - if (!this._clipping) ctx.fill(); - } - - // Stroke curves - if (this.states.strokeColor) { - if (!this._clipping) 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); + // Create Arc primitive and use visitor pattern + const vertex = new Vertex({ position: new Vector(centerX, centerY) }); + const arcPrimitive = new Arc(x, y, w, h, start, stop, mode, vertex); + + const visitor = new PrimitiveToPath2DConverter({ + strokeWeight: this.states.strokeWeight + }); + arcPrimitive.accept(visitor); + + if (this._clipping) { + this.clipPath.addPath(visitor.path); + } else { + // Fill curves + if (this.states.fillColor) { + if (!this._clipping) ctx.fill(visitor.path); } - if (mode === constants.PIE || mode === constants.CHORD) { - // Stroke connects back to path begin for both PIE and CHORD - ctx.closePath(); + // Stroke curves + if (this.states.strokeColor) { + if (!this._clipping) ctx.stroke(visitor.path); } - - if (!this._clipping) ctx.stroke(); } return this; @@ -725,16 +721,25 @@ class Renderer2D extends Renderer { centerY = y + h / 2, radiusX = w / 2, radiusY = h / 2; - if (!this._clipping) ctx.beginPath(); - ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI); - ctx.closePath(); - - if (!this._clipping && doFill) { - ctx.fill(); - } - if (!this._clipping && doStroke) { - ctx.stroke(); + // Create Ellipse primitive and use visitor pattern + const vertex = new Vertex({ position: new Vector(centerX, centerY) }); + const ellipsePrimitive = new Ellipse(x, y, w, h, vertex); + + const visitor = new PrimitiveToPath2DConverter({ + strokeWeight: this.states.strokeWeight + }); + ellipsePrimitive.accept(visitor); + + if (this._clipping) { + this.clipPath.addPath(visitor.path); + } else { + if (doFill) { + ctx.fill(visitor.path); + } + if (doStroke) { + ctx.stroke(visitor.path); + } } } diff --git a/src/shape/custom_shapes.js b/src/shape/custom_shapes.js index b65a363a76..9e4c34c745 100644 --- a/src/shape/custom_shapes.js +++ b/src/shape/custom_shapes.js @@ -466,6 +466,74 @@ class Quad extends ShapePrimitive { } } +class Ellipse extends ShapePrimitive { + #vertexCapacity = 1; + #x; + #y; + #w; + #h; + + constructor(x, y, w, h, ...vertices) { + super(...vertices); + this.#x = x; + this.#y = y; + this.#w = w; + this.#h = h; + } + + get vertexCapacity() { + return this.#vertexCapacity; + } + + get x() { return this.#x; } + get y() { return this.#y; } + get w() { return this.#w; } + get h() { return this.#h; } + + accept(visitor) { + visitor.visitEllipse(this); + } +} + +class Arc extends ShapePrimitive { + #vertexCapacity = 1; + #x; + #y; + #w; + #h; + #start; + #stop; + #mode; + + constructor(x, y, w, h, start, stop, mode, ...vertices) { + super(...vertices); + this.#x = x; + this.#y = y; + this.#w = w; + this.#h = h; + this.#start = start; + this.#stop = stop; + this.#mode = mode; + } + + get vertexCapacity() { + return this.#vertexCapacity; + } + + 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; } + + accept(visitor) { + visitor.visitArc(this); + } +} + + // ---- TESSELLATION PRIMITIVES ---- class TriangleFan extends ShapePrimitive { @@ -1017,6 +1085,12 @@ class PrimitiveVisitor { visitQuad(quad) { throw new Error('Method visitQuad() has not been implemented.'); } + visitEllipse(ellipse) { + throw new Error('Method visitEllipse() has not been implemented.'); + } + visitArc(arc) { + throw new Error('Method visitArc() has not been implemented.'); + } // tessellation primitives visitTriangleFan(triangleFan) { @@ -1129,6 +1203,27 @@ class PrimitiveToPath2DConverter extends PrimitiveVisitor { this.path.lineTo(v3.position.x, v3.position.y); this.path.closePath(); } + visitEllipse(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); + } + visitArc(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); + // Handle PIE and CHORD modes + if (arc.mode === constants.PIE || (arc.stop - arc.start) % constants.TWO_PI !== 0) { + if (!arc.mode || arc.mode === constants.PIE) { + this.path.lineTo(centerX, centerY); + } + this.path.closePath(); + } + } visitTriangleFan(triangleFan) { const [v0, ...rest] = triangleFan.vertices; for (let i = 0; i < rest.length - 1; i++) { @@ -1171,9 +1266,11 @@ class PrimitiveToVerticesConverter extends PrimitiveVisitor { contours = []; curveDetail; - constructor({ curveDetail = 1 } = {}) { + constructor({ curveDetail = 1, fillColor, strokeColor } = {}) { super(); this.curveDetail = curveDetail; + this.fillColor = fillColor || new Color([0, 0, 0, 0]); + this.strokeColor = strokeColor || new Color([0, 0, 0, 0]); } lastContour() { @@ -1271,6 +1368,106 @@ class PrimitiveToVerticesConverter extends PrimitiveVisitor { // WebGL itself interprets the vertices as a strip, no reformatting needed this.contours.push(quadStrip.vertices.slice()); } + visitEllipse(ellipse) { + const contour = []; + this.contours.push(contour); + const detail = Math.max( + 6, + Math.ceil(Math.max(ellipse.w, ellipse.h) * 2 * this.curveDetail) + ); + const centerX = ellipse.x + ellipse.w / 2; + const centerY = ellipse.y + ellipse.h / 2; + const radiusX = ellipse.w / 2; + const radiusY = ellipse.h / 2; + + for (let i = 0; i < detail; i++) { + const t = (i / detail) * 2 * Math.PI; + const x = centerX + radiusX * Math.cos(t); + const y = centerY + radiusY * Math.sin(t); + const u = (x - ellipse.x) / ellipse.w; + const v = (y - ellipse.y) / ellipse.h; + const vertex = new Vertex({ + position: new Vector(x, y, 0), + textureCoordinates: new Vector(u, v), + fill: this.fillColor, + stroke: this.strokeColor + }); + contour.push(vertex); + } + // Close the loop + contour.push(contour[0]); + } + visitArc(arc) { + const contour = []; + this.contours.push(contour); + // Calculate detail based on arc length and size + const arcLength = Math.abs(arc.stop - arc.start) * Math.max(arc.w, arc.h) / 2; + const detail = Math.max( + 4, + Math.ceil(arcLength * this.curveDetail) + ); + + const centerX = arc.x + arc.w / 2; + const centerY = arc.y + arc.h / 2; + const radiusX = arc.w / 2; + const radiusY = arc.h / 2; + + // Add center point for PIE mode + if (arc.mode === constants.PIE) { + const u = (centerX - arc.x) / arc.w; + const v = (centerY - arc.y) / arc.h; + const centerVertex = new Vertex({ + position: new Vector(centerX, centerY, 0), + fill: this.fillColor, + stroke: this.strokeColor, + textureCoordinates: new Vector(u, v) + }); + contour.push(centerVertex); + } + + for (let i = 0; i <= detail; i++) { + const t = arc.start + (i / detail) * (arc.stop - arc.start); + const x = centerX + radiusX * Math.cos(t); + const y = centerY + radiusY * Math.sin(t); + const u = (x - arc.x) / arc.w; + const v = (y - arc.y) / arc.h; + const vertex = new Vertex({ + position: new Vector(x, y, 0), + textureCoordinates: new Vector(u, v), + fill: this.fillColor, + stroke: this.strokeColor + }); + contour.push(vertex); + } + + // Close the loop for PIE and CHORD + if (arc.mode === constants.PIE || arc.mode === constants.CHORD) { + if (arc.mode === constants.PIE) { + const u = (centerX - arc.x) / arc.w; + const v = (centerY - arc.y) / arc.h; + const centerVertex = new Vertex({ + position: new Vector(centerX, centerY, 0), + textureCoordinates: new Vector(u, v), + fill: this.fillColor, + stroke: this.strokeColor + }); + contour.push(centerVertex); + } else if (arc.mode === constants.CHORD) { + const t = arc.start; + const x = centerX + radiusX * Math.cos(t); + const y = centerY + radiusY * Math.sin(t); + const u = (x - arc.x) / arc.w; + const v = (y - arc.y) / arc.h; + const vertex = new Vertex({ + position: new Vector(x, y, 0), + textureCoordinates: new Vector(u, v), + fill: this.fillColor, + stroke: this.strokeColor + }); + contour.push(vertex); + } + } + } } class PointAtLengthGetter extends PrimitiveVisitor { @@ -2856,10 +3053,9 @@ function customShapes(p5, fn) { export default customShapes; export { - Shape, - Contour, - ShapePrimitive, Vertex, + ShapePrimitive, + Contour, Anchor, Segment, LineSegment, @@ -2869,9 +3065,13 @@ export { Line, Triangle, Quad, + Ellipse, + Arc, TriangleFan, TriangleStrip, QuadStrip, + PrimitiveShapeCreators, + Shape, PrimitiveVisitor, PrimitiveToPath2DConverter, PrimitiveToVerticesConverter, diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index a671550f13..ba692c3831 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -11,6 +11,7 @@ import { RendererGL } from './p5.RendererGL'; import { Vector } from '../math/p5.Vector'; import { Geometry } from './p5.Geometry'; import { Matrix } from '../math/p5.Matrix'; +import { Vertex, Ellipse, Arc } from '../shape/custom_shapes'; function primitives3D(p5, fn){ /** @@ -1735,134 +1736,39 @@ function primitives3D(p5, fn){ }; RendererGL.prototype.ellipse = function(args) { - this.arc( - args[0], - args[1], - args[2], - args[3], - 0, - constants.TWO_PI, - constants.OPEN, - args[4] - ); + const x = args[0]; + const y = args[1]; + const w = args[2]; + const h = args[3]; + const detail = args[4] || 25; // detail is not used in ShapeEllipse directly but could be passed via curveDetail if needed, or handled in visitor + + // Create Ellipse primitive + const centerX = x + w / 2; + const centerY = y + h / 2; + const vertex = new Vertex({ position: new Vector(centerX, centerY) }); + const ellipsePrimitive = new Ellipse(x, y, w, h, vertex); + + this.drawShape(ellipsePrimitive); + return this; }; RendererGL.prototype.arc = function(...args) { const x = args[0]; const y = args[1]; - const width = args[2]; - const height = args[3]; + const w = args[2]; + const h = args[3]; const start = args[4]; const stop = args[5]; const mode = args[6]; const detail = args[7] || 25; - let shape; - let gid; - - // check if it is an ellipse or an arc - if (Math.abs(stop - start) >= constants.TWO_PI) { - shape = 'ellipse'; - gid = `${shape}|${detail}|`; - } else { - shape = 'arc'; - gid = `${shape}|${start}|${stop}|${mode}|${detail}|`; - } - - if (!this.geometryInHash(gid)) { - const _arc = function() { - - // if the start and stop angles are not the same, push vertices to the array - if (start.toFixed(10) !== stop.toFixed(10)) { - // if the mode specified is PIE or null, push the mid point of the arc in vertices - if (mode === constants.PIE || typeof mode === 'undefined') { - this.vertices.push(new Vector(0.5, 0.5, 0)); - this.uvs.push([0.5, 0.5]); - } - - // vertices for the perimeter of the circle - for (let i = 0; i <= detail; i++) { - const u = i / detail; - const theta = (stop - start) * u + start; - - const _x = 0.5 + Math.cos(theta) / 2; - const _y = 0.5 + Math.sin(theta) / 2; - - this.vertices.push(new Vector(_x, _y, 0)); - this.uvs.push([_x, _y]); - - if (i < detail - 1) { - this.faces.push([0, i + 1, i + 2]); - this.edges.push([i + 1, i + 2]); - } - } - - // check the mode specified in order to push vertices and faces, different for each mode - switch (mode) { - case constants.PIE: - this.faces.push([ - 0, - this.vertices.length - 2, - this.vertices.length - 1 - ]); - this.edges.push([0, 1]); - this.edges.push([ - this.vertices.length - 2, - this.vertices.length - 1 - ]); - this.edges.push([0, this.vertices.length - 1]); - break; - - case constants.CHORD: - this.edges.push([0, 1]); - this.edges.push([0, this.vertices.length - 1]); - break; - - case constants.OPEN: - this.edges.push([0, 1]); - break; - - default: - this.faces.push([ - 0, - this.vertices.length - 2, - this.vertices.length - 1 - ]); - this.edges.push([ - this.vertices.length - 2, - this.vertices.length - 1 - ]); - } - } - }; - - const arcGeom = new Geometry(detail, 1, _arc, this); - arcGeom.computeNormals(); - - if (detail <= 50) { - arcGeom._edgesToVertices(arcGeom); - } else if (this.states.strokeColor) { - console.log( - `Cannot apply a stroke to an ${shape} with more than 50 detail` - ); - } - - arcGeom.gid = gid; - this.geometryBufferCache.ensureCached(arcGeom); - } - - const uModelMatrix = this.states.uModelMatrix; - this.states.setValue('uModelMatrix', this.states.uModelMatrix.clone()); - - try { - this.states.uModelMatrix.translate([x, y, 0]); - this.states.uModelMatrix.scale(width, height, 1); - - this.model(this.geometryBufferCache.getGeometryByID(gid)); - } finally { - this.states.setValue('uModelMatrix', uModelMatrix); - } + // Create Arc primitive + const centerX = x + w / 2; + const centerY = y + h / 2; + const vertex = new Vertex({ position: new Vector(centerX, centerY) }); + const arcPrimitive = new Arc(x, y, w, h, start, stop, mode, vertex); + this.drawShape(arcPrimitive); return this; }; diff --git a/src/webgl/ShapeBuilder.js b/src/webgl/ShapeBuilder.js index 4b0099db50..e8b9744700 100644 --- a/src/webgl/ShapeBuilder.js +++ b/src/webgl/ShapeBuilder.js @@ -52,7 +52,11 @@ export class ShapeBuilder { this.geometry.reset(); this.contourIndices = []; // TODO: handle just some contours having non-PATH mode - this.shapeMode = shape.contours[0].kind; + if (shape.contours && shape.contours.length > 0) { + this.shapeMode = shape.contours[0].kind; + } else { + this.shapeMode = constants.PATH; + } const shouldProcessEdges = !!this.renderer.states.strokeColor; const userVertexPropertyHelpers = {}; diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 6008c0e3c2..354e53b3c4 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -562,7 +562,9 @@ class RendererGL extends Renderer { drawShape(shape) { const visitor = new PrimitiveToVerticesConverter({ - curveDetail: this.states.curveDetail + curveDetail: this.states.curveDetail, + fillColor: this.states.fillColor, + strokeColor: this.states.strokeColor }); shape.accept(visitor); this.shapeBuilder.constructFromContours(shape, visitor.contours); diff --git a/test/unit/visual/screenshots/Shape Modes/Shape arc/Mode CENTER/000.png b/test/unit/visual/screenshots/Shape Modes/Shape arc/Mode CENTER/000.png index d624061419..f5ddd5ef35 100644 Binary files a/test/unit/visual/screenshots/Shape Modes/Shape arc/Mode CENTER/000.png and b/test/unit/visual/screenshots/Shape Modes/Shape arc/Mode CENTER/000.png differ diff --git a/test/unit/visual/screenshots/Shape Modes/Shape arc/Mode CORNER/000.png b/test/unit/visual/screenshots/Shape Modes/Shape arc/Mode CORNER/000.png index d624061419..f5ddd5ef35 100644 Binary files a/test/unit/visual/screenshots/Shape Modes/Shape arc/Mode CORNER/000.png and b/test/unit/visual/screenshots/Shape Modes/Shape arc/Mode CORNER/000.png differ diff --git a/test/unit/visual/screenshots/Shape Modes/Shape arc/Mode CORNERS/000.png b/test/unit/visual/screenshots/Shape Modes/Shape arc/Mode CORNERS/000.png index d624061419..f5ddd5ef35 100644 Binary files a/test/unit/visual/screenshots/Shape Modes/Shape arc/Mode CORNERS/000.png and b/test/unit/visual/screenshots/Shape Modes/Shape arc/Mode CORNERS/000.png differ diff --git a/test/unit/visual/screenshots/Shape Modes/Shape arc/Mode RADIUS/000.png b/test/unit/visual/screenshots/Shape Modes/Shape arc/Mode RADIUS/000.png index d624061419..f5ddd5ef35 100644 Binary files a/test/unit/visual/screenshots/Shape Modes/Shape arc/Mode RADIUS/000.png and b/test/unit/visual/screenshots/Shape Modes/Shape arc/Mode RADIUS/000.png differ