HarmonyOS桌游卡版连载01:出血线与安全框坐标换算【共创稿事节】
给一套原创桌游制作卡面时,最容易在屏幕上被忽略的不是插画,而是印刷尺寸与界面尺寸属于两种坐标。编辑器里看着恰好居中的标题,切成成品后可能贴近边缘;靠画布边缘排列的装饰叶片,裁切偏移后反而露出白边。把尺寸判断放在用户拖动素材之后才做,往往意味着反复调整同一张卡。本篇先建立一套稳定的几何模型,让版面数据在手机、平板及未来的导出流程中使用同一组毫米坐标。

三种边界先统一口径
以原创卡面“森林邮局”为例,裁切后的成品尺寸为63×88毫米。印前画面在四周各留3毫米出血,制作画布变成69×94毫米;文字和重要标识还需要向裁切线以内退5毫米,安全内容区域则为53×78毫米。这三组数值不是三种卡片规格,而是针对同一张卡的三个边界。画布包含被裁走的部分,成品框描述实际外形,安全框约束不可丢失的重要信息。
采用左上角为原点的毫米坐标,画布区间是x从0到69、y从0到94。成品矩形左上角落在(3,3),宽63、高88;安全矩形的左上角落在(8,8),宽53、高78。计算过程要保留小数,不能把“每边出血3毫米”误写成总宽增加3毫米,也不能把“安全边距5毫米”从画布边缘直接扣除。做成有意区分颜色的辅助线比多写一行提示更有效:画布蓝线、裁切红线、安全绿线,展示与存储均遵循同一坐标原点。
项目将尺寸模型放在entry/src/main/ets/model/CardGeometry.ets,投影换算放在service/GeometryProjector.ets,预览与参数调整放在pages/CardFramePage.ets。以后添加文本碰撞、图层和多设备界面时仍引用这两个模型,而不是各页各算一次框线位置。数据使用整数毫米便于阅读;实际涉及半毫米或更细的尺寸,可统一改用整数微米保存,界面层再进行单位换算,避免浮点累计误差逐次传递。
export interface RectMm {
x: number;
y: number;
width: number;
height: number;
}
export interface CardSpec {
trimWidthMm: number;
trimHeightMm: number;
bleedMm: number;
safeInsetMm: number;
}
export interface CardGuides {
canvas: RectMm;
trim: RectMm;
safe: RectMm;
}
export function buildGuides(spec: CardSpec): CardGuides {
const { trimWidthMm: w, trimHeightMm: h,
bleedMm: b, safeInsetMm: s } = spec;
if (![w, h, b, s].every((v: number) => Number.isFinite(v))) {
throw new Error('尺寸必须为有限数值');
}
if (w <= 0 || h <= 0 || b < 0 || s < 0 || 2 * s >= w || 2 * s >= h) {
throw new Error('安全边距或出血尺寸不合理');
}
return {
canvas: { x: 0, y: 0, width: w + 2 * b, height: h + 2 * b },
trim: { x: b, y: b, width: w, height: h },
safe: { x: b + s, y: b + s, width: w - 2 * s, height: h - 2 * s }
};
}
这个函数没有依赖界面组件,因此可以在切换设备或重新进入页面时重复使用。校验不能只是判断大于零,例如安全边距达到成品宽度的一半,虽然数值合法,却会得到零宽度或负宽度的安全框。对于用户输入的空字符串,应由页面解析为“未填写”状态,而不是转成零再让几何层猜测原因。错误文本交给页面呈现,几何模型保留确定的失败条件,后续模块也能复用。
毫米模型不跟着预览缩放走
屏幕上的卡片预览通常被限制在一块较窄的容器内。把一毫米固定对应若干像素,会使平板上预览显得太小,也可能让手机出现水平滚动。因此只把毫米当作版面数据,另设一个从毫米到逻辑显示单位的比例系数。容器宽度扣除两侧预览留白后,按照画布实际宽高同比例缩放;纵向同样算一遍,取其中较小的值。变更容器尺寸只更新比例,不回写模型里的标题坐标与卡面尺寸。
这里的显示单位与打印设备的像素密度无关。导出时还需要单独约定分辨率与颜色处理,不能把当前屏幕截图的尺寸视为印前成品。对于手机缩放手势,可在容器自适应比例的基础上再叠加用户放大系数;这属于视图状态,不能混入出血宽度或安全边距。只要这种边界清晰,横竖屏切换后仍然能还原用户的编辑点位。
import { CardGuides, RectMm } from '../model/CardGeometry';
export interface RectView {
left: number;
top: number;
width: number;
height: number;
}
export function chooseScale(g: CardGuides, width: number, height: number): number {
const usableWidth = Math.max(1, width - 24);
const usableHeight = Math.max(1, height - 24);
return Math.min(usableWidth / g.canvas.width,
usableHeight / g.canvas.height);
}
export function toView(rect: RectMm, scale: number,
originX: number, originY: number): RectView {
if (scale <= 0 || !Number.isFinite(scale)) {
throw new Error('预览比例不可用');
}
return { left: originX + rect.x * scale, top: originY + rect.y * scale,
width: rect.width * scale, height: rect.height * scale };
}

