메인
home
소프트웨어
home

[나만의 웹사이트 제작하기][성남고][한태성]

1. 자유로운 웹사이트 코드펜 작업링크

웹사이트 구상: 고등학교때 배우는 수학2 의 미분 개념을 활용해서 그래프를 그렸을 때 그에 맞는 도함수를 그려주는 웹사이트를 제작했다.
문제점: 원함수를 그릴 때 손이 떨리는 미세한 값까지 모두 측정해서 도함수가 원하는대로 나오지 않았다. 그래서 스무딩 함수를 활용해서 좀 더 추상적으로 나타내고자 하였다.
보완후 작품:
<!DOCTYPE html> <html lang="ko"> <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; display: flex; flex-direction: column; align-items: center; padding: 20px; background-color: #f0f0f0; margin: 0; min-height: 100vh; box-sizing: border-box; } .container { display: flex; flex-wrap: wrap; /* 작은 화면에서 줄바꿈 */ gap: 20px; margin-bottom: 20px; justify-content: center; } .graph-area { background-color: white; padding: 10px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); text-align: center; } canvas { border: 1px solid #333; background-color: #fff; cursor: crosshair; touch-action: none; /* 터치 스크린에서 스크롤 방지 */ } h3 { color: #333; margin-top: 5px; margin-bottom: 10px; } #clearButton { padding: 10px 20px; font-size: 16px; cursor: pointer; background-color: #007bff; color: white; border: none; border-radius: 5px; transition: background-color 0.2s ease; } #clearButton:hover { background-color: #0056b3; } /* 반응형 디자인 */ @media (max-width: 850px) { .container { flex-direction: column; align-items: center; } } </style> </head> <body> <div class="container"> <div class="graph-area"> <h3>f(x) - 원본 함수 (마우스로 그리세요)</h3> <canvas id="functionCanvas" width="400" height="300"></canvas> </div> <div class="graph-area"> <h3>f'(x) - 도함수</h3> <canvas id="derivativeCanvas" width="400" height="300"></canvas> </div> </div> <button id="clearButton">모두 지우기</button> <script> // 캔버스 및 컨텍스트 설정 const funcCanvas = document.getElementById('functionCanvas'); const derivCanvas = document.getElementById('derivativeCanvas'); const fCtx = funcCanvas.getContext('2d'); const dCtx = derivCanvas.getContext('2d'); const clearButton = document.getElementById('clearButton'); // 그래프 데이터 저장 배열 (x, y 좌표) let points = []; let isDrawing = false; // 캔버스 크기 const WIDTH = funcCanvas.width; const HEIGHT = funcCanvas.height; // **스무딩 설정** const SMOOTHING_WINDOW = 10; // 평균을 낼 앞뒤 점의 개수 (값이 클수록 더 부드러워짐) // 그래프 그리기 기본 설정 function setupCanvas(ctx) { ctx.clearRect(0, 0, WIDTH, HEIGHT); // 좌표축 그리기 (선택 사항) ctx.strokeStyle = '#ccc'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, HEIGHT / 2); ctx.lineTo(WIDTH, HEIGHT / 2); // X축 (가운데) ctx.moveTo(WIDTH / 2, 0); ctx.lineTo(WIDTH / 2, HEIGHT); // Y축 (가운데) ctx.stroke(); } /** * 데이터 포인트 배열에 이동 평균(Moving Average) 필터를 적용하여 노이즈를 제거합니다. */ function smoothPoints(data) { if (data.length < SMOOTHING_WINDOW) return data; const smoothed = []; const halfWindow = Math.floor(SMOOTHING_WINDOW / 2); for (let i = 0; i < data.length; i++) { const start = Math.max(0, i - halfWindow); const end = Math.min(data.length - 1, i + halfWindow); let sumX = 0; let sumY = 0; let count = 0; for (let j = start; j <= end; j++) { sumX += data[j].x; sumY += data[j].y; count++; } smoothed.push({ x: sumX / count, y: sumY / count }); } return smoothed; } // 원본 함수 그리기 function drawFunction() { setupCanvas(fCtx); if (points.length < 2) return; fCtx.strokeStyle = 'blue'; fCtx.lineWidth = 2; fCtx.beginPath(); fCtx.moveTo(points[0].x, points[0].y); for (let i = 1; i < points.length; i++) { fCtx.lineTo(points[i].x, points[i].y); } fCtx.stroke(); } // 도함수 계산 및 그리기 (스무딩된 데이터를 사용) function drawDerivative() { setupCanvas(dCtx); // 도함수 캔버스 초기화 및 좌표축 // **스무딩된 데이터를 가져옵니다.** const smoothedPoints = smoothPoints(points); if (smoothedPoints.length < 3) return; dCtx.strokeStyle = 'red'; dCtx.lineWidth = 2; dCtx.beginPath(); const SCALE_FACTOR = 30; // 도함수 시각적 스케일링 팩터 // 중앙 차분법 (Central Difference)을 스무딩된 데이터에 적용 for (let i = 1; i < smoothedPoints.length - 1; i++) { const p_prev = smoothedPoints[i - 1]; const p_curr = smoothedPoints[i]; const p_next = smoothedPoints[i + 1]; const dx = p_next.x - p_prev.x; const dy = p_next.y - p_prev.y; let derivative = 0; if (dx !== 0) { // 실제 기울기: 캔버스 Y축 반전 고려 derivative = -dy / dx; } // 도함수 Y 좌표 변환 및 스케일링 let dY = HEIGHT / 2 - derivative * SCALE_FACTOR; if (i === 1) { dCtx.moveTo(p_curr.x, dY); } else { dCtx.lineTo(p_curr.x, dY); } } dCtx.stroke(); } // 마우스 이벤트 핸들러 funcCanvas.addEventListener('mousedown', (e) => { isDrawing = true; points = []; // 새 드로잉 시작 const rect = funcCanvas.getBoundingClientRect(); points.push({ x: e.clientX - rect.left, y: e.clientY - rect.top }); drawFunction(); }); funcCanvas.addEventListener('mousemove', (e) => { if (!isDrawing) return; const rect = funcCanvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const lastPoint = points[points.length - 1]; // x좌표가 증가하는 방향으로만 점을 저장하도록 제한하여 함수의 형태를 유지하고 미분 안정화 if (x > lastPoint.x) { points.push({ x, y }); drawFunction(); // 원본 함수 업데이트 drawDerivative(); // 도함수 업데이트 (스무딩 적용) } }); funcCanvas.addEventListener('mouseup', () => { isDrawing = false; }); funcCanvas.addEventListener('mouseout', () => { isDrawing = false; }); // 터치 이벤트 핸들러 (모바일 기기 지원) funcCanvas.addEventListener('touchstart', (e) => { e.preventDefault(); // 기본 스크롤 동작 방지 isDrawing = true; points = []; const rect = funcCanvas.getBoundingClientRect(); const touch = e.touches[0]; points.push({ x: touch.clientX - rect.left, y: touch.clientY - rect.top }); drawFunction(); }); funcCanvas.addEventListener('touchmove', (e) => { e.preventDefault(); // 기본 스크롤 동작 방지 if (!isDrawing) return; const rect = funcCanvas.getBoundingClientRect(); const touch = e.touches[0]; const x = touch.clientX - rect.left; const y = touch.clientY - rect.top; const lastPoint = points[points.length - 1]; if (x > lastPoint.x) { points.push({ x, y }); drawFunction(); drawDerivative(); } }); funcCanvas.addEventListener('touchend', () => { isDrawing = false; }); funcCanvas.addEventListener('touchcancel', () => { isDrawing = false; }); // 지우기 버튼 이벤트 핸들러 clearButton.addEventListener('click', () => { points = []; setupCanvas(fCtx); setupCanvas(dCtx); }); // 초기 캔버스 설정 setupCanvas(fCtx); setupCanvas(dCtx); </script> </body> </html>
Python
복사
간단한 설명: 사용자가 마우스 드로잉을 통해 임의의 함수 그래프를 직접 그리면, 웹사이트가 이 함수의 도함수 그래프를 실시간으로 계산하여 옆에 그려준다. 마우스 드로잉과 수치 미분 및 스무딩을 할용하여 사용자가 직접 함수를 조작하며 그 함수의 도함수을 체험하고 미적분적인 탐구를 할 수 있도록 돕는다.

