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
복사



