Skip to content

Instantly share code, notes, and snippets.

@suzusime
Last active January 12, 2026 08:34
Show Gist options
  • Select an option

  • Save suzusime/2db0d67fa7082e2b4368ee2cd3559a88 to your computer and use it in GitHub Desktop.

Select an option

Save suzusime/2db0d67fa7082e2b4368ee2cd3559a88 to your computer and use it in GitHub Desktop.
hanzura-calc-deno
// Licensed: CC0
// https://creativecommons.org/public-domain/cc0/
// 全体で利用する定数
const globalConst = {
// 1 ポイントを mm で表した値
// いわゆるDTPポイント(72分の1インチ)を利用する
point: 25.4 / 72,
// 1 級を mm で表した値
Q: 0.25,
// 用紙の定義(mm)
paperDef: {
A3: { width: 297, height: 420 },
A4: { width: 210, height: 297 },
A5: { width: 148, height: 210 },
A6: { width: 105, height: 148 },
B4: { width: 257, height: 364 },
B5: { width: 182, height: 257 },
B6: { width: 128, height: 182 },
}
} as const;
// 用紙種別のキーの型定義
type PaperType = keyof typeof globalConst.paperDef;
// 設定値の型定義
interface Config {
paperType: PaperType;
charSize: number;
lineSpacing: number;
targetMarginVertical: number;
targetMarginHorizontal: number;
gridNumRows: number;
gridNumCols: number;
fontAscentRatio: number;
}
// メイン処理
export function suggestPageLayout(config: Config) {
const paper = globalConst.paperDef[config.paperType];
const linesPerGrid = calculateLinesPerGrid(
paper.height,
config.targetMarginVertical,
config.charSize,
config.lineSpacing,
config.gridNumRows
);
const charsPerGrid = calculateCharsPerGrid(
paper.width,
config.targetMarginHorizontal,
config.charSize,
config.lineSpacing,
config.gridNumCols
);
console.log('--- 前提 ---');
console.log(`用紙種別: ${config.paperType}`);
console.log(`用紙高さ: ${paper.height} mm`);
console.log(`用紙幅: ${paper.width} mm`);
console.log(`文字サイズ: ${config.charSize.toFixed(2)} mm / ${(config.charSize / globalConst.Q).toFixed(2)} Q / ${(config.charSize / globalConst.point).toFixed(2)} pt`);
console.log(`行間: ${(config.lineSpacing * 100).toFixed(2)} %`);
console.log(`縦のグリッド数: ${config.gridNumRows} 段`);
console.log(`横のグリッド数: ${config.gridNumCols} 列`);
console.log(`上下それぞれの余白目標値: ${config.targetMarginVertical.toFixed(2)} mm`);
console.log(`左右それぞれの余白目標値: ${config.targetMarginHorizontal.toFixed(2)} mm`);
console.log('');
// 行と列それぞれに対して、+0 / +1 の4パターンを出力する
for(let i = 0; i < 4; i++) {
const lineAddition = i % 2;
const charAddition = Math.floor(i / 2);
printPageLayout(config, i+1, linesPerGrid + lineAddition, charsPerGrid + charAddition);
}
}
// 1グリッドあたりの行数を計算する関数
// 余白が targetMarginVertical 以上になるような最大の行数を求める
export function calculateLinesPerGrid(paperHeight: number, targetMarginVertical: number, charSize: number, lineSpacing: number, gridNumRows: number){
// コンテンツエリアの高さ
const contentHeight = paperHeight - 2 * targetMarginVertical;
// 行数を増やしていき、必要なスペースがcontentHeightを超えたら終了する
let result = 0;
for (let i = 0; ; i++) {
// グリッドを綺麗に配置するためには、 lines ≡ -1 (mod gridNumRows) である必要がある
const lines = i * gridNumRows + (gridNumRows - 1);
// 必要な高さを計算する
const requiredHeight = lines * charSize + (lines - 1) * lineSpacing * charSize;
// 必要な高さがコンテンツエリアに収まる場合、結果を更新する
if (requiredHeight <= contentHeight) {
result = i;
} else {
break;
}
}
return result;
}
// 1グリッドあたりの文字数(横幅)を計算する関数
export function calculateCharsPerGrid(paperWidth: number, targetMarginHorizontal: number, charSize: number, lineSpacing: number, gridNumCols: number) {
// グリッドの横の間隔
// Affinity の仕様上、縦の間隔と同じ値にする必要がある
const gutterWidth = charSize + 2 * charSize * lineSpacing;
// コンテンツエリアの横幅
const contentWidth = paperWidth - 2 * targetMarginHorizontal;
// 文字数を増やしていき、必要なスペースがcontentWidthを超えたら終了する
let result = 0;
for (let i=0; ; i++) {
const chars = i * gridNumCols;
// 必要な幅を計算する
const requiredWidth = chars * charSize + (gridNumCols - 1) * gutterWidth;
// 必要な幅がコンテンツエリアに収まる場合、結果を更新する
if (requiredWidth <= contentWidth) {
result = i;
} else {
break;
}
}
return result;
}
export function printPageLayout(config: Config, layoutId: number, linesPerGrid: number, charsPerGrid: number) {
console.log(`--- レイアウト案${layoutId} ---`);
const paper = globalConst.paperDef[config.paperType];
const linesPerPage = linesPerGrid * config.gridNumRows + (config.gridNumRows - 1);
const contentHeight = linesPerPage * config.charSize + (linesPerPage - 1) * config.lineSpacing * config.charSize;
const marginVertical = (paper.height - contentHeight) / 2;
const gutterWidth = config.charSize + 2 * config.charSize * config.lineSpacing;
const contentWidth = charsPerGrid * config.charSize * config.gridNumCols + (config.gridNumCols - 1) * gutterWidth;
const marginHorizontal = (paper.width - contentWidth) / 2;
const gridHeight = linesPerGrid * config.charSize + (linesPerGrid - 1) * config.lineSpacing * config.charSize;
const gridWidth = charsPerGrid * config.charSize;
const contentHeightPerPaperHeight = contentHeight / paper.height
const contentWidthPerPageWidth = contentWidth / paper.width;
const baselineGridStartY = marginVertical + config.charSize * config.fontAscentRatio;
const baselineGridHeight = config.charSize + config.lineSpacing * config.charSize;
console.log(`1グリッドあたりの行数: ${linesPerGrid} 行`);
console.log(`1ページあたりの行数: ${linesPerPage} 行`);
console.log(`1グリッド内の行あたりの字数: ${charsPerGrid} 字`);
console.log(`1ページあたりの総字数: ${linesPerPage * charsPerGrid * config.gridNumCols} 字`);
console.log('');
console.log(`上下それぞれの余白: ${marginVertical.toFixed(2)} mm`);
console.log(`左右それぞれの余白: ${marginHorizontal.toFixed(2)} mm`);
console.log('版面比率(縦): ' + (contentHeightPerPaperHeight * 100).toFixed(2) + ' %');
console.log('版面比率(横): ' + (contentWidthPerPageWidth * 100).toFixed(2) + ' %');
console.log('');
console.log(`グリッドの間隔: ${gutterWidth.toFixed(2)} mm / ${(gutterWidth / globalConst.Q).toFixed(2)} H / ${(gutterWidth / globalConst.point).toFixed(2)} pt`);
console.log(`1グリッドの高さ: ${gridHeight.toFixed(2)} mm`);
console.log(`1グリッドの幅: ${gridWidth.toFixed(2)} mm`);
console.log('');
console.log(`ベースライングリッドの開始位置: ${baselineGridStartY.toFixed(2)} mm / ${(baselineGridStartY / globalConst.Q).toFixed(2)} H / ${(baselineGridStartY / globalConst.point).toFixed(2)} pt`);
console.log(`ベースライングリッドの間隔(高さ): ${baselineGridHeight.toFixed(2)} mm / ${(baselineGridHeight / globalConst.Q).toFixed(2)} H / ${(baselineGridHeight / globalConst.point).toFixed(2)} pt`);
console.log('');
}
// Learn more at https://docs.deno.com/runtime/manual/examples/module_metadata#concepts
if (import.meta.main) {
// 設定値
const config: Config = {
// 用紙種別
paperType: 'A4',
// 文字サイズ
charSize: 12.0 * globalConst.Q,
// 行間(文字サイズに対する割合)
lineSpacing: 0.7,
// 上下それぞれの余白(mm)の目標値
targetMarginVertical: 10.0,
// 左右それぞれの余白(mm)の目標値
targetMarginHorizontal: 10.0,
// 縦のグリッド数(段数)
gridNumRows: 4,
// 横のグリッド数(列数)
gridNumCols: 2,
// フォントのグリフ高さのうちベースラインより上の割合
// (フォントによって異なるが、一般的には0.8~0.9程度)
fontAscentRatio: 0.8,
};
suggestPageLayout(config);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment