"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var bupan; (function (bupan) { class AreaUtil { static toCss(area) { let css = {}; css.width = area.width + "px"; css.height = area.height + "px"; css.left = area.x + "px"; css.top = area.y + "px"; return css; } } bupan.AreaUtil = AreaUtil; // ThumBox Class // Manages the canvas for individual thumbnail boxes, including image loading and drawing. class ImageBox { constructor(elemRoot) { this.image_idx = -1; this.imageViewArea = { x: 0, y: 0, width: 0, height: 0 }; this.imageUrl = ""; if (elemRoot) { this.$root = elemRoot; } this.$canvas = document.createElement("canvas"); this.$root.appendChild(this.$canvas); const dragHandle = document.createElement("div"); dragHandle.classList.add("drag-handle-overlay"); this.$root.appendChild(dragHandle); // ドラッグハンドルをCanvasの上に追加 this.context = this.$canvas.getContext("2d"); } /** * Loads an image from the given URL and draws it onto the canvas. * @param url The URL of the image. * @param defaultArea Optional. Specifies the initial display area. */ loadImage(url, defaultArea = null) { this.imageUrl = url; let self = this; return new Promise((resolve, reject) => { // ローディング表示のためのクラスを追加 if (defaultArea !== null) { self.imageViewArea = defaultArea; } // 既存の画像があればリセット if (self.$image) { self.$image.src = ''; } self.$image = new Image(); self.$image.src = (url instanceof Blob) ? URL.createObjectURL(url) : url; self.$image.onload = () => { if (url instanceof Blob) { URL.revokeObjectURL(self.$image.src); } if (self.imageViewArea.x == null || self.imageViewArea.x == 0) { self.centering(); } self.refresh(); setTimeout(function () { resolve(); }, 0); }; self.$image.onerror = (err) => { // alert("Image load error : " + url ); self.refresh(); setTimeout(function () { resolve(); }, 500); console.log(err); }; }); } /** * Calculates the largest square area from the center of the image and sets it to `imageViewArea`. */ centering() { const imgWidth = this.$image.naturalWidth; const imgHeight = this.$image.naturalHeight; console.log(imgWidth); // The size of the square will be the length of the shortest side of the image const size = Math.min(imgWidth, imgHeight); let x = 0; let y = 0; if (imgWidth > imgHeight) { // If the image is wider, calculate the x-coordinate to center based on height x = (imgWidth - size) / 2; y = 0; } else { // If the image is taller or square, calculate the y-coordinate to center based on width x = 0; y = (imgHeight - size) / 2; } // Set the calculated maximum square area to imageViewArea this.imageViewArea = { x: x, y: y, width: size, height: size }; } clear() { this.$image = null; this.refresh(); } /** * Clears the canvas and draws the image based on the current settings. */ refresh() { // Set the canvas drawing buffer size to match its actual display size // getBoundingClientRect().width/height gets the accurate pixel width/height of the element const displayWidth = this.$canvas.getBoundingClientRect().width; const displayHeight = this.$canvas.getBoundingClientRect().height; this.$canvas.width = displayWidth; this.$canvas.height = displayHeight; // // Clear the canvas // this.context.clearRect(0, 0, this.$canvas.width, this.$canvas.height); // 塗りつぶしの色をグレーに設定 (例: #808080 は中間的なグレー) this.context.fillStyle = '#808080'; // または 'gray'、'rgb(128, 128, 128)' など // キャンバス全体を塗りつぶす this.context.fillRect(0, 0, this.$canvas.width, this.$canvas.height); // Draw the image if it's loaded and imageViewArea is valid if (this.$image && this.$image.complete && this.imageViewArea.width > 0 && this.imageViewArea.height > 0) { const sx = this.imageViewArea.x; const sy = this.imageViewArea.y; const sWidth = this.imageViewArea.width; const sHeight = this.imageViewArea.height; const dx = 0; // Draw from the top-left of the canvas const dy = 0; // Draw from the top-left of the canvas const dWidth = this.$canvas.width; // Draw to fill the canvas width const dHeight = this.$canvas.height; // Draw to fill the canvas height this.context.drawImage(this.$image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); this.onRefreshed(); } } onRefreshed() { } /** * Generates a Base64-encoded image data URL (PNG) from the specified imageViewArea. * This data URL can be directly used as the src attribute of an tag. * @returns A Promise that resolves with the Base64 image data URL string, or null if an error occurs. */ createViewImage() { return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { if (!this.$image || !this.$image.complete || this.imageViewArea.width <= 0 || this.imageViewArea.height <= 0) { console.warn("Cannot create view image: original image not loaded or imageViewArea is invalid."); return resolve(null); } try { // OffscreenCanvasを作成し、指定領域を描画 const viewCanvas = new OffscreenCanvas(this.imageViewArea.width, this.imageViewArea.height); const viewContext = viewCanvas.getContext('2d'); if (!viewContext) { console.error("Failed to get 2D rendering context for viewCanvas."); return resolve(null); } // imageViewAreaで指定された元の画像の領域を、viewCanvas全体に描画する viewContext.drawImage(this.$image, this.imageViewArea.x, this.imageViewArea.y, this.imageViewArea.width, this.imageViewArea.height, // 元画像の切り抜き元 (source) 0, 0, viewCanvas.width, viewCanvas.height // viewCanvas全体に描画 (destination) ); // OffscreenCanvasの内容をBlobに変換 const blob = yield viewCanvas.convertToBlob({ type: 'image/png' }); resolve(blob); // // BlobをBase64に変換 // const reader = new FileReader(); // reader.onloadend = () => { // resolve(reader.result as string); // Base64文字列として解決 // }; // reader.onerror = (e) => { // console.error("FileReader error:", e); // resolve(null); // }; // reader.readAsDataURL(blob); } catch (error) { console.error("Error creating view image:", error); resolve(null); } })); } } bupan.ImageBox = ImageBox; class ImageEditBox extends ImageBox { constructor(elemRoot) { super(elemRoot); this.imageSelectArea = { x: 0, y: 0, width: 0, height: 0 }; this.MIN_SIZE = 50; this.MARGIN_RATE = 0.2; this.$resizeHandles = []; this.isDraggingCanvas = false; this.isResizing = false; this.activeHandleId = ''; // private initialBoxWidth: number; // private initialBoxLeft: number; // private initialBoxTop: number; // ImageEditBox クラスのプロパティに追加 this.isPinching = false; this.initialPinchDistance = 0; this.initialImageViewArea = {}; // ピンチ開始時のimageViewAreaを保存 this.initialBoxArea = {}; // ピンチ開始時のimageViewAreaを保存 this.history = []; this.$selectBox = document.createElement('div'); this.$selectBox.classList.add('select-box'); this.$root.appendChild(this.$selectBox); for (let i = 0; i < 4; i++) { let $handle = document.createElement('i'); $handle.classList.add('resize-handle', "ui", "icon"); this.$selectBox.appendChild($handle); // let $icon = document.createElement('i'); // $handle.appendChild($icon); switch (i) { case 0: $handle.id = "resize-righttop"; $handle.classList.add("large", "white", 'expand', 'arrows', 'alternate', 'icon'); break; case 1: $handle.id = "resize-rightbottom"; // Add the necessary classes for the icon (e.g., from Semantic UI or Font Awesome) $handle.classList.add("large", 'horizontally', "white", 'flipped', 'expand', 'alternate', 'icon'); // Append the icon to the handle break; case 2: $handle.id = "resize-x"; // Add the necessary classes for the icon (e.g., from Semantic UI or Font Awesome) $handle.classList.add("large", 'horizontal', "white", 'arrows', 'alternate', 'icon'); // Append the icon to the handle break; case 3: $handle.id = "resize-y"; // Add the necessary classes for the icon (e.g., from Semantic UI or Font Awesome) $handle.classList.add("large", 'vertical', "white", 'arrows', 'alternate', 'icon'); // Append the icon to the handle break; // case 3: // $handle.id = "resize-leftbottom"; // break; } this.$resizeHandles.push($handle); } // Replace $(window).on('resize', ...) with window.addEventListener('resize', ...) window.addEventListener('resize', () => this.resizeCanvas); // --- Start Select Box Drag --- // Add `mousedown` and `touchstart` event listeners this.$selectBox.addEventListener('mousedown', (e) => this.dragStartSelectBox(e)); this.$selectBox.addEventListener('touchstart', (e) => this.dragStartSelectBox(e), { passive: false }); this.$canvas.addEventListener('mousedown', (e) => this.dragStartCanvas(e)); this.$canvas.addEventListener('touchstart', (e) => this.dragStartCanvas(e), { passive: false }); // --- Start Resize --- // Loop through NodeList from `querySelectorAll` and add event listeners this.$resizeHandles.forEach(handle => { handle.addEventListener('mousedown', (e) => this.resizeStart(e)); handle.addEventListener('touchstart', (e) => this.resizeStart(e), { passive: false }); }); // --- Mouse Up/Touch End Handler --- document.addEventListener('mouseup', (e) => this.stopDragOrResize()); document.addEventListener('touchend', (e) => this.stopDragOrResize()); document.addEventListener('touchcancel', (e) => this.stopDragOrResize()); // Also add `touchcancel` // Alternative to `mouseleave`: listen on `document` document.addEventListener('mouseleave', (e) => { if (this.isResizing || this.isDraggingCanvas) { this.stopDragOrResize(); // Stop drag/resize if mouse leaves the document } }); // --- Mouse Move/Touch Move Handler --- document.addEventListener('mousemove', (e) => this.dragOrResizeMove(e)); document.addEventListener('touchmove', (e) => this.dragOrResizeMove(e), { passive: false }); // Allow `preventDefault` } initView(area) { this.history = []; this.imageSelectArea = Object.assign({}, area); this.history.push(Object.assign({}, this.imageSelectArea)); this.centerImageViewArea(); // Re-fit after resize const newArea = this.calcCanvasSelectArea(); Object.assign(this.$selectBox.style, bupan.AreaUtil.toCss(newArea)); this.refresh(); } /** * Converts the current image selection area (`imageSelectArea`) to canvas coordinates. * This is calculated based on how the image display area (`imageViewArea`) is shown on the canvas. * @returns The image selection area on the canvas. */ calcCanvasSelectArea() { if (!this.$canvas) { console.error("Canvas element is not set in AreaCalculator."); return { x: 0, y: 0, width: 0, height: 0 }; } // Use getBoundingClientRect().width/height to get the display size of the canvas const canvasWidth = this.$canvas.getBoundingClientRect().width; const canvasHeight = this.$canvas.getBoundingClientRect().height; const scaleX = canvasWidth / this.imageViewArea.width; const scaleY = canvasHeight / this.imageViewArea.height; const relativeSelectLeft = this.imageSelectArea.x - this.imageViewArea.x; const relativeSelectTop = this.imageSelectArea.y - this.imageViewArea.y; const canvasSelectLeft = relativeSelectLeft * scaleX; const canvasSelectTop = relativeSelectTop * scaleY; const canvasSelectWidth = this.imageSelectArea.width * scaleX; const canvasSelectHeight = this.imageSelectArea.height * scaleY; const canvasSelectArea = { x: canvasSelectLeft, y: canvasSelectTop, width: canvasSelectWidth, height: canvasSelectHeight }; return canvasSelectArea; } calcImageSelectArea($selectbox) { if (!this.$canvas) { console.error("Canvas element is not set in AreaCalculator."); return {}; } // Use getBoundingClientRect() to get the relative position and size of the element const selectBoxRect = $selectbox.getBoundingClientRect(); const canvasRect = this.$canvas.getBoundingClientRect(); // Relative position of the selection box within the canvas const canvasSelectLeft = selectBoxRect.left - canvasRect.left; const canvasSelectTop = selectBoxRect.top - canvasRect.top; const canvasSelectWidth = selectBoxRect.width; const canvasSelectHeight = selectBoxRect.height; const canvasWidth = canvasRect.width; const canvasHeight = canvasRect.height; // Inverse scale (convert canvas pixels to image pixels) const inverseScaleX = this.imageViewArea.width / canvasWidth; const inverseScaleY = this.imageViewArea.height / canvasHeight; const imageSelectWidth = canvasSelectWidth * inverseScaleX; const imageSelectHeight = canvasSelectHeight * inverseScaleY; // Calculate the position of the selection area on the image const imageSelectLeft = (canvasSelectLeft * inverseScaleX) + this.imageViewArea.x; const imageSelectTop = (canvasSelectTop * inverseScaleY) + this.imageViewArea.y; return { x: imageSelectLeft, y: imageSelectTop, width: imageSelectWidth, height: imageSelectHeight }; } /** * Fits the image display area to include the current image selection area, considering a margin. * @param marginRate The margin ratio when fitting the selection area. */ centerImageViewArea() { let marginX = this.imageSelectArea.width * this.MARGIN_RATE; let marginY = this.imageSelectArea.height * this.MARGIN_RATE; if (this.imageSelectArea.width > this.imageSelectArea.height) { marginY = marginX * this.imageSelectArea.width / this.imageSelectArea.height; } else { marginX = marginY * this.imageSelectArea.height / this.imageSelectArea.width; } this.imageViewArea.x = this.imageSelectArea.x - marginX; this.imageViewArea.y = this.imageSelectArea.y - marginY; this.imageViewArea.width = this.imageSelectArea.width + marginX * 2; this.imageViewArea.height = this.imageSelectArea.height + marginY * 2; // Adjust `imageViewArea` to prevent it from exceeding image boundaries // this.imageViewArea = ImageEditBox.limitAreaInParent(this.imageViewArea, { width : this.elemImage.width, height : this.elemImage.height } ); } /** * Limits the specified area within the boundaries of a parent area. * * @param {Area} area - The area to limit { x, y, width, height } * @param {Size} parentSize - The size of the parent area { width, height } * @returns {Area} The limited new area { x, y, width, height } */ limitAreaInParent(area) { let newArea = Object.assign({}, area); // Create a copy to avoid modifying the original object // // Limit width and height (if the area is larger than the parent, limit to parent's size) // if (newArea.width > parentSize.width) { // newArea.width = parentSize.width; // } // if (newArea.height > parentSize.height) { // newArea.height = parentSize.height; // } if (this.$image) { // Ensure x-coordinate is not less than 0, and right edge does not exceed parent newArea.x = Math.min(Math.max(-newArea.width / 2, newArea.x), this.$image.width - newArea.width / 2); // Ensure y-coordinate is not less than 0, and bottom edge does not.z exceed parent newArea.y = Math.min(Math.max(-newArea.height / 2, newArea.y), this.$image.height - newArea.height / 2); } return newArea; } calcCanvasSelectAreaCss() { const canvasSelectAreaCss = this.calcCanvasSelectArea(); return { left: canvasSelectAreaCss.x + "px", top: canvasSelectAreaCss.y + "px", height: canvasSelectAreaCss.height + "px", width: canvasSelectAreaCss.width + "px" }; } dragStartSelectBox(e) { const target = e.target; // リサイズハンドルでなければ、キャンバスドラッグ開始のロジックを呼び出す if (!target.classList.contains('resize-handle')) { this.dragStartCanvas(e); // ここが重要: selectBox上でのイベントをキャンバスドラッグに転送 } // リサイズハンドルの場合は、resizeStartが呼ばれるので、ここでは何もしません } // 既存のgetEventPageXYメソッドを修正 getEventPageXY(e) { if (e instanceof TouchEvent) { if (e.touches && e.touches.length > 0) { // 単一指のドラッグ用 const touch = e.touches[0]; return { pageX: touch.pageX, pageY: touch.pageY }; } // ピンチ操作の場合、最初の2本の指の中心を使用 if (e.touches.length >= 2) { const touch1 = e.touches[0]; const touch2 = e.touches[1]; return { pageX: (touch1.pageX + touch2.pageX) / 2, pageY: (touch1.pageY + touch2.pageY) / 2 }; } } else if (e instanceof MouseEvent) { return { pageX: e.pageX, pageY: e.pageY }; } return { pageX: 0, pageY: 0 }; } // 指間の距離を計算するヘルパーメソッドを追加 getDistanceBetweenTouches(touch1, touch2) { const dx = touch1.pageX - touch2.pageX; const dy = touch1.pageY - touch2.pageY; return Math.sqrt(dx * dx + dy * dy); } dragStartCanvas(e) { const target = e.target; if (e instanceof TouchEvent && e.touches.length >= 2) { this.isPinching = true; this.initialPinchDistance = this.getDistanceBetweenTouches(e.touches[0], e.touches[1]); this.initialImageViewArea = Object.assign({}, this.imageViewArea); // 現在のimageViewAreaを保存 e.preventDefault(); return; // ピンチ開始時は他の処理をスキップ } else { this.isDraggingCanvas = true; // キャンバスのドラッグフラグを立てる this.$canvas.classList.add('dragging'); const eventCoords = this.getEventPageXY(e); this.canvasDragStartX = eventCoords.pageX; this.canvasDragStartY = eventCoords.pageY; e.preventDefault(); } } resizeStart(e) { this.isResizing = true; this.activeHandleId = e.target.id; // Alternative to `this.id` this.$selectBox.classList.add('resizing'); const eventCoords = this.getEventPageXY(e); this.resizeStartX = eventCoords.pageX; this.resizeStartY = eventCoords.pageY; const selectBoxRect = this.$selectBox.getBoundingClientRect(); this.initialBoxArea.width = selectBoxRect.width; this.initialBoxArea.height = selectBoxRect.height; this.initialBoxArea.x = selectBoxRect.left - this.$root.getBoundingClientRect().left; this.initialBoxArea.y = selectBoxRect.top - this.$root.getBoundingClientRect().top; // this.initialBoxWidth = selectBoxRect.width; // // Calculate relative position within the container using getBoundingClientRect() // this.initialBoxLeft = selectBoxRect.left - this.$root.getBoundingClientRect().left; // this.initialBoxTop = selectBoxRect.top - this.$root.getBoundingClientRect().top; e.preventDefault(); e.stopPropagation(); } dragOrResizeMove(e) { const eventCoords = this.getEventPageXY(e); if (this.isPinching && e instanceof TouchEvent && e.touches.length >= 2) { const currentPinchDistance = this.getDistanceBetweenTouches(e.touches[0], e.touches[1]); const scale = currentPinchDistance / this.initialPinchDistance; let newWidth = this.initialImageViewArea.width / scale; let newHeight = this.initialImageViewArea.height / scale; // 最小・最大サイズ制限(既存のロジックをそのまま維持) const minImageViewSize = 50; // 最小の表示エリアサイズ const maxImageViewSize = Math.max(this.$image.width, this.$image.height) * 2; // 元画像より大きく表示可能 newWidth = Math.max(minImageViewSize, Math.min(newWidth, maxImageViewSize)); newHeight = Math.max(minImageViewSize, Math.min(newHeight, maxImageViewSize)); // アスペクト比を維持するために、大きい方の制限に合わせて小さい方も調整(既存のロジックをそのまま維持) // ※ ImageEditBoxでは通常正方形で扱っているため、このアスペクト比調整はPreviewBoxでより重要です // const currentAspectRatio = this.$image.width / this.$image.height; // if (newWidth / newHeight > currentAspectRatio) { // newWidth = newHeight * currentAspectRatio; // } else if (newWidth / newHeight < currentAspectRatio) { // newHeight = newWidth / currentAspectRatio; // } // ★ ここから拡大中心点をキャンバスの中心に固定するロジックに変更 ★ const canvasRect = this.$canvas.getBoundingClientRect(); const canvasCenterX_display = canvasRect.width / 2; const canvasCenterY_display = canvasRect.height / 2; // ピンチ開始時のキャンバススケール const initialScaleX = canvasRect.width / this.initialImageViewArea.width; const initialScaleY = canvasRect.height / this.initialImageViewArea.height; // キャンバス中心点を画像ピクセルに変換 (ピンチ開始時の座標) const imageCanvasCenterX = this.initialImageViewArea.x + (canvasCenterX_display / initialScaleX); const imageCanvasCenterY = this.initialImageViewArea.y + (canvasCenterY_display / initialScaleY); // 新しい imageViewArea の左上隅を計算 // (canvasCenterX_display / (canvasRect.width / newWidth)) は新しいスケールでのキャンバス中心点(ディスプレイ座標)に対応する画像ピクセル数 const newImageViewAreaX = imageCanvasCenterX - (canvasCenterX_display / (canvasRect.width / newWidth)); const newImageViewAreaY = imageCanvasCenterY - (canvasCenterY_display / (canvasRect.height / newHeight)); this.imageViewArea.width = newWidth; this.imageViewArea.height = newHeight; this.imageViewArea.x = newImageViewAreaX; this.imageViewArea.y = newImageViewAreaY; // `imageViewArea` を画像境界内に制限する(既存のロジックをそのまま維持) // this.imageViewArea = ImageEditBox.limitAreaInParent(this.imageViewArea, { width: this.$image.width, height: this.$image.height }); this.imageSelectArea = this.calcImageSelectArea(this.$selectBox); this.refresh(); e.preventDefault(); } else if (this.isDraggingCanvas) { const deltaX = eventCoords.pageX - this.canvasDragStartX; const deltaY = eventCoords.pageY - this.canvasDragStartY; let tempImageViewArea = Object.assign({}, this.imageViewArea); // Get canvas display size and convert movement to image pixels based on ratio const canvasDisplayWidth = this.$canvas.getBoundingClientRect().width; const canvasDisplayHeight = this.$canvas.getBoundingClientRect().height; tempImageViewArea.x += -deltaX / (canvasDisplayWidth / this.imageViewArea.width); tempImageViewArea.y += -deltaY / (canvasDisplayHeight / this.imageViewArea.height); const selectBoxRect = this.$selectBox.getBoundingClientRect(); let imageMarginX = selectBoxRect.left * this.imageViewArea.width / canvasDisplayWidth; let imageMarginY = selectBoxRect.top * this.imageViewArea.height / canvasDisplayHeight; // Limit imageViewArea within image boundaries // tempImageViewArea.x = Math.max(- imageMarginX, tempImageViewArea.x); // tempImageViewArea.y = Math.max(- imageMarginY, tempImageViewArea.y); // tempImageViewArea.x = Math.min(this.$image.width - tempImageViewArea.width + imageMarginX, tempImageViewArea.x); // tempImageViewArea.y = Math.min(this.$image.height - tempImageViewArea.height + imageMarginY, tempImageViewArea.y); tempImageViewArea = this.limitAreaInParent(tempImageViewArea); this.imageViewArea = tempImageViewArea; this.canvasDragStartX = eventCoords.pageX; this.canvasDragStartY = eventCoords.pageY; this.imageSelectArea = this.calcImageSelectArea(this.$selectBox); this.refresh(); } else if (this.isResizing) { let deltaX = eventCoords.pageX - this.resizeStartX; let deltaY = eventCoords.pageY - this.resizeStartY; let newSizeChangeX = 0; let newSizeChangeY = 0; switch (this.activeHandleId) { case 'resize-rightbottom': newSizeChangeX = Math.min(deltaX, deltaY); newSizeChangeY = newSizeChangeX * this.initialBoxArea.height / this.initialBoxArea.width; break; // case 'resize-leftbottom': // newSizeChange = Math.min(-deltaX, deltaY); // break; case 'resize-righttop': newSizeChangeX = Math.min(deltaX, -deltaY); newSizeChangeY = newSizeChangeX * this.initialBoxArea.height / this.initialBoxArea.width; break; case 'resize-x': newSizeChangeX = deltaX; break; case 'resize-y': newSizeChangeY = deltaY; break; } let newArea = Object.assign({}, this.initialBoxArea); // let adjustedLeft = this.initialBoxArea.x; // let adjustedTop = this.initialBoxArea.y; // // let newSize = this.initialBoxArea.width ; // if (this.activeHandleId === 'resize-rightbottom' || this.activeHandleId === 'resize-x') { // newArea.width += newSizeChangeX; // newArea.height += newSizeChangeY; // } else if (this.activeHandleId === 'resize-righttop') { // newArea.width += newSizeChangeX * 2; // newArea.height += newSizeChangeY * 2; // } switch (this.activeHandleId) { case 'resize-rightbottom': newArea.width += newSizeChangeX; newArea.height += newSizeChangeY; break; case 'resize-righttop': newArea.x = this.initialBoxArea.x + (this.initialBoxArea.width - newArea.width - newSizeChangeX); newArea.y = this.initialBoxArea.y + (this.initialBoxArea.height - newArea.height - newSizeChangeY); newArea.width += newSizeChangeX * 2; newArea.height += newSizeChangeY * 2; break; case 'resize-x': newArea.width += newSizeChangeX; break; case 'resize-y': newArea.height += newSizeChangeY; break; // case 'resize-lefttop': // adjustedLeft = this.initialBoxLeft + deltaX; // adjustedTop = this.initialBoxTop + deltaY; // break; } const containerWidth = this.$root.offsetWidth; const containerHeight = this.$root.offsetHeight; // if (this.activeHandleId === 'resize-x') { // adjustedHeight = this.initialBoxArea.height; // } // const newArea: Area = { // x: adjustedLeft, // y: adjustedTop, // width: adjustedWidth, // height: adjustedHeight // }; // Limit width and height (if the area is larger than the parent, limit to parent's size) if (newArea.width > containerWidth) { newArea.width = containerWidth; } if (newArea.height > containerHeight) { newArea.height = containerHeight; } // Limit position newArea.x = Math.min(Math.max(0, newArea.x), containerWidth - newArea.width); newArea.y = Math.min(Math.max(0, newArea.y), containerHeight - newArea.height); Object.assign(this.$selectBox.style, AreaUtil.toCss(newArea)); this.imageSelectArea = this.calcImageSelectArea(this.$selectBox); this.refresh(); } // Prevent default scrolling during resize or drag if (this.isResizing || this.isDraggingCanvas || this.isPinching) { // ピンチも追加 e.preventDefault(); } } // --- Canvas Size Adjustment --- resizeCanvas() { // Use offsetWidth as an alternative to jQuery's .outerWidth() const containerWidth = this.$root.offsetWidth; this.$canvas.width = containerWidth; this.$canvas.height = containerWidth; // Recalculate and apply CSS for the select area const selectArea = this.calcCanvasSelectArea(); this.$selectBox.style.width = selectArea.width + 'px'; this.$selectBox.style.height = selectArea.height + 'px'; this.$selectBox.style.left = selectArea.x + 'px'; this.$selectBox.style.top = selectArea.y + 'px'; this.refresh(); // Redraw image after canvas size changes } stopDragOrResize() { if (this.isDraggingCanvas) { this.isDraggingCanvas = false; this.$canvas.classList.remove('dragging'); this.centerImageViewArea(); // Re-fit after resize const newArea = this.calcCanvasSelectArea(); Object.assign(this.$selectBox.style, AreaUtil.toCss(newArea)); this.refresh(); this.history.push(Object.assign({}, this.imageSelectArea)); } if (this.isResizing) { this.isResizing = false; this.$selectBox.classList.remove('resizing'); this.activeHandleId = ''; this.centerImageViewArea(); // Re-fit after resize const newArea = this.calcCanvasSelectArea(); Object.assign(this.$selectBox.style, AreaUtil.toCss(newArea)); this.refresh(); this.history.push(Object.assign({}, this.imageSelectArea)); } // ピンチ操作の終了を追加 if (this.isPinching) { this.isPinching = false; // ピンチ終了後の追加調整が必要な場合はここに記述 this.centerImageViewArea(); // 必要に応じてリフィット this.refresh(); this.history.push(Object.assign({}, this.imageSelectArea)); } } refresh() { super.refresh(); const canvasWidth = this.$canvas.getBoundingClientRect().width; const canvasHeight = this.$canvas.getBoundingClientRect().height; this.context.fillStyle = 'rgba(0, 0, 0, 0.5)'; // 半透明のオーバーレイ const canvasSelectArea = this.calcCanvasSelectArea(); // (A) 上部の長方形を塗りつぶす this.context.fillRect(0, 0, canvasWidth, canvasSelectArea.y); // (B) 下部の長方形を塗りつぶす this.context.fillRect(0, canvasSelectArea.y + canvasSelectArea.height, canvasWidth, canvasHeight - (canvasSelectArea.y + canvasSelectArea.height)); // (C) 左側の長方形を塗りつぶす (中央の切り抜き領域のY軸範囲内) this.context.fillRect(0, canvasSelectArea.y, canvasSelectArea.x, canvasSelectArea.height); // (D) 右側の長方形を塗りつぶす (中央の切り抜き領域のY軸範囲内) this.context.fillRect(canvasSelectArea.x + canvasSelectArea.width, canvasSelectArea.y, canvasWidth - (canvasSelectArea.x + canvasSelectArea.width), canvasSelectArea.height); // --- ここから追加される描画処理 --- // 選択領域を白枠で囲む this.context.strokeStyle = 'white'; this.context.lineWidth = 1; // 枠線の太さ this.context.strokeRect(canvasSelectArea.x, canvasSelectArea.y, canvasSelectArea.width, canvasSelectArea.height); // 選択領域内を縦横に三等分する点線を描画 this.context.setLineDash([5, 5]); // 点線のパターン(5pxの線、5pxの空白) this.context.strokeStyle = 'rgba(255, 255, 255, 0.7)'; // 点線の色(半透明の白) // 縦の点線 const thirdWidth = canvasSelectArea.width / 3; this.context.beginPath(); this.context.moveTo(canvasSelectArea.x + thirdWidth, canvasSelectArea.y); this.context.lineTo(canvasSelectArea.x + thirdWidth, canvasSelectArea.y + canvasSelectArea.height); this.context.stroke(); this.context.beginPath(); this.context.moveTo(canvasSelectArea.x + thirdWidth * 2, canvasSelectArea.y); this.context.lineTo(canvasSelectArea.x + thirdWidth * 2, canvasSelectArea.y + canvasSelectArea.height); this.context.stroke(); // 横の点線 const thirdHeight = canvasSelectArea.height / 3; this.context.beginPath(); this.context.moveTo(canvasSelectArea.x, canvasSelectArea.y + thirdHeight); this.context.lineTo(canvasSelectArea.x + canvasSelectArea.width, canvasSelectArea.y + thirdHeight); this.context.stroke(); this.context.beginPath(); this.context.moveTo(canvasSelectArea.x, canvasSelectArea.y + thirdHeight * 2); this.context.lineTo(canvasSelectArea.x + canvasSelectArea.width, canvasSelectArea.y + thirdHeight * 2); this.context.stroke(); this.context.setLineDash([]); // 点線設定をリセット // 中心に十字を描画 const centerX = canvasSelectArea.x + canvasSelectArea.width / 2; const centerY = canvasSelectArea.y + canvasSelectArea.height / 2; const crossSize = 16; Math.min(canvasSelectArea.width, canvasSelectArea.height) * 0.05; // 領域の短い方の辺の10%を十字のサイズとする this.context.lineWidth = 2; this.context.strokeStyle = 'rgba(255, 255, 255, 0.7)'; // 半透明の白 this.context.beginPath(); this.context.moveTo(centerX - crossSize, centerY); this.context.lineTo(centerX + crossSize, centerY); this.context.stroke(); this.context.beginPath(); this.context.moveTo(centerX, centerY - crossSize); this.context.lineTo(centerX, centerY + crossSize); this.context.stroke(); // --- 追加される描画処理はここまで --- this.onRefreshed(); } undo() { if (this.history.length > 1) { this.history.pop(); this.imageSelectArea = this.history[this.history.length - 1]; this.centerImageViewArea(); this.refresh(); } } } bupan.ImageEditBox = ImageEditBox; // ThumBox Class // Manages the canvas for individual thumbnail boxes, including image loading and drawing. class CameraBox { constructor($root) { this.currentStream = null; // カメラのMediaStreamを保持 this.videoTrack = null; this.imageViewArea = { x: 0, y: 0, width: 0, height: 0 }; this.imageUrl = ""; if ($root) this.$root = $root; this.$video = document.createElement("video"); this.$video.setAttribute("autoplay", "autoplay"); this.$video.setAttribute("playsinline", "playsinline"); this.$root.appendChild(this.$video); } // Start camera stream startCamera() { return __awaiter(this, void 0, void 0, function* () { try { const stream = yield navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment', width: 1920, height: 1080 // 高さを48s0~1080ピクセルに設定 } }); this.$video.srcObject = stream; this.currentStream = stream; this.videoTrack = stream.getVideoTracks()[0]; const capabilities = this.videoTrack.getCapabilities(); if (capabilities.zoom) { console.log(`ズーム範囲: ${capabilities.zoom.min} - ${capabilities.zoom.max}`); } else { console.log("このデバイスはズームをサポートしていません。"); } } catch (err) { // alert("Error accessing camera: "); } }); } // Stop camera stream stopCamera() { if (this.currentStream) { this.currentStream.getTracks().forEach(track => track.stop()); } } setZoom(zoomValue) { return __awaiter(this, void 0, void 0, function* () { try { yield this.videoTrack.applyConstraints({ advanced: [{ zoom: zoomValue }] }); } catch (err) { console.error("ズームの設定に失敗しました:", err); } }); } capture() { const canvas = document.createElement('canvas'); canvas.width = this.$video.videoWidth; canvas.height = this.$video.videoHeight; const context = canvas.getContext('2d'); // Draw the video frame to the canvas, maintaining aspect ratio and covering the area const videoAspectRatio = this.$video.videoWidth / this.$video.videoHeight; const canvasAspectRatio = canvas.width / canvas.height; let sx, sy, sWidth, sHeight; // Source rectangle in the video if (videoAspectRatio > canvasAspectRatio) { // Video is wider than canvas, crop left/right sHeight = this.$video.videoHeight; sWidth = sHeight * canvasAspectRatio; sx = (this.$video.videoWidth - sWidth) / 2; sy = 0; } else { // Video is taller than canvas, crop top/bottom sWidth = this.$video.videoWidth; sHeight = sWidth / canvasAspectRatio; sx = 0; sy = (this.$video.videoHeight - sHeight) / 2; } context.drawImage(this.$video, sx, sy, sWidth, sHeight, 0, 0, canvas.width, canvas.height); const imageData = canvas.toDataURL('image/png'); // Convert to Base64 PNG return imageData; } } bupan.CameraBox = CameraBox; })(bupan || (bupan = {}));