2024/6/29_ハッカソン
![Van🤗](https://res.cloudinary.com/zenn/image/fetch/s--Ae3bCLsA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/aa244fefb1.jpeg)
開幕まで30分を切った。
勝つために必要なことを考えてみる。
- 審査員のペルソナの理解
- 時事ネタ
- 視覚、聴覚訴える
おそらく ゲーム
のプロダクトが多くなるのではないかと予想する。
レッドオーシャンの中で独自性を出すか、違う分野で攻めるか。
![Van🤗](https://res.cloudinary.com/zenn/image/fetch/s--Ae3bCLsA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/aa244fefb1.jpeg)
開幕までProtoPediaをみて、勝てるプロダクトのイメージをつけておく。
![Van🤗](https://res.cloudinary.com/zenn/image/fetch/s--Ae3bCLsA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/aa244fefb1.jpeg)
ハード系ばっかりでWebアプリの作品があまりなかった・・・
探し方が悪かったのだろうか
![Van🤗](https://res.cloudinary.com/zenn/image/fetch/s--Ae3bCLsA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/aa244fefb1.jpeg)
テーマは 「休日の癒しを提供してくれるプロダクト」
![Van🤗](https://res.cloudinary.com/zenn/image/fetch/s--Ae3bCLsA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/aa244fefb1.jpeg)
ArtifactsはReactで出力される傾向がある
動きのあるwebアプリケーションを要求すると、ArtifactsはReactというライブラリを利用するための形式で出力されます。
ですので、CodePenのようなwebエディターにそのまま貼り付けても動きません。
↓
VanillaのJavaScriptにしてみる
ですので、動かすために特にライブラリなどを使っていないJavaScript(Vanillaと言います、アイスクリームのバニラ味のようにプレーンなイメージのようです)に変換しましょう!
![Van🤗](https://res.cloudinary.com/zenn/image/fetch/s--Ae3bCLsA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/aa244fefb1.jpeg)
審査員はオーディエンス投票。皆さんの属性はバラバラだけど デジタルに強い
というのが共通点。
時事ネタでいえば Apple Vision Pro
![Van🤗](https://res.cloudinary.com/zenn/image/fetch/s--Ae3bCLsA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/aa244fefb1.jpeg)
以下のWebアプリをつくろうと思う
座禅をしているところをWebカメラで撮影して、体が傾いたら警告音を出す。逆に体が傾かなかったら10秒ごとに美少女の声で応援される。
![Van🤗](https://res.cloudinary.com/zenn/image/fetch/s--Ae3bCLsA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/aa244fefb1.jpeg)
プロンプト
## 命令
あなたはエキスパートのWebエンジニアです。
以下のWebアプリを実現するコードを教えて
"座禅をしているところをWebカメラで撮影して、体が傾いたら警告音を出す。逆に体が傾かなかったら10秒ごとに美少女の声で応援される。"
## 条件
* ライブラリを使わない実装としてください
* Reactの代わりにVanillaJSで出力して
![Van🤗](https://res.cloudinary.com/zenn/image/fetch/s--Ae3bCLsA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/aa244fefb1.jpeg)
最初のコード
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>座禅姿勢チェッカー</title>
<style>
#video {
transform: scaleX(-1);
}
</style>
</head>
<body>
<h1>座禅姿勢チェッカー</h1>
<video id="video" width="640" height="480" autoplay></video>
<canvas id="canvas" width="640" height="480" style="display:none;"></canvas>
<br>
<button id="startButton">開始</button>
<button id="stopButton" disabled>停止</button>
<script>
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const startButton = document.getElementById('startButton');
const stopButton = document.getElementById('stopButton');
let stream;
let intervalId;
let lastPosition = null;
let tiltThreshold = 0.1; // 傾きの閾値(ピクセル単位)
let encouragementTimer = 0;
const warningSound = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBTGJ0fPTgjMGHm7A7+OZSA0PVqzn77BdGAg+ltryxnMpBSd+zPLaizsIGGS57OihUBELTKXh8bllHgU2jdXzzn0vBSF1xe/glEILElyx6OyrWBUIQ5zd8sFuJAUuhM/z1YU2Bhxqvu7mnEoODlOq5O+zYBoGPJPY8cdmLAUme8rx3I4+CRZiturqpVITC0mj4PK8aB8GM4nU8tGAMQYfcsLu45ZFDBFZr+ftrVoXCECb3PLEcSYELIHO8diJOQcZaLvt559NEAxPqOPwtmMcBjiP1/PMeS0GI3fH8N2RQAoUXrTp66hVFApGnt/yvmwhBTCJ0fPTgjQGHW/A7eSaRw0PVqzl77BeGQc9ltvyw3IsBSZ+zPDaizsIGGS56+mjTxELTKXh8bllHgU1jdT0z3wvBSJ1xe/glEILElyx6OyrWRUIRJve8sFuJAUug8/y1oU2Bhxqvu3mnEoPDlOq5O+zYRsGPJLZ8s1mLAUmfMrx3I4+CRVht+rqpVMSC0mj4PG8aiAFM4nU8tGAMQYfccPu45ZFDBFYr+ftrVwWCEGa3PLEcSYGK4DN8tiIOQcZabzs56BODwxPpuPxtmIdBjiP1/PMeywGI3fH8N+RQAoUXrTp66hWEwlGnt/yv2wiBDCJ0fPTgzQHHG/A7eSaSQ0PVqvm77BeGQc9ltv0w3IsBSZ9y/HajDsIF2W56+mjUREKTKPi8blnHgU1jdTy0HwvBSF1xPDglEILElux6eyrWRUJQ5vd88FwJAQug8/y1oY2Bhxqvu3mnEwODVKp5e+zYRsGPJLX88xoLAUmfMrx3I4/CBVhtuvqpVMSC0mi3/G9aiAFM4nU8tGBMQYfccLv45dGCxFYr+ftrVwXB0Ca3PLEcicFKoDN8tiIOQcZabzt56BODwxPpuPxt2MeBjiP1/PMey4FI3bH8d+RQQkUXrTp66hWFAlFnt/yv2wiBDCH0fPTgzUGHG3A7uSaSQ0PVKzm7rJeGAc9ldr0xHMrBSV9y/HajDwIF2S46+mjUREKTKPi8blnHwU1jdTy0H4wBiF0xPDglUMMEVux5+2sWRUJQ5vd88NxJAQug8/y1oY3Bxtpve3mnUwODVKp5e+zYhsGOpHX88xoLQUmfMnx3Y8+CBZhtuvqpVMSC0mh4PG9aiAFM4nT89GBMgUfccLv45dGDBBYrujtrlwXB0Ca3PLEcycFKoDN8tiKOQcZabvt56BOEQxPpuPxt2QeBjiP1vPNfC4FI3bH79+RQQsUXbPq66hWFAlFnt/yv2wiBDCH0fPThDUGHG3A7uSaSQ0PVKzm7rJfGQc9lNr0xHQsBSV9y/HajDwJFmS46+mjUhEKTKLh8btnHwU1jdTy0H4wBiBzw/DglUQMEVux5+2sWhYIQprd88NyJAQug8/y14Y3Bxppve3mnk0NDVKo5PC1YhsHOpHX88xoLgQle8nx3Y9ACBVgtunqpVQSCkmi4PG9ayEEMojT89GBMgUfccLu5JdGDBBYrufurl4XB0CZ2/PEcycFKn/M8tiKOQgZabvt56FPEAxOpePxt2QeBjiO1vPNfS4FI3bH79+RQQsUXbPq66lXFAlEnt7zv24iBDCH0PPThDUGHG3A7uSbSg0PVKrl7rJfGQc9lNr0xHQsBSV9yvHajTwJFmS46umkUhIJTKLh8btnHwU1i9Ty0H4xBSBzw/DhloQMEFqw5+2sWhYIQprc88NyJQMtg87y2IY3Bxppve3mnk0ODlKo5PC1YxsGOZDW88xoLgQle8rx3Y9ACBVgterqpVQTCkidDw==');
const encouragementSound = new Audio('data:audio/wav;base64,UklGRqQDAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YYADAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBTGJ0fPTgjMGHm7A7+OZSA0PVqzn77BdGAg+ltryxnMpBSd+zPLaizsIGGS57OihUBELTKXh8bllHgU2jdXzzn0vBSF1xe/glEILElyx6OyrWBUIQ5zd8sFuJAUuhM/z1YU2Bhxqvu7mnEoODlOq5O+zYBoGPJPY8cdmLAUme8rx3I4+CRZiturqpVITC0mj4PK8aB8GM4nU8tGAMQYfcsLu45ZFDBFZr+ftrVoXCECb3PLEcSYELIHO8diJOQcZaLvt559NEAxPqOPwtmMcBjiP1/PMeS0GI3fH8N2RQAoUXrTp66hVFApGnt/yvmwhBTCJ0fPTgjQGHW/A7eSaRw0PVqzl77BeGQc9ltvyw3IsBSZ+zPDaizsIGGS56+mjTxELTKXh8bllHgU1jdT0z3wvBSJ1xe/glEILElyx6OyrWRUIRJve8sFuJAUug8/y1oU2Bhxqvu3mnEoPDlOq5O+zYRsGPJLZ8s1mLAUmfMrx3I4+CRVht+rqpVMSC0mj4PG8aiAFM4nU8tGAMQYfccPu45ZFDBFYr+ftrVwWCEGa3PLEcSYGK4DN8tiIOQcZabzs56BODwxPpuPxtmIdBjiP1/PMeywGI3fH8N+RQAoUXrTp66hWEwlGnt/yv2wiBDCJ0fPTgzQHHG/A7eSaSQ0PVqvm77BeGQc9ltv0w3IsBSZ9y/HajDsIF2W56+mjUREKTKPi8blnHgU1jdTy0HwvBSF1xPDglEILElux6eyrWRUJQ5vd88FwJAQug8/y1oY2Bhxqvu3mnEwODVKp5e+zYRsGPJLX88xoLAUmfMrx3I4/CBVhtuvqpVMSC0mi3/G9aiAFM4nU8tGBMQYfccLv45dGCxFYr+ftrVwXB0Ca3PLEcicFKoDN8tiIOQcZabzt56BODwxPpuPxt2MeBjiP1/PMey4FI3bH8d+RQQ==');
startButton.addEventListener('click', startCapture);
stopButton.addEventListener('click', stopCapture);
async function startCapture() {
try {
stream = await navigator.mediaDevices.getUserMedia({ video: true });
video.srcObject = stream;
startButton.disabled = true;
stopButton.disabled = false;
intervalId = setInterval(checkPosture, 500);
} catch (err) {
console.error("エラー: ", err);
}
}
function stopCapture() {
clearInterval(intervalId);
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
video.srcObject = null;
startButton.disabled = false;
stopButton.disabled = true;
lastPosition = null;
encouragementTimer = 0;
}
function checkPosture() {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const centerOfMass = calculateCenterOfMass(imageData);
if (lastPosition === null) {
lastPosition = centerOfMass;
}
const movement = Math.abs(centerOfMass.x - lastPosition.x) + Math.abs(centerOfMass.y - lastPosition.y);
if (movement > tiltThreshold) {
warningSound.play();
encouragementTimer = 0;
} else {
encouragementTimer++;
if (encouragementTimer >= 10) {
encouragementSound.play();
encouragementTimer = 0;
}
}
lastPosition = centerOfMass;
}
function calculateCenterOfMass(imageData) {
let sumX = 0;
let sumY = 0;
let count = 0;
for (let y = 0; y < imageData.height; y++) {
for (let x = 0; x < imageData.width; x++) {
const i = (y * imageData.width + x) * 4;
const r = imageData.data[i];
const g = imageData.data[i + 1];
const b = imageData.data[i + 2];
if (r + g + b > 100) { // 簡単な閾値処理
sumX += x;
sumY += y;
count++;
}
}
}
return {
x: sumX / count,
y: sumY / count
};
}
</script>
</body>
</html>
![Van🤗](https://res.cloudinary.com/zenn/image/fetch/s--Ae3bCLsA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/aa244fefb1.jpeg)
判定甘すぎるから閾値を厳しめにした
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>座禅姿勢チェッカー</title>
<style>
#video {
transform: scaleX(-1);
}
</style>
</head>
<body>
<h1>座禅姿勢チェッカー</h1>
<video id="video" width="640" height="480" autoplay></video>
<canvas id="canvas" width="640" height="480" style="display:none;"></canvas>
<br>
<button id="startButton">開始</button>
<button id="stopButton" disabled>停止</button>
<script>
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const startButton = document.getElementById('startButton');
const stopButton = document.getElementById('stopButton');
let stream;
let intervalId;
let lastPosition = null;
let tiltThreshold = 0.05; // 傾きの閾値(ピクセル単位)
let encouragementTimer = 0;
const warningSound = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBTGJ0fPTgjMGHm7A7+OZSA0PVqzn77BdGAg+ltryxnMpBSd+zPLaizsIGGS57OihUBELTKXh8bllHgU2jdXzzn0vBSF1xe/glEILElyx6OyrWBUIQ5zd8sFuJAUuhM/z1YU2Bhxqvu7mnEoODlOq5O+zYBoGPJPY8cdmLAUme8rx3I4+CRZiturqpVITC0mj4PK8aB8GM4nU8tGAMQYfcsLu45ZFDBFZr+ftrVoXCECb3PLEcSYELIHO8diJOQcZaLvt559NEAxPqOPwtmMcBjiP1/PMeS0GI3fH8N2RQAoUXrTp66hVFApGnt/yvmwhBTCJ0fPTgjQGHW/A7eSaRw0PVqzl77BeGQc9ltvyw3IsBSZ+zPDaizsIGGS56+mjTxELTKXh8bllHgU1jdT0z3wvBSJ1xe/glEILElyx6OyrWRUIRJve8sFuJAUug8/y1oU2Bhxqvu3mnEoPDlOq5O+zYRsGPJLZ8s1mLAUmfMrx3I4+CRVht+rqpVMSC0mj4PG8aiAFM4nU8tGAMQYfccPu45ZFDBFYr+ftrVwWCEGa3PLEcSYGK4DN8tiIOQcZabzs56BODwxPpuPxtmIdBjiP1/PMeywGI3fH8N+RQAoUXrTp66hWEwlGnt/yv2wiBDCJ0fPTgzQHHG/A7eSaSQ0PVqvm77BeGQc9ltv0w3IsBSZ9y/HajDsIF2W56+mjUREKTKPi8blnHgU1jdTy0HwvBSF1xPDglEILElux6eyrWRUJQ5vd88FwJAQug8/y1oY2Bhxqvu3mnEwODVKp5e+zYRsGPJLX88xoLAUmfMrx3I4/CBVhtuvqpVMSC0mi3/G9aiAFM4nU8tGBMQYfccLv45dGCxFYr+ftrVwXB0Ca3PLEcicFKoDN8tiIOQcZabzt56BODwxPpuPxt2MeBjiP1/PMey4FI3bH8d+RQQ==');
const encouragementSound = new Audio('data:audio/wav;base64,UklGRqQDAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YYADAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBTGJ0fPTgjMGHm7A7+OZSA0PVqzn77BdGAg+ltryxnMpBSd+zPLaizsIGGS57OihUBELTKXh8bllHgU2jdXzzn0vBSF1xe/glEILElyx6OyrWBUIQ5zd8sFuJAUuhM/z1YU2Bhxqvu7mnEoODlOq5O+zYBoGPJPY8cdmLAUme8rx3I4+CRZiturqpVITC0mj4PK8aB8GM4nU8tGAMQYfcsLu45ZFDBFZr+ftrVoXCECb3PLEcSYELIHO8diJOQcZaLvt559NEAxPqOPwtmMcBjiP1/PMeS0GI3fH8N2RQAoUXrTp66hVFApGnt/yvmwhBTCJ0fPTgjQGHW/A7eSaRw0PVqzl77BeGQc9ltvyw3IsBSZ+zPDaizsIGGS56+mjTxELTKXh8bllHgU1jdT0z3wvBSJ1xe/glEILElyx6OyrWRUIRJve8sFuJAUug8/y1oU2Bhxqvu3mnEoPDlOq5O+zYRsGPJLZ8s1mLAUmfMrx3I4+CRVht+rqpVMSC0mj4PG8aiAFM4nU8tGAMQYfccPu45ZFDBFYr+ftrVwWCEGa3PLEcSYGK4DN8tiIOQcZabzs56BODwxPpuPxtmIdBjiP1/PMeywGI3fH8N+RQAoUXrTp66hWEwlGnt/yv2wiBDCJ0fPTgzQHHG/A7eSaSQ0PVqvm77BeGQc9ltv0w3IsBSZ9y/HajDsIF2W56+mjUREKTKPi8blnHgU1jdTy0HwvBSF1xPDglEILElux6eyrWRUJQ5vd88FwJAQug8/y1oY2Bhxqvu3mnEwODVKp5e+zYRsGPJLX88xoLAUmfMrx3I4/CBVhtuvqpVMSC0mi3/G9aiAFM4nU8tGBMQYfccLv45dGCxFYr+ftrVwXB0Ca3PLEcicFKoDN8tiIOQcZabzt56BODwxPpuPxt2MeBjiP1/PMey4FI3bH8d+RQQ==');
startButton.addEventListener('click', startCapture);
stopButton.addEventListener('click', stopCapture);
async function startCapture() {
try {
stream = await navigator.mediaDevices.getUserMedia({ video: true });
video.srcObject = stream;
startButton.disabled = true;
stopButton.disabled = false;
intervalId = setInterval(checkPosture, 500);
} catch (err) {
console.error("エラー: ", err);
}
}
function stopCapture() {
clearInterval(intervalId);
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
video.srcObject = null;
startButton.disabled = false;
stopButton.disabled = true;
lastPosition = null;
encouragementTimer = 0;
}
function checkPosture() {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const centerOfMass = calculateCenterOfMass(imageData);
if (lastPosition === null) {
lastPosition = centerOfMass;
}
const movement = Math.abs(centerOfMass.x - lastPosition.x) + Math.abs(centerOfMass.y - lastPosition.y);
if (movement > tiltThreshold) {
warningSound.play();
encouragementTimer = 0;
} else {
encouragementTimer++;
if (encouragementTimer >= 10) {
encouragementSound.play();
encouragementTimer = 0;
}
}
lastPosition = centerOfMass;
}
function calculateCenterOfMass(imageData) {
let sumX = 0;
let sumY = 0;
let count = 0;
for (let y = 0; y < imageData.height; y++) {
for (let x = 0; x < imageData.width; x++) {
const i = (y * imageData.width + x) * 4;
const r = imageData.data[i];
const g = imageData.data[i + 1];
const b = imageData.data[i + 2];
if (r + g + b > 100) { // 簡単な閾値処理
sumX += x;
sumY += y;
count++;
}
}
}
return {
x: sumX / count,
y: sumY / count
};
}
</script>
</body>
</html>
![Van🤗](https://res.cloudinary.com/zenn/image/fetch/s--Ae3bCLsA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/aa244fefb1.jpeg)
もう制限きてしまった笑
![Van🤗](https://res.cloudinary.com/zenn/image/fetch/s--Ae3bCLsA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/aa244fefb1.jpeg)
だめだ。。。音声が上手くいかない。
切り替えて画面上にエフェクトがでるようにしよう
![Van🤗](https://res.cloudinary.com/zenn/image/fetch/s--Ae3bCLsA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/aa244fefb1.jpeg)
う~んそもそもこれはArtifacts機能の特徴関係なくなっているのでは???
![Van🤗](https://res.cloudinary.com/zenn/image/fetch/s--Ae3bCLsA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/aa244fefb1.jpeg)
姿勢が悪いときは音を鳴らして、いいときは赤ちゃんがダンスするようにした
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>座禅姿勢チェッカー</title>
<style>
#video {
transform: scaleX(-1);
}
#currentImage {
width: 100%;
max-width: 640px;
display: none; /* 最初は非表示 */
}
</style>
</head>
<body>
<h1>座禅姿勢チェッカー</h1>
<video id="video" width="640" height="480" autoplay></video>
<canvas id="canvas" width="640" height="480" style="display:none;"></canvas>
<br>
<button id="startButton">開始</button>
<button id="stopButton" disabled>停止</button>
<script>
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const startButton = document.getElementById('startButton');
const stopButton = document.getElementById('stopButton');
let stream;
let intervalId;
let lastPosition = null;
let tiltThreshold = 0.05; // 傾きの閾値(ピクセル単位)
let lastPosture = ''; // 前回の姿勢を記録する変数
const goodPostureImage = 'https://i.gyazo.com/f0a2be380c30b300283839b097ae0a01.gif';
const badPostureImage = 'https://i.gyazo.com/6c1a3deb542443bf79d59e01b996f990.png';
const warningSound = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBTGJ0fPTgjMGHm7A7+OZSA0PVqzn77BdGAg+ltryxnMpBSd+zPLaizsIGGS57OihUBELTKXh8bllHgU2jdXzzn0vBSF1xe/glEILElyx6OyrWBUIQ5zd8sFuJAUuhM/z1YU2Bhxqvu7mnEoODlOq5O+zYBoGPJPY8cdmLAUme8rx3I4+CRZiturqpVITC0mj4PK8aB8GM4nU8tGAMQYfcsLu45ZFDBFZr+ftrVoXCECb3PLEcSYELIHO8diJOQcZaLvt559NEAxPqOPwtmMcBjiP1/PMeS0GI3fH8N2RQAoUXrTp66hVFApGnt/yvmwhBTCJ0fPTgjQGHW/A7eSaRw0PVqzl77BeGQc9ltvyw3IsBSZ+zPDaizsIGGS56+mjTxELTKXh8bllHgU1jdT0z3wvBSJ1xe/glEILElyx6OyrWRUIRJve8sFuJAUug8/y1oU2Bhxqvu3mnEoPDlOq5O+zYRsGPJLZ8s1mLAUmfMrx3I4+CRVht+rqpVMSC0mj4PG8aiAFM4nU8tGAMQYfccPu45ZFDBFYr+ftrVwWCEGa3PLEcSYGK4DN8tiIOQcZabzs56BODwxPpuPxtmIdBjiP1/PMeywGI3fH8N+RQAoUXrTp66hVFApGnt/yv2wiBDCJ0fPTgzQHHG/A7eSaSQ0PVqvm77BeGQc9ltv0w3IsBSZ9y/HajDsIF2W56+mjUREKTKPi8blnHgU1jdTy0HwvBSF1xPDglEILElux6eyrWRUJQ5vd88FwJAQug8/y1oY2Bhxqvu3mnEwODVKp5e+zYRsGPJLX88xoLAUmfMrx3I4/CBVhtuvqpVMSC0mi3/G9aiAFM4nU8tGBMQYfccLv45dGCxFYr+ftrVwXB0Ca3PLEcicFKoDN8tiIOQcZabzt56BODwxPpuPxt2MeBjiP1/PMey4FI3bH8d+RQQ==');
startButton.addEventListener('click', startCapture);
stopButton.addEventListener('click', stopCapture);
async function startCapture() {
try {
stream = await navigator.mediaDevices.getUserMedia({ video: true });
video.srcObject = stream;
startButton.disabled = true;
stopButton.disabled = false;
intervalId = setInterval(checkPosture, 500); // 500ミリ秒ごとにチェック
} catch (err) {
console.error("エラー: ", err);
}
}
function stopCapture() {
clearInterval(intervalId);
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
video.srcObject = null;
startButton.disabled = false;
stopButton.disabled = true;
lastPosition = null;
lastPosture = '';
hideGoodPostureImage(); // 停止時に良い姿勢の画像を非表示にする
}
function checkPosture() {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const centerOfMass = calculateCenterOfMass(imageData);
if (lastPosition === null) {
lastPosition = centerOfMass;
}
const movement = Math.abs(centerOfMass.x - lastPosition.x) + Math.abs(centerOfMass.y - lastPosition.y);
if (movement > tiltThreshold) {
if (lastPosture !== 'bad') {
displayBadPosture();
warningSound.play(); // 姿勢が悪いときの音声再生
lastPosture = 'bad';
}
} else {
if (lastPosture !== 'good') {
displayGoodPosture();
lastPosture = 'good';
}
}
lastPosition = centerOfMass;
}
function displayGoodPosture() {
video.style.display = 'none';
canvas.style.display = 'none';
const currentImage = document.querySelector('#currentImage');
if (currentImage) {
currentImage.style.display = 'block'; // 画像を表示する
} else {
const img = document.createElement('img');
img.id = 'currentImage';
img.src = goodPostureImage;
img.style.width = '100%';
img.style.maxWidth = '640px';
document.body.appendChild(img);
}
}
function displayBadPosture() {
const currentImage = document.querySelector('#currentImage');
if (currentImage) {
currentImage.style.display = 'none'; // 画像を非表示にする
}
video.style.display = 'block';
canvas.style.display = 'none';
}
function hideGoodPostureImage() {
const currentImage = document.querySelector('#currentImage');
if (currentImage) {
currentImage.style.display = 'none'; // 画像を非表示にする
}
}
function calculateCenterOfMass(imageData) {
let sumX = 0;
let sumY = 0;
let count = 0;
for (let y = 0; y < imageData.height; y++) {
for (let x = 0; x < imageData.width; x++) {
const i = (y * imageData.width + x) * 4;
const r = imageData.data[i];
const g = imageData.data[i + 1];
const b = imageData.data[i + 2];
if (r + g + b > 100) { // 簡単な閾値処理
sumX += x;
sumY += y;
count++;
}
}
}
return {
x: sumX / count,
y: sumY / count
};
}
</script>
</body>
![Van🤗](https://res.cloudinary.com/zenn/image/fetch/s--Ae3bCLsA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/aa244fefb1.jpeg)
Poe制限もきてしまって
無料版のChatGPT GTP-3.5 Turboに聞くしかなくなってしまった・・・
リートンは長いコード貼れないのね
CSSも整えてもらった
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>坐禅姿勢チェッカー</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
background-color: #f7f7f7;
color: #333;
margin: 0;
padding: 20px;
box-sizing: border-box;
}
h1 {
color: #444;
margin-bottom: 20px;
}
#video, #canvas {
width: 100%;
max-width: 640px;
margin: 20px auto;
display: block;
border: 2px solid #ddd;
border-radius: 8px;
}
#imageContainer {
width: 100%;
max-width: 640px;
margin: 20px auto;
position: relative;
}
#currentImage {
width: 100%;
max-width: 640px;
display: none;
}
.button-container {
margin-top: 20px;
}
button {
display: inline-block;
margin: 10px;
padding: 10px 20px;
font-size: 16px;
color: #fff;
background-color: #007BFF;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
button:hover:not(:disabled) {
background-color: #0056b3;
}
</style>
</head>
<body>
<h1>坐禅姿勢チェッカー</h1>
<div id="imageContainer">
<img id="currentImage" src="https://i.gyazo.com/f0a2be380c30b300283839b097ae0a01.gif" alt="姿勢画像">
</div>
<video id="video" width="640" height="480" autoplay></video>
<canvas id="canvas" width="640" height="480" style="display:none;"></canvas>
<div class="button-container">
<button id="startButton">開始</button>
<button id="stopButton" disabled>停止</button>
</div>
<script>
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const startButton = document.getElementById('startButton');
const stopButton = document.getElementById('stopButton');
let stream;
let intervalId;
let lastPosition = null;
let tiltThreshold = 0.5; // 傾きの閾値(ピクセル単位)
let lastPosture = ''; // 前回の姿勢を記録する変数
const goodPostureImage = 'https://i.gyazo.com/f0a2be380c30b300283839b097ae0a01.gif';
const badPostureImage = 'https://i.gyazo.com/6c1a3deb542443bf79d59e01b996f990.png';
const warningSound = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBTGJ0fPTgjMGHm7A7+OZSA0PVqzn77BdGAg+ltryxnMpBSd+zPLaizsIGGS57OihUBELTKXh8bllHgU2jdXzzn0vBSF1xe/glEILElyx6OyrWBUIQ5zd8sFuJAUuhM/z1YU2Bhxqvu7mnEoODlOq5O+zYBoGPJPY8cdmLAUme8rx3I4+CRZiturqpVITC0mj4PK8aB8GM4nU8tGAMQYfcsLu45ZFDBFZr+ftrVoXCECb3PLEcSYELIHO8diJOQcZaLvt559NEAxPqOPwtmMcBjiP1/PMeS0GI3fH8N2RQAoUXrTp66hVFApGnt/yvmwhBTCJ0fPTgjQGHW/A7eSaRw0PVqzl77BeGQc9ltvyw3IsBSZ+zPDaizsIGGS56+mjTxELTKXh8bllHgU1jdT0z3wvBSJ1xe/glEILElyx6OyrWRUIRJve8sFuJAUug8/y1oU2Bhxqvu3mnEoPDlOq5O+zYRsGPJLZ8s1mLAUmfMrx3I4+CRVht+rqpVMSC0mj4PG8aiAFM4nU8tGAMQYfccPu45ZFDBFYr+ftrVwWCEGa3PLEcSYGK4DN8tiIOQcZabzs56BODwxPpuPxtmIdBjiP1/PMeywGI3fH8N+RQAoUXrTp66hVFApGnt/yv2wiBDCJ0fPTgzQHHG/A7eSaSQ0PVqvm77BeGQc9ltv0w3IsBSZ9y/HajDsIF2W56+mjUREKTKPi8blnHgU1jdTy0HwvBSF1xPDglEILElux6eyrWRUJQ5vd88FwJAQug8/y1oY2Bhxqvu3mnEwODVKp5e+zYRsGPJLX88xoLAUmfMrx3I4/CBVhtuvqpVMSC0mi3/G9aiAFM4nU8tGBMQYfccLv45dGCxFYr+ftrVwXB0Ca3PLEcicFKoDN8tiIOQcZabzt56BODwxPpuPxt2MeBjiP1/PMey4FI3bH8d+RQQ==');
startButton.addEventListener('click', startCapture);
stopButton.addEventListener('click', stopCapture);
async function startCapture() {
try {
stream = await navigator.mediaDevices.getUserMedia({ video: true });
video.srcObject = stream;
startButton.disabled = true;
stopButton.disabled = false;
intervalId = setInterval(checkPosture, 500); // 500ミリ秒ごとにチェック
} catch (err) {
console.error("エラー: ", err);
}
}
function stopCapture() {
clearInterval(intervalId);
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
video.srcObject = null;
startButton.disabled = false;
stopButton.disabled = true;
lastPosition = null;
lastPosture = '';
hideGoodPostureImage(); // 停止時に良い姿勢の画像を非表示にする
}
function checkPosture() {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const centerOfMass = calculateCenterOfMass(imageData);
if (lastPosition === null) {
lastPosition = centerOfMass;
}
const movement = Math.abs(centerOfMass.x - lastPosition.x) + Math.abs(centerOfMass.y - lastPosition.y);
if (movement > tiltThreshold) {
if (lastPosture !== 'bad') {
displayBadPosture();
warningSound.play(); // 姿勢が悪いときの音声再生
lastPosture = 'bad';
}
} else {
if (lastPosture !== 'good') {
displayGoodPosture();
lastPosture = 'good';
}
}
lastPosition = centerOfMass;
}
function displayGoodPosture() {
video.style.display = 'none';
canvas.style.display = 'none';
const currentImage = document.querySelector('#currentImage');
if (currentImage) {
currentImage.style.display = 'block'; // 画像を表示する
} else {
const img = document.createElement('img');
img.id = 'currentImage';
img.src = goodPostureImage;
img.style.width = '100%';
img.style.maxWidth = '640px';
document.body.appendChild(img);
}
}
function displayBadPosture() {
const currentImage = document.querySelector('#currentImage');
if (currentImage) {
currentImage.style.display = 'none'; // 画像を非表示にする
}
video.style.display = 'block';
canvas.style.display = 'none';
}
function hideGoodPostureImage() {
const currentImage = document.querySelector('#currentImage');
if (currentImage) {
currentImage.style.display = 'none'; // 画像を非表示にする
}
}
function calculateCenterOfMass(imageData) {
let sumX = 0;
let sumY = 0;
let count = 0;
for (let y = 0; y < imageData.height; y++) {
for (let x = 0; x < imageData.width; x++) {
const i = (y * imageData.width + x) * 4;
const r = imageData.data[i];
const g = imageData.data[i + 1];
const b = imageData.data[i + 2];
if (r + g + b > 100) { // 簡単な閾値処理
sumX += x;
sumY += y;
count++;
}
}
}
return {
x: sumX / count,
y: sumY / count
};
}
</script>
</body>
![Van🤗](https://res.cloudinary.com/zenn/image/fetch/s--Ae3bCLsA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/aa244fefb1.jpeg)
多分これ詰めたところで魅力上がらないと思うから、実装切り上げて動画とかスライドに力を入れる。