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

image.png

image.png

<!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>

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

image.png

<!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>