From 99a3b7e9b5b1d3e572ac36212ffb22d8d1174faa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 31 Dec 2025 10:58:45 +0000 Subject: [PATCH 1/3] Initial plan From deee9a08b97b893e2a67d0a22edcf909a5fe6a98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 31 Dec 2025 11:06:04 +0000 Subject: [PATCH 2/3] Add origin support for Sprite class Co-authored-by: mikebarkmin <2592379+mikebarkmin@users.noreply.github.com> --- .../java/reference/SpriteGetOrigin.java | 42 +++++ .../java/reference/SpriteSetOrigin.java | 55 ++++++ .../java/org/openpatch/scratch/Origin.java | 29 ++++ .../java/org/openpatch/scratch/Sprite.java | 159 ++++++++++++++++-- .../org/openpatch/scratch/internal/Image.java | 53 ++++++ .../org/openpatch/scratch/internal/Stamp.java | 25 ++- 6 files changed, 348 insertions(+), 15 deletions(-) create mode 100644 src/examples/java/reference/SpriteGetOrigin.java create mode 100644 src/examples/java/reference/SpriteSetOrigin.java create mode 100644 src/main/java/org/openpatch/scratch/Origin.java diff --git a/src/examples/java/reference/SpriteGetOrigin.java b/src/examples/java/reference/SpriteGetOrigin.java new file mode 100644 index 00000000..62de76d6 --- /dev/null +++ b/src/examples/java/reference/SpriteGetOrigin.java @@ -0,0 +1,42 @@ +package reference; + +import org.openpatch.scratch.Origin; +import org.openpatch.scratch.Sprite; +import org.openpatch.scratch.Stage; +import org.openpatch.scratch.extensions.recorder.*; + +public class SpriteGetOrigin { + public SpriteGetOrigin() { + Stage myStage = new Stage(600, 240); + Sprite mySprite = new Sprite("slime", "assets/slime.png"); + myStage.add(mySprite); + mySprite.setPosition(-100, 0); + + GifRecorder recorder = + new GifRecorder("examples/reference/" + this.getClass().getName() + ".gif"); + recorder.start(); + + // Show origin getter methods + mySprite.say("getOrigin(): " + mySprite.getOrigin()); + myStage.wait(1500); + + // Set custom origin and show getters + mySprite.setOrigin(-25, 15); + mySprite.say( + "getOriginX(): " + mySprite.getOriginX() + ", getOriginY(): " + mySprite.getOriginY()); + myStage.wait(1500); + + // Set preset origin and show computed values + mySprite.setOrigin(Origin.TOP_LEFT); + mySprite.say( + "TOP_LEFT -> X:" + (int) mySprite.getOriginX() + ", Y:" + (int) mySprite.getOriginY()); + myStage.wait(1500); + + recorder.stop(); + myStage.exit(); + } + + public static void main(String[] args) { + new SpriteGetOrigin(); + } +} diff --git a/src/examples/java/reference/SpriteSetOrigin.java b/src/examples/java/reference/SpriteSetOrigin.java new file mode 100644 index 00000000..6ab7cf27 --- /dev/null +++ b/src/examples/java/reference/SpriteSetOrigin.java @@ -0,0 +1,55 @@ +package reference; + +import org.openpatch.scratch.Origin; +import org.openpatch.scratch.Sprite; +import org.openpatch.scratch.Stage; +import org.openpatch.scratch.extensions.recorder.*; + +public class SpriteSetOrigin { + public SpriteSetOrigin() { + Stage myStage = new Stage(600, 240); + Sprite mySprite = new Sprite("slime", "assets/slime.png"); + myStage.add(mySprite); + mySprite.setPosition(0, 0); + + GifRecorder recorder = + new GifRecorder("examples/reference/" + this.getClass().getName() + ".gif"); + recorder.start(); + + // Show default center origin + mySprite.say("Origin: CENTER (default)"); + myStage.wait(1000); + + // Demonstrate TOP_LEFT origin + mySprite.setOrigin(Origin.TOP_LEFT); + mySprite.say("Origin: TOP_LEFT"); + myStage.wait(1000); + + // Demonstrate TOP_RIGHT origin + mySprite.setOrigin(Origin.TOP_RIGHT); + mySprite.say("Origin: TOP_RIGHT"); + myStage.wait(1000); + + // Demonstrate BOTTOM_LEFT origin + mySprite.setOrigin(Origin.BOTTOM_LEFT); + mySprite.say("Origin: BOTTOM_LEFT"); + myStage.wait(1000); + + // Demonstrate custom origin offset + mySprite.setOrigin(-30, 20); + mySprite.say("Origin: Custom (-30, 20)"); + myStage.wait(1000); + + // Back to center + mySprite.setOrigin(Origin.CENTER); + mySprite.say("Origin: CENTER"); + myStage.wait(1000); + + recorder.stop(); + myStage.exit(); + } + + public static void main(String[] args) { + new SpriteSetOrigin(); + } +} diff --git a/src/main/java/org/openpatch/scratch/Origin.java b/src/main/java/org/openpatch/scratch/Origin.java new file mode 100644 index 00000000..8879a6b8 --- /dev/null +++ b/src/main/java/org/openpatch/scratch/Origin.java @@ -0,0 +1,29 @@ +package org.openpatch.scratch; + +/** + * The Origin enum represents the different origin positions that a sprite can have. + * The origin determines the reference point for the sprite's position and rotation. + * By default, the origin is at the center of the sprite. + */ +public enum Origin { + /** Origin at the top-left corner of the sprite */ + TOP_LEFT, + /** Origin at the top-center of the sprite */ + TOP_CENTER, + /** Origin at the top-right corner of the sprite */ + TOP_RIGHT, + /** Origin at the center-left of the sprite */ + CENTER_LEFT, + /** Origin at the center of the sprite (default) */ + CENTER, + /** Origin at the center-right of the sprite */ + CENTER_RIGHT, + /** Origin at the bottom-left corner of the sprite */ + BOTTOM_LEFT, + /** Origin at the bottom-center of the sprite */ + BOTTOM_CENTER, + /** Origin at the bottom-right corner of the sprite */ + BOTTOM_RIGHT, + /** Custom origin position specified by x and y offsets */ + CUSTOM +} diff --git a/src/main/java/org/openpatch/scratch/Sprite.java b/src/main/java/org/openpatch/scratch/Sprite.java index 40dbac35..b2a4f4ea 100644 --- a/src/main/java/org/openpatch/scratch/Sprite.java +++ b/src/main/java/org/openpatch/scratch/Sprite.java @@ -163,6 +163,9 @@ public interface WhenKeyPressedHandler { private boolean hitboxDisabled = false; private final Text text; private boolean isUI; + private Origin origin = Origin.CENTER; + private double originX = 0; + private double originY = 0; private WhenIReceiveHandler whenIReceiveHandler = (sprite, msg) -> { }; @@ -253,6 +256,9 @@ public Sprite(Sprite s) { this.whenKeyReleasedHandler = s.whenKeyReleasedHandler; this.whenKeyPressedHandler = s.whenKeyPressedHandler; this.isUI = s.isUI; + this.origin = s.origin; + this.originX = s.originX; + this.originY = s.originY; } /** @@ -1024,6 +1030,98 @@ public void setRotationStyle(RotationStyle style) { this.rotationStyle = style; } + /** + * Sets the origin of the sprite to one of the predefined positions. + * The origin determines the reference point for the sprite's position and rotation. + * By default, the origin is at the center of the sprite (Origin.CENTER). + * + * @see Origin + * @param origin the origin position to set + */ + public void setOrigin(Origin origin) { + this.origin = origin; + if (origin != Origin.CUSTOM) { + this.originX = 0; + this.originY = 0; + } + } + + /** + * Sets a custom origin offset for the sprite relative to its center. + * Positive x values move the origin to the right, negative to the left. + * Positive y values move the origin up, negative y values move it down. + * For example, setOrigin(-50, -50) offsets the origin 50 pixels left and 50 pixels down from center. + * + * @param x the x offset from center in pixels + * @param y the y offset from center in pixels + */ + public void setOrigin(double x, double y) { + this.origin = Origin.CUSTOM; + this.originX = x; + this.originY = y; + } + + /** + * Returns the current origin setting of the sprite. + * + * @return the current origin + */ + public Origin getOrigin() { + return this.origin; + } + + /** + * Returns the x offset of the origin from the center of the sprite. + * For predefined origins (non-custom), this calculates the offset based on + * the sprite's current dimensions. + * + * @return the x offset of the origin in pixels + */ + public double getOriginX() { + if (this.origin == Origin.CUSTOM) { + return this.originX; + } + double halfWidth = this.getWidth() / 2.0; + switch (this.origin) { + case TOP_LEFT: + case CENTER_LEFT: + case BOTTOM_LEFT: + return -halfWidth; + case TOP_RIGHT: + case CENTER_RIGHT: + case BOTTOM_RIGHT: + return halfWidth; + default: + return 0; + } + } + + /** + * Returns the y offset of the origin from the center of the sprite. + * For predefined origins (non-custom), this calculates the offset based on + * the sprite's current dimensions. + * + * @return the y offset of the origin in pixels + */ + public double getOriginY() { + if (this.origin == Origin.CUSTOM) { + return this.originY; + } + double halfHeight = this.getHeight() / 2.0; + switch (this.origin) { + case TOP_LEFT: + case TOP_CENTER: + case TOP_RIGHT: + return halfHeight; + case BOTTOM_LEFT: + case BOTTOM_CENTER: + case BOTTOM_RIGHT: + return -halfHeight; + default: + return 0; + } + } + /** * Sets the position of the sprite * @@ -1497,6 +1595,10 @@ public Hitbox getHitbox() { var spriteWidth = this.show ? costumeWidth : this.pen.getSize(); var spriteHeight = this.show ? costumeHeight : this.pen.getSize(); + // Get origin offset - the image is drawn offset from position by -originX, -originY + var originOffsetX = -this.getOriginX(); + var originOffsetY = this.getOriginY(); + var rotation = this.direction - 90; if (this.rotationStyle == RotationStyle.DONT || this.rotationStyle == RotationStyle.LEFT_RIGHT) { @@ -1506,22 +1608,38 @@ public Hitbox getHitbox() { if (this.hitbox != null) { this.hitbox.translateAndRotateAndResize( rotation, - this.x, - -this.y, - this.x - spriteWidth / 2.0f, - -this.y - spriteHeight / 2.0f, + this.x + originOffsetX, + -this.y + originOffsetY, + this.x + originOffsetX - spriteWidth / 2.0f, + -this.y + originOffsetY - spriteHeight / 2.0f, this.size); return this.hitbox; } var cornerTopLeft = Utils.rotateXY( - this.x - spriteWidth / 2.0f, -this.y - spriteHeight / 2.0f, this.x, -this.y, rotation); + this.x + originOffsetX - spriteWidth / 2.0f, + -this.y + originOffsetY - spriteHeight / 2.0f, + this.x + originOffsetX, + -this.y + originOffsetY, + rotation); var cornerTopRight = Utils.rotateXY( - this.x + spriteWidth / 2.0f, -this.y - spriteHeight / 2.0f, this.x, -this.y, rotation); + this.x + originOffsetX + spriteWidth / 2.0f, + -this.y + originOffsetY - spriteHeight / 2.0f, + this.x + originOffsetX, + -this.y + originOffsetY, + rotation); var cornerBottomLeft = Utils.rotateXY( - this.x - spriteWidth / 2.0f, -this.y + spriteHeight / 2.0f, this.x, -this.y, rotation); + this.x + originOffsetX - spriteWidth / 2.0f, + -this.y + originOffsetY + spriteHeight / 2.0f, + this.x + originOffsetX, + -this.y + originOffsetY, + rotation); var cornerBottomRight = Utils.rotateXY( - this.x + spriteWidth / 2.0f, -this.y + spriteHeight / 2.0f, this.x, -this.y, rotation); + this.x + originOffsetX + spriteWidth / 2.0f, + -this.y + originOffsetY + spriteHeight / 2.0f, + this.x + originOffsetX, + -this.y + originOffsetY, + rotation); double[] xPoints = new double[4]; double[] yPoints = new double[4]; @@ -2275,7 +2393,16 @@ protected void draw(PGraphics buffer) { var shader = this.getCurrentShader(); this.costumes .get(this.currentCostume) - .draw(buffer, this.size, this.direction, this.x, this.y, this.rotationStyle, shader); + .draw( + buffer, + this.size, + this.direction, + this.x, + this.y, + this.rotationStyle, + shader, + this.getOriginX(), + this.getOriginY()); } } @@ -2293,7 +2420,15 @@ protected void drawDebug(PGraphics buffer) { if (this.costumes.size() > 0 && this.show) { this.costumes .get(this.currentCostume) - .drawDebug(buffer, this.size, this.direction, this.x, this.y, this.rotationStyle); + .drawDebug( + buffer, + this.size, + this.direction, + this.x, + this.y, + this.rotationStyle, + this.getOriginX(), + this.getOriginY()); } } @@ -2303,7 +2438,9 @@ private Stamp getStamp() { this.direction, this.x, this.y, - this.rotationStyle); + this.rotationStyle, + this.getOriginX(), + this.getOriginY()); return stamp; } diff --git a/src/main/java/org/openpatch/scratch/internal/Image.java b/src/main/java/org/openpatch/scratch/internal/Image.java index 8f2590d5..b8a308fe 100644 --- a/src/main/java/org/openpatch/scratch/internal/Image.java +++ b/src/main/java/org/openpatch/scratch/internal/Image.java @@ -324,6 +324,32 @@ public void draw( double y, RotationStyle style, Shader shader) { + this.draw(buffer, size, degrees, x, y, style, shader, 0, 0); + } + + /** + * Draw the scaled image at a given position with origin offset. + * + * @param buffer a buffer + * @param size a percentage value + * @param degrees direction + * @param x a x coordinate + * @param y a y coordinate + * @param style a rotation style + * @param shader a shader + * @param originX x offset of the origin from center + * @param originY y offset of the origin from center + */ + public void draw( + PGraphics buffer, + double size, + double degrees, + double x, + double y, + RotationStyle style, + Shader shader, + double originX, + double originY) { buffer.push(); buffer.translate((float) x, (float) -y); degrees -= 90; @@ -346,6 +372,10 @@ public void draw( buffer.shader(pshader); } + // Apply origin offset: shift the image so that the origin point appears at position (x,y) + // originX positive = origin to the right of center, so image shifts left (-originX) + // originY positive = origin above center, so image shifts down (+originY in screen coords) + buffer.translate((float) -originX, (float) originY); buffer.translate(-this.width / 2.0f, -this.height / 2.0f); buffer.tint( (float) this.tint.getRed(), @@ -460,6 +490,29 @@ private void drawNineSlice(PGraphics buffer) { */ public void drawDebug( PGraphics buffer, double size, double degrees, double x, double y, RotationStyle style) { + this.drawDebug(buffer, size, degrees, x, y, style, 0, 0); + } + + /** + * Draw debug information at a given position with origin offset. + * + * @param size a percentage value + * @param degrees direction + * @param x a x coordinate + * @param y a y coordinate + * @param style a rotation style + * @param originX x offset of the origin from center + * @param originY y offset of the origin from center + */ + public void drawDebug( + PGraphics buffer, + double size, + double degrees, + double x, + double y, + RotationStyle style, + double originX, + double originY) { buffer.push(); buffer.translate((float) x, (float) -y); buffer.fill(Window.DEBUG_COLOR[0], Window.DEBUG_COLOR[1], Window.DEBUG_COLOR[1]); diff --git a/src/main/java/org/openpatch/scratch/internal/Stamp.java b/src/main/java/org/openpatch/scratch/internal/Stamp.java index b14d3469..a52d9860 100644 --- a/src/main/java/org/openpatch/scratch/internal/Stamp.java +++ b/src/main/java/org/openpatch/scratch/internal/Stamp.java @@ -12,38 +12,55 @@ public class Stamp { private double y; private RotationStyle style; private double degrees; + private double originX; + private double originY; public Stamp(Image image, double x2, double y2) { - this(image, 0, x2, y2, RotationStyle.DONT); + this(image, 0, x2, y2, RotationStyle.DONT, 0, 0); } public Stamp(Image image, double degrees, double x, double y, RotationStyle style) { + this(image, degrees, x, y, style, 0, 0); + } + + public Stamp( + Image image, + double degrees, + double x, + double y, + RotationStyle style, + double originX, + double originY) { this.image = image; this.x = x; this.y = y; this.style = style; this.degrees = degrees; + this.originX = originX; + this.originY = originY; } public void draw(PGraphics g) { g.push(); g.imageMode(PConstants.CENTER); g.translate((float) this.x, (float) -this.y); - this.degrees -= 90; + double drawDegrees = this.degrees - 90; switch (this.style) { case DONT: break; case ALL_AROUND: - g.rotate(PApplet.radians((float) this.degrees)); + g.rotate(PApplet.radians((float) drawDegrees)); break; case LEFT_RIGHT: - if (this.degrees > -90 && this.degrees < 90) { + if (drawDegrees > -90 && drawDegrees < 90) { g.scale(1, 1); } else { g.scale(-1, 1); } break; } + // Apply origin offset + g.translate((float) -originX, (float) originY); g.tint( (float) this.image.tint.getRed(), (float) this.image.tint.getGreen(), From 105535867e464a97618cd0da55bd91e57e6c2009 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 31 Dec 2025 11:12:20 +0000 Subject: [PATCH 3/3] Add origin offset visualization in debug mode Co-authored-by: mikebarkmin <2592379+mikebarkmin@users.noreply.github.com> --- .../org/openpatch/scratch/internal/Image.java | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/openpatch/scratch/internal/Image.java b/src/main/java/org/openpatch/scratch/internal/Image.java index b8a308fe..139a49a7 100644 --- a/src/main/java/org/openpatch/scratch/internal/Image.java +++ b/src/main/java/org/openpatch/scratch/internal/Image.java @@ -515,10 +515,33 @@ public void drawDebug( double originY) { buffer.push(); buffer.translate((float) x, (float) -y); - buffer.fill(Window.DEBUG_COLOR[0], Window.DEBUG_COLOR[1], Window.DEBUG_COLOR[1]); + buffer.fill(Window.DEBUG_COLOR[0], Window.DEBUG_COLOR[1], Window.DEBUG_COLOR[2]); buffer.textAlign(PConstants.CENTER); buffer.text("Direction: " + Math.round((degrees) * 100) / 100.0, 0, -this.height / 2.0f - 10); buffer.text("(" + Math.round(x * 100) / 100.0 + ", " + Math.round(y * 100) / 100.0 + ")", 0, 0); + + // Draw origin marker and offset visualization + if (originX != 0 || originY != 0) { + // Draw text showing origin offset + buffer.text( + "Origin: (" + Math.round(originX * 100) / 100.0 + ", " + Math.round(originY * 100) / 100.0 + + ")", + 0, + this.height / 2.0f + 20); + + // Draw a line from the sprite position to where the image center is drawn + buffer.stroke(Window.DEBUG_COLOR[0], Window.DEBUG_COLOR[1], Window.DEBUG_COLOR[2]); + buffer.strokeWeight(1); + buffer.line(0, 0, (float) -originX, (float) originY); + + // Draw a small cross at the origin point (sprite position) + buffer.line(-5, 0, 5, 0); + buffer.line(0, -5, 0, 5); + + // Draw a small circle at the image center + buffer.noFill(); + buffer.ellipse((float) -originX, (float) originY, 8, 8); + } buffer.pop(); }