例如预览容器有宽340、高480个逻辑显示单位,扣掉留白后取画布69×94的适配比例;成品框再通过该比例投影。先有画布的缩放与偏移,再定位内部的裁切框和安全框,不能让三条线各自独立居中,否则宽度相等时看不出问题,而改变出血参数后就会错位。为了让用户判断更直观,右侧参数区同时展示“69×94、63×88、53×78”的文字,预览里的辅助线不承担全部解释工作。
页面状态与画布刷新
UI页面负责保存用户输入与当前预览容器尺寸,并使用@State表示能触发刷新显示的数据。Canvas适合绘制这种自定义矩形辅助线,CanvasRenderingContext2D的创建、Canvas组件及onReady绘制时机均属于ArkUI公开能力。为了避免在页面未完成布局时使用零尺寸,画布准备好以后再绘制首帧;参数发生变化则重新调用绘制方法。每次先清空已有画面,避免调整几次后旧线条残留。绘制操作不会修改毫米模型,也不依赖文件系统权限。
import { buildGuides, CardSpec, CardGuides } from '../model/CardGeometry';
@Entry
@Component
struct CardFramePage {
@State private bleed: number = 3;
@State private inset: number = 5;
@State private warning: string = '';
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
private drawFrame(): void {
this.context.clearRect(0, 0, 310, 440);
try {
const spec: CardSpec = { trimWidthMm: 63, trimHeightMm: 88,
bleedMm: this.bleed, safeInsetMm: this.inset };
const guides: CardGuides = buildGuides(spec);
const scale: number = Math.min(280 / guides.canvas.width, 400 / guides.canvas.height);
const offsetX: number = 15;
const offsetY: number = 15;
this.context.strokeStyle = '#2677E8';
this.context.strokeRect(offsetX, offsetY,
guides.canvas.width * scale, guides.canvas.height * scale);
this.context.strokeStyle = '#D94C52';
this.context.strokeRect(offsetX + guides.trim.x * scale,
offsetY + guides.trim.y * scale, guides.trim.width * scale,
guides.trim.height * scale);
this.context.strokeStyle = '#218F70';
this.context.strokeRect(offsetX + guides.safe.x * scale,
offsetY + guides.safe.y * scale, guides.safe.width * scale,
guides.safe.height * scale);
this.warning = '';
} catch (e) {
this.warning = '请检查卡片尺寸';
}
}
build() {
Column({ space: 12 }) {
Text('森林邮局 · 卡面边界').fontSize(22)
Canvas(this.context).width(310).height(440)
.onReady(() => this.drawFrame())
Text('蓝色画布 / 红色裁切 / 绿色安全区')
Button('安全边距加一毫米').onClick(() => {
this.inset += 1;
this.drawFrame();
})
if (this.warning.length > 0) { Text(this.warning).fontColor(Color.Red) }
}.padding(16)
}
}
页面切换与重新进入时要关注画布生命周期。只要组件被销毁并创建新的画布实例,onReady就会再次触发,页面字段要能根据当前输入恢复一致的边界;不能把一次绘制结果当作持久状态。预览容器从手机宽度变成平板分栏的窄列时,应重新计算可用区域,而不是复制上一设备的像素坐标。后续需要接入API 26新增的ContainerReader时,容器宽度可直接驱动上述chooseScale,无需改动实体卡面尺寸。https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/arkts-layout-development-container-reader对版本和布局约束有明确说明。
错误定位与边界约束
将出血边距输入为负数,应由几何层给出错误,UI保持最近一次有效数据,不能画出看似正常的负宽矩形。若预览区域高度小于留白需求,比例会退化到极小值,页面更合适的做法是显示“当前区域过窄,请展开编辑区”。当用户连续快速修改多个输入项时,建议先归并为完整的CardSpec再生成一次导视框,而不是在每次输入事件内分别更新三条边界。这样避免了画面短暂出现新裁切线与旧安全线不一致的状态。
对于长标题与说明文案,编辑区还要给出与实际印刷尺寸无关的字号预览级别。字号改变的是文字的占位估算和可读性,不会改变裁切框本身。可以给卡面右侧增加“显示辅助线”开关,开关关闭时只是隐藏线条,不是关闭规则检查;即使用户在展示模式下看不到绿色安全框,保存时仍应检查关键文字位置。后续如果增加不同语言的卡面文本,要重新估算文本边界并执行碰撞检查,但不应自动改变出血与安全边距。这样能避免版式规则因语言长度差异被偷偷改写。
排查路径从数据开始:核对成品、出血、安全边距四项;随后打印几何函数输出的三个矩形;最后检查画布缩放、偏移以及清空时机。对69×94毫米画布应能独立推导出(3,3)处的63×88裁切框与(8,8)处的53×78安全框,任何一级结果不符都可在绘图前发现。界面可用颜色辅助校对,但不能依赖颜色作为唯一信息,线型及文字说明也要保留。对于缩放后的细线,还要检查高分辨率屏幕上边线是否被抗锯齿模糊为难以识别的浅色。

这套模型的价值不在于把几条矩形线画得更漂亮,而在于让所有重要尺寸都只有一个解释。以毫米模型为源,界面只负责投影,后续把标题、插画、能力文字加入画布时,就能直接复用安全矩形做元素边界核验。下一篇将进一步处理图文矩形相交、越界和修复后的重检,而不用再改动当前的尺寸口径。
与本篇相关的官方说明包括ArkTS语言概述、Canvas组件接口及HarmonyOS 7.0 API 26升级适配说明。Canvas基础接口早于API 26提供,不能将其称为HarmonyOS 7新加的专属能力;本系列在下一阶段的多端适配部分才引入API 26的容器断点特性。
更多推荐

所有评论(0)