HTML+CSS+JavaScriptで見積書に押印をするデモ

HTML、CSS、JavaScriptを使って見積書に押印をするデモをChatGPT-3.5と一緒に作りました。見積書の上でマウスをドラッグすると正方形の領域が描かれ、ドラッグが終わると押印されます。

このデモは「見積書の好きな位置に押印ができること」を示すものです。実際の業務に利用するには、次のような点を工夫する必要があります。ご留意ください。
- PDFで作った見積もりを画像に変換する
- 押印データが抜き取られないようサーバーサイドで押印する
- 押印位置をクライアントからサーバーに伝える

ChatGPT-3.5に示してもらったコードは、一度だけ、コードレビューをして指摘をする必要がありました。とはいえ、5回のやりとりでやりたいことが実現できました。さらに、ChatGPT-4に読みやすいコードに修正してもらいました。

https://codepen.io/takatama/pen/oNPMMOL

<p class="codepen" data-height="900" data-default-tab="result" data-slug-hash="oNPMMOL" data-user="takatama" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/takatama/pen/oNPMMOL">
  Untitled</a> by Hirokazu Takatama (<a href="https://codepen.io/takatama">@takatama</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

素材の提供元（どうもありがとうございます！）：
- [見積書](https://japaclip.com/estimate/)（ジャパクリップ）
- [たいへんよくできました](https://www.irasutoya.com/2013/10/blog-post_9162.html)（いらすとや）

# 印鑑のサイズを固定する場合

マウスを見積書の上に移動するとグレーの四角形が表示され、クリックするとその位置に押印されます。押印後、位置を調整し直したい場合は、もう一度クリックしてください。

https://codepen.io/takatama/pen/PoywWdo

<p class="codepen" data-height="900" data-default-tab="result" data-slug-hash="PoywWdo" data-user="takatama" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/takatama/pen/PoywWdo">
  Untitled</a> by Hirokazu Takatama (<a href="https://codepen.io/takatama">@takatama</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

# ChatGPTによるまとめ

## HTML

1. canvas要素を作成します。
2. style.cssファイルをリンクします。
3. script.jsファイルをリンクします。

```html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Canvas Example</title>
	<link rel="stylesheet" href="style.css">
</head>
<body>
	<canvas id="canvas"></canvas>
	<script src="script.js"></script>
</body>
</html>
```

## CSS

1. canvas要素に、1ピクセルの黒い境界線を定義します。

```css
canvas {
	border: 1px solid #000000;
}
```

## JavaScript（リファクタリング前）

1. baseImageとstampImageのData URLを指定します。
2. canvasとctx変数を初期化します。
3. baseImageを読み込んで、canvasの幅と高さを設定します。
4. square positionとsizeを初期化します。
5. stampVisibleフラグをfalseに設定します。
6. squareを描画するためのdrawSquare関数を定義します。
7. mouse downイベントハンドラを追加し、マウスの位置を取得して、正方形の位置を設定します。
8. mouse moveイベントハンドラを追加し、マウスの位置から正方形のサイズを計算し、正方形を描画します。
9. mouse upイベントハンドラを追加し、正方形を削除してstampImageを描画します。
10. stampImageを読み込みます。

```javascript
// Base and stamp image data URLs
const baseImage = "..."; // ここにbaseImageのデータURLを入力してください
const stampImage = "..."; // ここにstampImageのデータURLを入力してください

// Canvas and context
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

// Set canvas width and height to match baseImage
const base = new Image();
base.onload = function() {
	canvas.width = base.width;
	canvas.height = base.height;
	ctx.drawImage(base, 0, 0);
}
base.src = baseImage;

// Variables for square position and size
let x = 0;
let y = 0;
let size = 0;

// Flag for stamp image visibility
let stampVisible = false;

// Function to draw square
function drawSquare() {
	ctx.clearRect(0, 0, canvas.width, canvas.height);
	ctx.drawImage(base, 0, 0);
	ctx.fillStyle = "#00000055";
	ctx.fillRect(x, y, size, size);
}

// Event listener for mouse down on canvas
canvas.addEventListener("mousedown", function(e) {
	// Get mouse position
	const rect = canvas.getBoundingClientRect();
	const mouseX = e.clientX - rect.left;
	const mouseY = e.clientY - rect.top;

	// Set square position
	x = mouseX;
	y = mouseY;

	// Set stamp image visibility to false
	stampVisible = false;

	// Event listener for mouse move on canvas
	canvas.addEventListener("mousemove", moveListener);
});

// Event listener for mouse move on canvas
function moveListener(e) {
	// Get mouse position
	const rect = canvas.getBoundingClientRect();
	const mouseX = e.clientX - rect.left;
	const mouseY = e.clientY - rect.top;

	// Set square size
	size = Math.abs(mouseX - x);

	// Draw square
	drawSquare();

	// Remove event listener if mouse up
	canvas.addEventListener("mouseup", function upListener() {
		// Remove event listener for mouse move
		canvas.removeEventListener("mousemove", moveListener);

		// Set stamp image visibility to true
		stampVisible = true;

		// Load stamp image and draw if stampVisible is true
		const stamp = new Image();
		stamp.onload = function() {
			if (stampVisible) {
				ctx.drawImage(stamp, x, y, size, size);
			}
		}
		stamp.src = stampImage;

		// Remove event listener for mouse up
		canvas.removeEventListener("mouseup", upListener);
	});
}
```

## JavaScript（リファクタリング後）

1. baseImage（背景画像）とstampImage（スタンプ画像）のデータURLを定義します。
1. HTMLキャンバス要素を取得し、その2Dコンテキストを取得します。
1. baseImageを読み込み、キャンバスのサイズをbaseImageのサイズに設定し、キャンバスに描画します。
1. stampImageを読み込みますが、まだ描画は行いません。
1. stampImageが表示されているかどうかを示すboolean変数stampVisibleを定義します。
1. マウスの座標を取得するgetMousePosition関数を定義します。
1. キャンバスをクリアしてbaseImageを描画するdrawBaseImage関数を定義します。
1. 正方形を描画するdrawSquare関数を定義します。この関数は、正方形の開始座標とサイズを引数に取ります。
1. スタンプ画像を描画するdrawStamp関数を定義します。この関数は、スタンプ画像、開始座標、サイズを引数に取ります。
1. キャンバス上でマウスボタンが押されたとき（mousedownイベント）に、start座標を設定し、stampVisibleをfalseに設定します。
1. キャンバス上でマウスが移動するとき（mousemoveイベント）、stampVisibleがfalseの場合に、正方形を描画するためにdrawSquare関数を呼び出します。
1. キャンバス上でマウスボタンが離されたとき（mouseupイベント）、stampVisibleがfalseの場合に、スタンプ画像を描画するためにdrawStamp関数を呼び出し、stampVisibleをtrueに設定します。

このコードにより、ユーザーがキャンバス上でマウスをドラッグすると正方形が表示され、マウスボタンを離すとスタンプ画像が表示される仕組みが実現されています。また、新たにマウスをドラッグすると、スタンプ画像が消えて正方形が表示されるようになっています。

```javascript
const baseImage = "...";
const stampImage = "...";

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

const base = new Image();
base.onload = function () {
  canvas.width = base.width;
  canvas.height = base.height;
  ctx.drawImage(base, 0, 0);
};
base.src = baseImage;

const stamp = new Image();
stamp.src = stampImage;

let stampVisible = false;
let start;
let size;

function getMousePosition(e) {
  const rect = canvas.getBoundingClientRect();
  const mouseX = e.clientX - rect.left;
  const mouseY = e.clientY - rect.top;
  return { x: mouseX, y: mouseY };
}

function drawBaseImage() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.drawImage(base, 0, 0);
}

function drawSquare(start, size) {
  drawBaseImage();
  ctx.fillStyle = "#00000055";
  ctx.fillRect(start.x, start.y, size, size);
}

function drawStamp(image, start, size) {
  drawBaseImage();
  ctx.drawImage(image, start.x, start.y, size, size);
}

canvas.addEventListener("mousedown", function (e) {
  start = getMousePosition(e);
  stampVisible = false;
});

canvas.addEventListener("mousemove", function (e) {
  if (!stampVisible) {
    const current = getMousePosition(e);
    size = Math.abs(current.x - start.x);
    drawSquare(start, size);
  }
});

canvas.addEventListener("mouseup", function (e) {
  if (!stampVisible) {
    const end = getMousePosition(e);
    size = Math.abs(end.x - start.x);

    drawStamp(stamp, start, size); // This will also clear the square before drawing the stamp image
    stampVisible = true;
  }
});
```

# ChatGPT-3.5への指示

## ステップ1

あなたは経験豊富なシニアプログマーです。特に、ソースコード読みやすさについてこだわりをもっています。私がこれから伝える要件を満たすソースコードを教えて下さい。

- HTML、CSS、JavaScriptで実現する。
- 2つの画像を使う。一つは baseImage、もう一つは stampImage と呼ぶ。画像の Data URL はソースコードの中に直接記述されている。
- Webページを表示すると、canvas に baseImage を表示する。次に、利用者が canvas 上でマウスをドラッグすると、正方形の領域が表示される。利用者がマウスのドラッグを終了すると、正方形の領域は消えて、その位置に stampImage を表示する。さらに利用者がcanvas上でマウスをドラッグし始めると、stampImageは消えて、正方形を表示しはじめる。

## ステップ2

- HTML, CSS, JSをそれぞれ分けて書いてください。

## ステップ3

- baseImage と stampImage の具体的な値は、私が直接置き換えるので"..."と書いておけば大丈夫です。
- canvasのサイズはbaseImageと同じにしてください。

## ステップ4（不具合を報告）

- baseImageは正しく表示されています。また、正方形も正しく表示されています。しかし、マウスドラッグが終わったあとに、stampImageが表示されません。

## ステップ5（不具合が解消されないのでコードをレビュー）

- mouseup のときに、stampVisible が false になっているので表示されないのではないでしょうか？

ここまでで、最初に示したコードが完成しました。

# ChatGPT-4への指示（リファクタリングを依頼）

## ステップ1

私が要件とソースコードを示します。ソースコードは正しく動作していますが、改善すべき点があれば教えて下さい。

### 回答

全体的にコードは問題なく機能すると思われますが、いくつかの改善点があります。以下に、コードの綺麗さや効率を改善するための提案を示します。

重複するコードを関数にまとめる
グローバル変数の使用を減らす
イベントリスナーの登録と解除を簡素化する

（改善後のコード）

## ステップ2

stampを毎回生成するのは無駄ではないでしょうか？

### 回答

おっしゃる通り、現在のコードでは毎回新しいImageオブジェクトを生成していますが、これは効率が悪いです。最初にImageオブジェクトを生成し、その後は同じオブジェクトを再利用することで、パフォーマンスを向上させることができます。

（改善後のコード）

## ステップ3

moveListenerとupListenerをいちいちaddしたりremoveしたりするのはなぜですか？

### 回答

moveListener と upListener を動的に追加および削除する理由は、特定の条件下でのみこれらのイベントリスナーが機能するようにするためです。特に、マウスがキャンバス上でドラッグされているときにのみ、正方形の表示が更新されるようにする必要があります。

moveListener: マウスが押されている間（ドラッグ中）にのみ、正方形を更新して再描画する必要があります。マウスが押されていない状態で mousemove イベントが発生しても、何も起こるべきではありません。mousedown イベントが発生したときに moveListener を追加することで、正方形のドラッグを開始したときにのみリスナーが機能するようにしています。

upListener: マウスがキャンバス上でドラッグされている間にのみ、マウスを離す（mouseup）ことでスタンプ画像を表示する必要があります。マウスがキャンバス上でドラッグされていない場合、mouseup イベントが発生しても何も起こるべきではありません。mousedown イベントが発生したときに upListener を追加することで、正方形のドラッグ中にのみリスナーが機能するようにしています。

また、マウスのドラッグ操作が終了した後に、moveListener と upListener を削除することで、不要なイベントリスナーが機能しないようにしています。これにより、パフォーマンスが向上し、イベントリスナーが適切なタイミングでのみ発火することが保証されます。

## ステップ4

stampを描画するかどうかを表すbooleanの変数を使って、同じことを実現することはできますか？

### 回答

はい、stampVisible という boolean 変数を使って、同様のことを実現することができます。以下に、その方法を示すコードを示します。

（改善後のコード）

このコードでは、mousedown イベントが発生すると stampVisible を false に設定し、mouseup イベントが発生したときにスタンプ画像が表示されるように stampVisible を true に設定しています。mousemove イベント内で、stampVisible が false の場合にのみ正方形を描画することで、正方形のドラッグ中にのみ正方形が表示されるようにしています。

ただし、この方法では、イベントリスナーが常に登録されているため、パフォーマンスに影響がある可能性があります。ただし、イベントリスナーの登録と削除のコードが簡略化されているため、コードの可読性が向上しています。実際のパフォーマンスへの影響は、アプリケーションの規模と使用状況によって異なります。

## ステップ5

stampImageを表示したときに、正方形が消えません。

### 回答

申し訳ありません。私の提案したコードでは、mouseupイベントが発生したときに、stampImageを表示する前に正方形が消えるようにする処理が抜けていました。以下のコードでは、この問題を修正しました。

（改善後のコード）

この修正では、drawBaseImageという新しい関数を作成し、キャンバスをクリアしてbaseImageを描画する処理をまとめています。この関数は、drawSquareおよびdrawStamp関数で使用され、stampImageを表示する前に正方形が消えるようになります。これにより、問題が解決されるはずです。