From 4aa567ba9695db9f2904c46e3eea4f8cc65531c6 Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Sat, 25 Feb 2023 15:27:21 +0800 Subject: [PATCH] Add New Feature --- core/src/com/isharryh/arkpets/ArkChar.java | 122 +++++++----- core/src/com/isharryh/arkpets/ArkPets.java | 59 +++--- .../arkpets/utils/FlexibleWindowCtrl.java | 176 ++++++++++++++++++ 3 files changed, 286 insertions(+), 71 deletions(-) create mode 100644 core/src/com/isharryh/arkpets/utils/FlexibleWindowCtrl.java diff --git a/core/src/com/isharryh/arkpets/ArkChar.java b/core/src/com/isharryh/arkpets/ArkChar.java index cc7ed767..43b90373 100644 --- a/core/src/com/isharryh/arkpets/ArkChar.java +++ b/core/src/com/isharryh/arkpets/ArkChar.java @@ -13,7 +13,7 @@ import com.badlogic.gdx.graphics.Pixmap.Format; import com.badlogic.gdx.graphics.g2d.TextureAtlas; import com.badlogic.gdx.graphics.OrthographicCamera; -import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.math.Vector3; import com.esotericsoftware.spine.Skeleton; @@ -24,40 +24,38 @@ import com.esotericsoftware.spine.Animation; import com.esotericsoftware.spine.AnimationState; import com.esotericsoftware.spine.AnimationStateData; -import com.esotericsoftware.spine.Animation.MixBlend; -import com.esotericsoftware.spine.Animation.MixDirection; import com.esotericsoftware.spine.utils.TwoColorPolygonBatch; import com.isharryh.arkpets.easings.EasingLinear; import com.isharryh.arkpets.easings.EasingLinearVector3; import com.isharryh.arkpets.utils.AnimData; +import com.isharryh.arkpets.utils.FlexibleWindowCtrl; import com.isharryh.arkpets.utils.FrameCtrl; -import java.nio.ByteBuffer; public class ArkChar { - private OrthographicCamera camera; - private TwoColorPolygonBatch batch; + private final OrthographicCamera camera; + private final TwoColorPolygonBatch batch; private Texture bgTexture; public Vector3 positionCur; public Vector3 positionTar; public EasingLinearVector3 positionEas; public int offset_y; - public Matrix4 transform; - private Skeleton skeleton; - private SkeletonRenderer renderer; + private final Skeleton skeleton; + private final SkeletonRenderer renderer; + private final int MAX_SKELETON_SIZE = 500; private SkeletonData skeletonData; private Animation animation; private AnimationState animationState; + private Pixmap lastTexture; - private int anim_width; - private int anim_height; + public FlexibleWindowCtrl flexibleLayout; public String[] anim_list; public AnimData[] anim_queue; public FrameCtrl anim_frame; public int anim_fps; - public float f_time; // Duration(Sec) per frame + /** Initialize an ArkPets character. * @param $fp_atlas The file path of the atlas file. @@ -75,7 +73,6 @@ public ArkChar(String $fp_atlas, String $fp_skel, float $anim_scale) { positionCur = new Vector3(0, 0, 0); positionTar = new Vector3(0, 0, 0); offset_y = 0; - transform = new Matrix4(); anim_queue = new AnimData[2]; // Transfer params @@ -135,15 +132,14 @@ public void setCanvas(int $anim_width, int $anim_height, int $anim_fps) { */ public void setCanvas(int $anim_width, int $anim_height, int $anim_fps, Color $bgColor) { // Transfer params - anim_width = $anim_width; - anim_height = $anim_height; + flexibleLayout = new FlexibleWindowCtrl( + new Vector2($anim_width, $anim_height), + ($anim_width + $anim_height) / 4 + ); anim_fps = $anim_fps; // Set position (center) - setPositionTar(anim_width / 2f, 0, 1); - camera.setToOrtho(false, anim_width, anim_height); - camera.update(); - batch.getProjectionMatrix().set(camera.combined); - transform = batch.getTransformMatrix(); + setPositionTar(MAX_SKELETON_SIZE * 0.5f, 0, 1); + updateCanvas(); // Set background image Pixmap pixmap = new Pixmap($anim_width, $anim_height, Format.RGBA8888); pixmap.setColor($bgColor); @@ -151,6 +147,36 @@ public void setCanvas(int $anim_width, int $anim_height, int $anim_fps, Color $b bgTexture = new Texture(pixmap); } + /** Fix the canvas size to make it adapted to the animation. + */ + public void fixCanvasSize() { + if (!flexibleLayout.fixToBestCroppedSize(getCurrentTexture(false), 15, 30, 5, false, true)) + return; + System.out.println( + "^"+flexibleLayout.curInsert.top+ + "\tv"+flexibleLayout.curInsert.bottom+ + "\t<"+flexibleLayout.curInsert.left+ + "\t>"+flexibleLayout.curInsert.right + ); + //if (anim_frame != null) + //PixmapIO.writePNG(new FileHandle("temp").child("temp" + (anim_frame.F_CUR % 50 + 1) + ".png"), getCurrentTexture(true)); + //PixmapIO.writePNG(new FileHandle("temp.png"), getCurrentTexture(true)); + //updateCanvas(); + } + + /** Update the canvas and the camera. + * If you didn't update the canvas in properly, unexpected rendering may cause. + */ + public void updateCanvas() { + camera.setToOrtho(false, flexibleLayout.getWidth(), flexibleLayout.getHeight()); + camera.translate( + ((MAX_SKELETON_SIZE - flexibleLayout.getHeight()) >> 1) - flexibleLayout.curInsert.left, + -flexibleLayout.curInsert.top + ); // Translated X = (Canvas - Camera) / 2 - Insert + camera.update(); + batch.getProjectionMatrix().set(camera.combined); + } + /** Set the target position. * @param $pos_x * @param $pos_y @@ -169,7 +195,11 @@ public void setPositionTar(float $pos_x, float $pos_y, float $flip) { */ public void setPositionCur(float $deltaTime) { // Set current position - positionCur.set(positionEas.eX.step($deltaTime), positionEas.eY.step($deltaTime), positionEas.eZ.step($deltaTime)); + positionCur.set( + positionEas.eX.step($deltaTime), + positionEas.eY.step($deltaTime), + positionEas.eZ.step($deltaTime) + ); skeleton.setPosition(positionCur.x, positionCur.y); skeleton.setScaleX(positionCur.z); skeleton.updateWorldTransform(); @@ -187,46 +217,50 @@ public boolean setAnimation(AnimData $animData) { return false; } - /** Render a specified frame to a byte buffer. - * @param $frame - * @return ByteBuffer object. + /** Get the current framebuffer contents as a Pixmap. + * Note that the image may not be flipped along the y-axis. + * @param debug Whether to show debug additions in the pixmap. + * @return Pixmap object. */ - public ByteBuffer toBuffer(int $frame) { - // Apply Animation - animation.apply(skeleton, ($frame - 1) * f_time, ($frame - 1) * f_time, false, null, 1, MixBlend.first, MixDirection.in); - skeleton.updateWorldTransform(); - // Render the skeleton to the FBO - ScreenUtils.clear(0, 0, 0, 0); - batch.begin(); - renderer.draw(batch, skeleton); - batch.end(); - // Copy the FBO to a pixmap - Pixmap pixmap = new Pixmap(200, 200, Format.RGBA8888); - Gdx.gl.glPixelStorei(GL20.GL_PACK_ALIGNMENT, 1); - Gdx.gl.glReadPixels(0, 0, anim_width, anim_height, - GL20.GL_RGBA, GL20.GL_UNSIGNED_BYTE, pixmap.getPixels()); - // Convert to byte buffer - return pixmap.getPixels(); + public Pixmap getCurrentTexture(boolean debug) { + Pixmap pixmap = lastTexture == null ? Pixmap.createFromFrameBuffer(0, 0, flexibleLayout.getWidth(), flexibleLayout.getHeight()) : lastTexture; + if (debug) { + pixmap.setColor(new Color(1, 0, 0, 0.75f)); + if (flexibleLayout.curInsert.bottom > 0) + pixmap.drawRectangle(0, 0, pixmap.getWidth(), flexibleLayout.curInsert.bottom); + if (flexibleLayout.curInsert.top > 0) + pixmap.drawRectangle(0, pixmap.getHeight() - flexibleLayout.curInsert.top, pixmap.getWidth(), flexibleLayout.curInsert.top); + if (flexibleLayout.curInsert.left > 0) + pixmap.drawRectangle(0, 0, flexibleLayout.curInsert.left, pixmap.getHeight()); + if (flexibleLayout.curInsert.right > 0) + pixmap.drawRectangle(pixmap.getWidth() - flexibleLayout.curInsert.right, 0, flexibleLayout.curInsert.right, pixmap.getHeight()); + } + return pixmap; } /** Render the animation to batch. * @param $frame */ - public void toScreen(int $frame) { + public void toScreen() { // Apply Animation setPositionTar(positionTar.x, positionTar.y, positionTar.z); setPositionCur(Gdx.graphics.getDeltaTime()); animationState.apply(skeleton); animationState.update(Gdx.graphics.getDeltaTime()); - // OLD METHOD: animation.apply(skeleton, ($frame - 1) * anim_frame.F_TIME, ($frame - 1) * anim_frame.F_TIME, false, null, 1, MixBlend.first, MixDirection.in); skeleton.updateWorldTransform(); - // Render the skeleton to the FBO + // Render the skeleton to the batch + updateCanvas(); ScreenUtils.clear(0, 0, 0, 0, true); batch.begin(); if (bgTexture != null) batch.draw(bgTexture, 0, 0); renderer.draw(batch, skeleton); batch.end(); + // Write the current graphic texture to cache + lastTexture = new Pixmap(flexibleLayout.getWidth(), flexibleLayout.getHeight(), Format.RGBA8888); + Gdx.gl.glPixelStorei(GL20.GL_PACK_ALIGNMENT, 1); + Gdx.gl.glReadPixels(0, 0, flexibleLayout.getWidth(), flexibleLayout.getHeight(), + GL20.GL_RGBA, GL20.GL_UNSIGNED_BYTE, lastTexture.getPixels()); } /** Render the next frame. @@ -257,7 +291,7 @@ public void next() { } } // Render the next frame - toScreen(anim_frame.F_CUR); + toScreen(); } private void changeAnimation() { diff --git a/core/src/com/isharryh/arkpets/ArkPets.java b/core/src/com/isharryh/arkpets/ArkPets.java index 0bc3a543..405f54e2 100644 --- a/core/src/com/isharryh/arkpets/ArkPets.java +++ b/core/src/com/isharryh/arkpets/ArkPets.java @@ -11,16 +11,15 @@ import com.badlogic.gdx.InputProcessor; import java.util.ArrayList; +import java.util.Objects; + import com.sun.jna.Pointer; import com.sun.jna.platform.win32.User32; import com.sun.jna.platform.win32.WinUser; import com.sun.jna.platform.win32.WinDef.HWND; +import com.isharryh.arkpets.utils.*; import com.isharryh.arkpets.behaviors.*; -import com.isharryh.arkpets.utils.AnimData; -import com.isharryh.arkpets.utils.HWndCtrl; -import com.isharryh.arkpets.utils.LoopCtrl; -import com.isharryh.arkpets.utils.Plane; import com.isharryh.arkpets.easings.EasingLinear; import com.isharryh.arkpets.easings.EasingLinearVector2; @@ -59,34 +58,34 @@ public void create() { Gdx.app.setLogLevel(3); Gdx.app.log("event", "AP:Create"); Gdx.input.setInputProcessor(this); - config = ArkConfig.getConfig(); + config = Objects.requireNonNull(ArkConfig.getConfig()); + APP_FPS = config.display_fps; + Gdx.graphics.setForegroundFPS(APP_FPS); + getHWndLoopCtrl = new LoopCtrl(1.0f / APP_FPS * 12); ScreenUtils.clear(0, 0, 0, 0, true); - // 2.Window setup + // 2.Character setup + int WD_ORI_W = 140; // Window Origin Width + int WD_ORI_H = 160; // Window Origin Height + cha = new ArkChar(config.character_recent+".atlas", config.character_recent+".skel", 0.33f); + cha.setCanvas(WD_ORI_W, WD_ORI_H, APP_FPS); + // 3.Window params setup WD_poscur = new Vector2(0, 0); WD_postar = new Vector2(0, 0); WD_poseas = new EasingLinearVector2(new EasingLinear(0, 1, 0.2f)); WD_SCALE = config.display_scale; - int WD_ORI_W = 140; // Window Origin Width - int WD_ORI_H = 160; // Window Origin Height - WD_W = (int) (WD_SCALE * WD_ORI_W); - WD_H = (int) (WD_SCALE * WD_ORI_H); + WD_W = (int)(WD_SCALE * cha.flexibleLayout.getWidth()); + WD_H = (int)(WD_SCALE * cha.flexibleLayout.getHeight()); SCR_W = config.display_monitor_info[0]; SCR_H = config.display_monitor_info[1]; - APP_FPS = config.display_fps; - getHWndLoopCtrl = new LoopCtrl(1f / APP_FPS * 4); intiWindow(100, SCR_H / 2); - setWindowPosTar(100, SCR_H / 2f); - Gdx.graphics.setForegroundFPS(APP_FPS); - // 3.Plane setup + setWindowPosTar(100, SCR_H / 2.0f); + // 4.Plane setup plane = new Plane(SCR_W, config.display_margin_bottom-SCR_H, SCR_H * 0.75f); plane.setFrict(SCR_W * 0.05f, SCR_W * 0.25f); plane.setBounce(0); plane.setObjSize(WD_W, -WD_H); - plane.setSpeedLimit(SCR_W * 0.5f, SCR_H * 1f); + plane.setSpeedLimit(SCR_W * 0.5f, SCR_H * 1.0f); plane.changePosition(0, WD_postar.x, -WD_postar.y); - // 4.Character setup - cha = new ArkChar(config.character_recent+".atlas", config.character_recent+".skel", 0.33f); - cha.setCanvas(WD_ORI_W, WD_ORI_H, APP_FPS); // 5.Behavior setup if (BehaviorOperBuild2.match(cha.anim_list)) behavior = new BehaviorOperBuild2(config); @@ -109,6 +108,7 @@ else if (BehaviorOperBuild3.match(cha.anim_list)) @Override public void render() { // 1.Render the next frame. + cha.fixCanvasSize(); cha.next(); if (cha.anim_frame.F_CUR == cha.anim_frame.F_MAX) { Gdx.app.log("info", "FPS" + Gdx.graphics.getFramesPerSecond() + ", Heap" + (int) (Gdx.app.getJavaHeap() / 1024) + "KB"); @@ -245,11 +245,11 @@ private boolean intiWindow(int x, int y) { // | WinUser.WS_EX_LAYERED | WinUser.WS_EX_TRANSPARENT; //System.out.println(User32.INSTANCE.GetWindowLong(hwnd, WinUser.GWL_EXSTYLE)); //User32.INSTANCE.SetWindowLong(HWND_MINE, WinUser.GWL_EXSTYLE, enable ? WL_TRAN_ON : WL_TRAN_OFF); - User32.INSTANCE.SetWindowLong(HWND_MINE, WinUser.GWL_EXSTYLE, 0x00000088); - User32.INSTANCE.SetWindowPos(HWND_MINE, HWND_TOPMOST, x, y, - WD_W, WD_H, WinUser.SWP_FRAMECHANGED); - User32.INSTANCE.SetWindowPos(HWND_MINE, HWND_TOPMOST, x, y, - WD_W, WD_H, WinUser.SWP_NOSIZE); + User32.INSTANCE.SetWindowPos(HWND_MINE, HWND_TOPMOST, + x, y, WD_W, WD_H, + WinUser.SWP_SHOWWINDOW | WinUser.SWP_NOACTIVATE + ); + Gdx.app.debug("debug", "JNA SetWindowLong returns " + Integer.toHexString(User32.INSTANCE.SetWindowLong(HWND_MINE, WinUser.GWL_EXSTYLE, 0x00000088))); return true; } @@ -257,6 +257,8 @@ private boolean setWindowPos(int x, int y, boolean override) { if (HWND_MINE == null) return false; if (getHWndLoopCtrl.isExecutable(Gdx.graphics.getDeltaTime())) { + WD_W = (int)(WD_SCALE * cha.flexibleLayout.getWidth()); + WD_H = (int)(WD_SCALE * cha.flexibleLayout.getHeight()); HWND new_hwnd_topmost = refreshWindowIdx(); if (new_hwnd_topmost != HWND_TOPMOST) { HWND_TOPMOST = new_hwnd_topmost; @@ -275,8 +277,11 @@ private boolean setWindowPos(int x, int y, boolean override) { WD_poseas.eX.curDuration = WD_poseas.eX.DURATION; WD_poseas.eY.curDuration = WD_poseas.eY.DURATION; } - User32.INSTANCE.SetWindowPos(HWND_MINE, HWND_TOPMOST, (int)WD_poscur.x, (int)WD_poscur.y, - 0, 0, WinUser.SWP_NOSIZE); + User32.INSTANCE.SetWindowPos(HWND_MINE, HWND_TOPMOST, + (int)WD_poscur.x - cha.flexibleLayout.curInsert.left, + (int)WD_poscur.y, + WD_W, WD_H, WinUser.SWP_NOACTIVATE + ); return true; } @@ -360,7 +365,7 @@ private int getArkPetsWindowNum(String title) { /* WINDOW OPERATION RELATED */ private void walkWindow(float len) { - float expectedLen = len * WD_SCALE * (30f / APP_FPS); + float expectedLen = len * WD_SCALE * (30.0f / APP_FPS); int realLen = randomRound(expectedLen); plane.changePosition(Gdx.graphics.getDeltaTime(), WD_postar.x + realLen, -WD_postar.y); } diff --git a/core/src/com/isharryh/arkpets/utils/FlexibleWindowCtrl.java b/core/src/com/isharryh/arkpets/utils/FlexibleWindowCtrl.java new file mode 100644 index 00000000..101a00a9 --- /dev/null +++ b/core/src/com/isharryh/arkpets/utils/FlexibleWindowCtrl.java @@ -0,0 +1,176 @@ +/** Copyright (c) 2022-2023, Harry Huang + * At GPL-3.0 License + */ +package com.isharryh.arkpets.utils; + +import com.badlogic.gdx.graphics.Pixmap; +import com.badlogic.gdx.math.Vector2; + + +public class FlexibleWindowCtrl { + public Vector2 origin; + public Insert maxInsert; + public Insert curInsert; + + /** Initialize a Flexible Window-size Controller instance. + * @param originSize The original size of the window. + * @param maxInsertLength The max insert length of each side. + */ + public FlexibleWindowCtrl(Vector2 originSize, int maxInsertLength) { + origin = originSize; + maxInsert = new Insert( + maxInsertLength, maxInsertLength, + maxInsertLength, maxInsertLength + ); + curInsert = new Insert(); + } + + /** Fix the window's size to the best cropped size using the given parameters. + * Note that {@code offset} should be set greater than {@code extensionLength}, or it may cause shaking. + * @param pixmap The pixmap got from the current rendered frame of libGDX. + * @param extensionLength The extension length to be added to each overflowed side (px), no negative. 2~6 recommended. + * @param offset The offset to apply to the white space (px). 4~8 recommended. + * @param moderateThreshold The threshold to moderate the variation (px), no negative. 2~4 recommended. + * @param flipX Flip the pixmap along the x-axis. + * @param flipY Flip the pixmap along the y-axis. + * @return False if the size didn't change, true otherwise. + */ + public boolean fixToBestCroppedSize(Pixmap pixmap, int extensionLength, int offset, int moderateThreshold, boolean flipX, boolean flipY) { + Insert insert = curInsert.clone(); + final int alphaThreshold = 128; + final int edgeWidth = pixmap.getWidth() - 1; + final int edgeHeight = pixmap.getHeight() - 1; + + // TOP + for (int y = 0; y <= edgeHeight; y++) + for (int x = 0; x <= edgeWidth; x++) + if ((pixmap.getPixel(x, y) & 0x000000FF) >= alphaThreshold) { + insert.top += (y == 0 ? extensionLength : - y); + if (insert.top < (flipY ? curInsert.bottom : curInsert.top)) + insert.top = (insert.top < -offset ? insert.top + 1 : (flipY ? curInsert.bottom : curInsert.top)); + x = Integer.MAX_VALUE - 1; + y = Integer.MAX_VALUE - 1; + } + // BOTTOM + for (int y = edgeHeight; y >= 0; y--) + for (int x = 0; x <= edgeWidth; x++) + if ((pixmap.getPixel(x, y) & 0x000000FF) >= alphaThreshold) { + insert.bottom += (y == edgeHeight ? extensionLength : y - edgeHeight); + if (insert.bottom < (flipY ? curInsert.top : curInsert.bottom)) + insert.bottom = (insert.bottom < -offset ? insert.bottom + 1 : (flipY ? curInsert.top : curInsert.bottom)); + x = Integer.MAX_VALUE - 1; + y = Integer.MIN_VALUE + 1; + } + // LEFT + for (int x = 0; x <= edgeWidth; x++) + for (int y = 0; y <= edgeHeight; y++) + if ((pixmap.getPixel(x, y) & 0x000000FF) >= alphaThreshold) { + insert.left += (x == 0 ? extensionLength : - x); + if (insert.left < (flipX ? curInsert.right : curInsert.left)) + insert.left = (insert.left < -offset ? insert.left + 1 : (flipX ? curInsert.right : curInsert.left)); + x = Integer.MAX_VALUE - 1; + y = Integer.MAX_VALUE - 1; + } + // RIGHT + for (int x = edgeWidth; x >= 0; x--) + for (int y = 0; y <= edgeHeight; y++) + if ((pixmap.getPixel(x, y) & 0x000000FF) >= alphaThreshold) { + insert.right += (x == edgeWidth ? extensionLength : x - edgeWidth); + if (insert.right < (flipX ? curInsert.left : curInsert.right)) + insert.right = (insert.right < -offset ? insert.right + 1 : (flipX ? curInsert.left : curInsert.right)); + x = Integer.MIN_VALUE + 1; + y = Integer.MAX_VALUE - 1; + } + + if (flipX) + insert.swapHorizontal(); + if (flipY) + insert.swapVertical(); + + insert.bottom = 0; // Temporarily set bottom insert value to 0 + insert.limitMaxNoNegative(maxInsert); + return curInsert.moderatelyModify(insert, moderateThreshold); + } + + /** Get the total width. + * @return The total width. + */ + public int getWidth() { + return (int)(curInsert.left + curInsert.right + origin.x); + } + + /** Get the total height. + * @return The total height. + */ + public int getHeight() { + return (int)(curInsert.top + curInsert.bottom + origin.y); + } + + public static class Insert { + public int top; + public int bottom; + public int left; + public int right; + + public Insert() { + top = 0; + bottom = 0; + left = 0; + right = 0; + } + + public Insert(int top, int bottom, int left, int right) { + this.top = top; + this.bottom = bottom; + this.left = left; + this.right = right; + } + + public void limitMaxNoNegative(Insert maxInsert) { + top = Math.max(Math.min(top, maxInsert.top), 0); + bottom = Math.max(Math.min(bottom, maxInsert.bottom), 0); + left = Math.max(Math.min(left, maxInsert.left), 0); + right = Math.max(Math.min(right, maxInsert.right), 0); + } + + public void limitMinNoNegative(Insert minAbsInsert) { + top = Math.min(Math.max(top, 0), minAbsInsert.top); + bottom = Math.min(Math.max(bottom, 0), minAbsInsert.bottom); + left = Math.min(Math.max(left, 0), minAbsInsert.left); + right = Math.min(Math.max(right, 0), minAbsInsert.right); + } + + public void swapHorizontal() { + left += right; + right = left - right; + left -= right; + } + + public void swapVertical() { + bottom += top; + top = bottom - top; + bottom -= top; + } + + public boolean moderatelyModify(Insert changeTo, int threshold) { + int top_ = Math.abs(changeTo.top - top) > threshold ? changeTo.top : top; + int bottom_ = Math.abs(changeTo.bottom - bottom) > threshold ? changeTo.bottom : bottom; + int left_ = Math.abs(changeTo.left - left) > threshold ? changeTo.left : left; + int right_ = Math.abs(changeTo.right - right) > threshold ? changeTo.right : right; + + if (top_ == top && bottom_ == bottom && left_ == left && right_ == right) { + return false; + } else { + top = top_; + bottom = bottom_; + left = left_; + right = right_; + return true; + } + } + + public Insert clone() { + return new Insert(top, bottom, left, right); + } + } +}