danoniplus 48.5.2 → 49.1.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.
package/js/danoni_main.js CHANGED
@@ -4,12 +4,12 @@
4
4
  *
5
5
  * Source by tickle
6
6
  * Created : 2018/10/08
7
- * Revised : 2026/06/23
7
+ * Revised : 2026/07/02
8
8
  *
9
9
  * https://github.com/cwtickle/danoniplus
10
10
  */
11
- const g_version = `Ver 48.5.2`;
12
- const g_revisedDate = `2026/06/23`;
11
+ const g_version = `Ver 49.1.0`;
12
+ const g_revisedDate = `2026/07/02`;
13
13
 
14
14
  // カスタム用バージョン (danoni_custom.js 等で指定可)
15
15
  let g_localVersion = ``;
@@ -1492,6 +1492,53 @@ const getEmojiForCanvas = _str => {
1492
1492
  return result;
1493
1493
  };
1494
1494
 
1495
+ /**
1496
+ * 複数行に跨る可能性のある文字列を、改行タグ付きの文字列とフォントサイズに変換
1497
+ * @param {string} _targetStr
1498
+ * @param {number} _maxWidth
1499
+ * @param {object} object
1500
+ * @param {string} [object.font=getBasicFont()]
1501
+ * @param {number} [object.maxSiz=14]
1502
+ * @param {number} [object.minSiz=5]
1503
+ * @param {number} [object.maxSizMulti=maxSiz]
1504
+ * @param {string} [object.prefix='']
1505
+ * @param {number} [object.len=30]
1506
+ * @param {string} [object.delim=' ']
1507
+ * @returns {[string, number]}
1508
+ */
1509
+ const getFontSizeMulti = (_targetStr, _maxWidth, { font = getBasicFont(),
1510
+ maxSiz = 14, minSiz = 5, maxSizMulti = maxSiz, prefix = ``, len = 20, delim = ` ` } = {}) => {
1511
+
1512
+ // _targetStr が長い場合のみ、その中身を 2つ に分解する
1513
+ if (_targetStr.length > len) {
1514
+ let breakNum = -1;
1515
+ const halfIndex = Math.floor(_targetStr.length / 2);
1516
+
1517
+ // 文字列の中央から前に向かってスペースを探す
1518
+ for (let j = halfIndex; j > 0; j--) {
1519
+ if (_targetStr[j] === delim) {
1520
+ breakNum = j;
1521
+ break;
1522
+ }
1523
+ }
1524
+ // スペースがなければ、中央(よりやや左)で強制分割
1525
+ if (breakNum === -1) {
1526
+ breakNum = halfIndex;
1527
+ }
1528
+ const isSpace = _targetStr[breakNum] === delim;
1529
+ const firstPart = _targetStr.slice(0, breakNum);
1530
+ const secondPart = _targetStr.slice(isSpace ? breakNum + delim.length : breakNum);
1531
+
1532
+ // 難易度名の中に <br> を仕込む
1533
+ _targetStr = `${firstPart}<br>${secondPart}`;
1534
+ }
1535
+
1536
+ // 3. 最終的な文字列を結合(prefix と難易度名の1つ目の塊が1行目になる)
1537
+ const fullStr = `${prefix}${_targetStr}`;
1538
+
1539
+ return [fullStr, getFontSize2(fullStr, _maxWidth, { font, maxSiz: _targetStr.includes(`<br>`) ? maxSizMulti : maxSiz, minSiz })];
1540
+ };
1541
+
1495
1542
  /**
1496
1543
  * 指定した横幅に合ったフォントサイズを取得
1497
1544
  * @param {string} _str
@@ -1502,8 +1549,16 @@ const getEmojiForCanvas = _str => {
1502
1549
  * @returns {number}
1503
1550
  */
