From 4b97e6044218cfca74d2293362cf1fa17da732d5 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Fri, 24 Jul 2026 20:52:32 +0800 Subject: [PATCH] add display width and height add a configurable option to display width and height in pixels instead of scale factor --- .../compiler/tags/FloatingImageCompiler.java | 72 +++++++++- .../guide/document/block/LytImage.java | 14 +- .../guide/document/block/LytImageBlock.java | 12 ++ .../autocomplete/TagAttributeRegistry.java | 2 + .../guide/GuideScreenEditorTextActions.java | 2 +- .../host/scripts/FloatingImageScript.java | 15 +- .../internal/host/scripts/ImageScript.java | 4 +- .../guide/render/GuidePageTexture.java | 11 ++ .../site/GuideSiteHtmlCompiler.java | 131 ++++++++++++++---- .../resources/assets/guidenh/lang/en_US.lang | 2 +- .../resources/assets/guidenh/lang/zh_CN.lang | 2 +- .../assets/guidenh/siteexport/app.js | 8 +- wiki/Images-And-Assets-zh-CN.md | 14 +- wiki/Images-And-Assets.md | 14 +- wiki/Tags-Reference-zh-CN.md | 4 +- wiki/Tags-Reference.md | 4 +- .../assets/guidenh/guidenh/_en_us/images.md | 13 +- .../assets/guidenh/guidenh/_zh_cn/images.md | 8 +- 18 files changed, 273 insertions(+), 59 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FloatingImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FloatingImageCompiler.java index 17e33157..f2f50972 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FloatingImageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FloatingImageCompiler.java @@ -36,6 +36,13 @@ private record CropSpec(int x, int y, int width, int height) {} private record ScaleSpec(double scaleX, double scaleY) {} + private record DisplaySpec(int width, int height) { + + private boolean isSpecified() { + return width > 0 || height > 0; + } + } + @Override public Set getTagNames() { return Collections.singleton(TAG_NAME); @@ -52,9 +59,17 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen var align = el.getAttributeString("align", "left"); var title = el.getAttributeString("title", null); var alt = el.getAttributeString("alt", null); - CropSpec crop = parseCropSpec(compiler, parent, el); + DisplaySpec display = parseDisplaySpec(compiler, parent, el); + CropSpec crop = parseCropSpec(compiler, parent, el, display != null && display.isSpecified()); ScaleSpec scale = parseScaleSpec(compiler, parent, el); - if (crop == null || scale == null) { + if (crop == null || display == null || scale == null) { + return; + } + if (display.isSpecified() && hasScaleAttributes(el)) { + parent.appendError( + compiler, + "FloatingImage displayWidth/displayHeight cannot be used with scaleX/scaleY.", + el); return; } @@ -75,6 +90,8 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen block.setCropHeight(crop.height()); block.setScaleX(scale.scaleX()); block.setScaleY(scale.scaleY()); + block.setDisplayWidth(display.width()); + block.setDisplayHeight(display.height()); // Resolve the image src to a string identifier for later script use without // loading the actual asset at compile time. @@ -243,7 +260,8 @@ public static int parseIntAttr(MdxJsxElementFields el, String name, int def) { } @Nullable - private static CropSpec parseCropSpec(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el) { + private static CropSpec parseCropSpec(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el, + boolean allowWholeImage) { String widthValue = el.getAttributeString("width", null); String widthAlias = el.getAttributeString("w", null); String heightValue = el.getAttributeString("height", null); @@ -256,6 +274,13 @@ private static CropSpec parseCropSpec(PageCompiler compiler, LytFlowParent paren parent.appendError(compiler, "FloatingImage cannot use both height and h.", el); return null; } + if (!hasCropAttributes(el)) { + if (allowWholeImage) { + return new CropSpec(0, 0, -1, -1); + } + parent.appendError(compiler, "FloatingImage requires x, y, width or w, and height or h.", el); + return null; + } Integer x = parseRequiredIntAttr(compiler, parent, el, "x"); Integer y = parseRequiredIntAttr(compiler, parent, el, "y"); Integer width = parseRequiredAliasedIntAttr(compiler, parent, el, "width", "w"); @@ -273,6 +298,14 @@ private static CropSpec parseCropSpec(PageCompiler compiler, LytFlowParent paren return new CropSpec(x, y, width, height); } + private static boolean hasCropAttributes(MdxJsxElementFields el) { + return el.getAttributeString("x", null) != null || el.getAttributeString("y", null) != null + || el.getAttributeString("width", null) != null + || el.getAttributeString("w", null) != null + || el.getAttributeString("height", null) != null + || el.getAttributeString("h", null) != null; + } + @Nullable private static ScaleSpec parseScaleSpec(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el) { Double scaleX = parseDoubleAttr(compiler, parent, el, "scaleX", 1.0d); @@ -287,6 +320,21 @@ private static ScaleSpec parseScaleSpec(PageCompiler compiler, LytFlowParent par return new ScaleSpec(scaleX, scaleY); } + @Nullable + private static DisplaySpec parseDisplaySpec(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el) { + Integer width = parseOptionalPositiveIntAttr(compiler, parent, el, "displayWidth"); + Integer height = parseOptionalPositiveIntAttr(compiler, parent, el, "displayHeight"); + if ((width == null && el.getAttributeString("displayWidth", null) != null) + || (height == null && el.getAttributeString("displayHeight", null) != null)) { + return null; + } + return new DisplaySpec(width != null ? width : -1, height != null ? height : -1); + } + + private static boolean hasScaleAttributes(MdxJsxElementFields el) { + return el.getAttributeString("scaleX", null) != null || el.getAttributeString("scaleY", null) != null; + } + @Nullable private static Integer parseRequiredAliasedIntAttr(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el, String primaryName, String aliasName) { @@ -328,6 +376,24 @@ private static Integer parseRequiredIntAttr(PageCompiler compiler, LytFlowParent } } + @Nullable + private static Integer parseOptionalPositiveIntAttr(PageCompiler compiler, LytFlowParent parent, + MdxJsxElementFields el, String name) { + String value = el.getAttributeString(name, null); + if (value == null || value.trim() + .isEmpty()) { + return null; + } + try { + int parsed = Integer.parseInt(value.trim()); + if (parsed > 0) { + return parsed; + } + } catch (NumberFormatException ignored) {} + parent.appendError(compiler, "FloatingImage " + name + " must be a positive integer.", el); + return null; + } + @Nullable private static Double parseDoubleAttr(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el, String name, double defaultValue) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytImage.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytImage.java index 5fe43640..29433169 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytImage.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytImage.java @@ -46,6 +46,8 @@ public class LytImage extends LytBlock implements InteractiveElement { private int cropHeight = -1; private double scaleX = 1.0d; private double scaleY = 1.0d; + private int displayWidth = -1; + private int displayHeight = -1; @Getter private final List annotations = new ArrayList<>(); @@ -86,6 +88,11 @@ public void setScale(double scaleX, double scaleY) { this.scaleY = scaleY > 0.0d ? scaleY : 1.0d; } + public void setDisplaySize(int displayWidth, int displayHeight) { + this.displayWidth = displayWidth > 0 ? displayWidth : -1; + this.displayHeight = displayHeight > 0 ? displayHeight : -1; + } + @Override protected LytRect computeLayout(LayoutContext context, int x, int y, int availableWidth) { if (texture == null) { @@ -97,7 +104,12 @@ protected LytRect computeLayout(LayoutContext context, int x, int y, int availab int sourceHeight = Math.max(1, cropHeight > 0 ? cropHeight : size.height()); int width; int height; - if (explicitWidth > 0 || explicitHeight > 0) { + if (displayWidth > 0 || displayHeight > 0) { + width = displayWidth > 0 ? displayWidth + : Math.max(1, (int) Math.round(displayHeight * sourceWidth / (double) sourceHeight)); + height = displayHeight > 0 ? displayHeight + : Math.max(1, (int) Math.round(displayWidth * sourceHeight / (double) sourceWidth)); + } else if (explicitWidth > 0 || explicitHeight > 0) { width = explicitWidth > 0 ? explicitWidth : Math.max(1, (int) Math.round(sourceWidth * scaleX)); height = explicitHeight > 0 ? explicitHeight : Math.max(1, (int) Math.round(sourceHeight * scaleY)); } else { diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytImageBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytImageBlock.java index d2f4ba0c..0efb92da 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytImageBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytImageBlock.java @@ -35,6 +35,10 @@ public class LytImageBlock extends LytParagraph { private double scaleX = 1.0d; @Getter private double scaleY = 1.0d; + @Getter + private int displayWidth = -1; + @Getter + private int displayHeight = -1; @Nullable private String align; @Getter @@ -99,6 +103,14 @@ public void setScaleY(double scaleY) { this.scaleY = scaleY > 0.0d ? scaleY : 1.0d; } + public void setDisplayWidth(int displayWidth) { + this.displayWidth = displayWidth > 0 ? displayWidth : -1; + } + + public void setDisplayHeight(int displayHeight) { + this.displayHeight = displayHeight > 0 ? displayHeight : -1; + } + @Nullable public String getAlign() { return align; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java index ea3fd579..f5f3aae2 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java @@ -80,6 +80,8 @@ public static void initialize() { new AttributeSpec("h", AttrType.INT), new AttributeSpec("scaleX", AttrType.FLOAT), new AttributeSpec("scaleY", AttrType.FLOAT), + new AttributeSpec("displayWidth", AttrType.INT), + new AttributeSpec("displayHeight", AttrType.INT), new AttributeSpec("sound", AttrType.STRING), new AttributeSpec("soundSrc", AttrType.FILE_PATH), new AttributeSpec("trigger", AttrType.STRING)); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/guide/GuideScreenEditorTextActions.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/guide/GuideScreenEditorTextActions.java index a44dfb6e..af06800a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/guide/GuideScreenEditorTextActions.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/guide/GuideScreenEditorTextActions.java @@ -1063,7 +1063,7 @@ private static Result applyFloatingImage(String source, int start, int end) { String selected = start < end ? sanitizeAttributeValue(source.substring(start, end)) : ""; String imagePath = selected.isEmpty() ? "test1.png" : selected; String replacement = ""; + + "\" x=\"0\" y=\"0\" width=\"128\" height=\"128\" displayWidth=\"128\" displayHeight=\"128\" wrap=\"inline\" title=\"Example\" />"; int caretStart = start + replacement.indexOf(imagePath); return new Result( source.substring(0, start) + replacement + source.substring(end), diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java index 2e2fb5ba..957fd22e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java @@ -12,6 +12,7 @@ import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import com.hfstudio.guidenh.guide.render.GuidePageTexture; public class FloatingImageScript implements LytScript { @@ -53,19 +54,20 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { return; } - byte[] imageData = ctx.loadAsset(imageId); LytImage image = new LytImage(); - image.setImage(imageId, imageData); // null imageData → GuidePageTexture.missing() + image.setTexture(imageId, GuidePageTexture.loadCached(imageId, () -> ctx.loadAsset(imageId))); String alt = placeholder.getAlt(); if (alt != null && !alt.isEmpty()) image.setAlt(alt); String title = placeholder.getTitle(); if (title != null && !title.isEmpty()) { image.setTitle(title); - } else if (imageData == null) { - image.setTitle("Missing image: " + src); - } - if (imageData != null) { + } else if (image.getTexture() + .isMissing()) { + image.setTitle("Missing image: " + src); + } + if (!image.getTexture() + .isMissing()) { var size = image.getTexture() .getSize(); int cropRight = placeholder.getCropX() + placeholder.getCropWidth(); @@ -81,6 +83,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { placeholder.getCropWidth(), placeholder.getCropHeight()); image.setScale(placeholder.getScaleX(), placeholder.getScaleY()); + image.setDisplaySize(placeholder.getDisplayWidth(), placeholder.getDisplayHeight()); image.setMarginTop(placeholder.getMarginTop()); image.setMarginLeft(placeholder.getMarginLeft()); image.setMarginRight(placeholder.getMarginRight()); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java index 26b0fb43..ee85e9a4 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java @@ -12,6 +12,7 @@ import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import com.hfstudio.guidenh.guide.render.GuidePageTexture; public class ImageScript implements LytScript { @@ -52,9 +53,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { return; } - byte[] imageData = ctx.loadAsset(imageId); LytImage image = new LytImage(); - image.setImage(imageId, imageData); // null imageData → GuidePageTexture.missing() + image.setTexture(imageId, GuidePageTexture.loadCached(imageId, () -> ctx.loadAsset(imageId))); String alt = placeholder.getAlt(); if (alt != null && !alt.isEmpty()) image.setAlt(alt); diff --git a/src/main/java/com/hfstudio/guidenh/guide/render/GuidePageTexture.java b/src/main/java/com/hfstudio/guidenh/guide/render/GuidePageTexture.java index e62fa83c..f131e598 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/render/GuidePageTexture.java +++ b/src/main/java/com/hfstudio/guidenh/guide/render/GuidePageTexture.java @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.function.Supplier; import javax.imageio.ImageIO; import javax.imageio.ImageReader; @@ -66,6 +67,11 @@ public static GuidePageTexture missing() { return MISSING; } + @Nullable + public static synchronized GuidePageTexture getCached(ResourceLocation id) { + return CACHE.get(id); + } + public static GuidePageTexture of(ResourceLocation texture) { return new GuidePageTexture(texture, 256, 256); } @@ -88,6 +94,11 @@ public static synchronized GuidePageTexture load(ResourceLocation id, byte[] ima } } + public static GuidePageTexture loadCached(ResourceLocation id, Supplier assetLoader) { + GuidePageTexture cached = getCached(id); + return cached != null ? cached : load(id, assetLoader.get()); + } + public static synchronized void clear() { TextureManager textureManager = Minecraft.getMinecraft() .getTextureManager(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java index 6678419d..cbec8c7a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java @@ -870,19 +870,25 @@ private String compileFloatingImage(MdxJsxElementFields element, GuideSiteTempla Integer cropY = parsePositiveOrZeroInt(element.getAttributeString("y", null)); Integer cropWidth = parseAliasedPositiveInt(element, "width", "w"); Integer cropHeight = parseAliasedPositiveInt(element, "height", "h"); + String displayWidthValue = element.getAttributeString("displayWidth", null); + String displayHeightValue = element.getAttributeString("displayHeight", null); + Integer explicitDisplayWidth = parsePositiveInt(displayWidthValue); + Integer explicitDisplayHeight = parsePositiveInt(displayHeightValue); Double scaleX = parsePositiveDouble(element.getAttributeString("scaleX", null), 1.0d); Double scaleY = parsePositiveDouble(element.getAttributeString("scaleY", null), 1.0d); - if (cropX == null || cropY == null || cropWidth == null || cropHeight == null) { - return renderExportError( - "FloatingImage requires non-negative x and y, plus positive width or w and height or h."); - } if (scaleX == null || scaleY == null) { return renderExportError("FloatingImage scaleX and scaleY must be positive numbers."); } + if ((displayWidthValue != null && explicitDisplayWidth == null) + || (displayHeightValue != null && explicitDisplayHeight == null)) { + return renderExportError("FloatingImage displayWidth and displayHeight must be positive integers."); + } + boolean hasExplicitDisplaySize = explicitDisplayWidth != null || explicitDisplayHeight != null; + if (hasExplicitDisplaySize && (element.getAttributeString("scaleX", null) != null + || element.getAttributeString("scaleY", null) != null)) { + return renderExportError("FloatingImage displayWidth/displayHeight cannot be used with scaleX/scaleY."); + } String src = resolveImageSource(rawSrc, currentPageId); - - double displayWidth = cropWidth * scaleX; - double displayHeight = cropHeight * scaleY; boolean inlineWrap = "inline".equals(element.getAttributeString("wrap", null)); String align = element.getAttributeString("align", "left"); if (!inlineWrap && !"left".equals(align) && !"right".equals(align)) { @@ -896,6 +902,61 @@ private String compileFloatingImage(MdxJsxElementFields element, GuideSiteTempla } else { wrapperStyle.append("float:left;margin:0 5px 5px 0;"); } + if (!hasCropAttributes(element)) { + if (!hasExplicitDisplaySize) { + return renderExportError( + "FloatingImage requires non-negative x and y, plus positive width or w and height or h."); + } + if (explicitDisplayWidth != null) { + wrapperStyle.append("width:") + .append(explicitDisplayWidth) + .append("px;"); + } + if (explicitDisplayHeight != null) { + wrapperStyle.append("height:") + .append(explicitDisplayHeight) + .append("px;"); + } + String imageStyle = explicitDisplayWidth != null && explicitDisplayHeight != null + ? "width:100%;height:100%;" + : explicitDisplayWidth != null ? "width:100%;height:auto;" : "width:auto;height:100%;"; + List annotations = collectImageAnnotations( + element, + templates, + defaultNamespace, + currentPageId, + sceneResolver, + null, + null); + return buildFloatingImageHtml( + src, + alt, + title, + wrapperStyle.toString(), + imageStyle, + false, + 0, + 0, + 0, + 0, + 1.0d, + 1.0d, + inlineWrap, + annotations); + } + if (cropX == null || cropY == null || cropWidth == null || cropHeight == null) { + return renderExportError( + "FloatingImage requires non-negative x and y, plus positive width or w and height or h."); + } + + double displayWidth = explicitDisplayWidth != null ? explicitDisplayWidth + : explicitDisplayHeight != null ? explicitDisplayHeight * cropWidth / (double) cropHeight + : cropWidth * scaleX; + double displayHeight = explicitDisplayHeight != null ? explicitDisplayHeight + : explicitDisplayWidth != null ? explicitDisplayWidth * cropHeight / (double) cropWidth + : cropHeight * scaleY; + double effectiveScaleX = displayWidth / cropWidth; + double effectiveScaleY = displayHeight / cropHeight; wrapperStyle.append("width:") .append(toCssNumber(displayWidth)) .append("px;height:") @@ -910,21 +971,31 @@ private String compileFloatingImage(MdxJsxElementFields element, GuideSiteTempla sceneResolver, cropWidth, cropHeight); - return buildCroppedFloatingImageHtml( + return buildFloatingImageHtml( src, alt, title, wrapperStyle.toString(), + "", + true, cropX, cropY, cropWidth, cropHeight, - scaleX, - scaleY, + effectiveScaleX, + effectiveScaleY, inlineWrap, annotations); } + private boolean hasCropAttributes(MdxJsxElementFields element) { + return element.getAttributeString("x", null) != null || element.getAttributeString("y", null) != null + || element.getAttributeString("width", null) != null + || element.getAttributeString("w", null) != null + || element.getAttributeString("height", null) != null + || element.getAttributeString("h", null) != null; + } + private List collectImageAnnotations(MdxJsxElementFields element, GuideSiteTemplateRegistry templates, String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver, @Nullable Integer imageWidth, @Nullable Integer imageHeight) { @@ -1029,9 +1100,9 @@ private void appendOptionalDataAttribute(StringBuilder html, String name, @Nulla .append("\""); } - private String buildCroppedFloatingImageHtml(String src, String alt, @Nullable String title, String wrapperStyle, - int cropX, int cropY, int cropWidth, int cropHeight, double scaleX, double scaleY, boolean inlineWrap, - List annotations) { + private String buildFloatingImageHtml(String src, String alt, @Nullable String title, String wrapperStyle, + String imageStyle, boolean cropped, int cropX, int cropY, int cropWidth, int cropHeight, double scaleX, + double scaleY, boolean inlineWrap, List annotations) { StringBuilder html = new StringBuilder(); html.append(""); + if (!imageStyle.isEmpty()) { + html.append(" style=\"") + .append(escapeAttribute(imageStyle)) + .append("\""); + } + if (cropped) { + html.append(" data-crop-x=\"") + .append(cropX) + .append("\" data-crop-y=\"") + .append(cropY) + .append("\" data-crop-width=\"") + .append(cropWidth) + .append("\" data-crop-height=\"") + .append(cropHeight) + .append("\" data-scale-x=\"") + .append(toCssNumber(scaleX)) + .append("\" data-scale-y=\"") + .append(toCssNumber(scaleY)) + .append("\""); + } + html.append(" loading=\"lazy\" decoding=\"async\">"); for (ImageAnnotationExport annotation : annotations) { html.append(" { - layoutCroppedFloatingImage(image); + if (image.dataset.cropWidth) { + layoutCroppedFloatingImage(image); + } layoutImageAnnotations(root); }, { once: true }); } diff --git a/wiki/Images-And-Assets-zh-CN.md b/wiki/Images-And-Assets-zh-CN.md index 0a55c750..a9d04da7 100644 --- a/wiki/Images-And-Assets-zh-CN.md +++ b/wiki/Images-And-Assets-zh-CN.md @@ -39,6 +39,8 @@ GuideNH 会解析路径,并从指南内容根目录加载对应的二进制资 | `height` / `h` | 是 | 原图裁剪高度,单位为源图像像素;两种写法只能二选一 | | `scaleX` | 否 | 水平显示缩放倍率,默认 `1.0` | | `scaleY` | 否 | 垂直显示缩放倍率,默认 `1.0` | +| `displayWidth` | 否 | 最终显示宽度,单位为像素;单独使用时按裁剪区域比例计算高度 | +| `displayHeight` | 否 | 最终显示高度,单位为像素;单独使用时按裁剪区域比例计算宽度 | | `wrap` | 否 | `inline` 表示真正行内放置,其他值使用常规环绕模式 | | `align` | 否 | 浮动时使用 `left` 或 `right`;`wrap="inline"` 时会被忽略 | | `title` | 否 | tooltip/title 文本 | @@ -48,9 +50,13 @@ GuideNH 会解析路径,并从指南内容根目录加载对应的二进制资 ### 说明 -- `x`、`y`、`width` / `w`、`height` / `h` 必须四个一起写 +- 裁剪时必须同时提供 `x`、`y`、`width` / `w` 与 `height` / `h` +- 全部省略裁剪属性时,可使用 `displayWidth` 或 `displayHeight` 显示整张源图 - `width` 和 `height` 现在表示裁剪区域,不再表示最终显示尺寸 -- 最终显示尺寸等于 `cropWidth * scaleX` 与 `cropHeight * scaleY` +- `scaleX` 与 `scaleY` 会将最终显示尺寸计算为 `cropWidth * scaleX` 与 `cropHeight * scaleY` +- `displayWidth` 或 `displayHeight` 以像素指定最终显示尺寸;只提供其中一个时,另一个尺寸按裁剪区域宽高比自动计算 +- 同时提供 `displayWidth` 与 `displayHeight` 时,可按指定尺寸进行非等比拉伸 +- `displayWidth` / `displayHeight` 不能与 `scaleX` / `scaleY` 同时使用 - 支持单轴拉伸,只修改一个缩放值即可 - 同时写 `width` 和 `w`,或同时写 `height` 和 `h`,都会渲染可见错误 - 旧版把 `width` / `height` 当作显示尺寸的 `` 内容会发生破坏性变更,需要手动迁移 @@ -65,8 +71,8 @@ GuideNH 会解析路径,并从指南内容根目录加载对应的二进制资 y="0" width="32" height="32" - scaleX="2.0" - scaleY="2.0" + displayWidth="64" + displayHeight="64" wrap="inline" title="Example" /> diff --git a/wiki/Images-And-Assets.md b/wiki/Images-And-Assets.md index 095d96ad..510bf29a 100644 --- a/wiki/Images-And-Assets.md +++ b/wiki/Images-And-Assets.md @@ -39,6 +39,8 @@ other mods directly. | `height` / `h` | yes | crop height in source-image pixels; exactly one spelling must be used | | `scaleX` | no | horizontal display multiplier, default `1.0` | | `scaleY` | no | vertical display multiplier, default `1.0` | +| `displayWidth` | no | final display width in pixels; preserves the crop aspect ratio when used alone | +| `displayHeight` | no | final display height in pixels; preserves the crop aspect ratio when used alone | | `wrap` | no | `inline` for true inline placement, otherwise use the normal wrapping modes | | `align` | no | `left` or `right` for floating placement; ignored when `wrap="inline"` | | `title` | no | tooltip/title text | @@ -48,9 +50,13 @@ other mods directly. ### Notes -- `x`, `y`, `width` / `w`, and `height` / `h` are all required together +- `x`, `y`, `width` / `w`, and `height` / `h` are all required together when cropping +- when the crop attributes are all omitted, `displayWidth` or `displayHeight` displays the full source image - `width` and `height` now describe the crop rectangle, not the final display size -- the final display size is `cropWidth * scaleX` by `cropHeight * scaleY` +- `scaleX` and `scaleY` calculate the final display size as `cropWidth * scaleX` by `cropHeight * scaleY` +- `displayWidth` or `displayHeight` sets the final display size in pixels; when only one is present, the other dimension is calculated from the crop aspect ratio +- providing both `displayWidth` and `displayHeight` allows intentional non-proportional stretching +- `displayWidth` / `displayHeight` cannot be combined with `scaleX` / `scaleY` - single-axis stretching is supported by setting only one scale differently - `width` with `w`, or `height` with `h`, is invalid and renders a visible error - old `FloatingImage width/height as display size` content is intentionally breaking and must be migrated manually @@ -65,8 +71,8 @@ other mods directly. y="0" width="32" height="32" - scaleX="2.0" - scaleY="2.0" + displayWidth="64" + displayHeight="64" wrap="inline" title="Example" /> diff --git a/wiki/Tags-Reference-zh-CN.md b/wiki/Tags-Reference-zh-CN.md index ac1f7ab5..a1b46964 100644 --- a/wiki/Tags-Reference-zh-CN.md +++ b/wiki/Tags-Reference-zh-CN.md @@ -57,7 +57,7 @@ | `` | 运行时 Markdown 脚注使用的限宽脚注容器 | `width` | | `` | 紧凑物品图标网格 | 子元素必须是 `` 或 `` | | `` | 非交互式的 3D 单方块预览 | `id` 或 `ore`,`scale`,`float`,`perspective`,`nbt` | -| `` | 支持浮动或真正行内放置的裁剪图片块 | `src`, `x`, `y`, `width` / `w`, `height` / `h`, `scaleX`, `scaleY`, `wrap`, `align`, `title` | +| `` | 支持浮动或真正行内放置的裁剪图片块 | `src`, `x`, `y`, `width` / `w`, `height` / `h`, `scaleX`, `scaleY`, `displayWidth`, `displayHeight`, `wrap`, `align`, `title` | | `` | 导航子页面列表 | `id`, `alphabetical` | | `` | 分类页面列表 | `name`, `rows` | | `` | 内置 MediaWiki 特殊页面列表 | `name`, `rows` | @@ -480,6 +480,8 @@ GuideNH 会在运行时 Markdown 脚注展开后内部使用这个块标签。 - `src` 支持相对路径、根路径,以及显式 `modid:path` 纹理 id - `x`、`y`、`width` / `w`、`height` / `h` 用于描述原图上的裁剪区域,且必须全部提供 - `scaleX` 与 `scaleY` 用于缩放裁剪结果,并支持横向 / 纵向独立拉伸 +- `displayWidth` 与 `displayHeight` 以像素指定最终尺寸;只提供一个值时保持裁剪区域比例,两个值都提供时可拉伸 +- `displayWidth` / `displayHeight` 不能与 `scaleX` / `scaleY` 同时使用 - `wrap="inline"` 会让图片真正作为行内内容插入文本流;此时 `align` 会被忽略 - 旧内容如果把 `width` / `height` 当成最终显示尺寸,需要手动迁移 diff --git a/wiki/Tags-Reference.md b/wiki/Tags-Reference.md index e46be7b8..58090434 100644 --- a/wiki/Tags-Reference.md +++ b/wiki/Tags-Reference.md @@ -55,7 +55,7 @@ Inline markdown also supports action links for sound playback: | `` | width-constrained footnote container used by runtime markdown footnotes | `width` | | `` | compact grid of item icons | children must be `` or `` | | `` | non-interactive 3D single-block preview | `id` or `ore`, `scale`, `float`, `perspective`, `nbt` | -| `` | cropped image block with float or true inline placement | `src`, `x`, `y`, `width` / `w`, `height` / `h`, `scaleX`, `scaleY`, `wrap`, `align`, `title` | +| `` | cropped image block with float or true inline placement | `src`, `x`, `y`, `width` / `w`, `height` / `h`, `scaleX`, `scaleY`, `displayWidth`, `displayHeight`, `wrap`, `align`, `title` | | `` | navigation child listing | `id`, `alphabetical` | | `` | list pages from a category | `name`, `rows` | | `` | list built-in MediaWiki special pages | `name`, `rows` | @@ -509,6 +509,8 @@ Quick rules: - `src` supports relative paths, rooted paths, and explicit `modid:path` texture ids - `x`, `y`, `width` / `w`, and `height` / `h` define the crop rectangle on the original image and must all be present - `scaleX` and `scaleY` resize the cropped result and support independent horizontal / vertical stretching +- `displayWidth` and `displayHeight` set final dimensions in pixels; one value preserves the crop aspect ratio, while two values allow stretching +- `displayWidth` / `displayHeight` cannot be combined with `scaleX` / `scaleY` - `wrap="inline"` places the image truly inline inside text flow; in that mode `align` is ignored - old content that used `width` / `height` as final display size must be migrated manually diff --git a/wiki/resourcepack/assets/guidenh/guidenh/_en_us/images.md b/wiki/resourcepack/assets/guidenh/guidenh/_en_us/images.md index 62024f30..d6094fa3 100644 --- a/wiki/resourcepack/assets/guidenh/guidenh/_en_us/images.md +++ b/wiki/resourcepack/assets/guidenh/guidenh/_en_us/images.md @@ -27,9 +27,10 @@ Relative path (`test1.png` in the same directory): Inline image mixed with text: here ![inline](test1.png) is an inline image. -`` now crops first and scales second. `x`, `y`, `width` / `w`, and `height` / `h` select the -source rectangle on the original image. `scaleX` and `scaleY` then resize that cropped result, including -single-axis stretching. +`` crops first and then determines the display size. `x`, `y`, `width` / `w`, and `height` / `h` +select the source rectangle on the original image. `scaleX` and `scaleY` resize that cropped result by a multiplier. +`displayWidth` and `displayHeight` specify final pixel dimensions instead. One display dimension preserves the crop +aspect ratio; both allow stretching. Display dimensions cannot be combined with `scaleX` or `scaleY`. Cropped 64×64 region displayed at native size: @@ -37,7 +38,11 @@ Cropped 64×64 region displayed at native size: Cropped 64×64 region stretched to 200×80: - + + +Set only one final dimension to preserve the cropped image ratio: + + Cross-mod texture with true inline placement: diff --git a/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/images.md b/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/images.md index 4c88ef31..6c62e154 100644 --- a/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/images.md +++ b/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/images.md @@ -27,7 +27,7 @@ categories: 段落内嵌图:这是一张图 ![inline](test1.png) 嵌在文字里。 -`` 现在采用“先裁剪,再缩放”的语义。`x`、`y`、`width` / `w`、`height` / `h` 用于从原图中选取子区域,`scaleX` 和 `scaleY` 再决定这个裁剪结果的最终显示尺寸,并支持单轴拉伸。 +`` 采用“先裁剪,再确定显示尺寸”的语义。`x`、`y`、`width` / `w`、`height` / `h` 用于从原图中选取子区域。`scaleX` 和 `scaleY` 按倍率缩放裁剪结果;`displayWidth` 和 `displayHeight` 则直接以像素指定最终尺寸。只提供一个显示尺寸时会保持裁剪区域比例,两个都提供时可拉伸。显示尺寸不能与 `scaleX` 或 `scaleY` 同时使用。 裁剪 64×64 区域并按原始大小显示: @@ -35,7 +35,11 @@ categories: 将裁剪出的 64×64 区域拉伸到 200×80: - + + +只设置一个最终尺寸时,会保持裁剪区域比例: + + 跨模组纹理并真正行内放置: