趣味でJavaScriptを使ってAAエディタを作っているのだが、そこで使う指定dotの空白を生成する関数を作った。絶対間違ってそうなのでAdvent Calenderとして晒し上げる。
基準は本来16pxのMS Pゴシックだが、WebkitだとDirectWriteの影響でAAが表示されないため計測にはSaitamaarを使う。
AAで使う空白
Shift-JISの空白
Shift-JISでは、空白は以下のものしかない。
名前 | 記号 | dot数 |
---|---|---|
全角スペース | [ ] |
11dot |
半角スペース | [ ] |
5dot |
さらに2ちゃんねる系掲示板では以下の制約がある
- 半角スペースの行頭禁止(自動的に削除される)
- 半角スペースの連続禁止(自動的に1個にされる)
当然この条件では自由な空白が作れない。そこでピリオド.
(3px)なども使いある程度自由に作っていたが、現在はやる夫系掲示板の多くでは容量と引き換えにUnicode空白の使用が可能である。
Unicode空白
Unicodeでは、以下の空白が使用可能である。
名前 | 記号 | ドット数 |
---|---|---|
EM SPACE | [ ] |
16dot |
FIGURE SPACE | [ ] |
10dot |
EN SPACE | [ ] |
8dot |
THREE-PER-EM SPACE | [ ] |
5dot |
FOUR-PER-EM SPACE | [ ] |
4dot |
SIX-PER-EM SPACE | [ ] |
3dot |
THIN SPACE | [ ] |
2dot |
HAIR SPACE | [ ] |
1dot |
ただし、Unicodeは通常は掲示板に送信できないので、& #x2006;
という形の文字参照で送る。すると、1文字につき12byteほど使うことになる。となると、1レスのbyte数に引っかかったりするので、出来るだけUnicode空白は少なくしたい。
条件
- 半角スペースは5dot、全角スペースは11dot。
- 半角スペースの連続禁止。
- 半角スペースが頭に来るの禁止。
- Unicode空白使用可能。ただし可能な限り少なくすること。
- できれば速度。レイヤー処理とか書くと何回も呼びだすので。
実装
// 大分省略したもの。全体はhttps://github.com/Duct-and-rice/misaki-editor/blob/master/src/lib/space.js。
export function generateSpaceFromAH (a, h) {
if (a === 0 && h === 1) {
return HALF_SPACE
}
if (a < h || a < 0 || h < 0) {
throw new Error('a:' + a + ',h:' + h)
}
return DOTS_TO_SPACE[11].str.repeat(a - h) +
(DOTS_TO_SPACE[11].str + HALF_SPACE).repeat(h) // a - h個の全角空白とh個の全角空白半角空白の連続文字列を並べる。
}
export function oneDotReduce (ah) {
ah = (() => {
if (ah.a - 1 >= ah.h + 2) {
return {a: ah.a - 1, h: ah.h + 2, adj: ah.adj}
} else if (ah.adj >= 1) {
return {a: ah.a, h: ah.h, adj: ah.adj - 1}
} else if (ah.h >= 1) {
return {a: ah.a, h: ah.h - 1, adj: ah.adj + 4}
} else if (ah.a >= 1 && ah.adj < 6) {
return {a: ah.a - 1, h: ah.h + 1, adj: ah.adj + 5}
}
})()
if (ah.h >= 1 && ah.adj <= 6) {
ah.h--
ah.adj += 5
}
return ah
}
export default function widthSpace (sp) {
if (typeof sp !== 'number') {
throw new TypeError()
}
const mod = sp % 11
let a = 0 // 全角スペースの数
let h = 0 // 半角スペースの数
let adj = 0 // 調整用dot
switch (mod) {
case 0:
a = Math.floor(sp / 11)
break
case 1:
case 2:
case 3:
case 4:
a = Math.floor(sp / 11)
h = 1
break
case 5:
if (sp === 5) {
return DOTS_TO_SPACE[5].str
}
a = Math.floor(sp / 11)
h = 1
break
case 6:
case 7:
case 8:
case 9:
case 10:
a = Math.ceil(sp / 11)
break
}
if (mod !== 0 && mod !== 5) {
while (a * 11 + h * 5 + adj !== sp) {
const ah = oneDotReduce({a, h, adj})
a = ah.a
h = ah.h
adj = ah.adj
}
const ah = adjToAH({a, h, adj})
a = ah.a
h = ah.h
adj = ah.adj
}
return generateSpaceFromAH(a, h) + adjustWithUnicode(adj)
}
まず指定dotから少し多めに全角スペースと半角スペース1or0だけで軽く見積る。
たとえば100dotの場合、全9、半1で11 * 9 + 5 * 1 = 104
となる
次に、それを1dotずつ減らしていく。減らすには全角スペースを1つ減らし半角スペースを2つ増やす。すると-11 * 1 + 5 * 2 = -11 + 10 = -1
で一つ減る。この処理は上のoneDotReduce
関数で行なっている。
当然、全2半1など対応できないケースが出てくるので、その時に前述のUnicode空白を用いて調整した。どこか間違っているかも……
あとはそれらを半角スペースの制約に引っかからないように文字列化する。全角スペースをA、半角スペースをHとすると、AHAHAHAH
というように半角スペースを配置すれば制約に引っかからないので、AAAAHAHAHAH
というようなスペースを目指す関数がgenerateSpaceAH
である。
あとは最後に調整用Unicode空白を付けて終わり!閉廷!
やばそうなところ
-
oneDotReduce
がちゃんと動くか - 速度(多分チューニングしてもJSの壁にぶちあたるので出来ればWASM化したい)
- この記事を書いていて気づいたのだが5dotの場合行頭がそうでないかで場合分けが必要では?
コード全体
参考文献
- 『Orinrin Editor』紹介 第02回 『Orinrin Editor』で「ユニコード空白」を使ってみよう - やる夫まとめもZ - http://matomemo3.blog.fc2.com/blog-entry-5263.html