2. ai를 활용한 인식관련된 웹사이트 코드펜 작업링크

웹사이트 구상: 가위바위보 게임 , 미디어파이프를 활용해서 가위바위보를 측정하여 랜덤으로 컴퓨터와 가위바위보를 할 수 있는 게임을 만들었다.
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>가위바위보 (MediaPipe Hands)</title> <style> body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background-color: #f0f2f5; color: #333; padding: 20px; box-sizing: border-box; } h1 { color: #2c3e50; margin-bottom: 20px; } .game-container { display: flex; flex-direction: column; gap: 20px; align-items: center; width: 100%; max-width: 900px; background-color: #fff; padding: 30px; border-radius: 12px; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1); } .video-wrapper { position: relative; width: 100%; max-width: 640px; /* 비디오 너비 제한 */ border-radius: 8px; overflow: hidden; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); } video, canvas { width: 100%; height: auto; display: block; transform: scaleX(-1); /* 거울 모드 */ } canvas { position: absolute; top: 0; left: 0; } .game-info { display: flex; flex-wrap: wrap; justify-content: center; gap: 15px; margin-top: 20px; width: 100%; font-size: 1.1em; text-align: center; } .info-box { background-color: #e9ecef; padding: 15px 20px; border-radius: 8px; flex: 1 1 auto; /* 유연한 너비 */ min-width: 180px; } .info-box span { font-weight: bold; color: #007bff; } #result { font-size: 1.5em; font-weight: bold; margin-top: 25px; color: #28a745; /* 기본 승리 색상 */ } #result.win { color: #28a745; } #result.lose { color: #dc3545; } #result.draw { color: #ffc107; } #startGameButton { padding: 12px 25px; font-size: 1.2em; background-color: #007bff; color: white; border: none; border-radius: 8px; cursor: pointer; transition: background-color 0.3s ease, transform 0.2s ease; margin-top: 30px; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); } #startGameButton:hover { background-color: #0056b3; transform: translateY(-2px); } #startGameButton:active { transform: translateY(0); } /* 미디어 쿼리 */ @media (max-width: 768px) { .game-info { flex-direction: column; } .info-box { min-width: unset; } .video-wrapper { max-width: 100%; } } </style> </head> <body> <h1>가위바위보 게임 (MediaPipe Hands)</h1> <div class="game-container"> <div class="video-wrapper"> <video id="webcamVideo" autoplay playsinline></video> <canvas id="outputCanvas"></canvas> </div> <div class="game-info"> <div class="info-box">내 손: <span id="playerGesture">준비</span></div> <div class="info-box">컴퓨터: <span id="computerGesture">준비</span></div> <div class="info-box">승리: <span id="wins">0</span></div> <div class="info-box">패배: <span id="losses">0</span></div> <div class="info-box">무승부: <span id="draws">0</span></div> </div> <div id="result">게임 시작!</div> <button id="startGameButton">게임 시작 / 다시 하기</button> </div> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands@0.4/hands.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils@0.3/drawing_utils.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils@0.1/camera_utils.js" crossorigin="anonymous"></script> <script> const videoElement = document.getElementById('webcamVideo'); const canvasElement = document.getElementById('outputCanvas'); const canvasCtx = canvasElement.getContext('2d'); const playerGestureElement = document.getElementById('playerGesture'); const computerGestureElement = document.getElementById('computerGesture'); const resultElement = document.getElementById('result'); const winsElement = document.getElementById('wins'); const lossesElement = document.getElementById('losses'); const drawsElement = document.getElementById('draws'); const startGameButton = document.getElementById('startGameButton'); let playerWins = 0; let playerLosses = 0; let playerDraws = 0; let gameActive = false; let recognitionTimeout; // 제스처 인식을 기다리는 타임아웃 const GAME_ROUND_DURATION = 3000; // 한 라운드당 제스처 인식 대기 시간 (3) const gestures = ['바위', '가위', '보']; // 0: 바위, 1: 가위, 2:// MediaPipe Hands 초기화 const hands = new Hands({ locateFile: (file) => { return `https://cdn.jsdelivr.net/npm/@mediapipe/hands@0.4/${file}`; } }); hands.setOptions({ maxNumHands: 1, // 한 번에 하나의 손만 인식 modelComplexity: 1, // 모델 복잡도 (0, 1) - 1이 더 정확하지만 느릴 수 있음 minDetectionConfidence: 0.7, // 손 감지 최소 신뢰도 minTrackingConfidence: 0.7 // 손 추적 최소 신뢰도 }); hands.onResults(onResults); // 웹캠 설정 const camera = new Camera(videoElement, { onFrame: async () => { if (gameActive) { await hands.send({ image: videoElement }); } }, width: 640, height: 480 }); camera.start(); function onResults(results) { canvasCtx.save(); canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height); if (results.multiHandLandmarks && gameActive) { for (const landmarks of results.multiHandLandmarks) { drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS, { color: '#00FF00', lineWidth: 5 }); drawLandmarks(canvasCtx, landmarks, { color: '#FF0000', lineWidth: 2 }); const playerChoice = recognizeGesture(landmarks); if (playerChoice !== -1) { playerGestureElement.textContent = gestures[playerChoice]; clearTimeout(recognitionTimeout); // 제스처 인식 성공 시 타임아웃 초기화 playRound(playerChoice); // 게임 라운드 진행 } else { playerGestureElement.textContent = "인식 불가"; } } } else if (gameActive) { playerGestureElement.textContent = "손 감지 안됨"; } canvasCtx.restore(); } // 제스처 인식 함수 // 랜드마크 분석을 통해 주먹, 가위, 보 판단 function recognizeGesture(landmarks) { // 엄지 손가락 (thumb)(landmark[4]) // 검지 손가락 (index finger)(landmark[8]) // 중지 손가락 (middle finger)(landmark[12]) // 약지 손가락 (ring finger)(landmark[16]) // 새끼 손가락 (pinky finger)(landmark[20]) const thumbTip = landmarks[4]; const indexTip = landmarks[8]; const middleTip = landmarks[12]; const ringTip = landmarks[16]; const pinkyTip = landmarks[20]; const wrist = landmarks[0]; // 엄지 기준으로 다른 손가락이 펴졌는지 확인하는 로직 (간단화) // Y축 값은 아래로 갈수록 커짐 // 손가락 끝이 그 아래 마디보다 위에 있으면 펴진 것으로 간주 let fingersUp = 0; // 엄지: 검지 끝보다 엄지 끝이 오른쪽에 있으면 (거울모드라서 실제로는 왼쪽에 있음) 엄지가 펴진 것으로 간주 // 엄지 끝의 X좌표가 손목의 X좌표보다 크면 (오른쪽으로 멀리 떨어져 있으면) 펴진 것으로 간주 (오른손 기준) // 웹캠이 거울모드이므로 실제로는 반대 // 엄지 손가락의 x 좌표가 검지 손가락의 x 좌표보다 작으면 (왼쪽에 있으면) 펴진 것으로 간주 if (thumbTip.x < landmarks[5].x) { // 엄지 끝이 엄지 두번째 마디보다 왼쪽에 있으면 (펴짐) fingersUp++; } // 검지: 검지 끝이 검지 두번째 마디보다 Y값이 작으면 (위에 있으면) 펴진 것으로 간주 if (indexTip.y < landmarks[6].y) { fingersUp++; } // 중지 if (middleTip.y < landmarks[10].y) { fingersUp++; } // 약지 if (ringTip.y < landmarks[14].y) { fingersUp++; } // 새끼 if (pinkyTip.y < landmarks[18].y) { fingersUp++; } // 제스처 판단 if (fingersUp === 0) { // 모든 손가락이 접힘 -> 주먹 (바위) return 0; // 바위 } else if (fingersUp === 2 && indexTip.y < landmarks[6].y && middleTip.y < landmarks[10].y && ringTip.y > landmarks[14].y && pinkyTip > landmarks[18].y) { // 검지와 중지만 펴짐 (약지, 새끼는 접힘) -> 가위 // 엄지 손가락 위치에 따라 달라질 수 있으므로, 일단 검지/중지만 보고 판단 // 더 정확하게 하려면 엄지 위치도 고려해야 함 return 1; // 가위 } else if (fingersUp >= 3) { // 3개 이상 펴짐 ->// 엄지가 펴지고 다른 손가락이 모두 펴진 경우 if (thumbTip.x < landmarks[5].x && indexTip.y < landmarks[6].y && middleTip.y < landmarks[10].y && ringTip.y < landmarks[14].y && pinkyTip.y < landmarks[18].y) { return 2; //(모든 손가락 펴짐) } // 엄지가 접히고 4손가락이 펴진 경우도 보로 인식 (하지만 흔치 않음) if (indexTip.y < landmarks[6].y && middleTip.y < landmarks[10].y && ringTip.y < landmarks[14].y && pinkyTip.y < landmarks[18].y) { return 2; //} } // 엄지만 펴진 경우는 바위로 간주 (혹은 특정 제스처로 분류하지 않음) if (fingersUp === 1 && thumbTip.x < landmarks[5].x) { return 0; // 바위 (엄지만 펴진 경우) } // 기본적으로는 미인식 상태 (-1) 반환 return -1; } // 게임 라운드 진행 function playRound(playerChoice) { gameActive = false; // 한 라운드 승패 판정 후 게임 일시 정지 const computerChoice = Math.floor(Math.random() * 3); // 0: 바위, 1: 가위, 2: 보 computerGestureElement.textContent = gestures[computerChoice]; let resultText = ''; resultElement.classList.remove('win', 'lose', 'draw'); if (playerChoice === computerChoice) { resultText = '비겼습니다!'; playerDraws++; resultElement.classList.add('draw'); } else if ( (playerChoice === 0 && computerChoice === 1) || // 바위 vs 가위 (playerChoice === 1 && computerChoice === 2) || // 가위 vs 보 (playerChoice === 2 && computerChoice === 0) // 보 vs 바위 ) { resultText = '이겼습니다!'; playerWins++; resultElement.classList.add('win'); } else { resultText = '졌습니다!'; playerLosses++; resultElement.classList.add('lose'); } resultElement.textContent = resultText; winsElement.textContent = playerWins; lossesElement.textContent = playerLosses; drawsElement.textContent = playerDraws; // 다음 라운드를 위한 준비 setTimeout(resetRound, GAME_ROUND_DURATION); // 3초 후에 다음 라운드 준비 } // 라운드 리셋 및 다음 제스처 대기 function resetRound() { playerGestureElement.textContent = "제스처 대기 중..."; computerGestureElement.textContent = "???"; resultElement.textContent = "새로운 라운드 시작!"; resultElement.classList.remove('win', 'lose', 'draw'); gameActive = true; // 일정 시간 내에 제스처를 인식하지 못하면 다시 대기 상태로 recognitionTimeout = setTimeout(() => { if (gameActive) { // 아직 제스처가 인식되지 않았다면 playerGestureElement.textContent = "시간 초과! 다시 시도하세요."; // 게임은 계속 활성화된 상태로 유지하여 다시 제스처를 시도할 수 있도록 함 } }, GAME_ROUND_DURATION); } startGameButton.addEventListener('click', () => { playerWins = 0; playerLosses = 0; playerDraws = 0; winsElement.textContent = 0; lossesElement.textContent = 0; drawsElement.textContent = 0; clearTimeout(recognitionTimeout); // 혹시 모를 이전 타임아웃 제거 resetRound(); // 게임 시작 }); // 초기 시작 playerGestureElement.textContent = "게임을 시작하려면 버튼을 누르세요"; computerGestureElement.textContent = "컴퓨터"; resultElement.textContent = "준비"; // 초기 캔버스 크기 조정 videoElement.addEventListener('loadeddata', () => { canvasElement.width = videoElement.videoWidth; canvasElement.height = videoElement.videoHeight; }); </script> </body> </html>
Python
복사