danoniplus 47.1.0 → 47.1.1

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.
Files changed (2) hide show
  1. package/js/danoni_main.js +240 -141
  2. package/package.json +1 -1
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/04/21
7
+ * Revised : 2026/04/22
8
8
  *
9
9
  * https://github.com/cwtickle/danoniplus
10
10
  */
11
- const g_version = `Ver 47.1.0`;
12
- const g_revisedDate = `2026/04/21`;
11
+ const g_version = `Ver 47.1.1`;
12
+ const g_revisedDate = `2026/04/22`;
13
13
 
14
14
  // カスタム用バージョン (danoni_custom.js 等で指定可)
15
15
  let g_localVersion = ``;
@@ -213,6 +213,7 @@ const g_detailObj = {
213
213
  speedData: [],
214
214
  boostData: [],
215
215
  toolDif: [],
216
+ miniMapParams: {},
216
217
  scoreMinimap: {},
217
218
  scoreMinimapReverse: {},
218
219
  scoreMinimapHeader: {},
@@ -3371,153 +3372,237 @@ const storeBaseData = (_scoreId, _scoreObj, _keyCtrlPtn) => {
3371
3372
  g_detailObj.playingFrame[_scoreId] = playingFrame;
3372
3373
  g_detailObj.playingFrameWithBlank[_scoreId] = lastFrame - startFrame;
3373
3374
 
3374
- const generateMinimap = (_scoreId, _scoreObj, _keyNum, _playingFrame, _firstArrowFrame, _isReverse = false) => {
3375
- // 高さを演奏時間に比例させる (例: 1フレーム = 0.5px)
3376
- const scale = 1.5;
3377
- const dpr = window.devicePixelRatio || 1; // デバイスのピクセル比を取得(通常 2〜3)
3378
-
3379
- const timeMargin = 35; // 時間表示用の左マージン
3380
- const mmWidth = (g_sWidth - 500) / 2 + 290; // 基準となるコマの横幅(親divより少し短くする)
3381
- const mmHeightTotal = _playingFrame * scale;
3382
- const mmMarginY = 2;
3383
- const laneWidth = Math.min(Math.floor((mmWidth - timeMargin) / _keyNum), 40);
3384
- const logicalWidth = timeMargin + (laneWidth * _keyNum);
3385
-
3386
- // ヘッダー用キャンバスの作成
3387
- const canvasHeader = document.createElement(`canvas`);
3388
- const ctxHeader = canvasHeader.getContext(`2d`);
3389
- canvasHeader.width = logicalWidth * dpr;
3390
- canvasHeader.height = 15 * dpr;
3391
- canvasHeader.style.width = `${logicalWidth}px`;
3392
- canvasHeader.style.height = `15px`;
3393
- ctxHeader.scale(dpr, dpr);
3394
- ctxHeader.fillStyle = '#999';
3395
- ctxHeader.font = `10px ${getBasicFont()}`;
3396
- ctxHeader.textAlign = 'center';
3397
-
3398
- for (let j = 0; j < _keyNum; j++) {
3399
- const x = timeMargin + j * laneWidth + laneWidth / 2;
3400
- ctxHeader.fillText(g_kCd[g_keyObj[`keyCtrl${_keyCtrlPtn}`][j][0]], x, 10);
3401
- ctxHeader.strokeStyle = '#444';
3402
- }
3403
-
3404
- g_detailObj.scoreMinimapHeader[_scoreId] = canvasHeader;
3405
-
3406
- // 譜面全体のキャンバスの作成
3407
- const MAX_CANVAS_HEIGHT = 10000; // 1枚あたりの高さ制限(安全圏)
3408
- const canvasCount = Math.ceil((mmHeightTotal + mmMarginY * 2) / MAX_CANVAS_HEIGHT);
3409
- const canvases = [];
3410
-
3411
- for (let i = 0; i < canvasCount; i++) {
3412
- const cvs = document.createElement('canvas');
3413
- const h = (i === canvasCount - 1)
3414
- ? (mmHeightTotal + mmMarginY * 2) - (MAX_CANVAS_HEIGHT * i)
3415
- : MAX_CANVAS_HEIGHT;
3416
-
3417
- cvs.width = logicalWidth * dpr;
3418
- cvs.height = h * dpr;
3419
- cvs.style.width = `${logicalWidth}px`;
3420
- cvs.style.height = `${h}px`;
3421
- cvs.style.display = 'block'; // 隙間防止
3422
- const ctx = cvs.getContext('2d');
3423
- ctx.scale(dpr, dpr);
3424
- canvases.push({ canvas: cvs, ctx: ctx, offsetTop: i * MAX_CANVAS_HEIGHT });
3425
- }
3426
-
3427
- // --- 描画先を振り分けるヘルパー関数 ---
3428
- const drawOnTarget = (y, height, drawFunc) => {
3429
- canvases.forEach(item => {
3430
- // 描画要素がCanvasの範囲内に入っているか判定
3431
- if (y + height >= item.offsetTop && y <= item.offsetTop + (item.canvas.height / dpr)) {
3432
- item.ctx.save();
3433
- item.ctx.translate(0, -item.offsetTop);
3434
- drawFunc(item.ctx);
3435
- item.ctx.restore();
3436
- }
3437
- });
3438
- };
3375
+ // --- ミニマップ設定 ---
3376
+ g_detailObj.miniMapParams[_scoreId] = {
3377
+ _scoreId, _scoreObj, _keyNum: keyNum,
3378
+ _playingFrame: playingFrame,
3379
+ _firstArrowFrame: firstArrowFrame,
3380
+ _keyCtrlPtn,
3381
+ config: {
3382
+ scale: 1.5,
3383
+ dpr: window.devicePixelRatio || 1,
3384
+ timeMargin: 35,
3385
+ mmWidthBase: (g_sWidth - 500) / 2 + 290,
3386
+ mmMarginY: 2,
3387
+ get laneWidth() {
3388
+ return Math.min(Math.floor((this.mmWidthBase - this.timeMargin) / keyNum), 40);
3389
+ },
3390
+ get logicalWidth() {
3391
+ return this.timeMargin + (this.laneWidth * keyNum);
3392
+ }
3393
+ },
3394
+ };
3439
3395
 
3440
- // --- Y座標計算用の関数 ---
3441
- // 通常:上(0)から下へ増える
3442
- // リバース:最大値(mmHeight)から上へ向かって減る
3443
- const getY = (frame) => {
3444
- const relativeFrame = frame - _firstArrowFrame;
3445
- const rawY = relativeFrame * scale;
3446
- return _isReverse ? (mmHeightTotal - rawY + mmMarginY) : (rawY + mmMarginY);
3447
- };
3396
+ // ヘッダー生成
3397
+ g_detailObj.scoreMinimapHeader[_scoreId] = createMinimapHeader(g_detailObj.miniMapParams[_scoreId].config, _keyCtrlPtn, keyNum);
3448
3398
 
3449
- // 時間表記用のフォーマット関数
3450
- const formatTime = (frame) => {
3451
- const [m, s] = transFrameToTimer(frame).split(`:`);
3452
- return `${m.padStart(2, `0`)}:${s}`;
3453
- };
3399
+ // Canvas保存用配列を空で初期化
3400
+ g_detailObj.scoreMinimap[_scoreId] = null;
3401
+ g_detailObj.scoreMinimapReverse[_scoreId] = null;
3402
+ };
3454
3403
 
3455
- // --- 1. 時間軸・ガイドライン ---
3456
- const interval = g_fps;
3457
- let startPoint = Math.ceil(_firstArrowFrame / interval) * interval;
3458
- for (let currentFrame = startPoint; currentFrame <= _firstArrowFrame + _playingFrame; currentFrame += interval) {
3459
- const y = getY(currentFrame);
3460
- drawOnTarget(y - 5, 10, (ctx) => {
3461
- ctx.strokeStyle = '#444';
3462
- ctx.fillStyle = '#999';
3463
- ctx.font = `10px ${getBasicFont()}`;
3464
- ctx.textAlign = 'right';
3465
- ctx.textBaseline = 'middle';
3466
- ctx.beginPath();
3467
- ctx.moveTo(timeMargin, y);
3468
- ctx.lineTo(timeMargin + laneWidth * _keyNum, y);
3469
- ctx.stroke();
3470
- ctx.fillText(formatTime(currentFrame), timeMargin, y);
3471
- });
3472
- }
3404
+ /**
3405
+ * 指定された高さに基づいて分割されたCanvasリストを生成する
3406
+ * @param {number} _width
3407
+ * @param {number} _totalHeight
3408
+ * @param {number} _dpr
3409
+ * @return {object[]} 分割されたCanvasとそのコンテキスト、オフセット情報を含むリスト
3410
+ */
3411
+ const createSplitCanvases = (_width, _totalHeight, _dpr) => {
3412
+ // バックバッファ(実際のピクセル数)の最大値を 8000 に設定(iOS Safari 8192px 対策)
3413
+ const BACKING_STORE_LIMIT = 8000;
3473
3414
 
3474
- // --- 2. フリーズノート ---
3475
- for (let j = 0; j < _keyNum; j++) {
3476
- const frz = _scoreObj.frzData[j];
3477
- for (let k = 0; k < frz.length; k += 2) {
3478
- const start = frz[k], end = frz[k + 1];
3479
- if (isNaN(start) || isNaN(end)) continue;
3480
- const y1 = getY(start), y2 = getY(end);
3481
- const x = timeMargin + j * laneWidth;
3482
- const top = Math.min(y1, y2), h = Math.abs(y2 - y1);
3483
-
3484
- drawOnTarget(top, h, (ctx) => {
3485
- ctx.fillStyle = 'rgba(0, 200, 255, 0.4)';
3486
- ctx.fillRect(x + 2, top, laneWidth - 3, h);
3487
- ctx.strokeStyle = 'rgba(0, 200, 255, 0.8)';
3488
- ctx.strokeRect(x + 2, top, laneWidth - 3, h);
3489
- });
3490
- }
3415
+ // 論理上の最大高さ(CSSピクセル)を計算
3416
+ // dpr=2なら4000px、dpr=3なら2666px が1枚の限界になる
3417
+ const maxLogicalHeight = Math.max(1, Math.floor(BACKING_STORE_LIMIT / _dpr));
3418
+ if (_totalHeight <= 0) return [];
3419
+
3420
+ const count = Math.ceil(_totalHeight / maxLogicalHeight);
3421
+ const list = [];
3422
+
3423
+ for (let i = 0; i < count; i++) {
3424
+ const cvs = document.createElement('canvas');
3425
+ // 残りの高さを計算
3426
+ const logicalH = (i === count - 1)
3427
+ ? _totalHeight - (maxLogicalHeight * i)
3428
+ : maxLogicalHeight;
3429
+
3430
+ // 実際の描画解像度をセット
3431
+ cvs.width = _width * _dpr;
3432
+ cvs.height = logicalH * _dpr;
3433
+
3434
+ // ブラウザ上の表示サイズをセット
3435
+ cvs.style.width = `${_width}px`;
3436
+ cvs.style.height = `${logicalH}px`;
3437
+ cvs.style.display = 'block';
3438
+
3439
+ const ctx = cvs.getContext('2d');
3440
+ ctx.scale(_dpr, _dpr);
3441
+
3442
+ list.push({
3443
+ canvas: cvs,
3444
+ ctx: ctx,
3445
+ offsetTop: i * maxLogicalHeight,
3446
+ logicalHeight: logicalH
3447
+ });
3448
+ }
3449
+ return list;
3450
+ };
3451
+
3452
+ /**
3453
+ * 描画対象のCanvasを判定して描画を実行する
3454
+ * @param {HTMLCanvasElement[]} _canvases
3455
+ * @param {number} _y
3456
+ * @param {number} _h
3457
+ * @param {number} _dpr
3458
+ * @param {Function} _drawFunc
3459
+ */
3460
+ const distributeDrawing = (_canvases, _y, _h, _dpr, _drawFunc) => {
3461
+ _canvases.forEach(item => {
3462
+ const canvasHeight = item.logicalHeight;
3463
+ if (_y + _h >= item.offsetTop && _y <= item.offsetTop + canvasHeight) {
3464
+ item.ctx.save();
3465
+ item.ctx.translate(0, -item.offsetTop);
3466
+ _drawFunc(item.ctx);
3467
+ item.ctx.restore();
3491
3468
  }
3469
+ });
3470
+ };
3492
3471
 
3493
- // --- 3. 通常ノート ---
3494
- for (let j = 0; j < _keyNum; j++) {
3495
- const color = g_dfColorObj.setColorType2[g_keyObj[`color${_keyCtrlPtn}_0`][j]] || '#ffffff';
3496
- _scoreObj.arrowData[j].forEach(note => {
3497
- const val = parseFloat(note);
3498
- if (isNaN(val)) return;
3499
- const y = getY(val), x = timeMargin + j * laneWidth;
3500
- drawOnTarget(y - 1.5, 3, (ctx) => {
3501
- ctx.fillStyle = color;
3502
- ctx.fillRect(x + 1, y - 1.5, laneWidth - 1, 3);
3503
- });
3472
+ /**
3473
+ * 譜面ミニマップ:キー名を表示するヘッダーキャンバスを作成する
3474
+ * @param {object} _config ミニマップの基本設定
3475
+ * @param {number} _config.dpr デバイスピクセル比
3476
+ * @param {number} _config.timeMargin 時間軸のマージン
3477
+ * @param {number} _config.laneWidth レーンの幅
3478
+ * @param {number} _config.logicalWidth キャンバスの論理幅
3479
+ * @param {string} _keyCtrlPtn キーコントロールパターン
3480
+ * @param {number} _keyNum キー数
3481
+ * @return {HTMLCanvasElement} ヘッダー用のキャンバス要素
3482
+ */
3483
+ const createMinimapHeader = (_config, _keyCtrlPtn, _keyNum) => {
3484
+ const { dpr, timeMargin, laneWidth, logicalWidth } = _config;
3485
+ const headerHeight = 15; // ヘッダーの固定高
3486
+
3487
+ const canvas = document.createElement('canvas');
3488
+ const ctx = canvas.getContext('2d');
3489
+
3490
+ // 解像度と表示サイズの設定
3491
+ canvas.width = logicalWidth * dpr;
3492
+ canvas.height = headerHeight * dpr;
3493
+ canvas.style.width = `${logicalWidth}px`;
3494
+ canvas.style.height = `${headerHeight}px`;
3495
+ canvas.style.display = 'block';
3496
+
3497
+ ctx.scale(dpr, dpr);
3498
+
3499
+ // テキストのスタイル設定
3500
+ ctx.fillStyle = '#999';
3501
+ ctx.font = `10px ${getBasicFont()}`;
3502
+ ctx.textAlign = 'center';
3503
+ ctx.textBaseline = 'middle';
3504
+
3505
+ // 各レーンのキー名を描画
3506
+ for (let j = 0; j < _keyNum; j++) {
3507
+ // config.laneWidth を使って中央座標を計算
3508
+ const x = timeMargin + j * laneWidth + laneWidth / 2;
3509
+ const keyText = g_kCd[g_keyObj[`keyCtrl${_keyCtrlPtn}`][j][0]];
3510
+
3511
+ ctx.fillText(keyText, x, headerHeight / 2 + 2); // 視覚的な中央調整で +2px
3512
+ }
3513
+
3514
+ return canvas;
3515
+ };
3516
+
3517
+ /**
3518
+ * 譜面ミニマップ:譜面ミニマップ本体生成
3519
+ * @param {object} _params ミニマップ生成のためのパラメータオブジェクト
3520
+ * @param {object} _params._scoreObj 譜面データオブジェクト
3521
+ * @param {number} _params._keyNum キー数
3522
+ * @param {number} _params._playingFrame 演奏時間(フレーム数)
3523
+ * @param {number} _params._firstArrowFrame 最初の矢印のフレーム位置
3524
+ * @param {string} _params._keyCtrlPtn キーコントロールパターン
3525
+ * @param {object} _params.config ミニマップの基本設定
3526
+ * @param {number} _params.config.scale ミニマップの時間軸のスケール
3527
+ * @param {number} _params.config.dpr デバイスピクセル比
3528
+ * @param {number} _params.config.timeMargin 時間軸のマージン
3529
+ * @param {number} _params.config.laneWidth レーンの幅
3530
+ * @param {number} _params.config.logicalWidth キャンバスの論理幅
3531
+ * @param {number} _params.config.mmMarginY ミニマップの上下マージン
3532
+ * @param {boolean} _isReverse ミニマップのリバース表示フラグ
3533
+ * @returns {HTMLCanvasElement[]} ミニマップ用のキャンバス要素の配列
3534
+ */
3535
+ const generateMinimapData = (_params, _isReverse) => {
3536
+ const { _scoreObj, _keyNum, _playingFrame, _firstArrowFrame, _keyCtrlPtn, config } = _params;
3537
+ const { scale, dpr, timeMargin, laneWidth, logicalWidth, mmMarginY } = config;
3538
+
3539
+ const mmHeightTotal = _playingFrame * scale + mmMarginY * 2;
3540
+ const canvases = createSplitCanvases(logicalWidth, mmHeightTotal, dpr);
3541
+
3542
+ const getY = (frame) => {
3543
+ const relativeFrame = frame - _firstArrowFrame;
3544
+ const rawY = relativeFrame * scale;
3545
+ // mmHeightTotalから引くのではなく、中身の演奏時間部分(_playingFrame * scale)を基準にリバース
3546
+ return _isReverse
3547
+ ? (_playingFrame * scale - rawY + mmMarginY)
3548
+ : (rawY + mmMarginY);
3549
+ };
3550
+
3551
+ // 1. 時間軸描画
3552
+ const interval = g_fps;
3553
+ for (let f = Math.ceil(_firstArrowFrame / interval) * interval; f <= _firstArrowFrame + _playingFrame; f += interval) {
3554
+ const y = getY(f);
3555
+ distributeDrawing(canvases, y - 5, 10, dpr, (ctx) => {
3556
+ ctx.strokeStyle = '#444';
3557
+ ctx.fillStyle = '#999';
3558
+ ctx.font = `10px ${getBasicFont()}`;
3559
+ ctx.textAlign = 'right';
3560
+ ctx.textBaseline = 'middle';
3561
+ ctx.beginPath(); ctx.moveTo(timeMargin, y); ctx.lineTo(timeMargin + laneWidth * _keyNum, y); ctx.stroke();
3562
+ const [m, s] = transFrameToTimer(f).split(':');
3563
+ ctx.fillText(`${m.padStart(2, '0')}:${s}`, timeMargin, y);
3564
+ });
3565
+ }
3566
+
3567
+ // 2. フリーズノート
3568
+ for (let j = 0; j < _keyNum; j++) {
3569
+ const frz = _scoreObj.frzData[j];
3570
+ for (let k = 0; k < frz.length; k += 2) {
3571
+ const start = frz[k];
3572
+ const end = frz[k + 1];
3573
+
3574
+ // 終了地点がない、またはどちらかが数値でない場合はスキップ
3575
+ if (end === undefined || isNaN(start) || isNaN(end)) {
3576
+ console.warn(`Minimap: Incomplete freeze note pair at lane ${j}, index ${k}`);
3577
+ continue;
3578
+ }
3579
+ const y1 = getY(start);
3580
+ const y2 = getY(end);
3581
+ const top = Math.min(y1, y2);
3582
+ const h = Math.abs(y2 - y1);
3583
+ const x = timeMargin + j * laneWidth;
3584
+ distributeDrawing(canvases, top, h, dpr, (ctx) => {
3585
+ ctx.fillStyle = 'rgba(0, 200, 255, 0.4)';
3586
+ ctx.fillRect(x + 2, top, laneWidth - 3, h);
3587
+ ctx.strokeStyle = 'rgba(0, 200, 255, 0.8)';
3588
+ ctx.strokeRect(x + 2, top, laneWidth - 3, h);
3504
3589
  });
3505
3590
  }
3591
+ }
3506
3592
 
3507
- // 保存(Canvasの配列を保存する)
3508
- const result = canvases.map(item => item.canvas);
3509
- if (_isReverse) {
3510
- g_detailObj.scoreMinimapReverse[_scoreId] = result;
3511
- } else {
3512
- g_detailObj.scoreMinimap[_scoreId] = result;
3513
- }
3514
- };
3593
+ // 3. 通常ノート
3594
+ for (let j = 0; j < _keyNum; j++) {
3595
+ const color = g_dfColorObj.setColorType2[g_keyObj[`color${_keyCtrlPtn}_0`][j]] || '#ffffff';
3596
+ _scoreObj.arrowData[j].forEach(note => {
3597
+ const y = getY(parseFloat(note));
3598
+ distributeDrawing(canvases, y - 1.5, 3, dpr, (ctx) => {
3599
+ ctx.fillStyle = color;
3600
+ ctx.fillRect(timeMargin + j * laneWidth + 1, y - 1.5, laneWidth - 1, 3);
3601
+ });
3602
+ });
3603
+ }
3515
3604
 
3516
- // storeBaseData の中で呼び出す
3517
- // 通常版を生成
3518
- generateMinimap(_scoreId, _scoreObj, keyNum, playingFrame, firstArrowFrame, false);
3519
- // リバース版を生成
3520
- generateMinimap(_scoreId, _scoreObj, keyNum, playingFrame, firstArrowFrame, true);
3605
+ return canvases.map(item => item.canvas);
3521
3606
  };
3522
3607
 
3523
3608
  /**
@@ -7763,10 +7848,24 @@ const drawMinimap = (_scoreId, _initFlg = false) => {
7763
7848
  deleteChildspriteAll(`detailMiniMap`);
7764
7849
 
7765
7850
  // drawMinimap 内の Canvas 追加部分
7766
- const savedCanvases = g_stateObj.miniMapRevFlg
7851
+ const isRev = g_stateObj.miniMapRevFlg;
7852
+ let savedCanvases = isRev
7767
7853
  ? g_detailObj.scoreMinimapReverse[_scoreId]
7768
7854
  : g_detailObj.scoreMinimap[_scoreId];
7769
7855
 
7856
+ if (!savedCanvases) {
7857
+ // 未作成の場合のみミニマップを生成(Lazy Generation)
7858
+ const params = g_detailObj.miniMapParams[_scoreId];
7859
+ savedCanvases = generateMinimapData(params, isRev);
7860
+
7861
+ // 生成したものをキャッシュに保存
7862
+ if (isRev) {
7863
+ g_detailObj.scoreMinimapReverse[_scoreId] = savedCanvases;
7864
+ } else {
7865
+ g_detailObj.scoreMinimap[_scoreId] = savedCanvases;
7866
+ }
7867
+ }
7868
+
7770
7869
  // --- ヘッダー部分 ---
7771
7870
  const detailMiniMapHeader = createEmptySprite(detailMiniMap, `detailMiniMapHeader`, g_windowObj.detailMiniMapHeader);
7772
7871
  $id(`detailMiniMapHeader`).top = (g_stateObj.miniMapRevFlg ? 230 : 0) + `px`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "danoniplus",
3
- "version": "47.1.0",
3
+ "version": "47.1.1",
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",