1504
1551
  const getFontSize2 = (_str, _maxWidth, { font = getBasicFont(), maxSiz = 14, minSiz = 5 } = {}) => {
1552
+ // 文字列を改行で分割(null/undefined 対策も含む)
1553
+ const lines = _str ? _str.split('<br>') : [];
1554
+ if (lines.length === 0) return maxSiz;
1555
+
1556
+ // 大きいサイズから順に試す
1505
1557
  for (let siz = maxSiz; siz >= minSiz; siz--) {
1506
- if (_maxWidth >= getStrWidth(getLongestStr(_str?.split(`<br>`)), siz, font)) {
1558
+ // すべての行が _maxWidth 以内に収まるかチェック
1559
+ const isFitAllLines = lines.every(line => _maxWidth >= getStrWidth(line, siz, font));
1560
+
1561
+ if (isFitAllLines) {
1507
1562
  return siz;
1508
1563
  }
1509
1564
  }
@@ -2459,7 +2514,7 @@ const drawTitleResultMotion = _displayName =>
2459
2514
  // WebAudioAPIでAudio要素風に再生するクラス
2460
2515
  class AudioPlayer {
2461
2516
  constructor() {
2462
- this._context = new AudioContext();
2517
+ this._context = getSharedAudioContext();
2463
2518
  this._gain = this._context.createGain();
2464
2519
  this._gain.connect(this._context.destination);
2465
2520
  this._startTime = 0;
@@ -2507,11 +2562,8 @@ class AudioPlayer {
2507
2562
  }
2508
2563
 
2509
2564
  close() {
2510
- if (this._context) {
2511
- this._context.close()?.catch(() => {/* ignore double-close */ });
2512
- this._buffer = null;
2513
- }
2514
2565
  if (this._source) {
2566
+ this._source.stop(0);
2515
2567
  this._source.disconnect(this._gain);
2516
2568
  this._source = null;
2517
2569
  }
@@ -2586,10 +2638,33 @@ class AudioPlayer {
2586
2638
  this._eventListeners[_type] = this._eventListeners[_type].filter(_element => _element !== _listener);
2587
2639
  }
2588
2640
 
2641
+ getBuffer() {
2642
+ return this._buffer;
2643
+ }
2644
+
2645
+ setBuffer(_buffer) {
2646
+ this._buffer = _buffer;
2647
+ this._duration = _buffer.duration;
2648
+ this._eventListeners[`canplaythrough`]?.forEach(_listener => _listener());
2649
+ }
2650
+
2589
2651
  load() { }
2590
2652
  dispatchEvent() { }
2591
2653
  }
2592
2654
 
2655
+ // グローバルで1つだけ保持(遅延生成)
2656
+ let g_sharedAudioContext = null;
2657
+ const getSharedAudioContext = () => {
2658
+ if (!g_sharedAudioContext) {
2659
+ g_sharedAudioContext = new AudioContext();
2660
+ }
2661
+ // タブのバックグラウンド化等でsuspendedになることがあるため念のためresume
2662
+ if (g_sharedAudioContext.state === `suspended`) {
2663
+ g_sharedAudioContext.resume();
2664
+ }
2665
+ return g_sharedAudioContext;
2666
+ };
2667
+
2593
2668
  /**
2594
2669
  * クリップボードコピー関数
2595
2670
  * 入力値をクリップボードへコピーし、メッセージを表示
@@ -6315,23 +6390,22 @@ const playBGM = async (_num, _currentLoopNum = g_settings.musicLoopNum) => {
6315
6390
 
6316
6391
  if (encodeFlg) {
6317
6392
  try {
6318
- // base64エンコードは読込に時間が掛かるため、曲変更時のみ読込
6319
- if (!hasVal(g_musicdata) || Math.abs(_num) % g_headerObj.musicIdxList.length !== 0) {
6320
- closeExistingAudio();
6393
+ closeExistingAudio();
6394
+ if (g_audioBufferCache.has(url)) {
6395
+ const tmpAudio = new AudioPlayer();
6396
+ tmpAudio.setBuffer(g_audioBufferCache.get(url)); // 新設メソッド、decode不要
6397
+ g_audioForMS = tmpAudio;
6398
+ } else {
6321
6399
  await loadScript2(url);
6322
6400
  musicInit();
6323
- if (!isTitle()) {
6324
- g_musicdata = ``;
6325
- return;
6326
- }
6401
+ if (!isTitle()) { g_musicdata = ``; return; }
6402
+
6327
6403
  const tmpAudio = new AudioPlayer();
6328
- const array = Uint8Array.from(atob(g_musicdata), v => v.charCodeAt(0));
6404
+ const array = base64ToUint8Array(g_musicdata);
6329
6405
  await tmpAudio.init(array.buffer);
6330
- if (!isTitle()) {
6331
- g_musicdata = ``;
6332
- tmpAudio.close();
6333
- return;
6334
- }
6406
+ if (!isTitle()) { g_musicdata = ``; tmpAudio.close(); return; }
6407
+
6408
+ cacheAudioBuffer(url, tmpAudio.getBuffer());
6335
6409
  g_audioForMS = tmpAudio;
6336
6410
  }
6337
6411
  g_audioForMS.volume = g_stateObj.bgmVolume / 100;
@@ -7179,12 +7253,21 @@ const makeDifList = (_difList, _targetKey = ``) => {
7179
7253
  g_headerObj.viewLists.forEach(j => {
7180
7254
  const keyLabel = g_headerObj.keyLabels[j];
7181
7255
  if (_targetKey === `` || keyLabel === _targetKey) {
7182
- let text = `${getKeyName(keyLabel)} / ${g_headerObj.difLabels[j]}`;
7256
+
7257
+ // 譜面名の表示
7258
+ const prefix = `${getKeyName(keyLabel)} / `;
7259
+ let text = `${g_headerObj.difLabels[j]}`;
7183
7260
  if (g_headerObj.makerView) {
7184
7261
  text += ` (${g_headerObj.creatorNames[j]})`;
7185
7262
  }
7186
- _difList.appendChild(makeDifLblCssButton(`dif${k}`, text, k, () => nextDifficulty(j - g_stateObj.scoreId),
7187
- { btnStyle: (j === g_stateObj.scoreId ? `Setting` : `Default`) }));
7263
+ // キー種と譜面名に分割し、譜面名が長すぎる場合は二段に分割して表示
7264
+ const [difText, difSiz] = getFontSizeMulti(text, g_limitObj.difSelectorWidth, {
7265
+ maxSiz: g_limitObj.difSelectorSiz, maxSizMulti: 9, prefix, len: 30,
7266
+ })
7267
+ _difList.appendChild(makeDifLblCssButton(`dif${k}`, difText, k, () => nextDifficulty(j - g_stateObj.scoreId), {
7268
+ btnStyle: (j === g_stateObj.scoreId ? `Setting` : `Default`), siz: difSiz,
7269
+ }));
7270
+ document.getElementById(`dif${k}`).style.lineHeight = `9px`;
7188
7271
  if (j === g_stateObj.scoreId) {
7189
7272
  pos = k + 6.5 * (g_sHeight - 239) / 261;
7190
7273
  curk = k;
@@ -8136,9 +8219,14 @@ const setDifficulty = (_initFlg) => {
8136
8219
  const difWidth = parseFloat(lnkDifficulty.style.width) - 20;
8137
8220
  const transKeyName = getTransKeyName();
8138
8221
  const keyUnitName = getStgDetailName(getKeyUnitName(g_keyObj.currentKey));
8139
- const difNames = [`${getKeyName(g_keyObj.currentKey)}${transKeyName} ${keyUnitName} / ${g_headerObj.difLabels[g_stateObj.scoreId]}`];
8140
- lnkDifficulty.style.fontSize = wUnit(getFontSize2(difNames[0], difWidth, { maxSiz: g_limitObj.setLblSiz }));
8141
8222
 
8223
+ const prefix = `${getKeyName(g_keyObj.currentKey)}${transKeyName} ${keyUnitName} / `;
8224
+ let difLabel = `${g_headerObj.difLabels[g_stateObj.scoreId]}`;
8225
+
8226
+ const [difName, difSiz] = getFontSizeMulti(difLabel, difWidth, { maxSiz: g_limitObj.setLblSiz, prefix });
8227
+ lnkDifficulty.style.fontSize = wUnit(difSiz);
8228
+
8229
+ const difNames = [difName];
8142
8230
  if (g_headerObj.makerView) {
8143
8231
  difNames.push(`(${g_headerObj.creatorNames[g_stateObj.scoreId]})`);
8144
8232
  difNames.forEach((difName, j) => {
@@ -8998,6 +9086,9 @@ const setExcessive = (_btn, _val) => {
8998
9086
  const getKeyCtrl = (_localStorage, _extraKeyName = ``) => {
8999
9087
  g_keyObj.storagePtn = _localStorage[`keyCtrlPtn${_extraKeyName}`];
9000
9088
  const basePtn = `${g_keyObj.currentKey}_${g_keyObj.storagePtn}`;
9089
+ if (g_keyObj[`keyCtrl${basePtn}`] === undefined || hasVal(g_keyObj[`transKey${basePtn}`])) {
9090
+ return;
9091
+ }
9001
9092
  const baseKeyNum = g_keyObj[`${g_keyObj.defaultProp}${basePtn}`].length;
9002
9093
 
9003
9094
  if (_localStorage[`keyCtrl${_extraKeyName}`]?.[0].length > 0) {
@@ -10098,6 +10189,11 @@ const keyConfigInit = (_kcType = g_kcType, _initFlg = false) => {
10098
10189
  // 譜面初期情報ロード許可フラグ
10099
10190
  g_canLoadDifInfoFlg = false;
10100
10191
 
10192
+ if (_initFlg) {
10193
+ g_stateObj.keyLockFlg = false;
10194
+ g_stateObj.kbPreviewFlg = false;
10195
+ }
10196
+
10101
10197
  multiAppend(divRoot,
10102
10198
 
10103
10199
  // キーコンフィグ画面タイトル
@@ -10105,8 +10201,7 @@ const keyConfigInit = (_kcType = g_kcType, _initFlg = false) => {
10105
10201
  `<div class="settings_Title">${g_lblNameObj.key}</div><div class="settings_Title2">${g_lblNameObj.config}</div>`
10106
10202
  .replace(/[\t\n]/g, ``), 0, 15, g_cssObj.flex_centering),
10107
10203
 
10108
- createDescDiv(`kcDesc`, g_lblNameObj.kcDesc.split(`{0}`).join(g_kCd[C_KEY_RETRY])
10109
- .split(`{1}:`).join(g_isMac ? `` : `Delete:`)),
10204
+ createDescDiv(`kcDesc`, getKcDescMsg()),
10110
10205
 
10111
10206
  createDescDiv(`kcShuffleDesc`,
10112
10207
  g_headerObj.shuffleUse && g_settings.shuffles.filter(val => val.endsWith(`+`)).length > 0
@@ -10236,11 +10331,24 @@ const keyConfigInit = (_kcType = g_kcType, _initFlg = false) => {
10236
10331
  * @param {number} _scrollNum
10237
10332
  */
10238
10333
  const changeTmpColor = (_j, _scrollNum = 1) => {
10239
- changeTmpData(`color`, g_headerObj.setColor.length, _j, _scrollNum);
10240
- const arrowColor = getKeyConfigColor(_j, g_keyObj[`color${keyCtrlPtn}`][_j]);
10241
- $id(`arrow${_j}`).background = arrowColor;
10242
- $id(`arrowShadow${_j}`).background = getShadowColor(g_keyObj[`color${keyCtrlPtn}`][_j], arrowColor);
10334
+ const changeTmpOneColor = _idx => {
10335
+ changeTmpData(`color`, g_headerObj.setColor.length, _idx, _scrollNum);
10336
+ const arrowColor = getKeyConfigColor(_idx, g_keyObj[`color${keyCtrlPtn}`][_idx]);
10337
+ $id(`arrow${_idx}`).background = arrowColor;
10338
+ $id(`arrowShadow${_idx}`).background = getShadowColor(g_keyObj[`color${keyCtrlPtn}`][_idx], arrowColor);
10339
+ };
10243
10340
 
10341
+ if (g_stateObj.keyLockFlg && keyIsShift()) {
10342
+ const tmpList = [];
10343
+ g_keyObj[`color${keyCtrlPtn}`].forEach((val, idx) => {
10344
+ if (val === g_keyObj[`color${keyCtrlPtn}`][_j]) {
10345
+ tmpList.push(idx);
10346
+ }
10347
+ });
10348
+ tmpList.forEach(idx => changeTmpOneColor(idx));
10349
+ } else {
10350
+ changeTmpOneColor(_j);
10351
+ }
10244
10352
  adjustScrollPoint(parseFloat($id(`arrow${_j}`).left));
10245
10353
  };
10246
10354
 
@@ -10250,10 +10358,23 @@ const keyConfigInit = (_kcType = g_kcType, _initFlg = false) => {
10250
10358
  * @param {number} _scrollNum
10251
10359
  */
10252
10360
  const changeTmpShuffleNum = (_j, _scrollNum = 1) => {
10253
- const tmpShuffle = changeTmpData(`shuffle`, g_keyObj[`keyCtrl${keyCtrlPtn}`].length - 1, _j, _scrollNum);
10254
- document.getElementById(`sArrow${_j}`).textContent = tmpShuffle + 1;
10361
+ const changeTmpOneShuffle = _idx => {
10362
+ const tmpShuffle = changeTmpData(`shuffle`, g_keyObj[`keyCtrl${keyCtrlPtn}`].length - 1, _idx, _scrollNum);
10363
+ document.getElementById(`sArrow${_idx}`).textContent = tmpShuffle + 1;
10364
+ changeShuffleConfigColor(keyCtrlPtn, g_keyObj[`shuffle${keyCtrlPtn}_${g_keycons.shuffleGroupNum}`][_idx], _idx);
10365
+ };
10255
10366
 
10256
- changeShuffleConfigColor(keyCtrlPtn, g_keyObj[`shuffle${keyCtrlPtn}_${g_keycons.shuffleGroupNum}`][_j], _j);
10367
+ if (g_stateObj.keyLockFlg && keyIsShift()) {
10368
+ const tmpList = [];
10369
+ g_keyObj[`shuffle${keyCtrlPtn}`].forEach((val, idx) => {
10370
+ if (val === g_keyObj[`shuffle${keyCtrlPtn}`][_j]) {
10371
+ tmpList.push(idx);
10372
+ }
10373
+ });
10374
+ tmpList.forEach(idx => changeTmpOneShuffle(idx));
10375
+ } else {
10376
+ changeTmpOneShuffle(_j);
10377
+ }
10257
10378
  adjustScrollPoint(parseFloat($id(`arrow${_j}`).left));
10258
10379
  };
10259
10380
 
@@ -10941,17 +11062,30 @@ const keyConfigInit = (_kcType = g_kcType, _initFlg = false) => {
10941
11062
  }
10942
11063
  }, g_lblPosObj.btnKcReset, g_cssObj.button_Reset),
10943
11064
 
11065
+ createCss2Button(`btnKeyLock`, getKeyLockName(), () => {
11066
+ g_stateObj.keyLockFlg = !g_stateObj.keyLockFlg;
11067
+ makeInfoWindow(g_msgInfoObj.I_0012.split(`{0}`).join(boolToSwitch(!g_stateObj.keyLockFlg)), `leftToRightFade`);
11068
+ toggleKcDesc();
11069
+ }, g_lblPosObj.btnKcKeyLock, g_cssObj.button_Mini),
11070
+
10944
11071
  // プレイ開始
10945
11072
  makePlayButton(() => loadMusic())
10946
11073
  );
11074
+ toggleKcDesc();
10947
11075
 
10948
11076
  // キーボード押下時処理
10949
11077
  setShortcutEvent(g_currentPage, (kbCode) => {
11078
+ const C_KEY_ESCAPE = 27;
11079
+ const C_KEY_IME = 229;
10950
11080
  const keyCdObj = document.getElementById(`keycon${g_currentj}_${g_currentk}`);
10951
11081
  let setKey = g_kCdN.findIndex(kCd => kCd === kbCode);
10952
11082
 
10953
- const C_KEY_ESCAPE = 27;
10954
- const C_KEY_IME = 229;
11083
+ if (g_stateObj.keyLockFlg) {
11084
+ if (setKey === C_KEY_ESCAPE) {
11085
+ btnBack.click();
11086
+ }
11087
+ return;
11088
+ }
10955
11089
 
10956
11090
  // 全角切替、BackSpace、Deleteキー、Escキーは割り当て禁止
10957
11091
  // また、直前と同じキーを押した場合(BackSpaceを除く)はキー操作を無効にする
@@ -11032,11 +11166,33 @@ const keyConfigInit = (_kcType = g_kcType, _initFlg = false) => {
11032
11166
  })
11033
11167
  );
11034
11168
 
11169
+ if (g_stateObj.kbPreviewFlg) {
11170
+ btnKbPreview.click();
11171
+ }
11172
+
11035
11173
  safeExecuteCustomHooks(`g_skinJsObj.keyconfig`, g_skinJsObj.keyconfig);
11036
11174
  document.onkeyup = evt => commonKeyUp(evt);
11037
11175
  document.oncontextmenu = () => false;
11038
11176
  };
11039
11177
 
11178
+ const toggleKcDesc = () => {
11179
+ if (document.getElementById(`kcDesc`) !== null) {
11180
+ kcDesc.textContent = getKcDescMsg();
11181
+ kcDesc.style.fontSize = wUnit(getFontSize2(kcDesc.textContent, g_lblPosObj.kcDesc.w, { maxSiz: g_limitObj.mainSiz }));
11182
+ kcDesc.classList.remove(g_cssObj.title_base, g_cssObj.keyconfig_Defaultkey);
11183
+ kcDesc.classList.add(g_stateObj.keyLockFlg ? g_cssObj.keyconfig_Defaultkey : g_cssObj.title_base);
11184
+ btnKeyLock.innerHTML = getKeyLockName();
11185
+ }
11186
+ };
11187
+
11188
+ const getKcDescMsg = () =>
11189
+ g_stateObj.keyLockFlg
11190
+ ? g_lblNameObj.kcNonDesc
11191
+ : g_lblNameObj.kcDesc.split(`{0}`).join(g_kCd[C_KEY_RETRY]).split(`{1}:`).join(g_isMac ? `` : `Delete:`);
11192
+
11193
+ const getKeyLockName = () =>
11194
+ `${g_lblNameObj.b_keyLock}${g_stateObj.keyLockFlg ? g_emojiObj.locked : g_emojiObj.unlocked}`;
11195
+
11040
11196
  /**
11041
11197
  * キーボードレイアウトプレビュー(Canvas版)
11042
11198
  *
@@ -11612,7 +11768,9 @@ const keyconfigKeyboardPreview = (() => {
11612
11768
 
11613
11769
  _state.visible = !_state.visible;
11614
11770
  area.style.display = _state.visible ? `block` : `none`;
11615
-
11771
+ g_stateObj.kbPreviewFlg = _state.visible;
11772
+ g_stateObj.keyLockFlg = _state.visible;
11773
+ toggleKcDesc();
11616
11774
  if (_state.visible) refresh();
11617
11775
  };
11618
11776
 
@@ -11830,7 +11988,7 @@ const loadMusic = () => {
11830
11988
  const blobUrl = URL.createObjectURL(request.response);
11831
11989
  createEmptySprite(divRoot, `loader`, g_windowObj.loader);
11832
11990
  lblLoading.textContent = g_lblNameObj.pleaseWait;
11833
- setAudio(blobUrl);
11991
+ setAudio(blobUrl, url);
11834
11992
  } else {
11835
11993
  makeWarningWindow(`${g_msgInfoObj.E_0041.split('{0}').join(getFullPath(url))}<br>(${request.status} ${request.statusText})`, { backBtnUse: true });
11836
11994
  }
@@ -11862,8 +12020,9 @@ const loadMusic = () => {
11862
12020
  * 音楽データの設定
11863
12021
  * iOSの場合はAudioタグによる再生
11864
12022
  * @param {string} _url
12023
+ * @param {string} _cacheKey
11865
12024
  */
11866
- const setAudio = async (_url) => {
12025
+ const setAudio = async (_url, _cacheKey = _url) => {
11867
12026
 
11868
12027
  const loadMp3 = () => {
11869
12028
  if (g_isFile) {
@@ -11895,7 +12054,7 @@ const setAudio = async (_url) => {
11895
12054
  await loadScript2(_url);
11896
12055
  if (typeof musicInit === C_TYP_FUNCTION) {
11897
12056
  musicInit();
11898
- readyToStart(() => initWebAudioAPIfromBase64(g_musicdata));
12057
+ readyToStart(() => initWebAudioAPIfromBase64(g_musicdata, _cacheKey));
11899
12058
  } else {
11900
12059
  makeWarningWindow(g_msgInfoObj.E_0031);
11901
12060
  musicAfterLoaded();
@@ -11906,11 +12065,19 @@ const setAudio = async (_url) => {
11906
12065
  };
11907
12066
 
11908
12067
  // Base64から音声データに変換してWebAudioAPIで再生する準備
11909
- const initWebAudioAPIfromBase64 = async (_base64) => {
12068
+ const initWebAudioAPIfromBase64 = async (_base64, _cacheKey) => {
11910
12069
  g_audio = new AudioPlayer();
11911
12070
  musicAfterLoaded();
11912
- const array = Uint8Array.from(atob(_base64), v => v.charCodeAt(0))
12071
+
12072
+ if (_cacheKey && g_audioBufferCache.has(_cacheKey)) {
12073
+ g_audio.setBuffer(g_audioBufferCache.get(_cacheKey)); // デコード完全スキップ
12074
+ return;
12075
+ }
12076
+ const array = base64ToUint8Array(_base64);
11913
12077
  await g_audio.init(array.buffer);
12078
+ if (_cacheKey) {
12079
+ cacheAudioBuffer(_cacheKey, g_audio.getBuffer());
12080
+ }
11914
12081
  };
11915
12082
 
11916
12083
  // 音声ファイルを読み込んでWebAudioAPIで再生する準備
@@ -11922,6 +12089,26 @@ const initWebAudioAPIfromURL = async (_url) => {
11922
12089
  await g_audio.init(arrayBuffer);
11923
12090
  };
11924
12091
 
12092
+ const g_audioBufferCache = new Map();
12093
+ const AUDIO_CACHE_MAX = 5;
12094
+
12095
+ const cacheAudioBuffer = (_key, _buffer) => {
12096
+ g_audioBufferCache.set(_key, _buffer);
12097
+ if (g_audioBufferCache.size > AUDIO_CACHE_MAX) {
12098
+ g_audioBufferCache.delete(g_audioBufferCache.keys().next().value); // 古い順に破棄
12099
+ }
12100
+ };
12101
+
12102
+ const base64ToUint8Array = (_base64Str) => {
12103
+ const binaryStr = atob(_base64Str);
12104
+ const len = binaryStr.length;
12105
+ const array = new Uint8Array(len);
12106
+ for (let i = 0; i < len; i++) {
12107
+ array[i] = binaryStr.charCodeAt(i);
12108
+ }
12109
+ return array;
12110
+ };
12111
+
11925
12112
  const musicAfterLoaded = () => {
11926
12113
  g_audio.load();
11927
12114
 
@@ -16725,7 +16912,8 @@ const resultInit = () => {
16725
16912
  makeCssResultPlayData(`lblMusicData`, dataRX, g_cssObj.result_style, 0, mTitleForView[0]),
16726
16913
  makeCssResultPlayData(`lblMusicData2`, dataRX, g_cssObj.result_style, 1, mTitleForView[1]),
16727
16914
  makeCssResultPlayData(`lblDifficulty`, lblRX, g_cssObj.result_lbl, 2, g_lblNameObj.rt_Difficulty, C_ALIGN_LEFT),
16728
- makeCssResultPlayData(`lblDifData`, dataRX, g_cssObj.result_style, 2, settingData.difData),
16915
+ makeCssResultPlayData(`lblDifData`, dataRX, g_cssObj.result_style, 2, settingData.difData, C_ALIGN_CENTER,
16916
+ { siz: getFontSize2(settingData.difData, 350) }),
16729
16917
  makeCssResultPlayData(`lblStyle`, lblRX, g_cssObj.result_lbl, 3, g_lblNameObj.rt_Style, C_ALIGN_LEFT),
16730
16918
  makeCssResultPlayData(`lblStyleData`, dataRX, g_cssObj.result_style, 3, settingData.playStyleData),
16731
16919
  makeCssResultPlayData(`lblDisplay`, lblRX, g_cssObj.result_lbl, 4, g_lblNameObj.rt_Display, C_ALIGN_LEFT),
@@ -16734,21 +16922,9 @@ const resultInit = () => {
16734
16922
  );
16735
16923
 
16736
16924
  // 設定項目が多い場合に2行に分解して表示する処理
16737
- let playStyleBreakNum = lblStyleData.textContent.length;
16738
- if (lblStyleData.textContent.length > 60) {
16739
- for (let j = Math.floor(lblStyleData.textContent.length / 2); j > 0; j--) {
16740
- if (lblStyleData.textContent[j] === `,`) {
16741
- playStyleBreakNum = j + 2;
16742
- break;
16743
- }
16744
- }
16745
- lblStyleData.style.top = `${parseFloat(lblStyleData.style.top) - 3}px`;
16746
- lblStyleData.innerHTML = `${lblStyleData.textContent.slice(0, playStyleBreakNum)}<br>` +
16747
- `${lblStyleData.textContent.slice(playStyleBreakNum)}`;
16748
- lblStyleData.style.fontSize = `${getFontSize2(lblStyleData.textContent.slice(0, playStyleBreakNum), 350, { maxSiz: 10 })}px`;
16749
- } else {
16750
- lblStyleData.style.fontSize = `${getFontSize2(lblStyleData.textContent, 350)}px`;
16751
- }
16925
+ const [styleStr, styleSiz] = getFontSizeMulti(settingData.playStyleData, 350, { maxSizMulti: 10, len: 60 });
16926
+ lblStyleData.innerHTML = styleStr;
16927
+ lblStyleData.style.fontSize = wUnit(styleSiz);
16752
16928
 
16753
16929
  /**
16754
16930
  * キャラクタ、スコア描画のID共通部、色CSS名、スコア変数名
@@ -17182,6 +17358,7 @@ const resultInit = () => {
17182
17358
  const artistName = g_headerObj.artistNames[g_headerObj.musicNos[g_stateObj.scoreId]] || g_headerObj.artistName;
17183
17359
  const logicalWidth = 400;
17184
17360
  const logicalHeight = g_sHeight - 90;
17361
+ const flapWidth = 370;
17185
17362
 
17186
17363
  canvas.id = `resultImage`;
17187
17364
  canvas.width = logicalWidth * g_dpr;
@@ -17210,13 +17387,14 @@ const resultInit = () => {
17210
17387
  drawText(unEscapeHtml(mTitleForView[1]), { hy: 2 });
17211
17388
  drawText(`${getEmojiForCanvas(g_emojiObj.memo)} ${unEscapeHtml(g_headerObj.tuning)} / ${getEmojiForCanvas(g_emojiObj.musical)} ${unEscapeHtml(artistName)}`,
17212
17389
  { hy: mTitleForView[1] !== `` ? 3 : 2, siz: 12 });
17213
- drawText(unEscapeHtml(settingData.difDataForImage), { hy: 4 });
17390
+ drawText(unEscapeHtml(settingData.difDataForImage), { hy: 4, siz: getFontSize2(settingData.difDataForImage, flapWidth) });
17214
17391
 
17215
17392
  if (settingData.playStyleData.length > 60) {
17216
- drawText(settingData.playStyleData.slice(0, playStyleBreakNum), { hy: 5, siz: getFontSize2(settingData.playStyleData.slice(0, playStyleBreakNum), 370) });
17217
- drawText(settingData.playStyleData.slice(playStyleBreakNum), { hy: 6, siz: getFontSize2(settingData.playStyleData.slice(playStyleBreakNum), 370) });
17393
+ const strs = styleStr.split(`<br>`);
17394
+ drawText(strs[0], { hy: 5, siz: getFontSize2(strs[0], flapWidth) });
17395
+ drawText(strs[1], { hy: 6, siz: getFontSize2(strs[1], flapWidth) });
17218
17396
  } else {
17219
- drawText(settingData.playStyleData, { hy: 5, siz: getFontSize2(settingData.playStyleData, 370, { maxSiz: 15 }) });
17397
+ drawText(settingData.playStyleData, { hy: 5, siz: getFontSize2(settingData.playStyleData, flapWidth, { maxSiz: 15 }) });
17220
17398
  }
17221
17399
  Object.keys(jdgScoreObj).forEach(score => {
17222
17400
  drawText(g_lblNameObj[`j_${score}`], { hy: 7 + jdgScoreObj[score].pos, color: jdgScoreObj[score].dfColor });
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Source by tickle
7
7
  * Created : 2019/11/19
8
- * Revised : 2026/06/14 (v48.5.0)
8
+ * Revised : 2026/06/23 (v49.0.0)
9
9
  *
10
10
  * https://github.com/cwtickle/danoniplus
11
11
  */
@@ -640,6 +640,10 @@ const updateWindowSiz = () => {
640
640
  x: g_btnX() + Math.floor((g_btnWidth() - 80) / 2), y: 3, w: 80, h: 18, siz: 11,
641
641
  title: g_msgObj.displayPreview,
642
642
  },
643
+ btnKcKeyLock: {
644
+ x: g_btnX() + Math.floor((g_btnWidth() - 80) / 2) + 90, y: 3, w: 80, h: 18, siz: 11,
645
+ title: g_msgObj.keyLock,
646
+ },
643
647
 
644
648
  btnKcBack: {
645
649
  x: g_btnX(1 / 3), y: g_sHeight - 75,
@@ -1062,6 +1066,8 @@ const g_emojiObj = {
1062
1066
  memo: `&#x1f4dd;`, // メモ (memo)
1063
1067
  musical: `&#x1f3b5;`, // 音符 (musical note)
1064
1068
  camera: `&#x1f4f7;`, // カメラ (camera)
1069
+ locked: `&#x1f512;`, // 鍵 (locked)
1070
+ unlocked: `&#x1f513;`, // 鍵開 (unlocked)
1065
1071
  };
1066
1072
 
1067
1073
  /** 設定・オプション画面用共通 */
@@ -1284,6 +1290,9 @@ const g_stateObj = {
1284
1290
  rotateEnabled: true,
1285
1291
  flatStepHeight: C_ARW_WIDTH,
1286
1292
 
1293
+ keyLockFlg: false,
1294
+ kbPreviewFlg: false,
1295
+
1287
1296
  dm_environment: C_FLG_OFF,
1288
1297
  dm_highscores: C_FLG_OFF,
1289
1298
  dm_customKey: C_FLG_OFF,
@@ -3597,7 +3606,7 @@ const g_keyObj = {
3597
3606
  // - _0 の数字部分をカウントアップすることで実現できる。
3598
3607
  keyCtrl5_1: [[`Space`], [`Left`], [`Down`], [`Up`], [`Right`]],
3599
3608
  keyCtrl7_1: [[`S`], [`E`], [`F`], [`Space`, `G`, `H`], [`J`], [`I`], [`L`]],
3600
- keyCtrl8_1: [[`Enter`], [`S`], [`D`], [`F`], [`Space`], [`J`], [`K`], [`L`]],
3609
+ keyCtrl8_1: [[`Tab`], [`S`], [`D`], [`F`], [`Space`], [`J`], [`K`], [`L`]],
3601
3610
  keyCtrl9A_1: [[`S`], [`D`], [`E`, `R`], [`F`], [`Space`], [`Left`], [`Down`], [`Up`], [`Right`]],
3602
3611
  keyCtrl9i_1: [[`A`], [`S`], [`D`], [`F`], [`Space`], [`Left`], [`Down`], [`Up`], [`Right`]],
3603
3612
  keyCtrl11_1: [[`S`], [`D`], [`F`], [`Space`], [`J`], [`K`], [`L`], [`Left`], [`Down`], [`Up`], [`Right`]],
@@ -3658,7 +3667,7 @@ const g_keyObj = {
3658
3667
  // ショートカットキーコード
3659
3668
  keyRetry: 8, // 8: Backspace
3660
3669
  keyRetry8_0: 9, // 9: Tab
3661
- keyRetry8_1: 9,
3670
+ keyRetry8_1: 8, // 8: Backspace
3662
3671
  keyRetry11j_0: 123, // 123: F12
3663
3672
 
3664
3673
  keyTitleBack: 46, // 46: Delete
@@ -3666,9 +3675,8 @@ const g_keyObj = {
3666
3675
 
3667
3676
  // 別キー
3668
3677
  transKey8_2: '12',
3669
- transKey15A_1: '',
3670
- transKey15B_0: '',
3671
- transKey15B_1: '',
3678
+ transKey15A_1: '15B', // 15A/15Bはキーパターンをコピーして生成するため、transKeyを明示的に指定
3679
+ transKey15B_0: '', // 15A/15Bはキーパターンをコピーして生成するため、transKeyを明示的に指定
3672
3680
 
3673
3681
  // キー置換用(ParaFla版との互換)
3674
3682
  keyTransPattern: {
@@ -4461,6 +4469,7 @@ const g_lang_msgInfoObj = {
4461
4469
  I_0006: `ローカルストレージ情報をクリップボードにコピーしました!`,
4462
4470
  I_0007: `オブジェクト情報をクリップボードにコピーしました!`,
4463
4471
  I_0011: `指定した部分キーが未定義のため、画面表示できません。設定を見直してください。`,
4472
+ I_0012: `キー割り当てモードを{0}に変更しました。`,
4464
4473
  },
4465
4474
  En: {
4466
4475
  W_0001: `Your browser is not guaranteed to work.<br>
@@ -4511,6 +4520,7 @@ const g_lang_msgInfoObj = {
4511
4520
  I_0006: `Local storage information copied to clipboard!`,
4512
4521
  I_0007: `Object information copied to clipboard!`,
4513
4522
  I_0011: `The specified partial key is undefined and cannot be displayed on the screen. Please review your settings.`,
4523
+ I_0012: `Key assignment mode changed to {0}.`,
4514
4524
  },
4515
4525
  };
4516
4526
 
@@ -4566,6 +4576,7 @@ const g_lblNameObj = {
4566
4576
  b_close: `Close`,
4567
4577
  b_cReset: `Reset`,
4568
4578
  b_precond: `Precondition`,
4579
+ b_keyLock: `KeyLock`,
4569
4580
 
4570
4581
  Difficulty: `Difficulty`,
4571
4582
  Speed: `Speed`,
@@ -4818,6 +4829,7 @@ const g_lang_lblNameObj = {
4818
4829
  dataDeleteONDesc: `セーフモード適用中はデータ消去は行えません。変更するにはセーフモードを解除してください`,
4819
4830
 
4820
4831
  kcDesc: `[{0}:スキップ / {1}:(代替キーのみ)キー無効化]`,
4832
+ kcNonDesc: `キー割り当て無効化中です。解除するには「KeyLock」を押してください`,
4821
4833
  kcShuffleDesc: `番号をクリックでシャッフルグループ、矢印をクリックでカラーグループを変更`,
4822
4834
  kcNoShuffleDesc: `矢印をクリックでカラーグループを変更`,
4823
4835
  sdDesc: `[クリックでON/OFFを切替、灰色でOFF]`,
@@ -4881,6 +4893,7 @@ const g_lang_lblNameObj = {
4881
4893
  dataDeleteONDesc: `Data erasure cannot be performed while safe mode is applied. <br>Please deactivate the safe mode to change the data.`,
4882
4894
 
4883
4895
  kcDesc: `[{0}:Skip / {1}:Key invalidation (Alternate keys only)]`,
4896
+ kcNonDesc: `Key assignments are currently disabled. Press "KeyLock" to disable this setting.`,
4884
4897
  kcShuffleDesc: `Click the number to change the shuffle group, and click the arrow to change the color.`,
4885
4898
  kcNoShuffleDesc: `Click the arrow to change the color group.`,
4886
4899
  sdDesc: `[Click to switch, gray to OFF]`,
@@ -5052,6 +5065,7 @@ const g_lang_msgObj = {
5052
5065
  stepRtnGroup: `矢印などノーツの種類、回転に関するパターンを切り替えます。\nあらかじめ設定されている場合のみ変更可能です。`,
5053
5066
  kcReset: `対応するキーの割り当てを元に戻します。`,
5054
5067
  kcPreview: `キーボードレイアウトのプレビューを表示/非表示します。`,
5068
+ keyLock: `キー割り当ての有効/無効を切り替えます。\n無効化時はシフトキー+番号/矢印クリックで同一グループの矢印群をまとめてグループ変更できます。`,
5055
5069
 
5056
5070
  pickArrow: `色番号ごとの矢印色(枠、塗りつぶし)、通常時のフリーズアロー色(枠、帯)を\nカラーピッカーから選んで変更できます。`,
5057
5071
  pickColorR: `設定する矢印色の種類を切り替えます。`,
@@ -5159,6 +5173,7 @@ const g_lang_msgObj = {
5159
5173
  stepRtnGroup: `Switches the type of notes, such as arrows, and the pattern regarding rotation.\nThis can only be changed if it has been set in advance.`,
5160
5174
  kcReset: `Restores the corresponding key assignments.`,
5161
5175
  kcPreview: `Show/hide the preview of the keyboard layout.`,
5176
+ keyLock: `Toggles key assignments on or off. \nWhen disabled, you can use the Shift key plus a number or arrow key \nto change the group of all arrows in the same group at once.`,
5162
5177
 
5163
5178
  pickArrow: `Change the frame or fill of arrow color and the frame or bar of normal freeze-arrow color\nfor each color number from the color picker.`,
5164
5179
  pickColorR: `Switches the arrow color type to be set.`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "danoniplus",
3
- "version": "48.5.2",
3
+ "version": "49.1.0",
4
4
  "description": "Dancing☆Onigiri (CW Edition) - Web-based Rhythm Game",
5
5
  "main": "./js/danoni_main.js",
6
6
  "jsdelivr": "./js/danoni_main.js",