danoniplus 50.4.0 → 50.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1932 @@
1
+ /**
2
+ * Dancing☆Onigiri (CW Edition)
3
+ * キーコンフィグ画面
4
+ * - ページ: keyConfig
5
+ *
6
+ * Source by tickle
7
+ * Created : 2026/09/13
8
+ * Revised :
9
+ *
10
+ * https://github.com/cwtickle/danoniplus
11
+ */
12
+
13
+ /*-----------------------------------------------------------*/
14
+ /* Scene : KEYCONFIG [orange] */
15
+ /*-----------------------------------------------------------*/
16
+
17
+ /**
18
+ * キーコンフィグ画面初期化
19
+ * @param {string} _kcType
20
+ * @param {boolean} _initFlg 初期表示フラグ
21
+ */
22
+ const keyConfigInit = (_kcType = g_kcType, _initFlg = false) => {
23
+
24
+ clearWindow();
25
+ const divRoot = document.getElementById(`divRoot`);
26
+ g_kcType = _kcType;
27
+ g_currentPage = `keyConfig`;
28
+ let selectedKc = `Default`;
29
+
30
+ // 譜面初期情報ロード許可フラグ
31
+ g_canLoadDifInfoFlg = false;
32
+
33
+ if (_initFlg) {
34
+ g_stateObj.keyLockFlg = false;
35
+ g_stateObj.kbPreviewFlg = false;
36
+ }
37
+
38
+ multiAppend(divRoot,
39
+
40
+ // キーコンフィグ画面タイトル
41
+ getTitleDivLabel(`lblTitle`,
42
+ `<div class="settings_Title">${g_lblNameObj.key}</div><div class="settings_Title2">${g_lblNameObj.config}</div>`
43
+ .replace(/[\t\n]/g, ``), 0, 15, g_cssObj.flex_centering),
44
+
45
+ createDescDiv(`kcDesc`, getKcDescMsg()),
46
+
47
+ createDescDiv(`kcShuffleDesc`,
48
+ g_headerObj.shuffleUse && g_settings.shuffles.filter(val => val.endsWith(`+`)).length > 0
49
+ ? g_lblNameObj.kcShuffleDesc : g_lblNameObj.kcNoShuffleDesc),
50
+ );
51
+
52
+ /**
53
+ * keyconSpriteのスクロール位置調整
54
+ * @param {number} _targetX
55
+ */
56
+ const adjustScrollPoint = _targetX => {
57
+ if (maxLeftX !== 0) {
58
+ keyconSprite.scrollLeft = Math.max(0, _targetX - g_sWidth / 2);
59
+ }
60
+ };
61
+
62
+ /**
63
+ * キーコンフィグ用の矢印色を取得
64
+ * @param {number} _j
65
+ * @param {number} _colorPos
66
+ * @returns {string}
67
+ */
68
+ const getKeyConfigColor = (_j, _colorPos) => {
69
+ let arrowColor = g_headerObj.setColor[_colorPos];
70
+
71
+ // 色変化データの利用条件設定(Default/Type0限定)
72
+ const storageObj = g_stateObj.extraKeyFlg ? g_localStorage : g_localKeyStorage;
73
+ const baseGroupNum = g_keycons.colorGroupNum === -1
74
+ ? (storageObj?.[`keyCtrlPtn${g_keyObj.currentKey}`] ?? storageObj?.keyCtrlPtn ?? 0)
75
+ : g_keycons.colorGroupNum;
76
+ const currentColorGr = g_keyObj[`color${keyCtrlPtn}_${g_keycons.colorGroupNum}`];
77
+ const baseColorGr = g_baseColorGrs?.[`color${keyCtrlPtn}_${baseGroupNum}`];
78
+ if (hasVal(initColors[_j]) && g_keycons.colorDefTypes.includes(g_colorType)
79
+ && currentColorGr?.[_j] === baseColorGr?.[_j]) {
80
+ arrowColor = initColors[_j];
81
+ }
82
+
83
+ // アシスト設定時はアシストの色を優先して適用
84
+ if (typeof g_keyObj[`assistPos${keyCtrlPtn}`] === C_TYP_OBJECT &&
85
+ g_keyObj[`assistPos${keyCtrlPtn}`][g_stateObj.autoPlay] !== undefined &&
86
+ !g_autoPlaysBase.includes(g_stateObj.autoPlay)) {
87
+ if (g_keyObj[`assistPos${keyCtrlPtn}`][g_stateObj.autoPlay][_j] === 1) {
88
+ arrowColor = g_headerObj.setDummyColor[_colorPos];
89
+ }
90
+ }
91
+ return arrowColor;
92
+ };
93
+
94
+ /**
95
+ * 対象割り当てキーの色変更
96
+ * @param {number} _j
97
+ * @param {number} _k
98
+ * @param {string} _cssName
99
+ */
100
+ const changeKeyConfigColor = (_j, _k, _cssName) =>
101
+ changeConfigColor(document.getElementById(`keycon${_j}_${_k}`), _cssName);
102
+
103
+ /**
104
+ * 一時的に矢印色・シャッフルグループを変更(共通処理)
105
+ * @param {string} _type
106
+ * @param {number} _len
107
+ * @param {number} _j
108
+ * @param {number} _scrollNum
109
+ * @returns {number}
110
+ */
111
+ const changeTmpData = (_type, _len, _j, _scrollNum) => {
112
+ const tmpNo = nextPos(g_keyObj[`${_type}${keyCtrlPtn}_${g_keycons[`${_type}GroupNum`]}`][_j], _scrollNum, _len);
113
+ g_keyObj[`${_type}${keyCtrlPtn}`][_j] = tmpNo;
114
+ g_keyObj[`${_type}${keyCtrlPtn}_${g_keycons[`${_type}GroupNum`]}`][_j] = tmpNo;
115
+
116
+ return tmpNo;
117
+ };
118
+
119
+ /**
120
+ * 一時的に矢印色を変更
121
+ * @param {number} _j
122
+ * @param {number} _scrollNum
123
+ */
124
+ const changeTmpColor = (_j, _scrollNum = 1) => {
125
+ const changeTmpOneColor = _idx => {
126
+ changeTmpData(`color`, g_headerObj.setColor.length, _idx, _scrollNum);
127
+ const arrowColor = getKeyConfigColor(_idx, g_keyObj[`color${keyCtrlPtn}`][_idx]);
128
+ $id(`arrow${_idx}`).background = arrowColor;
129
+ $id(`arrowShadow${_idx}`).background = getShadowColor(g_keyObj[`color${keyCtrlPtn}`][_idx], arrowColor);
130
+ };
131
+
132
+ if (g_stateObj.keyLockFlg && keyIsShift()) {
133
+ const tmpList = [];
134
+ g_keyObj[`color${keyCtrlPtn}`].forEach((val, idx) => {
135
+ if (val === g_keyObj[`color${keyCtrlPtn}`][_j]) {
136
+ tmpList.push(idx);
137
+ }
138
+ });
139
+ tmpList.forEach(idx => changeTmpOneColor(idx));
140
+ } else {
141
+ changeTmpOneColor(_j);
142
+ }
143
+ adjustScrollPoint(parseFloat($id(`keyGroup${_j}`).left));
144
+ };
145
+
146
+ /**
147
+ * 一時的にシャッフルグループ番号を変更
148
+ * @param {number} _j
149
+ * @param {number} _scrollNum
150
+ */
151
+ const changeTmpShuffleNum = (_j, _scrollNum = 1) => {
152
+ const changeTmpOneShuffle = _idx => {
153
+ const tmpShuffle = changeTmpData(`shuffle`, g_keyObj[`keyCtrl${keyCtrlPtn}`].length - 1, _idx, _scrollNum);
154
+ document.getElementById(`sArrow${_idx}`).textContent = tmpShuffle + 1;
155
+ changeShuffleConfigColor(keyCtrlPtn, g_keyObj[`shuffle${keyCtrlPtn}_${g_keycons.shuffleGroupNum}`][_idx], _idx);
156
+ };
157
+
158
+ if (g_stateObj.keyLockFlg && keyIsShift()) {
159
+ const tmpList = [];
160
+ g_keyObj[`shuffle${keyCtrlPtn}`].forEach((val, idx) => {
161
+ if (val === g_keyObj[`shuffle${keyCtrlPtn}`][_j]) {
162
+ tmpList.push(idx);
163
+ }
164
+ });
165
+ tmpList.forEach(idx => changeTmpOneShuffle(idx));
166
+ } else {
167
+ changeTmpOneShuffle(_j);
168
+ }
169
+ adjustScrollPoint(parseFloat($id(`keyGroup${_j}`).left));
170
+ };
171
+
172
+ /**
173
+ * 指定関数群だけを抽出したtransform文字列を作成
174
+ * @param {string} _cssString
175
+ * @param {string[]} _funcNames
176
+ * @returns {string}
177
+ */
178
+ const extractTransformFuncs = (_cssString, _funcNames) => {
179
+ const pattern = new RegExp(`\\b(${_funcNames.join(`|`)})\\([^)]*\\)`, `g`);
180
+ return (_cssString.match(pattern) || []).join(` `);
181
+ };
182
+
183
+ /**
184
+ * 対象レーンのレイヤー番号を取得
185
+ * @param {number} _j
186
+ * @param {number} _posj
187
+ * @returns {number}
188
+ */
189
+ const getLayerIdx = (_j, _posj) => {
190
+ const baseLayer = g_keyObj[`layerGroup${keyCtrlPtn}`]?.[_j] || 0;
191
+ const rowFlg = Number(_posj > divideCnt); // reverse/scrollDirは考慮せず固定
192
+ return baseLayer * 2 + rowFlg;
193
+ };
194
+
195
+ /**
196
+ * 対象レイヤーの回転変形を取得
197
+ * @param {string} _keyCtrlPtn
198
+ * @returns {string[]}
199
+ */
200
+ const getLayerRotateParts = _keyCtrlPtn => {
201
+ const raw = g_keyObj[`layerTrans${_keyCtrlPtn}`]?.[0] ?? [``, ``, ``, ``];
202
+ return raw.map(trans => trans === `` ? `` : extractTransformFuncs(trans, [`rotate`, `rotateX`, `rotateY`, `rotateZ`, `rotate3d`]));
203
+ };
204
+
205
+ // キーの一覧を表示するための準備
206
+ const C_LAYER_Y_OFFSET = 10; // baseLayerごとの縦オフセット量
207
+ const keyconSprite = createEmptySprite(divRoot, `keyconSprite`, g_windowObj.keyconSprite);
208
+ const tkObj = getKeyInfo();
209
+ const [keyCtrlPtn, keyNum, posMax, divideCnt] =
210
+ [tkObj.keyCtrlPtn, tkObj.keyNum, tkObj.posMax, tkObj.divideCnt];
211
+
212
+ g_keyCopyLists.simpleDef.forEach(header => updateKeyInfo(header, keyCtrlPtn));
213
+ addTransform(`keyconSprite`, `root`, `scale(${g_keyObj.scale})`, g_transPriority.scale);
214
+ keyconSprite.style.height = wUnit(parseFloat(keyconSprite.style.height) / ((1 + g_keyObj.scale) / 2));
215
+ const kWidth = parseInt(keyconSprite.style.width);
216
+ changeSetColor();
217
+
218
+ const maxLeftPos = Math.max(divideCnt, posMax - divideCnt - 2) / 2;
219
+ const maxLeftX = Math.min(0, (kWidth - C_ARW_WIDTH) / 2 - maxLeftPos * g_keyObj.blank);
220
+
221
+ g_keycons.cursorNumList = [...Array(keyNum).keys()].map(i => i);
222
+ const configKeyGroupList = g_headerObj.keyGroupOrder[g_stateObj.scoreId] ??
223
+ g_keyObj[`keyGroupOrder${keyCtrlPtn}`] ?? tkObj.keyGroupList;
224
+ g_keycons.colorCursorNum = 0;
225
+
226
+ // 色変化中の初期色を取得(矢印枠のみ)
227
+ const arrowColorTmp = g_detailObj.miniMapParams[g_stateObj.scoreId]._scoreObj.ncolorData.Arrow;
228
+ const arrowColors = Array.from({ length: Math.ceil(arrowColorTmp.length / 5) }, (_, i) =>
229
+ arrowColorTmp.slice(i * 5, i * 5 + 5)
230
+ ).filter(val => val[0] === 0);
231
+ const initColors = [];
232
+ arrowColors.forEach(val => {
233
+ const laneToken = val[1];
234
+ const laneStr = String(laneToken ?? ``);
235
+ if (laneStr.startsWith('g')) {
236
+ // g付きの場合は矢印グループから対象の矢印番号を検索
237
+ const groupVal = setIntVal(laneStr.slice(1));
238
+ for (let j = 0; j < tkObj.keyNum; j++) {
239
+ if (g_keyObj[`color${tkObj.keyCtrlPtn}`][j] === groupVal) {
240
+ initColors[j] = makeColorGradation(val[2]);
241
+ }
242
+ }
243
+ } else {
244
+ const laneIdx = setIntVal(laneToken, -1);
245
+ if (laneIdx >= 0 && laneIdx < tkObj.keyNum) {
246
+ initColors[laneIdx] = makeColorGradation(val[2]);
247
+ }
248
+ }
249
+ });
250
+ if (_initFlg) {
251
+ g_baseColorGrs = {};
252
+ const colorKey = Object.keys(g_keyObj).filter(val => val.startsWith(`color${g_keyObj.currentKey}`));
253
+ colorKey.forEach(val => g_baseColorGrs[val] = g_keyObj[val]);
254
+ }
255
+
256
+ const layerParts = getLayerRotateParts(keyCtrlPtn);
257
+ g_keycons.layerIdxList = [];
258
+
259
+ // キーコンフィグの矢印表示
260
+ const addLeft = (maxLeftX === 0 ? 0 : - maxLeftX + g_limitObj.kcColorPickerX);
261
+ for (let j = 0; j < keyNum; j++) {
262
+
263
+ const posj = g_keyObj[`pos${keyCtrlPtn}`][j];
264
+ const stdPos = posj - ((posj > divideCnt ? posMax : 0) + divideCnt) / 2;
265
+ const baseLayer = g_keyObj[`layerGroup${keyCtrlPtn}`]?.[j] || 0;
266
+ const layerIdx = getLayerIdx(j, posj); // baseLayer*2 + Number(posj > divideCnt)
267
+ const layerRotate = layerParts[layerIdx] || ``;
268
+ g_keycons.layerIdxList[j] = layerIdx;
269
+
270
+ const keyconX = g_keyObj.blank * stdPos + (kWidth - C_ARW_WIDTH) / 2 + addLeft;
271
+ const keyconY = C_KYC_HEIGHT * (Number(posj > divideCnt)) + 12 + baseLayer * C_LAYER_Y_OFFSET;
272
+ const colorPos = g_keyObj[`color${keyCtrlPtn}`][j];
273
+ const arrowColor = getKeyConfigColor(j, colorPos);
274
+
275
+ keyconSprite.appendChild(
276
+ createCss2Button(`color${j}`, ``, () => changeTmpColor(j), {
277
+ x: keyconX, y: keyconY, w: C_ARW_WIDTH, h: C_ARW_WIDTH,
278
+ cxtFunc: () => changeTmpColor(j, -1),
279
+ }, g_cssObj.button_Default_NoColor, g_cssObj.title_base)
280
+ );
281
+
282
+ // キーごとの親コンテナ(矢印要素を囲う)
283
+ const keyGroup = createEmptySprite(keyconSprite, `keyGroup${j}`, { x: keyconX, y: keyconY, w: C_ARW_WIDTH, h: C_ARW_WIDTH });
284
+ if (layerRotate !== ``) {
285
+ keyGroup.style.transform = layerRotate; // ここだけがlayer変形の適用箇所
286
+ }
287
+ // キーコンフィグ表示用の矢印・おにぎりを表示
288
+ multiAppend(keyGroup,
289
+ // 矢印の塗り部分
290
+ createColorObject2(`arrowShadow${j}`, {
291
+ background: hasVal(g_headerObj[`setShadowColor${g_colorType}`][colorPos]) ? getShadowColor(colorPos, arrowColor) : ``,
292
+ rotate: g_keyObj[`stepRtn${keyCtrlPtn}_${g_keycons.stepRtnGroupNum}`][j], styleName: `Shadow`,
293
+ }),
294
+ // 矢印本体
295
+ createColorObject2(`arrow${j}`, {
296
+ background: arrowColor, rotate: g_keyObj[`stepRtn${keyCtrlPtn}_${g_keycons.stepRtnGroupNum}`][j],
297
+ }),
298
+ );
299
+ if (g_headerObj.shuffleUse && g_keyObj[`shuffle${keyCtrlPtn}`] !== undefined) {
300
+ keyconSprite.appendChild(
301
+ createCss2Button(`sArrow${j}`, ``, () => changeTmpShuffleNum(j), {
302
+ x: keyconX, y: keyconY - 12, w: C_ARW_WIDTH, h: 15, siz: 12, fontWeight: `bold`,
303
+ pointerEvents: (g_settings.shuffles.filter(val => val.endsWith(`+`)).length > 0 ? C_DIS_AUTO : C_DIS_NONE),
304
+ cxtFunc: () => changeTmpShuffleNum(j, -1),
305
+ }, g_cssObj.button_Default_NoColor, g_cssObj.title_base)
306
+ );
307
+ }
308
+ if (g_isDebug) {
309
+ keyconSprite.appendChild(
310
+ createDivCss2Label(`arrowChara${j}`, g_keyObj[`chara${keyCtrlPtn}`][j], {
311
+ x: keyconX, y: keyconY + 40, w: C_ARW_WIDTH, h: 10, siz: 10, align: C_ALIGN_CENTER,
312
+ background: g_headerObj.baseBrightFlg ? `rgba(255,255,255,0.5)` : `rgba(0,0,0,0.5)`,
313
+ }, g_cssObj.title_base)
314
+ );
315
+ }
316
+
317
+ // 割り当て先のキー名を表示
318
+ for (let k = 0; k < g_keyObj[`keyCtrl${keyCtrlPtn}`][j].length; k++) {
319
+ g_keyObj[`keyCtrl${keyCtrlPtn}`][j][k] = setIntVal(g_keyObj[`keyCtrl${keyCtrlPtn}`][j][k]);
320
+ g_keyObj[`keyCtrl${keyCtrlPtn}d`][j][k] = setIntVal(g_keyObj[`keyCtrl${keyCtrlPtn}d`][j][k]);
321
+
322
+ keyconSprite.appendChild(
323
+ createCss2Button(`keycon${j}_${k}`, g_kCd[g_keyObj[`keyCtrl${keyCtrlPtn}`][j][k]], () => {
324
+ g_currentj = j;
325
+ g_currentk = k;
326
+ g_prevKey = -1;
327
+ selectedKc = `Default`;
328
+ g_keycons.cursorNum = g_keycons.cursorNumList.findIndex(val => val === g_currentj);
329
+ setKeyConfigCursor();
330
+ }, {
331
+ x: keyconX - 5, y: 50 + C_KYC_REPHEIGHT * k + keyconY,
332
+ w: C_ARW_WIDTH + 10, h: C_KYC_REPHEIGHT, siz: g_limitObj.keySetSiz,
333
+ }, g_cssObj.button_Default_NoColor, g_cssObj.title_base)
334
+ );
335
+
336
+ // キーに色付け
337
+ if (g_keyObj[`keyCtrl${keyCtrlPtn}d`][j][k] !== g_keyObj[`keyCtrl${keyCtrlPtn}`][j][k]) {
338
+ changeKeyConfigColor(j, k, g_cssObj.keyconfig_Changekey);
339
+ } else if (g_keyObj.currentPtn === -1) {
340
+ changeKeyConfigColor(j, k, g_cssObj.keyconfig_Defaultkey);
341
+ }
342
+ }
343
+ }
344
+ const posj = g_keyObj[`pos${keyCtrlPtn}`][0];
345
+
346
+ // カーソルの作成
347
+ const cursor = keyconSprite.appendChild(createImg(`cursor`, g_imgObj.cursor,
348
+ (kWidth - C_ARW_WIDTH) / 2 + g_keyObj.blank * (posj - divideCnt / 2) - 10, 57, 15, 30));
349
+ cursor.style.transitionDuration = `0.125s`;
350
+
351
+ const viewGroupObj = {
352
+ shuffle: (_type = ``) => {
353
+ if (g_keyObj[`shuffle${keyCtrlPtn}`] !== undefined) {
354
+ for (let j = 0; j < keyNum; j++) {
355
+ document.getElementById(`sArrow${j}`).textContent = g_keyObj[`shuffle${keyCtrlPtn}${_type}`][j] + 1;
356
+ }
357
+ }
358
+ },
359
+ color: (_type = ``) => {
360
+ for (let j = 0; j < keyNum; j++) {
361
+ const colorPos = g_keyObj[`color${keyCtrlPtn}${_type}`][j];
362
+ const arrowColor = getKeyConfigColor(j, colorPos);
363
+ $id(`arrow${j}`).background = arrowColor;
364
+ $id(`arrowShadow${j}`).background = hasVal(g_headerObj.setShadowColor[colorPos]) ?
365
+ getShadowColor(colorPos, arrowColor) : ``;
366
+ if (g_headerObj.setShadowColor[colorPos] === `Default`) {
367
+ $id(`arrowShadow${j}`).opacity = 0.5;
368
+ }
369
+ }
370
+ },
371
+ stepRtn: (_type = ``) => { },
372
+ };
373
+
374
+ /**
375
+ * カラー・シャッフルグループ設定の表示
376
+ * - シャッフルグループではデフォルトからの差異表示もここで行う
377
+ * @param {string} _type
378
+ */
379
+ const viewGroup = _type => {
380
+ if (g_headerObj[`${_type}Use`]) {
381
+ const num = g_keycons[`${_type}GroupNum`] === -1 ? g_keycons.groupSelf : g_keycons[`${_type}GroupNum`] + 1;
382
+ if (document.getElementById(`lnk${toCapitalize(_type)}Group`) !== null) {
383
+ document.getElementById(`lnk${toCapitalize(_type)}Group`).textContent = getStgDetailName(num);
384
+ }
385
+ viewGroupObj[_type](`_${g_keycons[`${_type}GroupNum`]}`);
386
+
387
+ if (_type === `shuffle`) {
388
+ changeShuffleConfigColor(keyCtrlPtn, g_keyObj[`shuffle${keyCtrlPtn}_${g_keycons.shuffleGroupNum}`]);
389
+ }
390
+ }
391
+ };
392
+ /**
393
+ * カラー・シャッフルグループ設定
394
+ * @param {string} _type
395
+ * @param {number} [_scrollNum=1]
396
+ */
397
+ const setGroup = (_type, _scrollNum = 1) => {
398
+ g_keycons[`${_type}GroupNum`] = g_keycons[`${_type}Groups`][getNextNum(_scrollNum, `${_type}Groups`, g_keycons[`${_type}GroupNum`])];
399
+ g_keyObj[`${_type}${keyCtrlPtn}`] = structuredClone(g_keyObj[`${_type}${keyCtrlPtn}_${g_keycons[`${_type}GroupNum`]}`]);
400
+ viewGroup(_type);
401
+ if (_type === `stepRtn`) {
402
+ keyConfigInit(g_kcType);
403
+ }
404
+ };
405
+
406
+ /**
407
+ * キーコンフィグ用設定ラベル
408
+ * @param {string} _id
409
+ * @param {string} _name
410
+ * @param {number} [object.x=g_btnX(5 / 6)]
411
+ * @param {number} [object.y=0]
412
+ * @param {number} [object.w=g_btnWidth(1 / 6)]
413
+ * @param {number} [object.h=20]
414
+ * @param {number} [object.siz=12]
415
+ * @param {string} [object.align='left']
416
+ * @param {...any} [object.rest]
417
+ * @param {...any} _classes
418
+ * @returns {HTMLDivElement}
419
+ */
420
+ const makeKCButtonHeader = (_id, _name, {
421
+ x = g_btnX(5 / 6) - 30, y = 0, w = g_btnWidth(1 / 6), h = 20, siz = 12, align = C_ALIGN_LEFT, ...rest
422
+ } = {}, ..._classes) => createDivCss2Label(_id, g_lblNameObj[_name], { x, y, w, h, siz, align, ...rest }, ..._classes);
423
+
424
+ /**
425
+ * キーコンフィグ用設定ボタン
426
+ * @param {string} _id
427
+ * @param {string} _text
428
+ * @param {Function} _func
429
+ * @param {number} [object.x=g_btnX(5 / 6) - 20]
430
+ * @param {number} [object.y=15]
431
+ * @param {number} [object.w=g_btnWidth(1 / 6)]
432
+ * @param {number} [object.h=18]
433
+ * @param {number} [object.siz=g_limitObj.jdgCntsSiz]
434
+ * @param {string} [object.borderStyle='solid']
435
+ * @param {Function} [object.cxtFunc]
436
+ * @param {...any} [object.rest]
437
+ * @param {string} [_mainClass=g_cssObj.button_RevOFF]
438
+ * @param {...any} _classes
439
+ * @returns {HTMLDivElement}
440
+ */
441
+ const makeKCButton = (_id, _text, _func, { x = g_btnX(5 / 6) - 20, y = 15, w = g_btnWidth(1 / 6), h = 18,
442
+ siz = g_limitObj.jdgCntsSiz, borderStyle = C_DIS_NONE, cxtFunc, ...rest } = {}, _mainClass = g_cssObj.button_RevOFF, ..._classes) =>
443
+ makeSettingLblCssButton(_id, getStgDetailName(_text), 0, _func, { x, y, w, h, siz, cxtFunc, borderStyle, ...rest }, _mainClass, ..._classes);
444
+
445
+ /**
446
+ * キーコンフィグ用ミニボタン
447
+ * @param {string} _id
448
+ * @param {string} _directionFlg
449
+ * @param {Function} _func
450
+ * @param {number} [object.x=g_btnX(5 / 6) - 30]
451
+ * @param {number} [object.y=15]
452
+ * @param {number} [object.w=15]
453
+ * @param {number} [object.h=20]
454
+ * @param {number} [object.siz=g_limitObj.mainSiz]
455
+ */
456
+ const makeMiniKCButton = (_id, _directionFlg, _func, { x = g_btnX(5 / 6) - 30, y = 15, w = 15, h = 20, siz = g_limitObj.mainSiz } = {}) =>
457
+ createCss2Button(`${_id}${_directionFlg}`, g_settingBtnObj.chara[_directionFlg], _func, { x, y, w, h, siz }, g_cssObj.button_Mini);
458
+
459
+ /**
460
+ * キーコンフィグ用グループ設定ラベル・ボタンの作成
461
+ * @param {string} _type
462
+ * @param {number} [object.baseX=g_btnX(5 / 6) - 20]
463
+ * @param {number} [object.baseY=0]
464
+ * @param {string} [object.cssName]
465
+ */
466
+ const makeGroupButton = (_type, { baseX = g_btnX(5 / 6) - 20, baseY = 0, cssName } = {}) => {
467
+ if (g_headerObj[`${_type}Use`] && g_keycons[`${_type}Groups`].length > 1) {
468
+ const typeName = toCapitalize(_type);
469
+ multiAppend(divRoot,
470
+ makeKCButtonHeader(`lbl${_type}Group`, `${typeName}Group`, { x: baseX - 10, y: baseY }, cssName),
471
+ makeKCButton(`lnk${typeName}Group`, ``, () => setGroup(_type), {
472
+ x: baseX, y: baseY + 13, w: g_btnWidth(1 / 18), title: g_msgObj[`${_type}Group`], cxtFunc: () => setGroup(_type, -1),
473
+ }),
474
+ makeMiniKCButton(`lnk${typeName}Group`, `L`, () => setGroup(_type, -1), { x: baseX - 10, y: baseY + 13 }),
475
+ makeMiniKCButton(`lnk${typeName}Group`, `R`, () => setGroup(_type), { x: baseX + g_btnWidth(1 / 18), y: baseY + 13 }),
476
+ );
477
+ } else {
478
+ g_keycons[`${_type}GroupNum`] = 0;
479
+ }
480
+ viewGroup(_type);
481
+ };
482
+
483
+ const kcSubX = parseFloat(keyconSprite.style.width) * ((1 - g_keyObj.scale) / 4);
484
+ const kcSubY = parseFloat(keyconSprite.style.height) / ((1 + g_keyObj.scale) / 2) - parseFloat(keyconSprite.style.height);
485
+ multiAppend(divRoot,
486
+
487
+ // ショートカットキーメッセージ
488
+ createDescDiv(`scMsg`, g_lblNameObj.kcShortcutDesc, { altId: `scKcMsg` }),
489
+
490
+ // タイトルバックのショートカットキー変更
491
+ createCss2Button(`scTitleBack`, getScMsg.TitleBack(), () => {
492
+ if (!g_isMac) {
493
+ cursor.style.left = wUnit(g_btnX(1 / 4) - kcSubX);
494
+ cursor.style.top = wUnit(g_sHeight - 160 + kcSubY);
495
+ selectedKc = `TitleBack`;
496
+ }
497
+ }, g_lblPosObj.scTitleBack, g_cssObj.button_Default_NoColor,
498
+ g_headerObj.keyTitleBack === g_headerObj.keyTitleBackDef2 ?
499
+ g_cssObj.title_base : g_cssObj.keyconfig_Changekey),
500
+
501
+ // リトライのショートカットキー変更
502
+ createCss2Button(`scRetry`, getScMsg.Retry(), () => {
503
+ cursor.style.left = wUnit(g_btnX(1 / 2) + kcSubX);
504
+ cursor.style.top = wUnit(g_sHeight - 160 + kcSubY);
505
+ selectedKc = `Retry`;
506
+ }, g_lblPosObj.scRetry, g_cssObj.button_Default_NoColor,
507
+ g_headerObj.keyRetry === g_headerObj.keyRetryDef2 ?
508
+ g_cssObj.title_base : g_cssObj.keyconfig_Changekey),
509
+
510
+ // ポーズのショートカットキー変更
511
+ createCss2Button(`scPause`, getScMsg.Pause(), () => {
512
+ cursor.style.left = wUnit(g_btnX(3 / 4) + kcSubX);
513
+ cursor.style.top = wUnit(g_sHeight - 160 + kcSubY);
514
+ selectedKc = `Pause`;
515
+ }, g_lblPosObj.scPause, g_cssObj.button_Default_NoColor,
516
+ g_headerObj.keyPause === g_headerObj.keyPauseDef2 ?
517
+ g_cssObj.title_base : g_cssObj.keyconfig_Changekey),
518
+
519
+ // 別キーモード警告メッセージ
520
+ createDivCss2Label(
521
+ `kcMsg`,
522
+ hasVal(g_keyObj[`transKey${keyCtrlPtn}`]) ? g_lblNameObj.transKeyDesc : ``,
523
+ g_lblPosObj.kcMsg, g_cssObj.keyconfig_warning
524
+ ),
525
+ // ColorType警告メッセージ
526
+ createDivCss2Label(
527
+ `kcMsg2`,
528
+ g_keycons.colorDefTypes.includes(g_colorType) ? `` : g_lblNameObj.colorTypeDesc,
529
+ g_lblPosObj.kcMsg2, g_cssObj.keyconfig_Defaultkey
530
+ ),
531
+
532
+ // キーカラータイプ切替ボタン
533
+ makeKCButtonHeader(`lblcolorType`, `ColorType`, { x: 10 + g_btnX() }, g_cssObj.keyconfig_ColorType),
534
+ makeKCButton(`lnkColorType`, g_colorType, () => setColorType(), {
535
+ x: 20 + g_btnX(), title: g_msgObj.colorType, cxtFunc: () => setColorType(-1),
536
+ }),
537
+ makeMiniKCButton(`lnkColorType`, `L`, () => setColorType(-1), { x: 10 + g_btnX() }),
538
+ makeMiniKCButton(`lnkColorType`, `R`, () => setColorType(), { x: 20 + g_btnX(1 / 6) }),
539
+ );
540
+
541
+ if (g_headerObj.imgType.length > 1) {
542
+ const [imgBaseX, imgBaseY] = [20 + g_btnX(), 50];
543
+ multiAppend(divRoot,
544
+ // オブジェクトタイプの切り替え(リロードあり)
545
+ makeKCButtonHeader(`lblImgType`, `ImgType`, { x: imgBaseX - 10, y: 37 }, g_cssObj.keyconfig_ConfigType),
546
+ makeKCButton(`lnkImgType`, g_imgType, () => setImgType(), {
547
+ x: imgBaseX, y: imgBaseY, title: g_msgObj.imgType, cxtFunc: () => setImgType(-1),
548
+ }),
549
+ makeMiniKCButton(`lnkImgType`, `L`, () => setImgType(-1), { x: imgBaseX - 10, y: imgBaseY }),
550
+ makeMiniKCButton(`lnkImgType`, `R`, () => setImgType(), { x: imgBaseX + g_btnWidth(1 / 6), y: imgBaseY }),
551
+ );
552
+ }
553
+
554
+ // カラー/シャッフルグループ切替ボタン(カラー/シャッフルパターンが複数ある場合のみ)
555
+ makeGroupButton(`color`, { cssName: g_cssObj.keyconfig_ColorGr });
556
+ makeGroupButton(`shuffle`, { baseX: g_btnX(11 / 12) - 10, cssName: g_cssObj.keyconfig_ShuffleGr });
557
+ makeGroupButton(`stepRtn`, { baseY: 37, cssName: g_cssObj.keyconfig_StepRtnGr });
558
+
559
+ /**
560
+ * カーソル位置の設定
561
+ */
562
+ const setKeyConfigCursor = () => {
563
+ const posj = g_keyObj[`pos${keyCtrlPtn}`][g_currentj];
564
+ const stdPos = posj - ((posj > divideCnt ? posMax : 0) + divideCnt) / 2;
565
+ const baseLayer = g_keyObj[`layerGroup${keyCtrlPtn}`]?.[g_currentj] || 0;
566
+
567
+ const nextLeft = (kWidth - C_ARW_WIDTH) / 2 + g_keyObj.blank * stdPos + addLeft - 10;
568
+ cursor.style.left = wUnit(nextLeft);
569
+ cursor.style.top = wUnit(C_KYC_HEIGHT * Number(posj > divideCnt) + 57 + C_KYC_REPHEIGHT * g_currentk + baseLayer * C_LAYER_Y_OFFSET);
570
+ g_kcType = (g_currentk === 0 ? `Main` : `Replaced`);
571
+
572
+ // 次の位置が見えなくなったらkeyconSpriteの位置を調整する
573
+ adjustScrollPoint(nextLeft);
574
+ };
575
+
576
+ /**
577
+ * カーソル位置の変更
578
+ * @param {number} [_nextj] 次のカーソル位置
579
+ */
580
+ const changeConfigCursor = (_nextj = ++g_keycons.cursorNum % g_keycons.cursorNumList.length) => {
581
+ g_keycons.cursorNum = _nextj;
582
+
583
+ g_currentj = g_keycons.cursorNumList[_nextj];
584
+ g_currentk = 0;
585
+ if (g_kcType === `Replaced`) {
586
+ g_currentk = 1;
587
+
588
+ // 代替キー設定の場合は次の代替キーが見つかるまで移動
589
+ while (g_keyObj[`keyCtrl${keyCtrlPtn}`][g_currentj][1] === undefined) {
590
+ g_keycons.cursorNum = (g_keycons.cursorNum + 1) % g_keycons.cursorNumList.length;
591
+ g_currentj = g_keycons.cursorNumList[g_keycons.cursorNum];
592
+
593
+ // 一周して対象が無い場合は代替キーが無いため処理を抜ける(無限ループ対策)
594
+ if (g_keycons.cursorNum === _nextj) {
595
+ g_kcType = `Main`;
596
+ g_currentk = 0;
597
+ break;
598
+ }
599
+ }
600
+ }
601
+ setKeyConfigCursor();
602
+ };
603
+
604
+ /**
605
+ * 次のカーソル位置の取得
606
+ * @param {number} _scrollNum
607
+ * @param {string} _groupName
608
+ * @param {string} _target
609
+ * @returns {number}
610
+ */
611
+ const getNextNum = (_scrollNum, _groupName, _target) =>
612
+ nextPos(g_keycons[_groupName].findIndex(value => value === _target), _scrollNum, g_keycons[_groupName].length);
613
+
614
+ /**
615
+ * ConfigTypeの制御
616
+ * @param {number} _scrollNum
617
+ */
618
+ const setConfigType = (_scrollNum = 1) => {
619
+ g_kcType = g_keycons.configTypes[getNextNum(_scrollNum, `configTypes`, g_kcType)];
620
+ changeConfigCursor(g_keycons.cursorNum);
621
+ };
622
+
623
+ /**
624
+ * 一部のキーコンフィグを表示する
625
+ * (キーグループ毎にフィルターされたもののみを表示する)
626
+ * @param {number} _num
627
+ */
628
+ const appearConfigSteps = _num => {
629
+
630
+ const appearConfigView = (_j, _display) => {
631
+ $id(`arrow${_j}`).display = _display;
632
+ $id(`arrowShadow${_j}`).display = _display;
633
+ $id(`color${_j}`).display = _display;
634
+ if (document.getElementById(`sArrow${_j}`) !== null) {
635
+ $id(`sArrow${_j}`).display = _display;
636
+ }
637
+ if (document.getElementById(`arrowChara${_j}`) !== null) {
638
+ $id(`arrowChara${_j}`).display = _display;
639
+ }
640
+ const ctrlPtn = g_keyObj[`keyCtrl${g_headerObj.keyLabels[g_stateObj.scoreId]}_${g_keyObj.currentPtn}`][_j];
641
+ for (let k = 0; k < ctrlPtn.length; k++) {
642
+ $id(`keycon${_j}_${k}`).display = _display;
643
+ }
644
+ };
645
+
646
+ g_keycons.keySwitchNum = _num;
647
+ g_keycons.cursorNumList = [];
648
+ for (let j = 0; j < keyNum; j++) {
649
+ appearConfigView(j, C_DIS_NONE);
650
+
651
+ if (tkObj.keyGroupMaps[j].includes(configKeyGroupList[_num])) {
652
+ g_keycons.cursorNumList.push(j);
653
+ appearConfigView(j, C_DIS_INHERIT);
654
+ }
655
+ }
656
+ if (g_keycons.cursorNumList.length === 0) {
657
+ makeInfoWindow(g_msgInfoObj.I_0011, ``);
658
+ } else {
659
+ changeConfigCursor(0);
660
+
661
+ // keySwitchボタンを一旦非選択にして、選択中のものを再度色付け
662
+ if (configKeyGroupList.length > 1) {
663
+ for (let j = 0; j < configKeyGroupList.length; j++) {
664
+ document.getElementById(`key${j}`).classList.replace(g_cssObj.button_Next, g_cssObj.button_Mini);
665
+ }
666
+ document.getElementById(`key${_num}`).classList.replace(g_cssObj.button_Mini, g_cssObj.button_Next);
667
+ }
668
+ }
669
+ keyconfigKeyboardPreview.refresh();
670
+ };
671
+
672
+ /**
673
+ * ColorPickerの色切替
674
+ * @param {number} _j
675
+ * @param {string} _type
676
+ * @param {string} _color
677
+ */
678
+ const changeColorPicker = (_j, _type, _color) => {
679
+ if (_color !== ``) {
680
+ document.getElementById(`pick${_type}${_j}`).value = _color.slice(0, 7);
681
+ $id(`pick${_type}${_j}`).display = C_DIS_INHERIT;
682
+ } else {
683
+ $id(`pick${_type}${_j}`).display = C_DIS_NONE;
684
+ }
685
+ };
686
+
687
+ /**
688
+ * ColorPickerに対応する実際の番号を取得
689
+ * @param {number} _j
690
+ */
691
+ const getGroupNum = _j => _j + g_keycons.colorCursorNum * g_limitObj.kcColorPickerNum;
692
+
693
+ /**
694
+ * ColorPicker(一式)の切替
695
+ */
696
+ const changeColorPickers = () => {
697
+ lnkColorR.textContent = `[${g_keycons.colorCursorNum + 1} /`;
698
+ for (let j = 0; j < g_limitObj.kcColorPickerNum; j++) {
699
+ const m = getGroupNum(j);
700
+ changeColorPicker(j, `arrow`, g_headerObj.setColor[m]);
701
+ changeColorPicker(j, `arrowShadow`, g_headerObj.setShadowColor[m]);
702
+ changeColorPicker(j, `frz`, g_headerObj.frzColor[m][0]);
703
+ changeColorPicker(j, `frzBar`, g_headerObj.frzColor[m][1]);
704
+ }
705
+ };
706
+
707
+ /**
708
+ * ColorTypeの制御
709
+ * @param {number} [_scrollNum=1]
710
+ * @param {boolean} [_reloadFlg=true]
711
+ */
712
+ const setColorType = (_scrollNum = 1, _reloadFlg = true) => {
713
+ const nextNum = getNextNum(_scrollNum, `colorTypes`, g_colorType);
714
+ g_colorType = g_keycons.colorTypes[nextNum];
715
+ const isDefault = g_keycons.colorDefTypes.includes(g_colorType);
716
+ if (g_headerObj.colorUse) {
717
+ g_stateObj.d_color = boolToSwitch(g_keycons.colorDefTypes.findIndex(val => val === g_colorType) !== -1);
718
+ }
719
+ changeSetColor();
720
+ viewGroupObj.color(`_${g_keycons.colorGroupNum}`);
721
+ lnkColorType.textContent = `${getStgDetailName(g_colorType)}${g_localStorage.colorType === g_colorType ? ' *' : ''}`;
722
+ kcMsg2.textContent = (isDefault || !g_headerObj.colorUse) ? `` : g_lblNameObj.colorTypeDesc;
723
+ kcMsg2.style.top = wUnit(hasVal(g_keyObj[`transKey${keyCtrlPtn}`]) ? g_lblPosObj.kcMsg2.y : g_lblPosObj.kcMsg.y);
724
+ kcMsg2.style.fontSize = wUnit(getFontSize2(kcMsg2.textContent, g_btnWidth()));
725
+ if (_reloadFlg) {
726
+ colorPickSprite.style.display = isDefault ? C_DIS_NONE : C_DIS_INHERIT;
727
+ g_keycons.colorCursorNum = g_keycons.colorCursorNum % Math.ceil(g_dfColorObj.setColorInit.length / g_limitObj.kcColorPickerNum);
728
+ changeColorPickers();
729
+ }
730
+ };
731
+
732
+ /**
733
+ * ImgTypeの制御
734
+ * @param {number} [_scrollNum=1]
735
+ */
736
+ const setImgType = (_scrollNum = 1) => {
737
+ const nextNum = getNextNum(_scrollNum, `imgTypes`, g_imgType);
738
+ g_imgType = g_keycons.imgTypes[nextNum];
739
+ g_stateObj.rotateEnabled = g_headerObj.imgType[nextNum].rotateEnabled;
740
+ g_stateObj.flatStepHeight = g_headerObj.imgType[nextNum].flatStepHeight;
741
+
742
+ changeSettingListsForImg();
743
+ updateImgType(g_headerObj.imgType[nextNum]);
744
+ keyConfigInit(g_kcType);
745
+ };
746
+
747
+ const colorPickSprite = createEmptySprite(divRoot, `colorPickSprite`, { ...g_windowObj.colorPickSprite, title: g_msgObj.pickArrow });
748
+ if (g_keycons.colorDefTypes.includes(g_colorType)) {
749
+ colorPickSprite.style.display = C_DIS_NONE;
750
+ }
751
+ multiAppend(colorPickSprite,
752
+
753
+ // ColorPickerの切替
754
+ createCss2Button(`lnkColorR`, `[${g_keycons.colorCursorNum + 1} /`, () => {
755
+ g_keycons.colorCursorNum = (g_keycons.colorCursorNum + 1) % Math.ceil(g_dfColorObj.setColorInit.length / g_limitObj.kcColorPickerNum);
756
+ changeColorPickers();
757
+ }, g_lblPosObj.lnkColorR, g_cssObj.button_Start),
758
+
759
+ // 矢印の配色をフリーズアローへ反映
760
+ createCss2Button(`lnkColorCopy`, `↓]`, () => {
761
+ if (window.confirm(g_msgObj.colorCopyConfirm)) {
762
+ for (let j = 0; j < g_dfColorObj.setColorInit.length; j++) {
763
+ g_headerObj.frzColor[j] = g_headerObj[`frzColor${g_colorType}`][j] =
764
+ fillArray(g_headerObj[`frzColor${g_colorType}`][j].length, g_headerObj[`setColor${g_colorType}`][j]);
765
+ }
766
+ for (let j = 0; j < g_limitObj.kcColorPickerNum; j++) {
767
+ const m = getGroupNum(j);
768
+ [``, `Bar`].forEach((val, k) =>
769
+ document.getElementById(`pickfrz${val}${j}`).value = g_headerObj[`frzColor${g_colorType}`][m][k]);
770
+ }
771
+ }
772
+ }, g_lblPosObj.lnkColorCopy, g_cssObj.button_Start),
773
+
774
+ createDivCss2Label(`lblPickArrow`, g_lblNameObj.s_arrow, { ...g_lblPosObj.pickPos, y: 0 }),
775
+ createDivCss2Label(`lblPickFrz`, g_lblNameObj.s_frz, { ...g_lblPosObj.pickPos, y: 140 }),
776
+
777
+ // ColorPickerの色を元に戻す
778
+ createCss2Button(`lnkColorReset`, g_lblNameObj.b_cReset, () => {
779
+ if (window.confirm(g_msgObj.colorResetConfirm)) {
780
+ resetColorType({ _from: g_colorType, _to: ``, _fromObj: g_dfColorObj });
781
+ resetColorType({ _from: g_colorType, _to: g_colorType, _fromObj: g_dfColorObj });
782
+
783
+ // 影矢印が未指定の場合はType1, Type2の影矢印指定を無くす
784
+ const _idHeader = setScoreIdHeader(g_stateObj.scoreId, false, true);
785
+ const _shadowDefault = g_headerObj[`setShadowColor${_idHeader}Default`];
786
+ if ((!Array.isArray(_shadowDefault) || !hasVal(_shadowDefault[0])) &&
787
+ [`Type1`, `Type2`].includes(g_colorType)) {
788
+
789
+ g_headerObj.setShadowColor = fillArray(g_headerObj.setColorInit.length, ``);
790
+ g_headerObj[`setShadowColor${g_colorType}`] = fillArray(g_headerObj.setColorInit.length, ``);
791
+ }
792
+
793
+ changeColorPickers();
794
+ viewGroupObj.color(`_${g_keycons.colorGroupNum}`);
795
+ }
796
+ }, g_lblPosObj.lnkColorReset, g_cssObj.button_Reset),
797
+ );
798
+
799
+ /**
800
+ * ColorPicker部分の作成
801
+ * @param {number} _j
802
+ * @param {string} _type
803
+ * @param {Function} _func
804
+ * @param {number} [object.x=0]
805
+ * @param {number} [object.y=15]
806
+ */
807
+ const createColorPickWindow = (_j, _type, _func, { x = 0, y = 15 } = {}) =>
808
+ createColorPicker(colorPickSprite, `pick${_type}${_j}`, _func, { x, y: y + 25 * _j });
809
+
810
+ for (let j = 0; j < g_limitObj.kcColorPickerNum; j++) {
811
+ createColorPickWindow(j, `arrow`, () => {
812
+ g_headerObj[`setColor${g_colorType}`][getGroupNum(j)] = document.getElementById(`pickarrow${j}`).value;
813
+ setColorType(0, false);
814
+ });
815
+
816
+ createColorPickWindow(j, `arrowShadow`, () => {
817
+ g_headerObj[`setShadowColor${g_colorType}`][getGroupNum(j)] = `${document.getElementById(`pickarrowShadow${j}`).value}80`;
818
+ setColorType(0, false);
819
+ }, { x: 25 });
820
+
821
+ [``, `Bar`].forEach((val, k) =>
822
+ createColorPickWindow(j, `frz${val}`, () => {
823
+ g_headerObj[`frzColor${g_colorType}`][getGroupNum(j)][k] = document.getElementById(`pickfrz${val}${j}`).value;
824
+ }, { x: 25 * k, y: 155 }));
825
+ }
826
+
827
+ // ConfigType, ColorTypeの初期設定
828
+ setConfigType(0);
829
+ setColorType(0);
830
+ keyconSprite.scrollLeft = - maxLeftX;
831
+
832
+ // キーパターン表示
833
+ const lblTransKey = hasVal(g_keyObj[`transKey${keyCtrlPtn}`]) ? `(${g_keyObj[`transKey${keyCtrlPtn}`] ?? ''})` : ``;
834
+
835
+ /**
836
+ * キーパターン検索
837
+ * @param {number} _tempPtn
838
+ * @param {number} _sign
839
+ * @param {boolean} [_transKeyUse=false]
840
+ * @param {boolean} [_skipFlg=false]
841
+ */
842
+ const searchPattern = (_tempPtn, _sign, _transKeyUse = false, _skipFlg = false) => {
843
+ let nextPtn = _tempPtn + _sign;
844
+ const initialPtn = hasVal(g_keyObj[`keyCtrl${g_keyObj.currentKey}_-1`]) ? -1 : 0;
845
+
846
+ const searchStart = () => {
847
+ if (!hasVal(g_keyObj[`keyCtrl${g_keyObj.currentKey}_${nextPtn}`])) {
848
+ nextPtn = 0;
849
+ while (hasVal(g_keyObj[`keyCtrl${g_keyObj.currentKey}_${nextPtn}`])) {
850
+ nextPtn -= _sign;
851
+ }
852
+ nextPtn += _sign;
853
+ }
854
+ };
855
+
856
+ const searchNextGroup = () => {
857
+ while (nextPtn !== initialPtn &&
858
+ g_keyObj[`transKey${g_keyObj.currentKey}_${_tempPtn}`] === g_keyObj[`transKey${g_keyObj.currentKey}_${nextPtn}`] &&
859
+ hasVal(g_keyObj[`keyCtrl${g_keyObj.currentKey}_${nextPtn}`])) {
860
+ nextPtn += _sign;
861
+ }
862
+ };
863
+
864
+ searchStart();
865
+ if (_skipFlg) {
866
+ searchNextGroup();
867
+ searchStart();
868
+ }
869
+ if (!_transKeyUse) {
870
+ while (hasVal(g_keyObj[`transKey${g_keyObj.currentKey}_${nextPtn}`])) {
871
+ nextPtn += _sign;
872
+ }
873
+ searchStart();
874
+ }
875
+ return nextPtn;
876
+ };
877
+
878
+ /**
879
+ * キーパターン変更時処理
880
+ * @param {number} [_sign=1]
881
+ * @param {boolean} [_skipFlg=false]
882
+ */
883
+ const changePattern = (_sign = 1, _skipFlg = false) => {
884
+
885
+ // キーパターンの変更
886
+ g_keyObj.currentPtn = searchPattern(g_keyObj.currentPtn, _sign, g_headerObj.transKeyUse, _skipFlg);
887
+
888
+ // カラーグループ、シャッフルグループの再設定
889
+ g_keycons.groups.forEach(type => resetGroupList(type, `${g_keyObj.currentKey}_${g_keyObj.currentPtn}`));
890
+
891
+ // 曲中ショートカットキーの切り替え
892
+ setPlayingShortcut();
893
+
894
+ // キーコンフィグ画面を再呼び出し
895
+ keyConfigInit();
896
+
897
+ // シャッフルグループのデフォルト値からの差異表示(色付け)
898
+ // 再描画後で無いと色付けできないため、keyConfigInit() 実行後に処理
899
+ if (g_headerObj.shuffleUse) {
900
+ changeShuffleConfigColor(`${g_keyObj.currentKey}_${g_keyObj.currentPtn}`, g_keyObj[`shuffle${g_keyObj.currentKey}_${g_keyObj.currentPtn}_${g_keycons.shuffleGroupNum}`]);
901
+ }
902
+ };
903
+
904
+ // ユーザカスタムイベント(初期)
905
+ safeExecuteCustomHooks(`g_customJsObj.keyconfig`, g_customJsObj.keyconfig);
906
+
907
+ // 部分キー表示用ボタン描画
908
+ if (configKeyGroupList.length > 1) {
909
+ multiAppend(divRoot,
910
+ createDivCss2Label(`lblkey`, `KeySwitch`, g_lblPosObj.lblkey));
911
+ configKeyGroupList.forEach((val, j) =>
912
+ divRoot.appendChild(
913
+ createCss2Button(`key${j}`, `${j + 1}`, () => appearConfigSteps(j),
914
+ { ...g_lblPosObj.lnkKeySwitch, y: 110 + j * 20 }, g_cssObj.button_Mini),
915
+ ));
916
+ }
917
+
918
+ // カーソル位置の初期化
919
+ appearConfigSteps(g_keycons.keySwitchNum);
920
+
921
+ keyconfigKeyboardPreview.dispose();
922
+ keyconfigKeyboardPreview.init(divRoot);
923
+
924
+ // ラベル・ボタン描画
925
+ multiAppend(divRoot,
926
+
927
+ // 設定画面へ戻る
928
+ createCss2Button(`btnBack`, g_lblNameObj.b_settings, () => {
929
+ g_currentj = 0;
930
+ g_currentk = 0;
931
+ g_prevKey = 0;
932
+ }, {
933
+ ...g_lblPosObj.btnKcBack, resetFunc: () => {
934
+ keyconfigKeyboardPreview.dispose();
935
+ g_moveSettingWindow(false);
936
+ }
937
+ }, g_cssObj.button_Back),
938
+
939
+ createDivCss2Label(`lblPattern`, `${g_lblNameObj.KeyPattern}: ${g_keyObj.currentPtn === -1 ?
940
+ 'Self' : g_keyObj.currentPtn + 1}${lblTransKey}`, g_lblPosObj.lblPattern),
941
+
942
+ // パターン変更ボタン描画(右回り)
943
+ createCss2Button(`btnPtnChangeR`, `>`, () => true, {
944
+ ...g_lblPosObj.btnPtnChangeR, resetFunc: () => changePattern(),
945
+ }, g_cssObj.button_Mini),
946
+
947
+ // パターン変更ボタン描画(左回り)
948
+ createCss2Button(`btnPtnChangeL`, `<`, () => true, {
949
+ ...g_lblPosObj.btnPtnChangeL, resetFunc: () => changePattern(-1),
950
+ }, g_cssObj.button_Mini),
951
+
952
+ // パターン変更ボタン描画(右回り/別キーモード間スキップ)
953
+ createCss2Button(`btnPtnChangeRR`, `|>`, () => true, {
954
+ ...g_lblPosObj.btnPtnChangeRR, resetFunc: () => changePattern(1, true),
955
+ }, g_cssObj.button_Setting),
956
+
957
+ // パターン変更ボタン描画(左回り/別キーモード間スキップ)
958
+ createCss2Button(`btnPtnChangeLL`, `<|`, () => true, {
959
+ ...g_lblPosObj.btnPtnChangeLL, resetFunc: () => changePattern(-1, true),
960
+ }, g_cssObj.button_Setting),
961
+
962
+ // キーコンフィグリセットボタン描画
963
+ createCss2Button(`btnReset`, g_lblNameObj.b_reset, () => {
964
+ if (window.confirm(g_msgObj.keyResetConfirm)) {
965
+ const keyCtrlPtn = `${g_keyObj.currentKey}_${g_keyObj.currentPtn}`;
966
+
967
+ for (let m = 0; m < g_keycons.cursorNumList.length; m++) {
968
+ const j = g_keycons.cursorNumList[m];
969
+ for (let k = 0; k < g_keyObj[`keyCtrl${keyCtrlPtn}`][j].length; k++) {
970
+ g_keyObj[`keyCtrl${keyCtrlPtn}`][j][k] = setIntVal(g_keyObj[`keyCtrl${keyCtrlPtn}d`][j][k]);
971
+ document.getElementById(`keycon${j}_${k}`).textContent = g_kCd[g_keyObj[`keyCtrl${keyCtrlPtn}`][j][k]];
972
+ changeKeyConfigColor(j, k, g_keyObj.currentPtn === -1 ? g_cssObj.keyconfig_Defaultkey : g_cssObj.title_base);
973
+ }
974
+ }
975
+ changeConfigCursor(0);
976
+ }
977
+ }, g_lblPosObj.btnKcReset, g_cssObj.button_Reset),
978
+
979
+ createCss2Button(`btnKeyLock`, getKeyLockName(), () => {
980
+ g_stateObj.keyLockFlg = !g_stateObj.keyLockFlg;
981
+ makeInfoWindow(g_msgInfoObj.I_0012.split(`{0}`).join(boolToSwitch(!g_stateObj.keyLockFlg)), `leftToRightFade`);
982
+ toggleKcDesc();
983
+ }, g_lblPosObj.btnKcKeyLock, g_cssObj.button_Mini),
984
+
985
+ // プレイ開始
986
+ makePlayButton(() => loadMusic())
987
+ );
988
+ toggleKcDesc();
989
+
990
+ // Selfパターン取込ボタン(条件成立時のみ表示)
991
+ const keyCtrlPtnForSelf = `${g_keyObj.currentKey}_${g_keyObj.currentPtn}`;
992
+ if (getTransKeySelfCtrl(keyCtrlPtnForSelf) !== undefined) {
993
+ divRoot.appendChild(
994
+ createCss2Button(`btnPtnSelf`, `Self`, () => applyTransKeySelfPattern(), {
995
+ x: g_lblPosObj.lblPattern.x + g_lblPosObj.lblPattern.w - 50,
996
+ y: g_lblPosObj.lblPattern.y - 14,
997
+ w: 50, h: 14, siz: 10, title: g_msgObj.ptnSelfImport,
998
+ }, g_cssObj.button_Mini)
999
+ );
1000
+ }
1001
+
1002
+ // キーボード押下時処理
1003
+ setShortcutEvent(g_currentPage, (kbCode) => {
1004
+ const C_KEY_ESCAPE = 27;
1005
+ const C_KEY_IME = 229;
1006
+ const keyCdObj = document.getElementById(`keycon${g_currentj}_${g_currentk}`);
1007
+ let setKey = g_kCdN.findIndex(kCd => kCd === kbCode);
1008
+
1009
+ if (g_stateObj.keyLockFlg) {
1010
+ if (setKey === C_KEY_ESCAPE) {
1011
+ btnBack.click();
1012
+ }
1013
+ return;
1014
+ }
1015
+
1016
+ // 全角切替、BackSpace、Deleteキー、Escキーは割り当て禁止
1017
+ // また、直前と同じキーを押した場合(BackSpaceを除く)はキー操作を無効にする
1018
+ const disabledKeys = [240, 242, 243, 244, 91, 29, 28, 259, g_prevKey];
1019
+
1020
+ if (g_localeObj.val === `Ja`) {
1021
+ disabledKeys.unshift(C_KEY_IME);
1022
+ }
1023
+ if (disabledKeys.includes(setKey) || g_kCdN[setKey] === undefined) {
1024
+ makeInfoWindow(g_msgInfoObj.I_0002, `fadeOut0`);
1025
+ return;
1026
+ } else if ((keyIsDown(g_kCdNameObj.metaLKey) || keyIsDown(g_kCdNameObj.metaRKey)) && keyIsShift()) {
1027
+ return;
1028
+ }
1029
+ if (selectedKc === `TitleBack` || selectedKc === `Retry` || selectedKc === `Pause`) {
1030
+ // タイトルバックキー、リトライキー、ポーズキーはプレイ中の操作キーと重複しないようにする(簡易的)
1031
+ if (g_keyObj[`keyCtrl${keyCtrlPtn}`].flat().includes(setKey)) {
1032
+ makeInfoWindow(g_msgInfoObj.I_0002, `fadeOut0`);
1033
+ return;
1034
+ }
1035
+ // プレイ中ショートカットキー変更
1036
+ g_headerObj[`key${selectedKc}`] = setKey;
1037
+ g_headerObj[`key${selectedKc}Def`] = setKey;
1038
+ document.getElementById(`sc${selectedKc}`).textContent = getScMsg[selectedKc]();
1039
+ document.getElementById(`sc${selectedKc}`).style.fontSize =
1040
+ wUnit(getFontSize2(getScMsg[selectedKc](), g_btnWidth(1 / 4) - 40, { maxSiz: g_limitObj.mainSiz }));
1041
+ if (g_isMac) {
1042
+ scTitleBack.textContent = getScMsg.TitleBack();
1043
+ scTitleBack.style.fontSize = wUnit(getFontSize2(getScMsg.TitleBack(), g_btnWidth(1 / 4) - 40, { maxSiz: g_limitObj.mainSiz }));
1044
+ }
1045
+ changeConfigColor(document.getElementById(`sc${selectedKc}`),
1046
+ g_headerObj[`key${selectedKc}`] === g_headerObj[`key${selectedKc}Def2`] ?
1047
+ g_cssObj.title_base : g_cssObj.keyconfig_Changekey);
1048
+ return;
1049
+ }
1050
+
1051
+ if (setKey === C_KEY_ESCAPE) {
1052
+ // リトライキー、タイトルバックキーにEscキーを割り当て可能にするため、
1053
+ // 例外的にEscキーで戻る対応をここで処理
1054
+ btnBack.click();
1055
+ return;
1056
+ } else if (setKey === C_KEY_TITLEBACK && g_currentk === 0) {
1057
+ return;
1058
+ }
1059
+
1060
+ if (setKey === C_KEY_RETRY && (!g_isMac || g_currentk === 0)) {
1061
+ // スキップ
1062
+ } else {
1063
+ // キー割り当て処理
1064
+ if (setKey === C_KEY_TITLEBACK || setKey === C_KEY_RETRY) {
1065
+ // キー無効化(代替キーのみ)
1066
+ setKey = 0;
1067
+ }
1068
+ if (g_keyObj[`keyCtrl${keyCtrlPtn}d`][g_currentj][g_currentk] !== setKey) {
1069
+ // 既定キーと異なる場合は色付け
1070
+ changeKeyConfigColor(g_currentj, g_currentk, g_cssObj.keyconfig_Changekey);
1071
+ }
1072
+ keyCdObj.textContent = g_kCd[setKey];
1073
+ g_keyObj[`keyCtrl${keyCtrlPtn}`][g_currentj][g_currentk] = setKey;
1074
+ g_prevKey = setKey;
1075
+ }
1076
+
1077
+ // カーソル移動
1078
+ if (g_currentk < g_keyObj[`keyCtrl${keyCtrlPtn}`][g_currentj].length - 1 && g_kcType !== `Main`) {
1079
+ // 後続に代替キーが存在する場合
1080
+ g_currentk++;
1081
+ cursor.style.top = wUnit(parseInt(cursor.style.top) + C_KYC_REPHEIGHT);
1082
+
1083
+ } else {
1084
+ changeConfigCursor();
1085
+ }
1086
+ });
1087
+
1088
+ // 戻るボタンのショートカットキー表示
1089
+ multiAppend(btnBack,
1090
+ createDivCss2Label(`scKeyConfigBack`, `${g_lblNameObj.sc_keyConfigBack})`, {
1091
+ x: 0, siz: 12, fontWeight: `bold`, opacity: 0.75, align: C_ALIGN_LEFT,
1092
+ })
1093
+ );
1094
+
1095
+ if (g_stateObj.kbPreviewFlg) {
1096
+ btnKbPreview.click();
1097
+ }
1098
+
1099
+ safeExecuteCustomHooks(`g_skinJsObj.keyconfig`, g_skinJsObj.keyconfig);
1100
+ document.onkeyup = evt => commonKeyUp(evt);
1101
+ document.oncontextmenu = () => false;
1102
+ };
1103
+
1104
+ /**
1105
+ * transKeyPtnの自動補完処理
1106
+ * - 明示指定(transKeyPtn)が無いパターンについて、
1107
+ * キー内で同一のtransKey対象へ向かうパターンを出現順(パターン番号昇順)に
1108
+ * 0から連番で補完する
1109
+ * @param {string[]} _keyList 対象とするキー名一覧
1110
+ */
1111
+ const completeTransKeyPtn = (_keyList) => {
1112
+ _keyList.forEach(keyName => {
1113
+ const counterMap = {};
1114
+ let ptnNum = 0;
1115
+ while (g_keyObj[`keyCtrl${keyName}_${ptnNum}`] !== undefined) {
1116
+ const keyPtn = `${keyName}_${ptnNum}`;
1117
+ const targetKey = g_keyObj[`transKey${keyPtn}`];
1118
+ if (hasVal(targetKey)) {
1119
+ if (g_keyObj[`transKeyPtn${keyPtn}`] === undefined) {
1120
+ counterMap[targetKey] = counterMap[targetKey] ?? 0;
1121
+ g_keyObj[`transKeyPtn${keyPtn}`] = counterMap[targetKey];
1122
+ counterMap[targetKey]++;
1123
+ } else {
1124
+ // 明示指定がある場合は以降の自動採番と重複しないよう位置を合わせる
1125
+ counterMap[targetKey] = Math.max(counterMap[targetKey] ?? 0, g_keyObj[`transKeyPtn${keyPtn}`] + 1);
1126
+ }
1127
+ }
1128
+ ptnNum++;
1129
+ }
1130
+ });
1131
+ };
1132
+
1133
+ // KeyLockボタンを押したときの表示切替
1134
+ const toggleKcDesc = () => {
1135
+ if (document.getElementById(`kcDesc`) !== null) {
1136
+ kcDesc.textContent = getKcDescMsg();
1137
+ kcDesc.style.fontSize = wUnit(getFontSize2(kcDesc.textContent, g_lblPosObj.kcDesc.w, { maxSiz: g_limitObj.mainSiz }));
1138
+ kcDesc.classList.remove(g_cssObj.title_base, g_cssObj.keyconfig_Defaultkey);
1139
+ kcDesc.classList.add(g_stateObj.keyLockFlg ? g_cssObj.keyconfig_Defaultkey : g_cssObj.title_base);
1140
+ btnKeyLock.innerHTML = getKeyLockName();
1141
+ }
1142
+ };
1143
+
1144
+ // KeyLockボタンの表示文字列を取得
1145
+ const getKcDescMsg = () =>
1146
+ g_stateObj.keyLockFlg
1147
+ ? g_lblNameObj.kcNonDesc
1148
+ : g_lblNameObj.kcDesc.split(`{0}`).join(g_kCd[C_KEY_RETRY]).split(`{1}:`).join(g_isMac ? `` : `Delete:`);
1149
+
1150
+ // KeyLockボタンで使用する絵文字の取得
1151
+ const getKeyLockName = () =>
1152
+ `${g_lblNameObj.b_keyLock}${g_stateObj.keyLockFlg ? g_emojiObj.locked : g_emojiObj.unlocked}`;
1153
+
1154
+ /**
1155
+ * キーボードレイアウトプレビュー(Canvas版)
1156
+ *
1157
+ * キーコンフィグ画面(divRoot)配下に以下の要素を追加する:
1158
+ * - [Preview] ボタン (createCss2Button)
1159
+ * - プレビュー用コンテナ div (createEmptySprite)
1160
+ * ├ キーボード背景 canvas (_state.canvasBase) ← 起動時のみ描画
1161
+ * └ マッピング強調 canvas (_state.canvasMap) ← キー変更・表示時に再描画
1162
+ *
1163
+ * 前提:
1164
+ * - g_kCd : グローバル定義済み。ロケール切替後は g_lang_kCd がマージ済み。
1165
+ * 未設定キーは空文字列 `""` で初期化。
1166
+ * 右Shift/Ctrl/Alt は 256/257/258 の独自コードで定義。
1167
+ * - g_btnWidth() : 画面の横幅(px)を返すグローバル関数。
1168
+ * - g_sHeight : 画面の縦幅(px)を保持するグローバル変数。
1169
+ * - createCss2Button, createEmptySprite : danoniplus 本体のグローバル関数。
1170
+ *
1171
+ * 使い方:
1172
+ * 【初期化時】
1173
+ * keyconfigKeyboardPreview.init(divRoot);
1174
+ *
1175
+ * 【キー割り当てが変わった時】
1176
+ * keyconfigKeyboardPreview.refresh();
1177
+ * ※ g_keyObj.currentKey / currentPtn から自動取得する
1178
+ *
1179
+ * 【キーコンフィグ画面を離脱する時】
1180
+ * keyconfigKeyboardPreview.dispose();
1181
+ */
1182
+
1183
+ const keyconfigKeyboardPreview = (() => {
1184
+
1185
+ // -------------------------------------------------------------------------
1186
+ // 定数
1187
+ // -------------------------------------------------------------------------
1188
+ const C_PREVIEW_ID = `kbPreviewArea`;
1189
+ const C_CANVAS_BASE_ID = `kbCanvasBase`;
1190
+ const C_CANVAS_MAP_ID = `kbCanvasMap`;
1191
+
1192
+ // 色定義(既存ゲームの配色に合わせたダーク系)
1193
+ const C_COLOR = {
1194
+ normal: {
1195
+ fill: `#1a1a2e`, // 通常キー背景
1196
+ stroke: `#555577`, // 通常キー枠
1197
+ text: `#ccccdd`, // 通常キー文字
1198
+ subText: `#888899`, // サブラベル(Shift面)
1199
+ },
1200
+ mapped: {
1201
+ fill: `#003366`, // メインキー背景
1202
+ stroke: `#4488ff`, // メインキー枠
1203
+ text: `#aaddff`, // メインキー文字
1204
+ },
1205
+ alt: {
1206
+ fill: `#3e3e1a`, // 代替キー背景
1207
+ stroke: `#777755`, // 代替キー枠
1208
+ text: `#eeeecc`, // 代替キー文字
1209
+ },
1210
+ shortcut: {
1211
+ fill: `#330011`, // ショートカットキー背景
1212
+ stroke: `#ff4466`, // ショートカットキー枠
1213
+ text: `#ffaacc`, // ショートカットキー文字
1214
+ },
1215
+ bgFill: `#0d0d1a`, // Canvas 背景
1216
+ legendText: `#cccccc`, // 凡例テキスト
1217
+ };
1218
+
1219
+ // 凡例エリアの高さ
1220
+ const LEGEND_H = 25;
1221
+
1222
+ // -------------------------------------------------------------------------
1223
+ // キーレイアウト定義
1224
+ //
1225
+ // 各行: { offsetX, keys }
1226
+ // offsetX : 行左端の水平オフセット(単位: BASE_KEY_W)。未指定時は0
1227
+ // keys : キー定義の配列
1228
+ //
1229
+ // 各キー: { code, w?, h?, label? }
1230
+ // code : KeyboardEvent.code(文字列)。空文字 "" はスペーサー(描画・キャッシュなし)。
1231
+ // w : 幅倍率(BASE_KEY_W 基準。省略時 1)
1232
+ // h : 高さ倍率(BASE_KEY_H 基準。省略時 1)
1233
+ // label : 省略時は g_kCd[keyCode] を参照。g_kCd が空文字のキーや
1234
+ // 左右を区別したいキーに指定する。
1235
+ // -------------------------------------------------------------------------
1236
+ /**
1237
+ * g_localeObj.val に応じた MAIN_ROWS を生成して返す。
1238
+ * drawBase / calcScale の都度呼び出し、locale 変化を反映する。
1239
+ *
1240
+ * @returns {Array} MAIN_ROWS 相当の配列
1241
+ */
1242
+ const buildMainRows = () => {
1243
+ const isJa = g_localeObj.val === `Ja`;
1244
+ return [
1245
+ // Row0: Fn キー行(JIS/US 共通)
1246
+ {
1247
+ keys: [
1248
+ { code: `Escape` },
1249
+ { code: ``, w: 0.5 }, // スペーサー
1250
+ { code: `F1` }, { code: `F2` }, { code: `F3` }, { code: `F4` },
1251
+ { code: ``, w: 0.25 }, // スペーサー
1252
+ { code: `F5` }, { code: `F6` }, { code: `F7` }, { code: `F8` },
1253
+ { code: ``, w: 0.25 }, // スペーサー
1254
+ { code: `F9` }, { code: `F10` }, { code: `F11` }, { code: `F12` },
1255
+ ],
1256
+ },
1257
+ // Row1: 数字行
1258
+ // JIS: ..., Minus, Equal, IntlYen, BS
1259
+ // US : ..., Minus, Equal, BS
1260
+ {
1261
+ keys: [
1262
+ { code: `Backquote` },
1263
+ { code: `Digit1` }, { code: `Digit2` }, { code: `Digit3` }, { code: `Digit4` }, { code: `Digit5` }, { code: `Digit6` },
1264
+ { code: `Digit7` }, { code: `Digit8` }, { code: `Digit9` }, { code: `Digit0` }, { code: `Minus` }, { code: `Equal` },
1265
+ ...(isJa
1266
+ ? [{ code: `IntlYen`, w: 0.75 }, { code: `Backspace`, label: `Back\nSpace` }] // JIS: IntlYen + BS
1267
+ : [{ code: `Backspace`, w: 1.7 }] // US : BS のみ(広い)
1268
+ ),
1269
+ ],
1270
+ },
1271
+ // Row2: QWERTY
1272
+ // JIS: ..., BracketLeft, BracketRight, Enter(縦長)
1273
+ // US : ..., BracketLeft, BracketRight, Backslash
1274
+ {
1275
+ keys: [
1276
+ { code: `Tab`, w: 1.5 },
1277
+ { code: `KeyQ` }, { code: `KeyW` }, { code: `KeyE` }, { code: `KeyR` }, { code: `KeyT` }, { code: `KeyY` },
1278
+ { code: `KeyU` }, { code: `KeyI` }, { code: `KeyO` }, { code: `KeyP` }, { code: `BracketLeft` },
1279
+ ...(isJa
1280
+ ? [{ code: `BracketRight` }, { code: `Enter`, w: 1.25, h: 2 }] // JIS: BracketRight + Enter縦長
1281
+ : [{ code: `BracketRight` }, { code: `Backslash`, w: 1.2 }] // US : BracketRight + Backslash
1282
+ ),
1283
+ ],
1284
+ },
1285
+ // Row3: ASDF
1286
+ // JIS: ..., KeyL, Semicolon, Quote, Backslash
1287
+ // US : ..., KeyL, Semicolon, Quote, Enter(横長)
1288
+ {
1289
+ keys: [
1290
+ { code: `CapsLock`, w: 1.75, label: `Caps\nLock` },
1291
+ { code: `KeyA` }, { code: `KeyS` }, { code: `KeyD` }, { code: `KeyF` }, { code: `KeyG` }, { code: `KeyH` },
1292
+ { code: `KeyJ` }, { code: `KeyK` }, { code: `KeyL` }, { code: `Semicolon` }, { code: `Quote` },
1293
+ ...(isJa
1294
+ ? [{ code: `Backslash` }] // JIS: Backslash(¥)
1295
+ : [{ code: `Enter`, w: 1.9 }] // US : Enter横長
1296
+ ),
1297
+ ],
1298
+ },
1299
+ // Row4: ZXCV
1300
+ // L)Shift の幅で行頭位置を揃える
1301
+ // JIS: L)Shift, ..., IntlRo, R)Shift
1302
+ // US : L)Shift, ..., R)Shift
1303
+ {
1304
+ keys: [
1305
+ { code: `ShiftLeft`, w: 2.25 },
1306
+ { code: `KeyZ` }, { code: `KeyX` }, { code: `KeyC` }, { code: `KeyV` }, { code: `KeyB` }, { code: `KeyN` },
1307
+ { code: `KeyM` }, { code: `Comma` }, { code: `Period` }, { code: `Slash` },
1308
+ ...(isJa
1309
+ ? [{ code: `IntlRo` }, { code: `ShiftRight`, w: 1.5 }] // JIS: IntlRo + R)Shift
1310
+ : [{ code: `ShiftRight`, w: 2.4 }] // US : R)Shift のみ(広い)
1311
+ ),
1312
+ ],
1313
+ },
1314
+ // Row5: スペースバー行
1315
+ // JIS: ..., NonConvert, Space, Convert, KanaMode, ...
1316
+ // US : ..., Space, ...
1317
+ {
1318
+ keys: [
1319
+ { code: `ControlLeft`, w: 1.25 }, { code: `MetaLeft` }, { code: `AltLeft` },
1320
+ ...(isJa
1321
+ // JIS: NonConvert + Space + Convert + KanaMode
1322
+ ? [{ code: `NonConvert` }, { code: `Space`, w: 5.25 }, { code: `Convert` }, { code: `KanaMode` }]
1323
+ // US : Space のみ(広い)
1324
+ : [{ code: `Space`, w: 8.25 }]
1325
+ ),
1326
+ { code: `AltRight` }, { code: `ContextMenu` },
1327
+ ...(isJa
1328
+ ? [{ code: `ControlRight`, w: 1.2 }]
1329
+ : [{ code: `ControlRight`, w: 1.05 }]
1330
+ ),
1331
+ ],
1332
+ },
1333
+ ];
1334
+ };
1335
+
1336
+ // 編集キークラスター(PrintSc/ScrollLk/Pause/Insert/Delete/Home/End/PgUp/PgDn + 矢印キー)
1337
+ // MAIN_ROWS と行インデックスを揃えて配置する。空行はスキップされる。
1338
+ const NAV_ROWS = [
1339
+ { keys: [{ code: `PrintScreen`, label: `Print\nScreen` }, { code: `ScrollLock`, label: `Scroll\nLock` }, { code: `Pause` }] },
1340
+ { keys: [{ code: `Insert` }, { code: `Home` }, { code: `PageUp`, label: `Page\nUp` }] }, // Insert Home PgUp
1341
+ { keys: [{ code: `Delete` }, { code: `End` }, { code: `PageDown`, label: `Page\nDown` }] }, // Delete End PgDn
1342
+ { keys: [] }, // ASDF行:空
1343
+ { keys: [{ code: `` }, { code: `ArrowUp` }, { code: `` }] }, // ↑
1344
+ { keys: [{ code: `ArrowLeft` }, { code: `ArrowDown` }, { code: `ArrowRight` }] }, // ← ↓ →
1345
+ ];
1346
+
1347
+ // テンキー(MAIN_ROWS と行インデックスを揃えて配置。1行目は空行で Fn行に揃える)
1348
+ // 標準テンキーレイアウト(2行目から):
1349
+ // [NumLk] [T/] [T*] [T-]
1350
+ // [T7][T8][T9] [T+]
1351
+ // [T4][T5][T6] [T+] ← T+ は縦2u
1352
+ // [T1][T2][T3] [TEnter]
1353
+ // [ T0 ][T.] [TEnter] ← T0 は横2u、TEnter は縦2u
1354
+ const NUM_ROWS = [
1355
+ { keys: [] },
1356
+ { keys: [{ code: `NumLock`, label: `Num\nLock` }, { code: `NumpadDivide` }, { code: `NumpadMultiply` }, { code: `NumpadSubtract` }] }, // NumLk T/ T* T-
1357
+ { keys: [{ code: `Numpad7` }, { code: `Numpad8` }, { code: `Numpad9` }, { code: `NumpadAdd`, h: 2 }] }, // T7 T8 T9 T+(縦2u)
1358
+ { keys: [{ code: `Numpad4` }, { code: `Numpad5` }, { code: `Numpad6` }] }, // T4 T5 T6
1359
+ { keys: [{ code: `Numpad1` }, { code: `Numpad2` }, { code: `Numpad3` }, { code: `NumpadEnter`, h: 2 }] }, // T1 T2 T3 TEnter(縦2u)
1360
+ { keys: [{ code: `Numpad0`, w: 2 }, { code: `NumpadDecimal` }] }, // T0(横2u) T.
1361
+ ];
1362
+
1363
+ // -------------------------------------------------------------------------
1364
+ // 内部状態
1365
+ // -------------------------------------------------------------------------
1366
+ const _state = {
1367
+ visible: false,
1368
+ mappedSet: new Set(), // メインキー(code文字列)
1369
+ altSet: new Set(), // 代替キー(code文字列)
1370
+ shortcutSet: new Set(), // ショートカットキー(code文字列)
1371
+ canvasBase: null,
1372
+ canvasMap: null,
1373
+ keyDataList: [], // { code, x, y, w, h, label } — drawMap で照合するキャッシュ
1374
+ scale: 1, // BASE_KEY_W/H に掛けるスケール係数
1375
+ cvsX: 0, // Canvas の左上 X 座標(divRoot 内の相対座標)
1376
+ cvsW: 500, // 実際の Canvas 幅(スケール計算後)
1377
+ cvsH: 240, // 実際の Canvas 高さ(スケール計算後)
1378
+ };
1379
+
1380
+ // -------------------------------------------------------------------------
1381
+ // スケール計算
1382
+ // -------------------------------------------------------------------------
1383
+
1384
+ /**
1385
+ * g_btnWidth() / g_sHeight を元にスケール係数と Canvas サイズを算出して
1386
+ * _state に書き込む。init 時に呼ぶ。
1387
+ *
1388
+ * 基準サイズ(フルキーボード = メイン + ナビクラスター):
1389
+ * 横: MAIN最大行幅 + ナビ幅(3キー) + 余白
1390
+ * 縦: 6行 × (BASE_KEY_H + BASE_KEY_GAP) - BASE_KEY_GAP
1391
+ * BASE_KEY_W = BASE_KEY_H = 28px、BASE_KEY_GAP = 3px を基準とする。
1392
+ */
1393
+ const BASE_KEY_W = 28;
1394
+ const BASE_KEY_H = 28;
1395
+ const BASE_KEY_GAP = 3;
1396
+ const MAIN_ROWS_LEN = 6; // MAIN_ROWS の行数(Fn行を含む)
1397
+
1398
+ // 行幅計算(スペーサー含む)
1399
+ const calcRowBaseW = row =>
1400
+ row.keys.reduce((acc, k) => acc + (k.w || 1) * BASE_KEY_W + BASE_KEY_GAP, -BASE_KEY_GAP);
1401
+
1402
+ const BASE_NAV_W = 3 * BASE_KEY_W + 2 * BASE_KEY_GAP; // NAV は 3列固定
1403
+ const BASE_ROW_H = MAIN_ROWS_LEN * (BASE_KEY_H + BASE_KEY_GAP) - BASE_KEY_GAP; // MAIN+NAV 分の高さ
1404
+ const NUM_ROWS_LEN = 6; // テンキーの行数(1行目は空行、2行目からテンキー配置)
1405
+ const NUM_GAP_H = BASE_KEY_H * 0.4; // テンキー上部の余白(基準キー高の40%)
1406
+ const BASE_NUM_ROW_H = NUM_ROWS_LEN * (BASE_KEY_H + BASE_KEY_GAP) - BASE_KEY_GAP; // テンキー部の高さ
1407
+ const BASE_NUM_W = 4 * BASE_KEY_W + 3 * BASE_KEY_GAP; // テンキー横幅(4列固定)
1408
+
1409
+ const calcScale = () => {
1410
+ // locale に依存した行幅を毎回計算する
1411
+ const rows = buildMainRows();
1412
+ const baseMainW = Math.max(...rows.map(calcRowBaseW));
1413
+ // 横幅: メイン + NAV + テンキー + 余白
1414
+ const totalW = baseMainW + BASE_KEY_GAP * 2 + BASE_NAV_W + BASE_KEY_GAP * 2
1415
+ + BASE_KEY_GAP * 3 + BASE_NUM_W;
1416
+
1417
+ const tkObj = getKeyInfo();
1418
+ const keyCtrlPtn = `${g_keyObj.currentKey}_${g_keyObj.currentPtn}`;
1419
+ const configKeyGroupList = g_headerObj.keyGroupOrder[g_stateObj.scoreId] ??
1420
+ g_keyObj[`keyGroupOrder${keyCtrlPtn}`] ?? tkObj.keyGroupList;
1421
+
1422
+ const availW = g_btnWidth() + (configKeyGroupList.length > 1 ? -g_lblPosObj.lnkKeySwitch.w - 10 : 0);
1423
+ const availH = g_sHeight - 200 - LEGEND_H; // 下部 UI ぶんを除いた高さ
1424
+
1425
+ // 縦幅基準: MAIN/NAV 高さ と テンキー高さ(余白込み)の大きい方
1426
+ const totalH = Math.max(BASE_ROW_H, BASE_NUM_ROW_H + Math.ceil(NUM_GAP_H));
1427
+
1428
+ const scaleW = availW / totalW;
1429
+ const scaleH = availH / totalH;
1430
+ _state.scale = Math.min(scaleW, scaleH, 1.5); // 最大 1.5 倍まで拡大可
1431
+
1432
+ _state.cvsX = configKeyGroupList.length > 1 ? (-g_lblPosObj.lnkKeySwitch.w - 10) / 2 : 0;
1433
+ _state.cvsW = Math.floor(totalW * _state.scale);
1434
+ _state.cvsH = Math.floor(totalH * _state.scale) + LEGEND_H;
1435
+ };
1436
+
1437
+ // -------------------------------------------------------------------------
1438
+ // ラベル取得
1439
+ // -------------------------------------------------------------------------
1440
+
1441
+ /**
1442
+ * code に対応する [primaryLabel, subLabel] を返す。
1443
+ *
1444
+ * @param {string} code
1445
+ * @param {string|undefined} forcedLabel - ROWS の label 指定がある場合に優先
1446
+ * @returns {string[]} [primary, sub]
1447
+ */
1448
+ const getKeyLabels = (code, forcedLabel) => {
1449
+ if (!code) return [``, ``];
1450
+ if (forcedLabel !== undefined) return [forcedLabel, ``];
1451
+
1452
+ // code 文字列から従来の keyCode を逆引きして g_kCd から取得
1453
+ const kc = g_kCdN.indexOf(code);
1454
+ if (kc >= 0) {
1455
+ const raw = g_kCd[kc];
1456
+ if (raw && raw !== g_kCd[0] && raw !== g_kCd[1]) {
1457
+ const parts = raw.split(` `);
1458
+ return [parts[0] || ``, parts[1] || ``];
1459
+ }
1460
+ }
1461
+ return [`?`, ``];
1462
+ };
1463
+
1464
+ // -------------------------------------------------------------------------
1465
+ // Canvas ヘルパー
1466
+ // -------------------------------------------------------------------------
1467
+
1468
+ // スケール済みの各寸法を返すヘルパー
1469
+ const kw = w => Math.floor(w * BASE_KEY_W * _state.scale + (w - 1) * BASE_KEY_GAP * _state.scale);
1470
+ const kh = h => Math.floor(h * BASE_KEY_H * _state.scale + (h - 1) * BASE_KEY_GAP * _state.scale);
1471
+ const kg = () => Math.max(1, Math.round(BASE_KEY_GAP * _state.scale));
1472
+ const kr = () => Math.max(2, Math.round(4 * _state.scale));
1473
+
1474
+ /**
1475
+ * Canvasの共通初期化処理
1476
+ * @param {HTMLCanvasElement} canvas
1477
+ * @returns {CanvasRenderingContext2D|null}
1478
+ */
1479
+ const setupCanvasContext = (canvas) => {
1480
+ if (!canvas) return null;
1481
+ canvas.style.top = wUnit(40);
1482
+ canvas.width = _state.cvsW * g_dpr;
1483
+ canvas.height = _state.cvsH * g_dpr;
1484
+ canvas.style.width = wUnit(_state.cvsW);
1485
+ canvas.style.height = wUnit(_state.cvsH);
1486
+
1487
+ const ctx = canvas.getContext(`2d`);
1488
+ ctx.scale(g_dpr, g_dpr);
1489
+ return ctx;
1490
+ };
1491
+
1492
+ /**
1493
+ * 円角矩形を描画する
1494
+ * @param {CanvasRenderingContext2D} ctx
1495
+ * @param {number} x
1496
+ * @param {number} y
1497
+ * @param {number} w
1498
+ * @param {number} h
1499
+ * @param {number} r
1500
+ */
1501
+ const roundRect = (ctx, x, y, w, h, r) => {
1502
+ ctx.beginPath();
1503
+ ctx.moveTo(x + r, y);
1504
+ ctx.lineTo(x + w - r, y);
1505
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
1506
+ ctx.lineTo(x + w, y + h - r);
1507
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
1508
+ ctx.lineTo(x + r, y + h);
1509
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
1510
+ ctx.lineTo(x, y + r);
1511
+ ctx.quadraticCurveTo(x, y, x + r, y);
1512
+ ctx.closePath();
1513
+ };
1514
+
1515
+ /**
1516
+ * 単一キーを描画する(枠の描画と内部テキストの書き込みを一括化)
1517
+ * @param {CanvasRenderingContext2D} ctx
1518
+ * @param {Object} keyData - 位置・サイズ・キーコードを含むキー情報
1519
+ * @param {Object} style - fill, stroke, text 等の色セット
1520
+ * @param {number} lw - 枠線の太さ(lineWidth)
1521
+ */
1522
+ const drawOneKey = (ctx, { keyData, style, lw = 1 }) => {
1523
+ const { x, y, w: keyW, h: keyH, code, label } = keyData;
1524
+
1525
+ // 1. キーの枠線・背景を描画
1526
+ roundRect(ctx, x + 0.5, y + 0.5, keyW - 1, keyH - 1, kr());
1527
+ ctx.fillStyle = style.fill;
1528
+ ctx.strokeStyle = style.stroke;
1529
+ ctx.lineWidth = lw;
1530
+ ctx.fill();
1531
+ ctx.stroke();
1532
+
1533
+ // 2. キー内部のテキスト(メイン・サブ)を描画
1534
+ const [primary, sub] = getKeyLabels(code, label);
1535
+
1536
+ const fs = (_textLen) => _textLen >= 5 * keyW / BASE_KEY_W
1537
+ ? Math.max(6, Math.floor(9 * _state.scale))
1538
+ : Math.max(7, Math.floor(11 * _state.scale));
1539
+
1540
+ // サブラベル(Shift面などの表記)がある場合
1541
+ if (sub) {
1542
+ ctx.fillStyle = style.subText || style.text;
1543
+ ctx.font = `bold ${Math.max(6, Math.floor(9 * _state.scale))}px monospace`;
1544
+ ctx.textAlign = `right`;
1545
+ ctx.textBaseline = `top`;
1546
+ ctx.fillText(sub, x + keyW - 2, y + 2);
1547
+ }
1548
+
1549
+ // メインラベルの描画(改行表記に対応)
1550
+ const [primary1, primary2] = primary.split(`\n`);
1551
+ ctx.fillStyle = style.text;
1552
+ ctx.textAlign = `center`;
1553
+ ctx.textBaseline = `middle`;
1554
+ const subDiff = sub ? 2 : 0;
1555
+
1556
+ if (primary2) {
1557
+ const siz = fs(Math.max(primary1.length, primary2.length));
1558
+ ctx.font = `bold ${siz}px monospace`;
1559
+ ctx.fillText(primary1, x + keyW / 2, y + keyH / 2 - siz / 2 + subDiff);
1560
+ ctx.fillText(primary2, x + keyW / 2, y + keyH / 2 + siz / 2 + subDiff);
1561
+ } else {
1562
+ ctx.font = `bold ${fs(primary.length)}px monospace`;
1563
+ ctx.fillText(primary, x + keyW / 2, y + keyH / 2 + subDiff);
1564
+ }
1565
+ };
1566
+
1567
+ // -------------------------------------------------------------------------
1568
+ // レイアウト計算・描画
1569
+ // -------------------------------------------------------------------------
1570
+
1571
+ /**
1572
+ * rows 配列から各キーの矩形座標を計算し、
1573
+ * canvas に描画しながら keyDataList へキャッシュする。
1574
+ *
1575
+ * @param {CanvasRenderingContext2D} ctx
1576
+ * @param {Array} rows - MAIN_ROWS または NAV_ROWS({offsetX, keys} 形式)
1577
+ * @param {number} originX - セクション左端の X 座標(canvas 座標)
1578
+ * @param {number} originY - セクション上端の Y 座標(canvas 座標)
1579
+ */
1580
+ const layoutSection = (ctx, rows, originX, originY) => {
1581
+ const gap = kg();
1582
+ const baseKeyH = kh(1);
1583
+
1584
+ rows.forEach((rowDef, rowIdx) => {
1585
+ if (rowDef.keys.length === 0) return;
1586
+
1587
+ const rowY = originY + rowIdx * (baseKeyH + gap);
1588
+ const startX = originX + Math.floor((rowDef.offsetX || 0) * BASE_KEY_W * _state.scale);
1589
+ let curX = startX;
1590
+
1591
+ rowDef.keys.forEach(keyDef => {
1592
+ const keyW = kw(keyDef.w || 1);
1593
+ const keyH = kh(keyDef.h || 1);
1594
+
1595
+ if (keyDef.code !== ``) {
1596
+ const keyData = {
1597
+ code: keyDef.code,
1598
+ x: curX,
1599
+ y: rowY,
1600
+ w: keyW,
1601
+ h: keyH,
1602
+ label: keyDef.label,
1603
+ };
1604
+ _state.keyDataList.push(keyData);
1605
+ drawOneKey(ctx, { keyData, style: C_COLOR.normal, lw: 1 });
1606
+ }
1607
+
1608
+ curX += keyW + gap;
1609
+ });
1610
+ });
1611
+ };
1612
+
1613
+ /**
1614
+ * キーボード背景レイヤーを描画し keyDataList をキャッシュする。
1615
+ * init 時に呼ぶ。
1616
+ */
1617
+ const drawBase = () => {
1618
+ const ctx = setupCanvasContext(_state.canvasBase);
1619
+ if (!ctx) return;
1620
+
1621
+ ctx.clearRect(0, 0, _state.cvsW, _state.cvsH);
1622
+ ctx.fillStyle = C_COLOR.bgFill;
1623
+ ctx.fillRect(0, 0, _state.cvsW, _state.cvsH);
1624
+
1625
+ _state.keyDataList = [];
1626
+
1627
+ const mainRows = buildMainRows();
1628
+ const gap = kg();
1629
+ const baseMainW = Math.max(...mainRows.map(calcRowBaseW));
1630
+ const mainW = Math.floor(baseMainW * _state.scale);
1631
+ const originY = gap;
1632
+ const navOriginX = mainW + gap * 3;
1633
+
1634
+ // テンキー: NAV クラスターの右に gap*3 の余白を空けて配置
1635
+ const numOriginX = navOriginX + Math.floor(BASE_NAV_W * _state.scale) + gap * 3;
1636
+ // テンキーは MAIN 全体に対して縦方向センタリング
1637
+ const numH = Math.floor(BASE_NUM_ROW_H * _state.scale);
1638
+ const mainH = Math.floor(BASE_ROW_H * _state.scale);
1639
+ const numOriginY = originY + Math.floor((mainH - numH) / 2);
1640
+
1641
+ layoutSection(ctx, mainRows, 0, originY);
1642
+ layoutSection(ctx, NAV_ROWS, navOriginX, originY);
1643
+ layoutSection(ctx, NUM_ROWS, numOriginX, numOriginY);
1644
+
1645
+ // 凡例
1646
+ const ly = _state.cvsH - 10;
1647
+ ctx.font = `${Math.max(9, Math.floor(12 * _state.scale))}px ${getBasicFont()}`;
1648
+ ctx.textAlign = `left`;
1649
+ ctx.textBaseline = `middle`;
1650
+
1651
+ const legends = [
1652
+ { style: C_COLOR.normal, label: g_lblNameObj.unallocated },
1653
+ { style: C_COLOR.mapped, label: g_lblNameObj.allocated },
1654
+ { style: C_COLOR.alt, label: g_lblNameObj.altAllocated },
1655
+ { style: C_COLOR.shortcut, label: g_lblNameObj.shortcutKey }
1656
+ ];
1657
+
1658
+ let lx = 8;
1659
+ legends.forEach(item => {
1660
+ roundRect(ctx, lx, ly - 5, 10, 10, 2);
1661
+ ctx.fillStyle = item.style.fill; ctx.fill();
1662
+ ctx.strokeStyle = item.style.stroke; ctx.lineWidth = 1; ctx.stroke();
1663
+ ctx.fillStyle = C_COLOR.legendText;
1664
+ ctx.fillText(item.label, lx + 14, ly);
1665
+ lx += ctx.measureText(item.label).width + 28;
1666
+ });
1667
+ };
1668
+
1669
+ /**
1670
+ * マッピング強調レイヤーを再描画する。
1671
+ * メインキー(mappedSet)を青系、代替キー(altSet)を黄系で色分けする。
1672
+ * 同一キーにメインと代替が重なる場合はメインを優先する。
1673
+ */
1674
+ const drawMap = () => {
1675
+ const ctx = setupCanvasContext(_state.canvasMap);
1676
+ if (!ctx) return;
1677
+
1678
+ ctx.clearRect(0, 0, _state.cvsW, _state.cvsH);
1679
+
1680
+ // キー状態に応じた色取得ロジック(優先度: ショートカット > メイン > 代替)
1681
+ const getKeyStyle = (code) => {
1682
+ if (_state.shortcutSet.has(code)) return C_COLOR.shortcut;
1683
+ if (_state.mappedSet.has(code)) return C_COLOR.mapped;
1684
+ if (_state.altSet.has(code)) return C_COLOR.alt;
1685
+ return null;
1686
+ };
1687
+
1688
+ _state.keyDataList.forEach(keyData => {
1689
+ const style = getKeyStyle(keyData.code);
1690
+ if (style) {
1691
+ drawOneKey(ctx, { keyData, style, lw: 1.5 });
1692
+ }
1693
+ });
1694
+ };
1695
+
1696
+ // -------------------------------------------------------------------------
1697
+ // 公開 API
1698
+ // -------------------------------------------------------------------------
1699
+
1700
+ /**
1701
+ * キーボードプレビューを初期化し、ボタンと Canvas コンテナを divRoot に追加する。
1702
+ *
1703
+ * @param {HTMLElement} divRoot - キーコンフィグ画面のルート div
1704
+ */
1705
+ const init = divRoot => {
1706
+ calcScale();
1707
+
1708
+ // "↓ Preview" → 展開、"↑ Preview" → 閉じる のトグルボタン
1709
+ const btn = createCss2Button(`btnKbPreview`, `↓ Preview`, () => {
1710
+ togglePreview();
1711
+ btn.textContent = _state.visible ? `↑ Preview` : `↓ Preview`;
1712
+ }, g_lblPosObj.btnKbPreview, g_cssObj.button_Setting);
1713
+ divRoot.appendChild(btn);
1714
+
1715
+ // プレビューエリア: 水平・垂直センタリング
1716
+ const areaX = g_btnX() + Math.floor((g_btnWidth() - _state.cvsW) / 2) + _state.cvsX;
1717
+ const areaY = 130;
1718
+
1719
+ const areaDiv = createEmptySprite(divRoot, C_PREVIEW_ID, {
1720
+ x: areaX, y: areaY - 60, w: _state.cvsW, h: _state.cvsH + 80,
1721
+ pointerEvents: C_DIS_AUTO, background: `#00000080`, display: C_DIS_NONE, overflow: `hidden`,
1722
+ });
1723
+
1724
+ const canvasBase = document.createElement(`canvas`);
1725
+ canvasBase.id = C_CANVAS_BASE_ID;
1726
+ areaDiv.appendChild(canvasBase);
1727
+ _state.canvasBase = canvasBase;
1728
+
1729
+ const canvasMap = document.createElement(`canvas`);
1730
+ canvasMap.id = C_CANVAS_MAP_ID;
1731
+ areaDiv.appendChild(canvasMap);
1732
+ _state.canvasMap = canvasMap;
1733
+
1734
+ drawBase();
1735
+ };
1736
+
1737
+ /**
1738
+ * プレビューの表示 / 非表示を切り替える。
1739
+ * 表示するたびにマッピングレイヤーを最新化する。
1740
+ */
1741
+ const togglePreview = () => {
1742
+ const area = document.getElementById(C_PREVIEW_ID);
1743
+ if (!area) return;
1744
+
1745
+ _state.visible = !_state.visible;
1746
+ area.style.display = _state.visible ? `block` : `none`;
1747
+ g_stateObj.kbPreviewFlg = _state.visible;
1748
+ g_stateObj.keyLockFlg = _state.visible;
1749
+ toggleKcDesc();
1750
+ if (_state.visible) refresh();
1751
+ };
1752
+
1753
+ /**
1754
+ * g_keyObj から現在のキー割り当てを取得して mappedSet / altSet を更新し、
1755
+ * プレビューが表示中なら即時再描画する。
1756
+ *
1757
+ * g_keyObj[`keyCtrl${keyCtrlPtn}`] は二次元配列:
1758
+ * [ [矢印0のメインkeyCode, 代替keyCode, ...], [矢印1のメインkeyCode, ...], ... ]
1759
+ * 各サブ配列の index 0 がメインキー、index 1 以降が代替キー。
1760
+ */
1761
+ const refresh = () => {
1762
+ const tkObj = getKeyInfo();
1763
+ const configKeyGroupList = g_headerObj.keyGroupOrder[g_stateObj.scoreId] ??
1764
+ g_keyObj[`keyGroupOrder${tkObj.keyCtrlPtn}`] ?? tkObj.keyGroupList;
1765
+ const ctrl = g_keyObj[`keyCtrl${tkObj.keyCtrlPtn}`]
1766
+ .filter((val, idx) => tkObj.keyGroupMaps[idx].includes(configKeyGroupList[g_keycons.keySwitchNum]));
1767
+
1768
+ // 数値から code 文字列へ安全に変換するヘルパー
1769
+ const toCodeStr = (num) => g_kCdN[num] || ``;
1770
+
1771
+ _state.mappedSet = new Set(ctrl.map(arr => toCodeStr(arr[0])).filter(v => v !== ``));
1772
+ _state.altSet = new Set(ctrl.flatMap(arr => arr.slice(1)).map(toCodeStr).filter(v => v !== ``));
1773
+
1774
+ // プレイ中ショートカット: keyRetry / keyTitleBack / keyPause は g_headerObj から取得、PgDn(34) / PgUp(33) は固定
1775
+ _state.shortcutSet = new Set(
1776
+ [g_headerObj.keyRetry, g_headerObj.keyTitleBack, g_headerObj.keyPause, 34, 33].map(toCodeStr).filter(v => v !== ``)
1777
+ );
1778
+
1779
+ if (_state.visible) drawMap();
1780
+ };
1781
+
1782
+ /**
1783
+ * プレビューを強制非表示にしてリセットする。
1784
+ * キーコンフィグ画面の離脱時に呼ぶ。
1785
+ */
1786
+ const dispose = () => {
1787
+ _state.visible = false;
1788
+ _state.mappedSet = new Set();
1789
+ _state.altSet = new Set();
1790
+ _state.shortcutSet = new Set();
1791
+ _state.keyDataList = [];
1792
+ _state.canvasBase = null;
1793
+ _state.canvasMap = null;
1794
+ };
1795
+
1796
+ return { init, refresh, togglePreview, dispose };
1797
+
1798
+ })();
1799
+
1800
+ /**
1801
+ * 回転できないオブジェクトの場合に設定の自動絞り込みを行う
1802
+ */
1803
+ const changeSettingListsForImg = () => {
1804
+ if (g_stateObj.rotateEnabled) {
1805
+ g_settings.camoufrages = g_settings.camoufrageRotates.concat();
1806
+ } else {
1807
+ g_settings.camoufrages = g_settings.camoufrageNoRotates.concat();
1808
+ if (g_stateObj.camoufrage.startsWith(`Color`)) {
1809
+ g_stateObj.camoufrage = `Color`;
1810
+ } else {
1811
+ g_stateObj.camoufrage = C_FLG_OFF;
1812
+ }
1813
+ }
1814
+ g_settings.camoufrageNum = g_settings.camoufrages.findIndex(val => val === g_stateObj.camoufrage);
1815
+ };
1816
+
1817
+ /**
1818
+ * 影矢印色の取得
1819
+ * @param {number} _colorPos
1820
+ * @param {string} _arrowColor
1821
+ * @returns {string}
1822
+ */
1823
+ const getShadowColor = (_colorPos, _arrowColor) => g_headerObj.setShadowColor[_colorPos] === `Default` ?
1824
+ _arrowColor : g_headerObj.setShadowColor[_colorPos];
1825
+
1826
+ /**
1827
+ * デフォルトの曲中ショートカットをキー数・キーパターンにより切り替え
1828
+ * - キーコンフィグ画面で個別に変えていた場合は変更しない
1829
+ */
1830
+ const setPlayingShortcut = () => {
1831
+ const keyCtrlPtn = `${g_keyObj.currentKey}_${g_keyObj.currentPtn}`;
1832
+ if (g_headerObj.keyRetryDef === C_KEY_RETRY) {
1833
+ g_headerObj.keyRetry = setIntVal(getKeyCtrlVal(g_keyObj[`keyRetry${keyCtrlPtn}`]), g_headerObj.keyRetryDef);
1834
+ }
1835
+ if (g_headerObj.keyTitleBackDef === C_KEY_TITLEBACK) {
1836
+ g_headerObj.keyTitleBack = setIntVal(getKeyCtrlVal(g_keyObj[`keyTitleBack${keyCtrlPtn}`]), g_headerObj.keyTitleBackDef);
1837
+ }
1838
+ if (g_headerObj.keyPauseDef === C_KEY_PAUSE) {
1839
+ g_headerObj.keyPause = setIntVal(getKeyCtrlVal(g_keyObj[`keyPause${keyCtrlPtn}`]), g_headerObj.keyPauseDef);
1840
+ }
1841
+ };
1842
+
1843
+ /**
1844
+ * ステップゾーン間隔、大きさの更新
1845
+ * @param {string} _header
1846
+ * @param {string} _keyCtrlPtn
1847
+ */
1848
+ const updateKeyInfo = (_header, _keyCtrlPtn) => {
1849
+ const kcPtn = hasVal(g_keyObj[`${_header}${_keyCtrlPtn}`]) ? _keyCtrlPtn : `_def`;
1850
+ g_keyObj[_header] = g_keyObj[`${_header}${kcPtn}`];
1851
+ };
1852
+
1853
+ /**
1854
+ * 初期矢印色・フリーズアロー色の変更
1855
+ * - ここでのID管理は1譜面目も区別して設定する (setScoreIdHeaderの第三引数を使用)
1856
+ */
1857
+ const changeSetColor = () => {
1858
+ const isDefault = g_keycons.colorDefTypes.includes(g_colorType);
1859
+ const idHeader = setScoreIdHeader(g_stateObj.scoreId, false, true);
1860
+ const defaultType = idHeader + g_colorType;
1861
+ const currentTypes = {
1862
+ '': (isDefault ? defaultType : g_colorType),
1863
+ 'Shadow': (isDefault ? defaultType : `${idHeader}Default`),
1864
+ };
1865
+ Object.keys(currentTypes).forEach(pattern => {
1866
+ g_headerObj[`set${pattern}Color`] = structuredClone(g_headerObj[`set${pattern}Color${currentTypes[pattern]}`]);
1867
+ if (g_headerObj[`frz${pattern}Color`] === undefined) {
1868
+ g_headerObj[`frz${pattern}Color`] = fillArray(g_headerObj.setColorInit.length, ``);
1869
+ }
1870
+ for (let j = 0; j < g_headerObj.setColorInit.length; j++) {
1871
+ g_headerObj[`frz${pattern}Color`][j] = structuredClone(g_headerObj[`frz${pattern}Color${currentTypes[pattern]}`][j]);
1872
+ }
1873
+ if (!isDefault) {
1874
+ g_headerObj[`set${pattern}Color`] = structuredClone(g_headerObj[`set${pattern}Color${g_colorType}`]);
1875
+ }
1876
+ });
1877
+
1878
+ // 影矢印が未指定の場合はType1, Type2の影矢印指定を無くす
1879
+ if (!hasVal(g_headerObj[`setShadowColor${idHeader}Default`][0]) && [`Type1`, `Type2`].includes(g_colorType)) {
1880
+ g_headerObj.setShadowColor = fillArray(g_headerObj.setColorInit.length, ``);
1881
+ }
1882
+ };
1883
+
1884
+ /**
1885
+ * コンフィグの色変更
1886
+ * @param {HTMLDivElement} _obj
1887
+ * @param {string} _cssName
1888
+ */
1889
+ const changeConfigColor = (_obj, _cssName) => {
1890
+ _obj.classList.remove(g_cssObj.keyconfig_Changekey, g_cssObj.keyconfig_Defaultkey, g_cssObj.title_base);
1891
+ _obj.classList.add(_cssName);
1892
+ };
1893
+
1894
+ /**
1895
+ * シャッフルグループの色変更
1896
+ * - デフォルト値と違う番号になった場合、色付けする
1897
+ * @param {string} _keyCtrlPtn キーコンフィグパターン
1898
+ * @param {number[]} _vals シャッフルグループ番号(群)
1899
+ * @param {number} _j (-1: 全体に対して色付け, それ以外: 指定箇所のみ色付け)
1900
+ */
1901
+ const changeShuffleConfigColor = (_keyCtrlPtn, _vals, _j = -1) => {
1902
+ const changeTargetColor = (_val, _k) => {
1903
+ const isEqualShuffleGr = (_val === g_keyObj[`shuffle${_keyCtrlPtn}_0d`][_k]);
1904
+ changeConfigColor(document.getElementById(`sArrow${_k}`), isEqualShuffleGr ? g_cssObj.title_base : g_cssObj.keyconfig_Changekey);
1905
+ };
1906
+
1907
+ if (_j === -1) {
1908
+ _vals.forEach((val, m) => changeTargetColor(val, m));
1909
+ } else {
1910
+ changeTargetColor(_vals, _j);
1911
+ }
1912
+ };
1913
+
1914
+ /**
1915
+ * カラーグループ、シャッフルグループの再設定
1916
+ * @param {string} _type
1917
+ * @param {string} _keyCtrlPtn
1918
+ */
1919
+ const resetGroupList = (_type, _keyCtrlPtn) => {
1920
+ let k = 1;
1921
+ g_keycons[`${_type}Groups`] = [0];
1922
+
1923
+ while (g_keyObj[`${_type}${_keyCtrlPtn}_${k}`] !== undefined) {
1924
+ g_keycons[`${_type}Groups`].push(k);
1925
+ k++;
1926
+ }
1927
+ if (g_keyObj.currentPtn === -1
1928
+ && (_type !== `stepRtn` || (_type === `stepRtn` && g_keycons[`${_type}Groups`].length > 1))) {
1929
+ g_keycons[`${_type}Groups`] = addValtoArray(g_keycons[`${_type}Groups`], -1);
1930
+ }
1931
+ g_keycons[`${_type}GroupNum`] = Math.min(g_keyObj.currentPtn, 0);
1932
+ };