Spaces:
Runtime error
Runtime error
CC Company
commited on
Commit
·
ff9e592
1
Parent(s):
165bdd3
✨ オフセット調整UIの改善・ドラッグ対応
Browse files- オフセット値をスライダーで直感的に調整可能に
- プレビューcanvas上でドラッグ操作によるオフセット変更を実装
- プレビュー幅を80%に拡大しUX向上
- index.html +175 -9
index.html
CHANGED
|
@@ -207,6 +207,8 @@
|
|
| 207 |
display: flex;
|
| 208 |
align-items: center;
|
| 209 |
justify-content: center;
|
|
|
|
|
|
|
| 210 |
}
|
| 211 |
|
| 212 |
.preview-grid {
|
|
@@ -398,19 +400,23 @@
|
|
| 398 |
<div class="settings-grid">
|
| 399 |
<div class="setting-item">
|
| 400 |
<label for="offsetLeft">左オフセット(px)</label>
|
| 401 |
-
<input type="number" id="offsetLeft" value="0" min="0">
|
|
|
|
| 402 |
</div>
|
| 403 |
<div class="setting-item">
|
| 404 |
<label for="offsetRight">右オフセット(px)</label>
|
| 405 |
-
<input type="number" id="offsetRight" value="0" min="0">
|
|
|
|
| 406 |
</div>
|
| 407 |
<div class="setting-item">
|
| 408 |
<label for="offsetTop">上オフセット(px)</label>
|
| 409 |
-
<input type="number" id="offsetTop" value="0" min="0">
|
|
|
|
| 410 |
</div>
|
| 411 |
<div class="setting-item">
|
| 412 |
<label for="offsetBottom">下オフセット(px)</label>
|
| 413 |
-
<input type="number" id="offsetBottom" value="0" min="0">
|
|
|
|
| 414 |
</div>
|
| 415 |
</div>
|
| 416 |
</div>
|
|
@@ -448,19 +454,49 @@
|
|
| 448 |
const resultGrid = document.getElementById('resultGrid');
|
| 449 |
const progressBar = document.getElementById('progressBar');
|
| 450 |
const progressFill = document.getElementById('progressFill');
|
| 451 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
// ファイルアップロード処理
|
| 453 |
uploadArea.addEventListener('click', () => fileInput.click());
|
| 454 |
uploadArea.addEventListener('dragover', handleDragOver);
|
| 455 |
uploadArea.addEventListener('drop', handleDrop);
|
| 456 |
uploadArea.addEventListener('dragleave', handleDragLeave);
|
| 457 |
fileInput.addEventListener('change', handleFileSelect);
|
| 458 |
-
|
| 459 |
// 設定変更時のプレビュー更新
|
| 460 |
-
['columns', 'rows'
|
| 461 |
document.getElementById(id).addEventListener('input', updatePreview);
|
| 462 |
});
|
| 463 |
-
|
| 464 |
splitButton.addEventListener('click', splitImage);
|
| 465 |
downloadAllButton.addEventListener('click', downloadAll);
|
| 466 |
|
|
@@ -524,7 +560,8 @@
|
|
| 524 |
const splitHeight = originalImage.height - offsetTop - offsetBottom;
|
| 525 |
|
| 526 |
// プレビューcanvasサイズ
|
| 527 |
-
|
|
|
|
| 528 |
const previewScale = Math.min(1, previewMaxWidth / originalImage.width);
|
| 529 |
const canvasWidth = originalImage.width * previewScale;
|
| 530 |
const canvasHeight = originalImage.height * previewScale;
|
|
@@ -564,6 +601,135 @@
|
|
| 564 |
ctx.lineWidth = 2;
|
| 565 |
|
| 566 |
// 垂直線
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 567 |
for (let i = 1; i < columns; i++) {
|
| 568 |
const x = offsetLeft + (i * splitWidth) / columns;
|
| 569 |
const xScaled = x * previewScale;
|
|
|
|
| 207 |
display: flex;
|
| 208 |
align-items: center;
|
| 209 |
justify-content: center;
|
| 210 |
+
width: 80%;
|
| 211 |
+
margin: 0 auto;
|
| 212 |
}
|
| 213 |
|
| 214 |
.preview-grid {
|
|
|
|
| 400 |
<div class="settings-grid">
|
| 401 |
<div class="setting-item">
|
| 402 |
<label for="offsetLeft">左オフセット(px)</label>
|
| 403 |
+
<input type="number" id="offsetLeft" value="0" min="0" style="margin-bottom:8px;">
|
| 404 |
+
<input type="range" id="offsetLeftSlider" value="0" min="0" max="500">
|
| 405 |
</div>
|
| 406 |
<div class="setting-item">
|
| 407 |
<label for="offsetRight">右オフセット(px)</label>
|
| 408 |
+
<input type="number" id="offsetRight" value="0" min="0" style="margin-bottom:8px;">
|
| 409 |
+
<input type="range" id="offsetRightSlider" value="0" min="0" max="500">
|
| 410 |
</div>
|
| 411 |
<div class="setting-item">
|
| 412 |
<label for="offsetTop">上オフセット(px)</label>
|
| 413 |
+
<input type="number" id="offsetTop" value="0" min="0" style="margin-bottom:8px;">
|
| 414 |
+
<input type="range" id="offsetTopSlider" value="0" min="0" max="500">
|
| 415 |
</div>
|
| 416 |
<div class="setting-item">
|
| 417 |
<label for="offsetBottom">下オフセット(px)</label>
|
| 418 |
+
<input type="number" id="offsetBottom" value="0" min="0" style="margin-bottom:8px;">
|
| 419 |
+
<input type="range" id="offsetBottomSlider" value="0" min="0" max="500">
|
| 420 |
</div>
|
| 421 |
</div>
|
| 422 |
</div>
|
|
|
|
| 454 |
const resultGrid = document.getElementById('resultGrid');
|
| 455 |
const progressBar = document.getElementById('progressBar');
|
| 456 |
const progressFill = document.getElementById('progressFill');
|
| 457 |
+
|
| 458 |
+
// オフセットスライダー要素取得
|
| 459 |
+
const offsetLeftInput = document.getElementById('offsetLeft');
|
| 460 |
+
const offsetLeftSlider = document.getElementById('offsetLeftSlider');
|
| 461 |
+
const offsetRightInput = document.getElementById('offsetRight');
|
| 462 |
+
const offsetRightSlider = document.getElementById('offsetRightSlider');
|
| 463 |
+
const offsetTopInput = document.getElementById('offsetTop');
|
| 464 |
+
const offsetTopSlider = document.getElementById('offsetTopSlider');
|
| 465 |
+
const offsetBottomInput = document.getElementById('offsetBottom');
|
| 466 |
+
const offsetBottomSlider = document.getElementById('offsetBottomSlider');
|
| 467 |
+
|
| 468 |
+
// input <-> slider 双方向同期
|
| 469 |
+
function bindOffsetSync(input, slider) {
|
| 470 |
+
input.addEventListener('input', () => {
|
| 471 |
+
slider.value = input.value;
|
| 472 |
+
updatePreview();
|
| 473 |
+
});
|
| 474 |
+
slider.addEventListener('input', () => {
|
| 475 |
+
input.value = slider.value;
|
| 476 |
+
updatePreview();
|
| 477 |
+
});
|
| 478 |
+
}
|
| 479 |
+
bindOffsetSync(offsetLeftInput, offsetLeftSlider);
|
| 480 |
+
bindOffsetSync(offsetRightInput, offsetRightSlider);
|
| 481 |
+
bindOffsetSync(offsetTopInput, offsetTopSlider);
|
| 482 |
+
bindOffsetSync(offsetBottomInput, offsetBottomSlider);
|
| 483 |
+
|
| 484 |
+
// プレビューセクションの幅を80%に
|
| 485 |
+
previewContainer.style.width = "80%";
|
| 486 |
+
previewContainer.style.margin = "0 auto";
|
| 487 |
+
|
| 488 |
// ファイルアップロード処理
|
| 489 |
uploadArea.addEventListener('click', () => fileInput.click());
|
| 490 |
uploadArea.addEventListener('dragover', handleDragOver);
|
| 491 |
uploadArea.addEventListener('drop', handleDrop);
|
| 492 |
uploadArea.addEventListener('dragleave', handleDragLeave);
|
| 493 |
fileInput.addEventListener('change', handleFileSelect);
|
| 494 |
+
|
| 495 |
// 設定変更時のプレビュー更新
|
| 496 |
+
['columns', 'rows'].forEach(id => {
|
| 497 |
document.getElementById(id).addEventListener('input', updatePreview);
|
| 498 |
});
|
| 499 |
+
|
| 500 |
splitButton.addEventListener('click', splitImage);
|
| 501 |
downloadAllButton.addEventListener('click', downloadAll);
|
| 502 |
|
|
|
|
| 560 |
const splitHeight = originalImage.height - offsetTop - offsetBottom;
|
| 561 |
|
| 562 |
// プレビューcanvasサイズ
|
| 563 |
+
// プレビュー最大幅を80%に
|
| 564 |
+
const previewMaxWidth = previewContainer.offsetWidth > 0 ? previewContainer.offsetWidth * 0.8 : 600;
|
| 565 |
const previewScale = Math.min(1, previewMaxWidth / originalImage.width);
|
| 566 |
const canvasWidth = originalImage.width * previewScale;
|
| 567 |
const canvasHeight = originalImage.height * previewScale;
|
|
|
|
| 601 |
ctx.lineWidth = 2;
|
| 602 |
|
| 603 |
// 垂直線
|
| 604 |
+
// --- オフセットラインをcanvas上でドラッグ操作するための処理 ---
|
| 605 |
+
// ドラッグ対象: left, right, top, bottom
|
| 606 |
+
const DRAG_MARGIN = 10; // ハンドルの感知範囲(px)
|
| 607 |
+
let dragging = null; // 'left'|'right'|'top'|'bottom'|null
|
| 608 |
+
let dragStart = {x:0, y:0};
|
| 609 |
+
let dragOffsetStart = {left:0, right:0, top:0, bottom:0};
|
| 610 |
+
|
| 611 |
+
// プレビューcanvasが存在する場合のみイベントを付与
|
| 612 |
+
canvas.style.cursor = "crosshair";
|
| 613 |
+
canvas.addEventListener('mousedown', function(e) {
|
| 614 |
+
const rect = canvas.getBoundingClientRect();
|
| 615 |
+
const x = (e.clientX - rect.left) / previewScale;
|
| 616 |
+
const y = (e.clientY - rect.top) / previewScale;
|
| 617 |
+
|
| 618 |
+
// どのラインが近いか判定
|
| 619 |
+
if (Math.abs(x - offsetLeft) < DRAG_MARGIN) {
|
| 620 |
+
dragging = 'left';
|
| 621 |
+
dragStart = {x, y};
|
| 622 |
+
dragOffsetStart = {
|
| 623 |
+
left: offsetLeft,
|
| 624 |
+
right: offsetRight,
|
| 625 |
+
top: offsetTop,
|
| 626 |
+
bottom: offsetBottom
|
| 627 |
+
};
|
| 628 |
+
canvas.style.cursor = "ew-resize";
|
| 629 |
+
} else if (Math.abs(x - (originalImage.width - offsetRight)) < DRAG_MARGIN) {
|
| 630 |
+
dragging = 'right';
|
| 631 |
+
dragStart = {x, y};
|
| 632 |
+
dragOffsetStart = {
|
| 633 |
+
left: offsetLeft,
|
| 634 |
+
right: offsetRight,
|
| 635 |
+
top: offsetTop,
|
| 636 |
+
bottom: offsetBottom
|
| 637 |
+
};
|
| 638 |
+
canvas.style.cursor = "ew-resize";
|
| 639 |
+
} else if (Math.abs(y - offsetTop) < DRAG_MARGIN) {
|
| 640 |
+
dragging = 'top';
|
| 641 |
+
dragStart = {x, y};
|
| 642 |
+
dragOffsetStart = {
|
| 643 |
+
left: offsetLeft,
|
| 644 |
+
right: offsetRight,
|
| 645 |
+
top: offsetTop,
|
| 646 |
+
bottom: offsetBottom
|
| 647 |
+
};
|
| 648 |
+
canvas.style.cursor = "ns-resize";
|
| 649 |
+
} else if (Math.abs(y - (originalImage.height - offsetBottom)) < DRAG_MARGIN) {
|
| 650 |
+
dragging = 'bottom';
|
| 651 |
+
dragStart = {x, y};
|
| 652 |
+
dragOffsetStart = {
|
| 653 |
+
left: offsetLeft,
|
| 654 |
+
right: offsetRight,
|
| 655 |
+
top: offsetTop,
|
| 656 |
+
bottom: offsetBottom
|
| 657 |
+
};
|
| 658 |
+
canvas.style.cursor = "ns-resize";
|
| 659 |
+
} else {
|
| 660 |
+
dragging = null;
|
| 661 |
+
}
|
| 662 |
+
});
|
| 663 |
+
|
| 664 |
+
window.addEventListener('mousemove', function(e) {
|
| 665 |
+
if (!dragging) return;
|
| 666 |
+
const rect = canvas.getBoundingClientRect();
|
| 667 |
+
const x = (e.clientX - rect.left) / previewScale;
|
| 668 |
+
const y = (e.clientY - rect.top) / previewScale;
|
| 669 |
+
if (dragging === 'left') {
|
| 670 |
+
let delta = x - dragStart.x;
|
| 671 |
+
let val = dragOffsetStart.left + delta;
|
| 672 |
+
val = Math.round(val);
|
| 673 |
+
val = Math.max(0, Math.min(originalImage.width - offsetRight - 1, val));
|
| 674 |
+
offsetLeftInput.value = val;
|
| 675 |
+
offsetLeftSlider.value = val;
|
| 676 |
+
} else if (dragging === 'right') {
|
| 677 |
+
let delta = dragStart.x - x;
|
| 678 |
+
let val = dragOffsetStart.right + delta;
|
| 679 |
+
val = Math.round(val);
|
| 680 |
+
val = Math.max(0, Math.min(originalImage.width - offsetLeft - 1, val));
|
| 681 |
+
offsetRightInput.value = val;
|
| 682 |
+
offsetRightSlider.value = val;
|
| 683 |
+
} else if (dragging === 'top') {
|
| 684 |
+
let delta = y - dragStart.y;
|
| 685 |
+
let val = dragOffsetStart.top + delta;
|
| 686 |
+
val = Math.round(val);
|
| 687 |
+
val = Math.max(0, Math.min(originalImage.height - offsetBottom - 1, val));
|
| 688 |
+
offsetTopInput.value = val;
|
| 689 |
+
offsetTopSlider.value = val;
|
| 690 |
+
} else if (dragging === 'bottom') {
|
| 691 |
+
let delta = dragStart.y - y;
|
| 692 |
+
let val = dragOffsetStart.bottom + delta;
|
| 693 |
+
val = Math.round(val);
|
| 694 |
+
val = Math.max(0, Math.min(originalImage.height - offsetTop - 1, val));
|
| 695 |
+
offsetBottomInput.value = val;
|
| 696 |
+
offsetBottomSlider.value = val;
|
| 697 |
+
}
|
| 698 |
+
updatePreview();
|
| 699 |
+
});
|
| 700 |
+
|
| 701 |
+
window.addEventListener('mouseup', function(e) {
|
| 702 |
+
if (dragging) {
|
| 703 |
+
dragging = null;
|
| 704 |
+
canvas.style.cursor = "crosshair";
|
| 705 |
+
}
|
| 706 |
+
});
|
| 707 |
+
|
| 708 |
+
// ハンドルを太く描画
|
| 709 |
+
ctx.save();
|
| 710 |
+
ctx.strokeStyle = "#e74c3c";
|
| 711 |
+
ctx.lineWidth = 6;
|
| 712 |
+
// left
|
| 713 |
+
ctx.beginPath();
|
| 714 |
+
ctx.moveTo(offsetLeft * previewScale, offsetTop * previewScale);
|
| 715 |
+
ctx.lineTo(offsetLeft * previewScale, (originalImage.height - offsetBottom) * previewScale);
|
| 716 |
+
ctx.stroke();
|
| 717 |
+
// right
|
| 718 |
+
ctx.beginPath();
|
| 719 |
+
ctx.moveTo((originalImage.width - offsetRight) * previewScale, offsetTop * previewScale);
|
| 720 |
+
ctx.lineTo((originalImage.width - offsetRight) * previewScale, (originalImage.height - offsetBottom) * previewScale);
|
| 721 |
+
ctx.stroke();
|
| 722 |
+
// top
|
| 723 |
+
ctx.beginPath();
|
| 724 |
+
ctx.moveTo(offsetLeft * previewScale, offsetTop * previewScale);
|
| 725 |
+
ctx.lineTo((originalImage.width - offsetRight) * previewScale, offsetTop * previewScale);
|
| 726 |
+
ctx.stroke();
|
| 727 |
+
// bottom
|
| 728 |
+
ctx.beginPath();
|
| 729 |
+
ctx.moveTo(offsetLeft * previewScale, (originalImage.height - offsetBottom) * previewScale);
|
| 730 |
+
ctx.lineTo((originalImage.width - offsetRight) * previewScale, (originalImage.height - offsetBottom) * previewScale);
|
| 731 |
+
ctx.stroke();
|
| 732 |
+
ctx.restore();
|
| 733 |
for (let i = 1; i < columns; i++) {
|
| 734 |
const x = offsetLeft + (i * splitWidth) / columns;
|
| 735 |
const xScaled = x * previewScale;
|