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

卡面尺寸与三层边界

三种边界先统一口径

以原创卡面“森林邮局”为例,裁切后的成品尺寸为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的容器断点特性。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