简单来说
一本日本小学练习册:一页是竖式、填空、选择和判断题,另一页是从上往下排的汉字和读音。每道题下面的答案都倒着印,孩子把本子转过来就能对答案。
成品一览
一套三年级练习册中的两页,B5 开本。算术页横排:一条浅蓝色带,上面有科目标签、标题和吉祥物狸猫,下面是写名字的横线和分数框。接着是ひっ算(竖式),结果的每一位都有一个方框;六道算式后面各有一个方框;ア イ ウ三个选项排成一行;○×判断题在行末留一个括号;两道应用题留着写算式的横线。汉字页按国語练习册的印法排,从上到下,从右到左:读音旁边有写汉字的方框,划了线的词在括号里写读音,还有要圈出的选项和要挑的词义。每道题下面,答案用灰色小字倒着印。孩子看得到答案在哪里,一眼却读不出来,要把本子转过来才能对答案。
这道食谱解答
- 怎样把答案倒印在题目下方?
- 怎样用与横排页相同的设计排一页竖排的日语练习?
- 怎样按各国学校的写法排长除法竖式?
简短回答
// `placement: 'inverted'` sets each exercise's key right after it in a box turned 180°: the
// pupil sees where the answers are without reading them, and turns the book to check. The
// key's words come from the exercise (the options' katakana, ○ and ×, the operations), so
// the Markdown holds nothing twice.
const drill = {
id: 'drill',
number: { shape: 'square', fontFamily: TEXT, fontWeight: 600, background: col('blue') },
instruction: { fontFamily: ROUND, bold: false },
options: { layout: 'row' }, // ア イ ウ side by side (the default labels in Japanese)
trueFalse: { look: 'bracket' }, // ( ) for ○ or ×
blank: { look: 'box', width: 'answer', color: col('rule') }, // □ to write in
area: { kind: 'lines', color: col('rule') },
grid: { rule: col('rule') },
solutions: { placement: 'inverted', box: 'kotae' },
marginTop: em(1.1), marginBottom: em(0.4),
};
const kotae = { id: 'kotae', title: '', backgroundEnabled: false, border: { enabled: false },
padding: { top: pt(0), right: pt(0), bottom: pt(0), left: pt(0) },
body: { fontFamily: ROUND, fontSize: pt(8), lineHeight: pt(12), color: col('muted'),
firstLineIndent: pt(0) },
marginTop: mm(1), marginBottom: mm(2), keepTogether: true };
用料
- 功能
- 答案练习样式竖排练习竖式计算填空与词库选择题与判断题开放题与书写区假名、汉字与罗马字中日韩字体日文旁线(傍線)右翻书行内标签三栏及以上设计过的章首页标题属性页面设计中的图片锚定设计元素语义调色板导出PDF
- 类型
- Klee One, Zen Maru Gothic(SIL OFL 1.1)
- 素材
kitsune-600.jpgtanuki-600.jpg- A young tanuki in a yellow school hat holding a big pencil. (Generated With Diffusion Models, 原创)
- A fox cub in a red scarf reading a book. (Generated With Diffusion Models, 原创)
做法
#1 · 把每道题的答案倒过来
代码就在上面的简短回答里。placement: 'inverted' 把答案放在每道题后面一个旋转 180° 的框里,这个框又放在题目自己的框里,所以分页永远不会把两者分开。答案从题目里来:算式连同结果、正确选项的片假名、○ 和 ×、填空的词。答案的框样式 kotae 只把字设成小号灰色。
#2 · 一条色带,横排页和竖排页都用
// A heading design on a vertical page is laid along its column: x runs down the page and y
// to the left, so the same band stands at the right edge, from head to foot.
const BAND = 30; // mm: the band's depth (its height across, its width down)
const header = { enabled: true, minHeight: mm(BAND + 3), slot: { elements: [
{ kind: 'box', id: 'band', reserve: false, style: { backgroundColor: col('tint'),
borderRadius: mm(4) }, placement: { anchor: { to: 'container', edge: 'top-left' },
size: { width: 'fill', height: mm(BAND) } } },
{ kind: 'image', id: 'mascot', resourceId: '{attr.pic}', decorative: true, reserve: false,
placement: { anchor: { to: 'container', edge: 'top-right' }, offset: { x: mm(-4), y: mm(1) },
size: { width: mm(28), height: mm(28) } } },
{ kind: 'text', id: 'subject', content: '{attr.kicker}', fontFamily: ROUND, fontSize: pt(10),
fontWeight: 700, color: col('ink'), box: { backgroundColor: col('sun'), borderRadius: mm(3),
padding: { top: mm(0.6), bottom: mm(0.6), left: mm(2.4), right: mm(2.4) } },
placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { x: mm(5), y: mm(5) } } },
{ kind: 'text', id: 'title', content: '{titleText}', fontFamily: ROUND, fontSize: pt(20),
fontWeight: 700, color: col('ink'),
placement: { anchor: { to: '#subject', edge: 'below' }, offset: { x: mm(0), y: mm(2.5) } } },
] } };
在竖排页上,标题的设计沿着所在的栏铺开:x 沿页面往下,y 向左。以 top-left 锚定、占满整个宽度的色带,在算术页上横在顶部,在汉字页上沿右边缘竖下来;以 top-right 锚定的吉祥物在两页上都落在色带的末端。设计里没有一处写明方向。
#3 · 一份文字,两本练习册
// Two drill books of one series: arithmetic across, bound on the left; kanji in columns,
// bound on the right (vertical text binds there by itself).
const [sansu, kokugo] = markdown.split(/(?=^# 春のことば)/m);
const build = (text) => withLoadedFonts(() => buildDocument({ markdown: text, resources },
config()), { ...kitFonts(FONTS), text: markdown });
const across = await build(sansu);
// 国語 is set in columns, top to bottom, right to left: the same design, turned.
writingMode = 'vertical-rl';
const docs = [across, await build(kokugo)];
配置是一个函数,所以每次排版都得到自己的对象,两次之间只改 writingMode。竖排时,国語练习册自己就会在右边装订;showBook 把两本练习册分别放在书脊两侧。正文用教科书体 Klee One,标签用 Zen Maru Gothic;两种字体都加载了竖排标点,给汉字页的括号用。
完整食谱
// ═══ Postext Cookbook · Nº 172 · A Japanese drill with the answers upside down ═══
// https://postext.dev/en/cookbook/japanese-drill-upside-down-answers
// Code: MIT · Text: original (CC BY 4.0) · Pictures: generated with diffusion models
// Fonts: Klee One, Zen Maru Gothic (SIL OFL 1.1) · Needs postext ≥ 1.59.0
import {
buildDocument, withLoadedFonts, renderPageToCanvas, registerResourceImage,
loadVerticalAlternates,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';
const LANG = 'ja'; // @lang: the language of the frame; the drill is Japanese in all
const RECIPE = 'japanese-drill-upside-down-answers';
// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: a drill book's blue, a warm yellow and a pale sky for the band
const palette = {
ink: '#23272c', // text
blue: '#2a6cb0', // the accent: exercise numbers, the title band's rule, the score box
sun: '#f2c14e', // the subject badge
tint: '#eaf2fb', // the title band
rule: '#a8bfd8', // writing lines and boxes
muted: '#69717b', // folios, the name line, the upside-down answers
paper: '#ffffff',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
{ id: 'main-color', name: 'accent (defaults)', value: { hex: palette.blue, model: 'hex' } },
];
// #endregion
// 教科書体 for the text a child copies from, a round gothic for the labels.
const [TEXT, ROUND] = ['Klee One', 'Zen Maru Gothic'];
// #region answer: the answers printed upside down under each exercise
// `placement: 'inverted'` sets each exercise's key right after it in a box turned 180°: the
// pupil sees where the answers are without reading them, and turns the book to check. The
// key's words come from the exercise (the options' katakana, ○ and ×, the operations), so
// the Markdown holds nothing twice.
const drill = {
id: 'drill',
number: { shape: 'square', fontFamily: TEXT, fontWeight: 600, background: col('blue') },
instruction: { fontFamily: ROUND, bold: false },
options: { layout: 'row' }, // ア イ ウ side by side (the default labels in Japanese)
trueFalse: { look: 'bracket' }, // ( ) for ○ or ×
blank: { look: 'box', width: 'answer', color: col('rule') }, // □ to write in
area: { kind: 'lines', color: col('rule') },
grid: { rule: col('rule') },
solutions: { placement: 'inverted', box: 'kotae' },
组合好的script.js可以直接运行:把它粘贴到任何页面的模块脚本中,或在CodePen上打开这道食谱。 GitHub上的食谱文件夹 ↗ (在新标签页中打开)
变化
常见问题
易错点
中文字体通过cjk块分片加载
Fontsource把一个中文、日文或韩文字体家族按每个字重约一百个文件提供,每个文件覆盖一段字符范围。loadFonts只获取latin文件,所以屏幕上的汉字来自系统字体,测量不准;fontsourceProvider交给PDF的也是这个latin文件,汉字印出来是空框。列出kit中的cjk块,在loadFonts之后调用loadCjkFonts(FONTS, markdown)(书中用到几种CJK字体时,每种字体调用一次,并传入它所排的文字),并给renderToPdf传fontProvider: cjkPdfProvider:两者都会取用包含文中字符的那些文件。 中日韩字体 →
易错点
日文文本标记为'ja',不要用zh-Hans或LANG
食谱的版本是en和es,但日文示例在两个版本里都是日文:`locale: LANG`会把它标记为英文或西班牙文,中文标记则会按中文规则排它(开明式标点、小假名可以出现在行首、图代替図、PDF中用中文字形)。postext 1.30起,这个标记也是文字成形所用的语言(测量、绘制和嵌入都按它进行),所以'ja'以外的标记给出的不是日文字形(中文标记给出中文字形),断行的位置也不同。写'ja':它选用日本地区(JLReq断行和标点、芝麻着重号、振假名的分布、図和表标签),并关闭断词。lint会让带假名却标记为zh或ko的文本失败。 日文禁则处理 →
- 读音暂时还不能放在空格上方:
[[…]]里的注音(ruby)属于被隐藏的答案。漢字の書き取り这道题把每个读音单独放一行,方框跟在后面,用:::columns组每行排三个;在竖排页上,读音像振假名那样立在方框旁边。 - Klee One 只有 400 和 600 两种字重:
boldFontWeight: 600让所有粗体都落在实际存在的字重上,小题号和大题号也一样。
致谢
- 文本
- 原创文字, CC BY 4.0
- 图片
- A young tanuki in a yellow school hat holding a big pencil. · Generated With Diffusion Models · 原创
- A fox cub in a red scarf reading a book. · Generated With Diffusion Models · 原创
- 字体
- Klee One (SIL OFL 1.1) · Zen Maru Gothic (SIL OFL 1.1)


