<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>물리시뮬레이터 &#8211; MyEngNote</title>
	<atom:link href="https://myengnote.com/tag/%EB%AC%BC%EB%A6%AC%EC%8B%9C%EB%AE%AC%EB%A0%88%EC%9D%B4%ED%84%B0/feed/" rel="self" type="application/rss+xml" />
	<link>https://myengnote.com</link>
	<description></description>
	<lastBuildDate>Thu, 28 May 2026 23:33:35 +0000</lastBuildDate>
	<language>ko-KR</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0</generator>
	<item>
		<title>선끝속도 계산기 &#038; 시뮬레이터</title>
		<link>https://myengnote.com/linear-tip-speed-simulator-calculator/</link>
					<comments>https://myengnote.com/linear-tip-speed-simulator-calculator/#respond</comments>
		
		<dc:creator><![CDATA[동동]]></dc:creator>
		<pubDate>Wed, 27 May 2026 05:12:57 +0000</pubDate>
				<category><![CDATA[공학계산기]]></category>
		<category><![CDATA[기계설계]]></category>
		<category><![CDATA[물리시뮬레이터]]></category>
		<category><![CDATA[선끝속도]]></category>
		<category><![CDATA[원심력 가속도]]></category>
		<category><![CDATA[접선속도]]></category>
		<category><![CDATA[회전속도 계산]]></category>
		<guid isPermaLink="false">https://myengnote.com/linear-tip-speed-simulator-calculator/</guid>

					<description><![CDATA[지름과 회전수(RPM)를 입력하여 선끝속도(접선 속도)를 실시간으로 계산하고 시각화하는 초정밀 2D 물리 시뮬레이터입니다.]]></description>
										<content:encoded><![CDATA[
<h2 style="font-size: 1.6em; font-weight: 800; color: #0c0e25; border-bottom: 2px solid #00f2fe; padding-bottom: 8px; margin-bottom: 20px;"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 선끝속도 계산기 &#038; 시뮬레이터</h2>



<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&#038;family=Outfit:wght@400;500;600;700;800&#038;display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* Scoped & Isolated Styles for rotaspeed-calculator-wrapper */

        /* Modern Reset and Core Design Tokens */
        .rotaspeed-calculator-wrapper *, .rotaspeed-calculator-wrapper *::before, .rotaspeed-calculator-wrapper *::after {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }

        /* Variables matching style.css */
        .rotaspeed-calculator-wrapper {
            --color-bg-dark: #f8f9fd;
            --color-bg-deep: #ffffff;
            --color-panel-bg: rgba(255, 255, 255, 0.85);
            --color-border: rgba(0, 0, 0, 0.06);
            --color-border-hover: rgba(0, 0, 0, 0.12);
            
            --color-text-main: #1e293b;
            --color-text-muted: #475569;
            --color-text-dark: #94a3b8;

            --color-cyan: #0284c7;
            --color-cyan-glow: rgba(2, 132, 199, 0.15);
            --color-magenta: #db2777;
            --color-magenta-glow: rgba(219, 39, 119, 0.15);
            --color-purple: #7c3aed;
            --color-purple-glow: rgba(124, 58, 237, 0.12);
            --color-success: #10b981;

            --gradient-primary: linear-gradient(135deg, var(--color-cyan) 0%, var(--color-purple) 100%);
            --gradient-accent: linear-gradient(135deg, var(--color-magenta) 0%, var(--color-purple) 100%);
            --gradient-panel: linear-gradient(180deg, rgba(255, 255, 255, 0.9) 0%, rgba(248, 250, 252, 0.95) 100%);

            --font-heading: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            --font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
            
            --shadow-neon-cyan: 0 0 15px var(--color-cyan-glow);
            --shadow-neon-magenta: 0 0 15px var(--color-magenta-glow);
            --shadow-card: 0 8px 32px 0 rgba(15, 23, 42, 0.05);
            --blur-glass: blur(16px);
        }

        .rotaspeed-calculator-wrapper {
            background-color: var(--color-bg-dark); color: var(--color-text-main); color: var(--color-text-main);
            color: var(--color-text-main);
            font-family: var(--font-body);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            overflow-x: hidden;
            position: relative;
        }

        .app-background-glow {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: -1;
            overflow: hidden;
            pointer-events: none;
        }

        .app-background-glow::before, 
        .app-background-glow::after {
            content: '';
            position: absolute;
            width: 600px;
            height: 600px;
            border-radius: 50%;
            filter: blur(140px);
            opacity: 0.15;
        }

        .app-background-glow::before {
            background: var(--color-cyan);
            top: -10%;
            right: -5%;
            animation: pulse-slow 15s infinite alternate;
        }

        .app-background-glow::after {
            background: var(--color-magenta);
            bottom: -10%;
            left: -5%;
            animation: pulse-slow 20s infinite alternate-reverse;
        }

        @keyframes pulse-slow {
            0% { transform: scale(1) translate(0, 0); opacity: 0.12; }
            100% { transform: scale(1.2) translate(50px, 50px); opacity: 0.22; }
        }

        .app-container {
            width: 100%;
            max-width: 1440px;
            padding: 24px;
            display: flex;
            flex-direction: column;
            gap: 24px;
            container-type: inline-size;
            container-name: rotaspeed-container;
        }

        .app-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 20px 24px;
            background: var(--color-panel-bg);
            backdrop-filter: var(--blur-glass);
            border: 1px solid var(--color-border);
            border-radius: 16px;
            box-shadow: var(--shadow-card);
        }

        .logo-area {
            display: flex;
            align-items: center;
            gap: 16px;
        }

        .logo-icon {
            font-size: 32px;
            background: linear-gradient(135deg, var(--color-cyan), var(--color-magenta));
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .logo-area h1 {
            font-family: var(--font-heading);
            font-weight: 800;
            font-size: 24px;
            letter-spacing: 1.5px;
            background: linear-gradient(90deg, var(--color-text-main), var(--color-text-dark));
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }

        .logo-area .subtitle {
            font-size: 12px;
            color: var(--color-text-muted);
            font-weight: 500;
            letter-spacing: 0.5px;
        }

        .fa-spin-slow {
            animation: fa-spin 12s linear infinite;
        }

        .header-badge {
            display: flex;
            align-items: center;
            gap: 10px;
            background: rgba(16, 185, 129, 0.1);
            border: 1px solid rgba(16, 185, 129, 0.2);
            padding: 6px 14px;
            border-radius: 20px;
        }

        .pulse-dot {
            width: 8px;
            height: 8px;
            background-color: var(--color-success);
            border-radius: 50%;
            box-shadow: 0 0 10px var(--color-success);
            animation: pulse-dot-anim 1.5s infinite;
        }

        @keyframes pulse-dot-anim {
            0% { transform: scale(0.9); opacity: 0.6; }
            50% { transform: scale(1.2); opacity: 1; }
            100% { transform: scale(0.9); opacity: 0.6; }
        }

        .badge-text {
            font-size: 11px;
            font-weight: 600;
            color: var(--color-success);
            letter-spacing: 0.5px;
        }

        .app-main-grid {
            display: grid;
            grid-template-columns: 340px 1fr 340px;
            gap: 24px;
            align-items: start;
        }

        .panel {
            background: var(--gradient-panel);
            backdrop-filter: var(--blur-glass);
            border: 1px solid var(--color-border);
            border-radius: 20px;
            padding: 24px;
            box-shadow: var(--shadow-card);
            display: flex;
            flex-direction: column;
            gap: 20px;
            transition: border-color 0.3s ease, box-shadow 0.3s ease;
        }

        .panel:hover {
            border-color: var(--color-border-hover);
        }

        .panel-header {
            display: flex;
            align-items: center;
            gap: 12px;
            border-bottom: 1px solid var(--color-border);
            padding-bottom: 14px;
        }

        .panel-header i {
            font-size: 18px;
        }

        .panel-header h2 {
            font-family: var(--font-heading);
            font-size: 18px;
            font-weight: 600;
            letter-spacing: 0.5px;
        }

        .text-cyan { color: #0284c7; }
        .text-magenta { color: #db2777; }

        .control-panel {
            grid-column: 1;
        }

        .input-group {
            display: flex;
            flex-direction: column;
            gap: 12px;
        }

        .input-label-row {
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .input-label-row label {
            font-size: 13px;
            font-weight: 600;
            color: var(--color-text-main);
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .helper-text {
            font-size: 10px;
            color: var(--color-text-muted);
            font-weight: 500;
        }

        .unit-selector-wrapper {
            position: relative;
        }

        .custom-select {
            background: rgba(0, 0, 0, 0.02);
            border: 1px solid var(--color-border);
            color: var(--color-text-main);
            padding: 4px 10px;
            border-radius: 6px;
            font-size: 11px;
            font-weight: 600;
            cursor: pointer;
            outline: none;
            transition: all 0.2s ease;
        }

        .custom-select:hover, .custom-select:focus {
            background: rgba(0, 0, 0, 0.03);
            border-color: var(--color-border);
        }

        .input-control-row {
            display: flex;
            flex-direction: column;
            gap: 10px;
        }

        .number-input-wrapper {
            display: flex;
            position: relative;
            border-radius: 8px;
            overflow: hidden;
            border: 1px solid var(--color-border);
            background: #ffffff;
            transition: all 0.2s ease;
        }

        .number-input-wrapper:focus-within {
            border-color: #0284c7;
            box-shadow: 0 0 10px var(--color-cyan-glow);
        }

        .custom-number-input {
            width: 100%;
            background: #f8fafc !important;
            border: none !important;
            outline: none !important;
            color: #0f172a !important;
            padding: 10px 14px !important;
            font-family: var(--font-body) !important;
            font-size: 15px !important;
            font-weight: 700 !important;
            text-align: right !important;
            padding-right: 60px !important;
            text-shadow: none !important;
            box-shadow: none !important;
            -webkit-appearance: none !important;
            -moz-appearance: textfield !important;
        }

        .custom-number-input::-webkit-outer-spin-button,
        .custom-number-input::-webkit-inner-spin-button {
            -webkit-appearance: none;
            margin: 0;
        }
        .custom-number-input {
            -moz-appearance: textfield;
        }

        .unit-badge {
            position: absolute;
            right: 1px;
            top: 1px;
            bottom: 1px;
            background: rgba(0, 0, 0, 0.03);
            color: var(--color-text-muted);
            font-size: 11px;
            font-weight: 700;
            padding: 0 14px;
            display: flex;
            align-items: center;
            justify-content: center;
            border-left: 1px solid var(--color-border);
            pointer-events: none;
            letter-spacing: 0.5px;
        }

        .custom-slider {
            -webkit-appearance: none;
            width: 100%;
            height: 6px;
            border-radius: 3px;
            background: rgba(0, 0, 0, 0.05);
            outline: none;
            margin: 8px 0;
            transition: background 0.2s ease;
        }

        .custom-slider::-webkit-slider-thumb {
            -webkit-appearance: none;
            appearance: none;
            width: 18px;
            height: 18px;
            border-radius: 50%;
            cursor: pointer;
            transition: transform 0.1s ease, box-shadow 0.2s ease;
        }

        .custom-slider::-moz-range-thumb {
            width: 18px;
            height: 18px;
            border: none;
            border-radius: 50%;
            cursor: pointer;
            transition: transform 0.1s ease, box-shadow 0.2s ease;
        }

        #slider-diameter::-webkit-slider-thumb {
            background: var(--color-cyan);
            box-shadow: 0 0 10px var(--color-cyan);
        }
        #slider-diameter::-moz-range-thumb {
            background: var(--color-cyan);
            box-shadow: 0 0 10px var(--color-cyan);
        }
        #slider-diameter::-webkit-slider-thumb:hover {
            transform: scale(1.2);
            box-shadow: 0 0 15px var(--color-cyan), 0 0 5px #fff;
        }
        #slider-diameter::-moz-range-thumb:hover {
            transform: scale(1.2);
            box-shadow: 0 0 15px var(--color-cyan), 0 0 5px #fff;
        }

        #slider-rpm::-webkit-slider-thumb {
            background: var(--color-magenta);
            box-shadow: 0 0 10px var(--color-magenta);
        }
        #slider-rpm::-moz-range-thumb {
            background: var(--color-magenta);
            box-shadow: 0 0 10px var(--color-magenta);
        }
        #slider-rpm::-webkit-slider-thumb:hover {
            transform: scale(1.2);
            box-shadow: 0 0 15px var(--color-magenta), 0 0 5px #fff;
        }
        #slider-rpm::-moz-range-thumb:hover {
            transform: scale(1.2);
            box-shadow: 0 0 15px var(--color-magenta), 0 0 5px #fff;
        }

        .control-actions {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 10px;
            margin-top: 10px;
        }

        #btn-particles {
            grid-column: span 2;
        }

        .action-btn {
            background: rgba(0, 0, 0, 0.02);
            border: 1px solid var(--color-border);
            color: var(--color-text-main);
            padding: 10px;
            border-radius: 10px;
            font-size: 12px;
            font-weight: 600;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
            transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
        }

        .action-btn:hover {
            background: rgba(0, 0, 0, 0.03);
            border-color: var(--color-border);
        }

        .action-btn.active#btn-particles {
            background: var(--color-magenta-glow);
            border-color: #db2777;
            color: #db2777;
            box-shadow: 0 0 12px var(--color-magenta-glow);
            
        }

        .presets-section {
            display: flex;
            flex-direction: column;
            gap: 12px;
            border-top: 1px solid var(--color-border);
            padding-top: 18px;
        }

        .presets-section h3 {
            font-size: 13px;
            font-weight: 700;
            color: var(--color-text-main);
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .presets-grid {
            display: grid;
            grid-template-columns: 1fr;
            gap: 8px;
        }

        .preset-btn {
            background: rgba(0, 0, 0, 0.015);
            border: 1px solid var(--color-border);
            border-radius: 10px;
            padding: 8px 12px;
            cursor: pointer;
            color: var(--color-text-main);
            display: flex;
            align-items: center;
            gap: 12px;
            text-align: left;
            transition: all 0.2s ease;
        }

        .preset-btn:hover {
            background: rgba(0, 0, 0, 0.05);
            border-color: var(--color-border);
            transform: translateX(4px);
        }

        .preset-btn.active {
            background: linear-gradient(90deg, var(--color-cyan-glow), var(--color-purple-glow));
            border-color: #0284c7;
            box-shadow: 0 0 10px var(--color-cyan-glow);
        }

        .preset-icon {
            width: 28px;
            height: 28px;
            background: rgba(0, 0, 0, 0.02);
            border-radius: 8px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 12px;
            color: #0284c7;
            transition: all 0.2s ease;
        }

        .preset-btn:hover .preset-icon {
            background: var(--gradient-primary);
            color: #fff;
            box-shadow: var(--shadow-neon-cyan);
        }

        .preset-btn.active .preset-icon {
            background: var(--gradient-primary);
            color: #fff;
        }

        .preset-name {
            font-size: 12px;
            font-weight: 600;
            letter-spacing: 0.25px;
        }

        .simulation-panel {
            grid-column: 2;
            align-self: stretch;
            justify-content: space-between;
        }

        .simulation-panel .panel-header {
            justify-content: space-between;
        }

        .canvas-scale-indicator {
            font-size: 11px;
            color: var(--color-text-muted);
            background: rgba(0, 0, 0, 0.02);
            padding: 4px 10px;
            border-radius: 6px;
            border: 1px solid var(--color-border);
        }

        .canvas-wrapper {
            position: relative;
            width: 100%;
            background: radial-gradient(circle at center, #ffffff 0%, #f1f5f9 100%);
            border: 1px solid rgba(0, 0, 0, 0.06);
            border-radius: 16px;
            overflow: hidden;
        }

        #physics-canvas {
            display: block;
            width: 100%;
            height: auto;
            aspect-ratio: 1 / 1;
        }

        .canvas-overlay-data {
            position: absolute;
            top: 16px;
            left: 16px;
            pointer-events: none;
        }

        .overlay-item {
            background: rgba(255, 255, 255, 0.85);
            backdrop-filter: blur(4px);
            border: 1px solid var(--color-border);
            padding: 6px 12px;
            border-radius: 8px;
            display: flex;
            align-items: center;
            gap: 8px;
            font-size: 11px;
        }

        .overlay-item .label {
            color: var(--color-text-muted);
            font-weight: 500;
        }

        .overlay-item .value {
            font-family: var(--font-body);
            font-weight: 700;
        }

        .simulation-metrics-strip {
            display: flex;
            background: rgba(0, 0, 0, 0.01);
            border: 1px solid var(--color-border);
            border-radius: 12px;
            padding: 12px 20px;
            justify-content: space-around;
            align-items: center;
            gap: 10px;
            margin-top: 10px;
        }

        .mini-metric {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 4px;
            text-align: center;
        }

        .mini-metric .label {
            font-size: 10px;
            font-weight: 600;
            color: var(--color-text-muted);
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }

        .mini-metric .value {
            font-size: 14px;
            font-weight: 700;
            color: var(--color-text-main);
            font-family: var(--font-heading);
        }

        .mini-divider {
            width: 1px;
            height: 24px;
            background: rgba(0, 0, 0, 0.03);
        }

        .results-panel {
            grid-column: 3;
        }

        .gauge-container {
            display: flex;
            flex-direction: column;
            align-items: center;
            position: relative;
            margin: 10px 0;
            text-align: center;
        }

        .speedometer-svg {
            max-width: 220px;
        }

        .gauge-track {
            stroke: #e2e8f0;
        }

        .gauge-progress {
            stroke: var(--color-cyan);
            filter: drop-shadow(0px 0px 6px rgba(0, 242, 254, 0.6));
            stroke-dasharray: 251.2;
            stroke-dashoffset: 251.2;
            transition: stroke-dashoffset 0.25s cubic-bezier(0.4, 0, 0.2, 1);
        }

        .gauge-readout {
            position: absolute;
            bottom: 22px;
            display: flex;
            flex-direction: column;
            align-items: center;
        }

        .gauge-value {
            font-family: var(--font-heading);
            font-weight: 800;
            font-size: 32px;
            letter-spacing: -1px;
            background: linear-gradient(135deg, var(--color-text-main) 40%, var(--color-cyan) 100%);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }

        .gauge-unit {
            font-size: 11px;
            font-weight: 700;
            color: #0284c7;
            letter-spacing: 0.5px;
            text-transform: uppercase;
        }

        .gauge-label {
            font-size: 11px;
            font-weight: 600;
            color: var(--color-text-muted);
            margin-top: 14px;
        }

        .results-grid {
            display: grid;
            grid-template-columns: 1fr;
            gap: 12px;
        }

        .result-card {
            background: rgba(255, 255, 255, 0.85); box-shadow: 0 2px 8px rgba(0,0,0,0.03);
            border: 1px solid var(--color-border);
            border-radius: 12px;
            padding: 12px 16px;
            display: flex;
            align-items: center;
            gap: 16px;
            transition: all 0.2s ease;
        }

        .result-card:hover {
            transform: translateY(-2px);
            border-color: rgba(0, 242, 254, 0.2);
            background: rgba(0, 242, 254, 0.03);
        }

        .result-card.featured {
            background: linear-gradient(135deg, rgba(0, 242, 254, 0.08) 0%, rgba(138, 43, 226, 0.03) 100%);
            border-color: rgba(0, 242, 254, 0.3);
            box-shadow: 0 0 15px rgba(0, 242, 254, 0.05);
        }

        .card-icon {
            width: 38px;
            height: 38px;
            background: rgba(0, 0, 0, 0.015);
            border: 1px solid var(--color-border);
            border-radius: 10px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 14px;
            color: var(--color-text-muted);
            transition: all 0.2s ease;
        }

        .result-card:hover .card-icon {
            color: #0284c7;
            border-color: rgba(0, 242, 254, 0.3);
            background: rgba(0, 242, 254, 0.1);
        }

        .card-content {
            display: flex;
            flex-direction: column;
            gap: 2px;
        }

        .card-unit {
            font-size: 11px;
            font-weight: 600;
            color: var(--color-text-muted);
        }

        .card-value {
            font-family: var(--font-heading);
            font-weight: 700;
            font-size: 18px;
            color: var(--color-text-main);
        }

        .result-card.featured .card-value {
            font-size: 22px;
            font-weight: 800;
            background: linear-gradient(90deg, var(--color-text-main), var(--color-cyan));
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }

        .formula-card {
            background: rgba(0, 0, 0, 0.01);
            border: 1px solid var(--color-border);
            border-radius: 12px;
            padding: 16px;
            display: flex;
            flex-direction: column;
            gap: 12px;
        }

        .formula-title {
            font-size: 12px;
            font-weight: 700;
            display: flex;
            align-items: center;
            gap: 8px;
            letter-spacing: 0.25px;
        }

        .formula-latex {
            background: #f8fafc; border-color: var(--color-border);
            border-radius: 8px;
            padding: 10px;
            display: flex;
            justify-content: center;
            align-items: center;
            font-size: 16px;
            border: 1px solid rgba(255, 255, 255, 0.03);
        }

        .formula-desc {
            font-size: 11px;
            color: var(--color-text-muted);
            line-height: 1.6;
        }

        .formula-desc ul {
            list-style: none;
            display: flex;
            flex-direction: column;
            gap: 4px;
            margin-bottom: 8px;
            padding-bottom: 8px;
            border-bottom: 1px solid var(--color-border);
        }

        .app-footer {
            text-align: center;
            padding: 16px;
            font-size: 11px;
            color: var(--color-text-dark);
            font-weight: 500;
            border-top: 1px solid rgba(255, 255, 255, 0.03);
            margin-top: 10px;
        }

        @container rotaspeed-container (max-width: 1050px) {
            .app-main-grid {
                grid-template-columns: 1fr 1fr;
                gap: 20px;
            }
            .control-panel {
                grid-column: 1;
            }
            .simulation-panel {
                grid-column: 2;
            }
            .results-panel {
                grid-column: 1 / span 2;
            }
            .results-grid {
                grid-template-columns: repeat(2, 1fr);
            }
        }

        @container rotaspeed-container (max-width: 768px) {
            .app-main-grid {
                grid-template-columns: 1fr;
            }
            .control-panel, .simulation-panel, .results-panel {
                grid-column: 1;
            }
            .results-grid {
                grid-template-columns: repeat(2, 1fr);
            }
            .app-header {
                flex-direction: column;
                gap: 16px;
                text-align: center;
            }
            .logo-area {
                flex-direction: column;
                gap: 8px;
            }
        }

        @container rotaspeed-container (max-width: 480px) {
            .app-container {
                padding: 12px;
                gap: 16px;
            }
            .panel {
                padding: 16px;
            }
            .results-grid {
                grid-template-columns: 1fr;
            }
            .simulation-metrics-strip {
                flex-direction: column;
                gap: 12px;
            }
            .mini-divider {
                width: 80%;
                height: 1px;
            }
            .control-actions {
                grid-template-columns: 1fr;
            }
            #btn-particles {
                grid-column: span 1;
            }
        }
    
</style>

<div class="rotaspeed-calculator-wrapper" style="position: relative; width: 100%; box-sizing: border-box; overflow: hidden; margin: 30px auto; border-radius: 20px;">
    <div class="app-background-glow" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; overflow: hidden;"></div>
    <div style="position: relative; z-index: 2; width: 100%;">
        
    
    
    <div class="app-container">
        <!-- HEADER -->
        <header class="app-header">
            <div class="logo-area">
                <div class="logo-icon"><i class="fa-solid fa-circle-notch fa-spin-slow"></i></div>
                <div>
                    <h1>ROTASPEED</h1>
                    <p class="subtitle">Linear Tip Speed Simulator &#038; Calculator</p>
                </div>
            </div>
            <div class="header-badge">
                <span class="pulse-dot"></span>
                <span class="badge-text">실시간 물리 엔진 구동 중</span>
            </div>
        </header>

        <!-- MAIN GRID LAYOUT -->
        <main class="app-main-grid">
            <!-- LEFT PANEL: CONTROLS & PRESETS -->
            <section class="panel control-panel" aria-label="입력 제어판">
                <div class="panel-header">
                    <i class="fa-solid fa-sliders text-cyan"></i>
                    <h2>시뮬레이터 제어</h2>
                </div>
                
                <div class="input-group">
                    <div class="input-label-row">
                        <label for="input-diameter"><i class="fa-solid fa-arrows-left-right"></i> 회전체 지름 (Diameter)</label>
                        <div class="unit-selector-wrapper">
                            <select id="select-diameter-unit" class="custom-select" aria-label="지름 단위 선택">
                                <option value="mm" selected>mm</option>
                                <option value="cm">cm</option>
                                <option value="m">m</option>
                                <option value="inch">inch</option>
                            </select>
                        </div>
                    </div>
                    <div class="input-control-row">
                        <input type="range" id="slider-diameter" min="1" max="2000" step="1" value="500" class="custom-slider" aria-label="지름 슬라이더">
                        <div class="number-input-wrapper">
                            <input type="number" id="input-diameter" value="500" min="0.001" step="any" class="custom-number-input" style="background: #f8fafc !important; color: #0f172a !important;">
                            <span class="unit-badge" id="diameter-unit-label">mm</span>
                        </div>
                    </div>
                </div>

                <div class="input-group">
                    <div class="input-label-row">
                        <label for="input-rpm"><i class="fa-solid fa-gauge-high"></i> 회전수 (Rotational Speed)</label>
                        <span class="helper-text">RPM (Revolutions Per Minute)</span>
                    </div>
                    <div class="input-control-row">
                        <input type="range" id="slider-rpm" min="0" max="8000" step="1" value="1500" class="custom-slider" aria-label="회전수 슬라이더">
                        <div class="number-input-wrapper">
                            <input type="number" id="input-rpm" value="1500" min="0" step="any" class="custom-number-input" style="background: #f8fafc !important; color: #0f172a !important;">
                            <span class="unit-badge">RPM</span>
                        </div>
                    </div>
                </div>

                <div class="control-actions">
                    <button id="btn-play-pause" class="action-btn" title="일시정지/재생" aria-label="일시정지 및 재생">
                        <i class="fa-solid fa-pause"></i> <span>일시정지</span>
                    </button>
                    <button id="btn-reverse" class="action-btn" title="회전 방향 전환" aria-label="회전 방향 전환">
                        <i class="fa-solid fa-rotate-left"></i> <span>방향 전환</span>
                    </button>
                    <button id="btn-particles" class="action-btn active" title="스파크 시각 효과 토글" aria-label="스파크 효과 토글">
                        <i class="fa-solid fa-wand-magic-sparkles"></i> <span>스파크 이펙트</span>
                    </button>
                </div>

                <div class="presets-section">
                    <h3><i class="fa-solid fa-list-check text-magenta"></i> 사전 정의 프리셋 (Presets)</h3>
                    <div class="presets-grid">
                        <button class="preset-btn" data-preset="bicycle" aria-label="자전거 바퀴 프리셋">
                            <span class="preset-icon"><i class="fa-solid fa-bicycle"></i></span>
                            <span class="preset-name">자전거 바퀴</span>
                        </button>
                        <button class="preset-btn" data-preset="grinder" aria-label="고속 그라인더 프리셋">
                            <span class="preset-icon"><i class="fa-solid fa-compact-disc"></i></span>
                            <span class="preset-name">앵글 그라인더</span>
                        </button>
                        <button class="preset-btn" data-preset="turbine" aria-label="풍력 발전기 프리셋">
                            <span class="preset-icon"><i class="fa-solid fa-wind"></i></span>
                            <span class="preset-name">풍력 터빈</span>
                        </button>
                        <button class="preset-btn" data-preset="hdd" aria-label="HDD 디스크 프리셋">
                            <span class="preset-icon"><i class="fa-solid fa-hdd"></i></span>
                            <span class="preset-name">HDD 플래터</span>
                        </button>
                        <button class="preset-btn" data-preset="earth" aria-label="지구 자전 프리셋">
                            <span class="preset-icon"><i class="fa-solid fa-earth-asia"></i></span>
                            <span class="preset-name">지구 자전(적도)</span>
                        </button>
                    </div>
                </div>
            </section>

            <!-- CENTER PANEL: LIVE SIMULATION CANVAS -->
            <section class="panel simulation-panel" aria-label="물리 애니메이션 시뮬레이션">
                <div class="panel-header">
                    <i class="fa-solid fa-dharmachakra text-magenta"></i>
                    <h2>실시간 회전 시뮬레이션</h2>
                    <span class="canvas-scale-indicator" id="canvas-scale-text">화면 스케일: 자동</span>
                </div>
                
                <div class="canvas-wrapper">
                    <canvas id="physics-canvas" width="600" height="480"></canvas>
                    <div class="canvas-overlay-data">
                        <div class="overlay-item">
                            <span class="label">선속도 벡터 길이:</span>
                            <span class="value text-cyan" id="overlay-vector-val">0.00 m/s</span>
                        </div>
                    </div>
                </div>

                <div class="simulation-metrics-strip">
                    <div class="mini-metric">
                        <span class="label">주파수 (Frequency)</span>
                        <span class="value" id="val-frequency">25.0 Hz</span>
                    </div>
                    <div class="mini-divider"></div>
                    <div class="mini-metric">
                        <span class="label">각속도 (Angular Velocity)</span>
                        <span class="value" id="val-angular-speed">157.1 rad/s</span>
                    </div>
                    <div class="mini-divider"></div>
                    <div class="mini-metric">
                        <span class="label">향심가속도 (Centripetal Accel.)</span>
                        <span class="value" id="val-centripetal-g">64.2 G</span>
                    </div>
                </div>
            </section>

            <!-- RIGHT PANEL: CALCULATED SPEED RESULTS -->
            <section class="panel results-panel" aria-label="선끝속도 결과 분석">
                <div class="panel-header">
                    <i class="fa-solid fa-chart-line text-cyan"></i>
                    <h2>선끝속도 계산 결과</h2>
                </div>

                <div class="gauge-container">
                    <svg class="speedometer-svg" viewBox="0 0 200 120" width="100%">
                        <path class="gauge-track" d="M 20 100 A 80 80 0 0 1 180 100" fill="none" stroke-width="12" stroke-linecap="round" />
                        <path id="gauge-progress" class="gauge-progress" d="M 20 100 A 80 80 0 0 1 180 100" fill="none" stroke-width="12" stroke-linecap="round" />
                    </svg>
                    <div class="gauge-readout">
                        <span class="gauge-value" id="gauge-primary-value">39.27</span>
                        <span class="gauge-unit">m/s</span>
                    </div>
                    <div class="gauge-label">접선 선끝속도 (Tangential Tip Speed)</div>
                </div>

                <div class="results-grid">
                    <div class="result-card featured" id="card-ms">
                        <div class="card-icon"><i class="fa-solid fa-bolt"></i></div>
                        <div class="card-content">
                            <span class="card-unit">초속 (m/s)</span>
                            <span class="card-value" id="val-ms">39.27</span>
                        </div>
                    </div>
                    <div class="result-card" id="card-kmh">
                        <div class="card-icon"><i class="fa-solid fa-gauge"></i></div>
                        <div class="card-content">
                            <span class="card-unit">시속 (km/h)</span>
                            <span class="card-value" id="val-kmh">141.4</span>
                        </div>
                    </div>
                    <div class="result-card" id="card-mmin">
                        <div class="card-icon"><i class="fa-solid fa-stopwatch"></i></div>
                        <div class="card-content">
                            <span class="card-unit">분당 속도 (m/min)</span>
                            <span class="card-value" id="val-mmin">2,356</span>
                        </div>
                    </div>
                    <div class="result-card" id="card-mph">
                        <div class="card-icon"><i class="fa-solid fa-wind"></i></div>
                        <div class="card-content">
                            <span class="card-unit">시속 (mph)</span>
                            <span class="card-value" id="val-mph">87.8</span>
                        </div>
                    </div>
                </div>

                <div class="formula-card">
                    <div class="formula-title"><i class="fa-solid fa-circle-info text-cyan"></i> 선끝속도 물리 공식</div>
                    <div class="formula-latex" style="font-family: 'Outfit', 'Cambria Math', 'Times New Roman', serif; font-size: 19px; font-weight: 700; color: #0284c7; text-align: center; margin: 12px 0; letter-spacing: 0.5px; background: #f8fafc; padding: 10px; border-radius: 8px; border: 1px solid var(--color-border);">
                        v = π × D × (N / 60)
                    </div>
                    <div class="formula-desc">
                        <ul>
                            <li><strong><em>v</em></strong> : 선속도 (m/s)</li>
                            <li><strong><em>D</em></strong> : 지름 (m)</li>
                            <li><strong><em>N</em></strong> : 회전수 (RPM)</li>
                        </ul>
                        <p class="formula-detail-text" style="margin-top: 10px; line-height: 1.6;">
                            회전체 중심에서 반경 <strong><em>r</em></strong>만큼 떨어진 지점의 <strong>선속도(Tangential Velocity)</strong>는 매 초당 이동하는 호의 길이를 뜻합니다. 지름(<strong><em>D</em></strong>)과 회전수(<strong><em>N</em></strong>)가 비례할수록 선끝속도는 선형적으로 빨라집니다.
                        </p>
                    </div>
                </div>
            </section>
        </main>

        <!-- FOOTER -->
        <footer class="app-footer">
            <p>Designed with <i class="fa-solid fa-heart text-magenta"></i> for High Fidelity Physics Visualization. &copy; 2026 ROTASPEED Project.</p>
        </footer>
    </div>

    <!-- MathJax for rendering gorgeous equations -->
    <script>
        window.MathJax = {
            tex: {
                inlineMath: [['$', '$'], ['\\(', '\\)']],
                displayMath: [['$$', '$$'], ['\\[', '\\]']]
            },
            svg: {
                fontCache: 'global'
            }
        };
    </script>
    <script type="text/javascript" id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script>

    <!-- CORE SIMULATION LOGIC -->
    <script>
        (function() {
            if (window.__rotaspeed_initialized) return;

            function initRotaspeed() {
                if (window.__rotaspeed_initialized) return;

                const selectUnit = document.getElementById('select-diameter-unit');
                const sliderDiameter = document.getElementById('slider-diameter');
                const inputDiameter = document.getElementById('input-diameter');
                const canvas = document.getElementById('physics-canvas');

                if (!selectUnit || !sliderDiameter || !inputDiameter || !canvas) {
                    return;
                }

                window.__rotaspeed_initialized = true;
                const labelDiameterUnit = document.getElementById('diameter-unit-label');

            const sliderRpm = document.getElementById('slider-rpm');
            const inputRpm = document.getElementById('input-rpm');

            const btnPlayPause = document.getElementById('btn-play-pause');
            const btnReverse = document.getElementById('btn-reverse');
            const btnParticles = document.getElementById('btn-particles');

            const presetButtons = document.querySelectorAll('.preset-btn');

            const txtFrequency = document.getElementById('val-frequency');
            const txtAngularSpeed = document.getElementById('val-angular-speed');
            const txtCentripetalG = document.getElementById('val-centripetal-g');
            const txtCanvasScale = document.getElementById('canvas-scale-text');

            const gaugeProgress = document.getElementById('gauge-progress');
            const txtGaugePrimaryValue = document.getElementById('gauge-primary-value');
            const txtMs = document.getElementById('val-ms');
            const txtKmh = document.getElementById('val-kmh');
            const txtMmin = document.getElementById('val-mmin');
            const txtMph = document.getElementById('val-mph');
            const txtOverlayVectorVal = document.getElementById('overlay-vector-val');

            const ctx = canvas.getContext('2d');

            function resizeCanvas() {
                const dpr = window.devicePixelRatio || 1;
                const wrapper = canvas.parentElement;
                if (!wrapper) return;
                
                let targetWidth = wrapper.clientWidth;
                let targetHeight = targetWidth; // strictly maintain 1:1 square aspect ratio
                
                if (targetWidth < 50) {
                    targetWidth = 500;
                    targetHeight = 500;
                }
                
                canvas.width = targetWidth * dpr;
                canvas.height = targetHeight * dpr;
                ctx.scale(dpr, dpr);
            }
            
            // Robust event hook-ups to guarantee rendering size after dynamic WordPress DOM layout settles
            resizeCanvas();
            window.addEventListener('resize', resizeCanvas);
            window.addEventListener('load', resizeCanvas);
            setTimeout(resizeCanvas, 100);
            setTimeout(resizeCanvas, 400);
            setTimeout(resizeCanvas, 1000);

            let state = {
                diameterMeters: 0.5,
                rpm: 1500,
                isPlaying: true,
                direction: 1,
                showParticles: true,
                currentUnit: 'mm',
                angle: 0,
                lastTime: performance.now(),
                particles: []
            };

            const unitConfigs = {
                mm: {
                    label: 'mm',
                    toMeters: (val) => val / 1000,
                    fromMeters: (m) => m * 1000,
                    slider: { min: 1, max: 2000, step: 1 }
                },
                cm: {
                    label: 'cm',
                    toMeters: (val) => val / 100,
                    fromMeters: (m) => m * 100,
                    slider: { min: 0.1, max: 200, step: 0.1 }
                },
                m: {
                    label: 'm',
                    toMeters: (val) => val,
                    fromMeters: (m) => m,
                    slider: { min: 0.001, max: 25, step: 0.001 }
                },
                inch: {
                    label: 'inch',
                    toMeters: (val) => val * 0.0254,
                    fromMeters: (m) => m / 0.0254,
                    slider: { min: 0.1, max: 80, step: 0.1 }
                }
            };

            const presets = {
                bicycle: {
                    name: '자전거 바퀴 (27.5인치)',
                    diameter: 0.7,
                    rpm: 270,
                    unit: 'mm'
                },
                grinder: {
                    name: '초고속 앵글 그라인더',
                    diameter: 0.125,
                    rpm: 11000,
                    unit: 'mm'
                },
                turbine: {
                    name: '초대형 풍력 터빈',
                    diameter: 120,
                    rpm: 15,
                    unit: 'm'
                },
                hdd: {
                    name: '컴퓨터 하드디스크 Platter',
                    diameter: 0.0889,
                    rpm: 7200,
                    unit: 'inch'
                },
                earth: {
                    name: '지구 자전 (적도 둘레)',
                    diameter: 12742000,
                    rpm: 0.0006944,
                    unit: 'm'
                }
            };

            function calculatePhysics() {
                const D = state.diameterMeters;
                const RPM = state.rpm;
                const freqHz = RPM / 60;
                const omega = 2 * Math.PI * freqHz;
                const speedMs = Math.PI * D * freqHz;

                const speedKmh = speedMs * 3.6;
                const speedMmin = speedMs * 60;
                const speedMph = speedMs * 2.23694;

                const R = D / 2;
                let centripetalAccel = 0;
                if (R > 0) {
                    centripetalAccel = (speedMs * speedMs) / R;
                }
                const centripetalG = centripetalAccel / 9.80665;

                return {
                    freqHz,
                    omega,
                    speedMs,
                    speedKmh,
                    speedMmin,
                    speedMph,
                    centripetalG
                };
            }

            function updateOutputs() {
                const physics = calculatePhysics();

                txtFrequency.innerText = physics.freqHz.toFixed(2) + ' Hz';
                txtAngularSpeed.innerText = physics.omega.toFixed(1) + ' rad/s';
                
                if (physics.centripetalG > 100000) {
                    txtCentripetalG.innerText = physics.centripetalG.toExponential(3) + ' G';
                } else {
                    txtCentripetalG.innerText = physics.centripetalG.toLocaleString('ko-KR', { maximumFractionDigits: 1 }) + ' G';
                }

                txtGaugePrimaryValue.innerText = physics.speedMs.toLocaleString('ko-KR', { maximumFractionDigits: 2 });
                txtOverlayVectorVal.innerText = physics.speedMs.toFixed(2) + ' m/s';

                txtMs.innerText = physics.speedMs.toLocaleString('ko-KR', { maximumFractionDigits: 2 });
                txtKmh.innerText = physics.speedKmh.toLocaleString('ko-KR', { maximumFractionDigits: 1 });
                txtMmin.innerText = physics.speedMmin.toLocaleString('ko-KR', { maximumFractionDigits: 0 });
                txtMph.innerText = physics.speedMph.toLocaleString('ko-KR', { maximumFractionDigits: 1 });

                const cardMs = document.getElementById('card-ms');
                if (physics.speedMs > 100) {
                    cardMs.style.boxShadow = '0 0 20px rgba(255, 0, 127, 0.2)';
                    cardMs.style.borderColor = 'rgba(255, 0, 127, 0.4)';
                } else {
                    cardMs.style.boxShadow = '';
                    cardMs.style.borderColor = '';
                }

                const maxGaugeSpeed = 100;
                const percentage = Math.min(physics.speedMs / maxGaugeSpeed, 1);
                const dashoffset = 251.2 * (1 - percentage);
                gaugeProgress.style.strokeDashoffset = dashoffset;

                if (physics.speedMs > 80) {
                    gaugeProgress.style.stroke = '#db2777';
                    gaugeProgress.style.filter = 'drop-shadow(0px 0px 8px rgba(255, 0, 127, 0.7))';
                } else {
                    gaugeProgress.style.stroke = '#0284c7';
                    gaugeProgress.style.filter = 'drop-shadow(0px 0px 6px rgba(0, 242, 254, 0.6))';
                }
            }

            function syncDiameterInputFromSlider() {
                const val = parseFloat(sliderDiameter.value);
                inputDiameter.value = val;
                
                const config = unitConfigs[state.currentUnit];
                state.diameterMeters = config.toMeters(val);
                
                clearActivePresets();
                updateOutputs();
            }

            function syncDiameterSliderFromInput() {
                let val = parseFloat(inputDiameter.value);
                if (isNaN(val) || val <= 0) val = 0.001;
                
                const config = unitConfigs[state.currentUnit];
                const sliderConfig = config.slider;
                
                sliderDiameter.min = sliderConfig.min;
                sliderDiameter.max = sliderConfig.max;
                sliderDiameter.step = sliderConfig.step;
                sliderDiameter.value = Math.min(Math.max(val, sliderConfig.min), sliderConfig.max);
                
                state.diameterMeters = config.toMeters(val);
                
                clearActivePresets();
                updateOutputs();
            }

            function syncRpmInputFromSlider() {
                const val = parseInt(sliderRpm.value);
                inputRpm.value = val;
                state.rpm = val;
                
                clearActivePresets();
                updateOutputs();
            }

            function syncRpmSliderFromInput() {
                let val = parseFloat(inputRpm.value);
                if (isNaN(val) || val < 0) val = 0;
                
                if (val > sliderRpm.max) {
                    sliderRpm.max = Math.ceil(val * 1.2);
                } else if (val < 8000) {
                    if (sliderRpm.max > 8000) {
                        sliderRpm.max = 8000;
                    }
                }

                sliderRpm.value = val;
                state.rpm = val;
                
                clearActivePresets();
                updateOutputs();
            }

            function handleUnitChange() {
                const oldUnit = state.currentUnit;
                const newUnit = selectUnit.value;
                state.currentUnit = newUnit;

                const configNew = unitConfigs[newUnit];
                labelDiameterUnit.innerText = configNew.label;

                sliderDiameter.min = configNew.slider.min;
                sliderDiameter.max = configNew.slider.max;
                sliderDiameter.step = configNew.slider.step;

                const convertedVal = configNew.fromMeters(state.diameterMeters);
                
                inputDiameter.value = Number(convertedVal.toFixed(3));
                sliderDiameter.value = Math.min(Math.max(convertedVal, configNew.slider.min), configNew.slider.max);

                updateOutputs();
            }

            function loadPreset(presetKey) {
                const preset = presets[presetKey];
                if (!preset) return;

                presetButtons.forEach(btn => {
                    if (btn.dataset.preset === presetKey) {
                        btn.classList.add('active');
                    } else {
                        btn.classList.remove('active');
                    }
                });

                state.diameterMeters = preset.diameter;
                state.rpm = preset.rpm;
                state.currentUnit = preset.unit;

                selectUnit.value = preset.unit;
                labelDiameterUnit.innerText = preset.unit;

                const config = unitConfigs[preset.unit];
                sliderDiameter.min = config.slider.min;
                sliderDiameter.max = config.slider.max;
                sliderDiameter.step = config.slider.step;

                const uiDiameterVal = config.fromMeters(preset.diameter);
                inputDiameter.value = Number(uiDiameterVal.toFixed(4));
                sliderDiameter.value = uiDiameterVal;

                if (preset.rpm > sliderRpm.max) {
                    sliderRpm.max = Math.ceil(preset.rpm * 1.1);
                }
                inputRpm.value = preset.rpm;
                sliderRpm.value = preset.rpm;

                updateOutputs();
            }

            function clearActivePresets() {
                presetButtons.forEach(btn => btn.classList.remove('active'));
            }

            class SparkParticle {
                constructor(x, y, angle, linearSpeed) {
                    this.x = x;
                    this.y = y;
                    
                    const speedFactor = Math.min(linearSpeed * 0.08, 4) + Math.random() * 1.5;
                    const directionAngle = angle + (Math.random() - 0.5) * 0.25;
                    
                    this.vx = Math.cos(directionAngle) * speedFactor;
                    this.vy = Math.sin(directionAngle) * speedFactor;
                    
                    this.size = Math.random() * 1.5 + 0.5;
                    this.maxLife = Math.random() * 15 + 15;
                    this.life = this.maxLife;
                    
                    const colors = ['#ffe600', '#ff8400', '#ff007f', '#ffffff'];
                    this.color = colors[Math.floor(Math.random() * colors.length)];
                }

                update() {
                    this.vx *= 0.95;
                    this.vy *= 0.95;
                    this.vy += 0.08;
                    
                    this.x += this.vx;
                    this.y += this.vy;
                    
                    this.life--;
                }

                draw(c) {
                    const alpha = this.life / this.maxLife;
                    c.save();
                    c.globalAlpha = alpha;
                    c.beginPath();
                    c.arc(this.x, this.y, this.size, 0, Math.PI * 2);
                    c.fillStyle = this.color;
                    c.shadowBlur = 4;
                    c.shadowColor = this.color;
                    c.fill();
                    c.restore();
                }
            }

            function animate(currentTime) {
                requestAnimationFrame(animate);

                let dt = (currentTime - state.lastTime) / 1000;
                if (isNaN(dt) || dt < 0 || dt > 1.0) {
                    dt = 0.016; // default to 60fps frame time
                }
                state.lastTime = currentTime;

                const width = canvas.width / (window.devicePixelRatio || 1);
                const height = canvas.height / (window.devicePixelRatio || 1);
                const cx = width / 2;
                const cy = height / 2;

                ctx.clearRect(0, 0, width, height);

                ctx.save();
                ctx.strokeStyle = 'rgba(2, 132, 199, 0.06)';
                ctx.lineWidth = 1;
                for (let x = 0; x < width; x += 30) {
                    ctx.beginPath();
                    ctx.moveTo(x, 0);
                    ctx.lineTo(x, height);
                    ctx.stroke();
                }
                for (let y = 0; y < height; y += 30) {
                    ctx.beginPath();
                    ctx.moveTo(0, y);
                    ctx.lineTo(width, y);
                    ctx.stroke();
                }
                ctx.restore();

                const physics = calculatePhysics();
                if (isNaN(physics.omega)) physics.omega = 0;
                if (isNaN(physics.speedMs)) physics.speedMs = 0;

                if (state.isPlaying) {
                    if (state.rpm > 0) {
                        let clampedDt = Math.min(dt, 0.1);
                        if (isNaN(clampedDt) || clampedDt < 0) clampedDt = 0.016;
                        
                        // Stroboscopic Effect Fix (Wagon-Wheel illusion)
                        const maxVisualOmega = 12;
                        const visualOmega = maxVisualOmega * (1 - Math.exp(-physics.omega / maxVisualOmega));
                        
                        state.angle += state.direction * (isNaN(visualOmega) ? 0 : visualOmega) * clampedDt;
                    }
                }

                if (state.showParticles) {
                    if (state.isPlaying) {
                        if (state.rpm > 0) {
                            const emissionRate = Math.min(Math.floor(physics.speedMs / 12) + 1, 4);
                            const visualRadius = getVisualRadius();

                            for (let i = 0; i < emissionRate; i++) {
                                if (Math.random() > 0.6) {
                                    let emitAngle = Math.random() * Math.PI * 2;
                                    if (physics.speedMs > 150) {
                                        emitAngle = Math.PI / 4 + (Math.random() - 0.5) * 0.15;
                                    }
                                    
                                    const ex = cx + Math.cos(emitAngle) * visualRadius;
                                    const ey = cy + Math.sin(emitAngle) * visualRadius;
                                    const tangAngle = emitAngle + (state.direction * Math.PI / 2);

                                    state.particles.push(new SparkParticle(ex, ey, tangAngle, physics.speedMs));
                                }
                            }
                        }
                    }
                }

                state.particles = state.particles.filter(p => p.life > 0);
                state.particles.forEach(p => {
                    p.update();
                    p.draw(ctx);
                });

                ctx.save();
                ctx.translate(cx, cy);
                const drawRadius = getVisualRadius();
                displayVisualScaleText(physics.speedMs);
                
                if (isNaN(state.angle)) {
                    state.angle = 0;
                }
                ctx.rotate(state.angle);

                // 1. Draw outer tire
                ctx.shadowBlur = 10;
                ctx.shadowColor = 'rgba(0, 242, 254, 0.4)';
                ctx.beginPath();
                ctx.arc(0, 0, drawRadius, 0, Math.PI * 2);
                ctx.strokeStyle = 'rgba(0, 0, 0, 0.05)';
                ctx.lineWidth = 6;
                ctx.stroke();

                ctx.beginPath();
                ctx.arc(0, 0, drawRadius - 3, 0, Math.PI * 2);
                ctx.strokeStyle = 'rgba(0, 242, 254, 0.55)';
                ctx.lineWidth = 2;
                ctx.stroke();

                // 2. Draw interior spokes
                ctx.shadowBlur = 0;
                const spokesCount = 8;
                for (let i = 0; i < spokesCount; i++) {
                    const spokeAngle = (i * Math.PI * 2) / spokesCount;
                    ctx.save();
                    ctx.rotate(spokeAngle);
                    ctx.beginPath();
                    ctx.moveTo(0, 0);
                    ctx.lineTo(0, drawRadius - 10);
                    ctx.strokeStyle = i % 2 === 0 ? 'rgba(0, 242, 254, 0.4)' : 'rgba(138, 43, 226, 0.4)';
                    ctx.lineWidth = i % 2 === 0 ? 4 : 2;
                    ctx.stroke();

                    ctx.beginPath();
                    ctx.moveTo(-4, drawRadius - 12);
                    ctx.lineTo(4, drawRadius - 12);
                    ctx.strokeStyle = 'rgba(15, 23, 42, 0.15)';
                    ctx.lineWidth = 2;
                    ctx.stroke();
                    ctx.restore();
                }

                ctx.beginPath();
                ctx.arc(0, 0, drawRadius * 0.45, 0, Math.PI * 2);
                ctx.strokeStyle = 'rgba(138, 43, 226, 0.25)';
                ctx.lineWidth = 1;
                ctx.stroke();

                // 3. Central Axis Hub
                ctx.beginPath();
                ctx.arc(0, 0, 16, 0, Math.PI * 2);
                ctx.fillStyle = '#ffffff';
                ctx.strokeStyle = '#0284c7';
                ctx.lineWidth = 3;
                ctx.shadowBlur = 8;
                ctx.shadowColor = '#0284c7';
                ctx.fill();
                ctx.stroke();

                ctx.shadowBlur = 0;
                ctx.beginPath();
                ctx.arc(0, 0, 6, 0, Math.PI * 2);
                ctx.fillStyle = '#ffffff';
                ctx.fill();

                ctx.restore();

                // --- BLUEPRINT ANNOTATIONS ---
                ctx.save();
                ctx.translate(cx, cy);
                const config = unitConfigs[state.currentUnit];

                // 1. Radius Line
                ctx.beginPath();
                ctx.moveTo(0, 0);
                ctx.lineTo(0, -drawRadius);
                ctx.strokeStyle = 'rgba(16, 185, 129, 0.5)';
                ctx.lineWidth = 1.5;
                ctx.setLineDash([4, 4]);
                ctx.stroke();
                ctx.setLineDash([]);

                ctx.fillStyle = '#10b981';
                ctx.font = 'bold 10px Inter';
                ctx.textAlign = 'right';
                const uiRadVal = config.fromMeters(state.diameterMeters / 2);
                ctx.fillText(`반경 r = ${uiRadVal.toLocaleString('ko-KR', { maximumFractionDigits: 2 })} ${state.currentUnit}`, -8, -drawRadius / 2);

                // 2. Diameter Line
                const dimAngle = -45 * Math.PI / 180;
                const dx = Math.cos(dimAngle) * drawRadius;
                const dy = Math.sin(dimAngle) * drawRadius;

                ctx.beginPath();
                ctx.moveTo(-dx, -dy);
                ctx.lineTo(dx, dy);
                ctx.strokeStyle = 'rgba(0, 242, 254, 0.45)';
                ctx.lineWidth = 1.2;
                ctx.stroke();

                function drawArrowhead(c, x, y, angle) {
                    c.save();
                    c.translate(x, y);
                    c.rotate(angle);
                    c.beginPath();
                    c.moveTo(0, 0);
                    c.lineTo(-8, -3.5);
                    c.lineTo(-8, 3.5);
                    c.closePath();
                    c.fillStyle = 'rgba(0, 242, 254, 0.7)';
                    c.fill();
                    c.restore();
                }
                drawArrowhead(ctx, dx, dy, dimAngle);
                drawArrowhead(ctx, -dx, -dy, dimAngle + Math.PI);

                const uiDiamVal = config.fromMeters(state.diameterMeters);
                const diamText = `지름 D = ${uiDiamVal.toLocaleString('ko-KR', { maximumFractionDigits: 2 })} ${state.currentUnit}`;
                
                ctx.save();
                ctx.translate(dx * 0.35, dy * 0.35);
                ctx.rotate(dimAngle);
                
                ctx.fillStyle = '#0284c7';
                ctx.font = 'bold 10px Inter';
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                ctx.fillText(diamText, 0, 0);
                ctx.restore();

                // 3. Central Rotation Indicator
                if (state.rpm > 0) {
                    const arcRadius = 34;
                    ctx.beginPath();
                    const startAngle = state.direction > 0 ? -Math.PI * 0.75 : -Math.PI * 0.25;
                    const endAngle = state.direction > 0 ? -Math.PI * 0.25 : -Math.PI * 0.75;
                    ctx.arc(0, 0, arcRadius, startAngle, endAngle, state.direction < 0);
                    ctx.strokeStyle = '#7c3aed';
                    ctx.lineWidth = 2.5;
                    ctx.shadowBlur = 6;
                    ctx.shadowColor = '#7c3aed';
                    ctx.stroke();
                    ctx.shadowBlur = 0;

                    const tipAngle = state.direction > 0 ? endAngle : endAngle;
                    ctx.save();
                    ctx.translate(Math.cos(tipAngle) * arcRadius, Math.sin(tipAngle) * arcRadius);
                    ctx.rotate(tipAngle + (state.direction > 0 ? Math.PI / 2 : -Math.PI / 2));
                    ctx.beginPath();
                    ctx.moveTo(0, 0);
                    ctx.lineTo(-6, -3);
                    ctx.lineTo(-6, 3);
                    ctx.closePath();
                    ctx.fillStyle = '#7c3aed';
                    ctx.fill();
                    ctx.restore();

                    ctx.fillStyle = '#1e293b';
                    ctx.font = '900 10px Outfit';
                    ctx.textAlign = state.direction > 0 ? 'left' : 'right';
                    const rpmX = state.direction > 0 ? arcRadius + 8 : -(arcRadius + 8);
                    ctx.fillText(`${state.direction > 0 ? '↻' : '↺'} ${state.rpm.toLocaleString()} RPM`, rpmX, 4);
                }
                ctx.restore();

                // --- TANGENTIAL SPEED VECTOR ARROW ---
                if (state.rpm > 0) {
                    ctx.save();
                    ctx.translate(cx, cy);

                    const arrowDir = state.direction;
                    const arrowStartY = -drawRadius;
                    
                    ctx.beginPath();
                    ctx.moveTo(-120, arrowStartY);
                    ctx.lineTo(120, arrowStartY);
                    ctx.strokeStyle = 'rgba(15, 23, 42, 0.12)';
                    ctx.lineWidth = 1;
                    ctx.setLineDash([3, 3]);
                    ctx.stroke();
                    ctx.setLineDash([]);

                    const arrowLength = Math.min(physics.speedMs * 1.3 + 25, 170);

                    ctx.shadowBlur = 12;
                    ctx.shadowColor = '#db2777';
                    ctx.strokeStyle = '#db2777';
                    ctx.fillStyle = '#db2777';
                    ctx.lineWidth = 4.5;
                    ctx.lineCap = 'round';

                    ctx.beginPath();
                    ctx.moveTo(0, arrowStartY);
                    ctx.lineTo(arrowDir * arrowLength, arrowStartY);
                    ctx.stroke();

                    ctx.beginPath();
                    ctx.moveTo(arrowDir * arrowLength, arrowStartY);
                    ctx.lineTo(arrowDir * (arrowLength - 10), arrowStartY - 6);
                    ctx.lineTo(arrowDir * (arrowLength - 10), arrowStartY + 6);
                    ctx.closePath();
                    ctx.fill();

                    ctx.shadowBlur = 4;
                    ctx.shadowColor = '#db2777';
                    ctx.fillStyle = '#db2777';
                    ctx.beginPath();
                    ctx.arc(0, arrowStartY, 5, 0, Math.PI * 2);
                    ctx.fill();

                    ctx.shadowBlur = 0;
                    ctx.fillStyle = '#0f172a';
                    ctx.font = 'bold 11px Inter';
                    ctx.textAlign = arrowDir > 0 ? 'left' : 'right';
                    
                    const vectorLabel = `선속도 v = ${physics.speedMs.toFixed(2)} m/s`;
                    ctx.fillText(
                        vectorLabel, 
                        arrowDir * (arrowLength / 2), 
                        arrowStartY - 12
                    );
                    ctx.restore();
                }

                // --- BLUEPRINT SCALE BAR ---
                ctx.save();
                const scaleMeters = getNiceScaleStep(state.diameterMeters / 3.5);
                if (scaleMeters > 0) {
                    const scaleBarPx = scaleMeters / (state.diameterMeters / (drawRadius * 2));
                    const startX = 20;
                    const startY = height - 20;
                    
                    ctx.strokeStyle = 'rgba(15, 23, 42, 0.35)';
                    ctx.lineWidth = 1.5;
                    ctx.beginPath();
                    ctx.moveTo(startX, startY - 4);
                    ctx.lineTo(startX, startY);
                    ctx.lineTo(startX + scaleBarPx, startY);
                    ctx.lineTo(startX + scaleBarPx, startY - 4);
                    ctx.stroke();

                    const uiScaleVal = config.fromMeters(scaleMeters);
                    let displayScaleText = `${uiScaleVal.toLocaleString('ko-KR', { maximumFractionDigits: 2 })} ${state.currentUnit}`;
                    
                    ctx.fillStyle = 'rgba(15, 23, 42, 0.6)';
                    ctx.font = 'bold 9px Inter';
                    ctx.textAlign = 'left';
                    ctx.fillText(displayScaleText, startX + 4, startY - 6);
                    ctx.fillStyle = 'rgba(15, 23, 42, 0.6)'; ctx.fillStyle = 'rgba(15, 23, 42, 0.6)'; ctx.fillText("SCALE", startX + scaleBarPx + 8, startY + 3);
                }
                ctx.restore();
            }

            function getVisualRadius() {
                const D = state.diameterMeters;
                const minVal = 0.01;
                const maxVal = 2.0; 
                
                const dpr = window.devicePixelRatio || 1;
                const height = canvas.height / dpr;
                
                // Dynamically calculate bounds to prevent any text clipping at the top/bottom/sides
                const maxRadius = height / 2 - 60; // 60px padding leaves ample space for tangent labels and scale bar
                const minRadius = Math.max(50, height / 6);
                
                if (D <= minVal) return minRadius;
                if (D >= maxVal) return maxRadius;
                
                const pct = (D - minVal) / (maxVal - minVal);
                return minRadius + pct * (maxRadius - minRadius);
            }

            function displayVisualScaleText(speedMs) {
                const D = state.diameterMeters;
                let text = '화면 스케일: ';
                if (D < 0.01) {
                    text += `마이크로 (1:${(0.1 / D).toFixed(0)} 비율)`;
                } else if (D < 1) {
                    text += `밀리미터급`;
                } else if (D < 10) {
                    text += `인체/자전거급 (1:1 실제 비율)`;
                } else if (D < 1000) {
                    text += `항공기/터빈급 (축소 출력)`;
                } else {
                    text += `행성 스케일 (초고도 축소)`;
                }
                txtCanvasScale.innerText = text;
            }

            function getNiceScaleStep(approxVal) {
                if (approxVal <= 0) return 0;
                const exponent = Math.floor(Math.log10(approxVal));
                const fraction = approxVal / Math.pow(10, exponent);
                let step;
                if (fraction < 1.5) step = 1;
                else if (fraction < 3.5) step = 2;
                else if (fraction < 7.5) step = 5;
                else step = 10;
                return step * Math.pow(10, exponent);
            }

            // Sync Event Bindings
            sliderDiameter.addEventListener('input', syncDiameterInputFromSlider);
            inputDiameter.addEventListener('change', syncDiameterSliderFromInput);
            inputDiameter.addEventListener('input', syncDiameterSliderFromInput);

            sliderRpm.addEventListener('input', syncRpmInputFromSlider);
            inputRpm.addEventListener('change', syncRpmSliderFromInput);
            inputRpm.addEventListener('input', syncRpmSliderFromInput);

            selectUnit.addEventListener('change', handleUnitChange);

            btnPlayPause.addEventListener('click', () => {
                state.isPlaying = !state.isPlaying;
                if (state.isPlaying) {
                    btnPlayPause.innerHTML = '<i class="fa-solid fa-pause"></i> <span>일시정지</span>';
                    btnPlayPause.classList.remove('active');
                } else {
                    btnPlayPause.innerHTML = '<i class="fa-solid fa-play"></i> <span>재생</span>';
                    btnPlayPause.classList.add('active');
                }
            });

            btnReverse.addEventListener('click', () => {
                state.direction *= -1;
                btnReverse.classList.add('press-glow');
                setTimeout(() => btnReverse.classList.remove('press-glow'), 300);
            });

            btnParticles.addEventListener('click', () => {
                state.showParticles = !state.showParticles;
                if (state.showParticles) {
                    btnParticles.classList.add('active');
                } else {
                    btnParticles.classList.remove('active');
                }
            });

            presetButtons.forEach(btn => {
                btn.addEventListener('click', () => {
                    const key = btn.dataset.preset;
                    loadPreset(key);
                });
            });

            loadPreset('bicycle');
            requestAnimationFrame(animate);

            // ── 우클릭 & 소스 복사 방지 (전체 문서 레벨 + 알림창 동기화) ──
            document.addEventListener('contextmenu', function(e) {
                e.preventDefault();
                alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                return false;
            }, { capture: true });

            document.addEventListener('selectstart', function(e) {
                e.preventDefault();
                return false;
            }, { capture: true });

            // 개발자도구 단축키 및 복사 단축키 차단 (F12, Ctrl+U, Ctrl+Shift+I/J/C, Ctrl+C, Ctrl+S)
            // && 문자를 사용하면 WordPress API에서 &amp;&amp;로 강제 치환되므로 nested if로 구현
            document.addEventListener('keydown', function(e) {
                if (e.key === 'F12') {
                    e.preventDefault();
                    alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                    return false;
                }
                if (e.ctrlKey) {
                    if (e.key === 'u' || e.key === 'c' || e.key === 's' || e.key === 'U' || e.key === 'C' || e.key === 'S') {
                        e.preventDefault();
                        alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                        return false;
                    }
                    if (e.shiftKey) {
                        if (['i','I','j','J','c','C'].includes(e.key)) {
                            e.preventDefault();
                            alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                            return false;
                        }
                    }
                }
            }, { capture: true });
        } // <-- CRITICAL: Closed the initRotaspeed() function!

        // Robust ReadyState hook-up with polling to guarantee execution even if DOM elements load late
        let initAttempts = 0;
        function tryInit() {
            initAttempts++;
            initRotaspeed();
            if (!window.__rotaspeed_initialized) {
                if (initAttempts < 50) {
                    setTimeout(tryInit, 100);
                }
            }
        }

        if (document.readyState === 'complete' || document.readyState === 'interactive') {
            tryInit();
        } else {
            document.addEventListener('DOMContentLoaded', tryInit);
            window.addEventListener('load', tryInit);
        }
        })();
    </script>

    </div>
</div>

<script>
        window.MathJax = {
            tex: {
                inlineMath: [['$', '$'], ['\\(', '\\)']],
                displayMath: [['$$', '$$'], ['\\[', '\\]']]
            },
            svg: {
                fontCache: 'global'
            }
        };
    </script>

<script type="text/javascript" id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script>

<script>
        (function() {
            if (window.__rotaspeed_initialized) return;

            function initRotaspeed() {
                if (window.__rotaspeed_initialized) return;

                const selectUnit = document.getElementById('select-diameter-unit');
                const sliderDiameter = document.getElementById('slider-diameter');
                const inputDiameter = document.getElementById('input-diameter');
                const canvas = document.getElementById('physics-canvas');

                if (!selectUnit || !sliderDiameter || !inputDiameter || !canvas) {
                    return;
                }

                window.__rotaspeed_initialized = true;
                const labelDiameterUnit = document.getElementById('diameter-unit-label');

            const sliderRpm = document.getElementById('slider-rpm');
            const inputRpm = document.getElementById('input-rpm');

            const btnPlayPause = document.getElementById('btn-play-pause');
            const btnReverse = document.getElementById('btn-reverse');
            const btnParticles = document.getElementById('btn-particles');

            const presetButtons = document.querySelectorAll('.preset-btn');

            const txtFrequency = document.getElementById('val-frequency');
            const txtAngularSpeed = document.getElementById('val-angular-speed');
            const txtCentripetalG = document.getElementById('val-centripetal-g');
            const txtCanvasScale = document.getElementById('canvas-scale-text');

            const gaugeProgress = document.getElementById('gauge-progress');
            const txtGaugePrimaryValue = document.getElementById('gauge-primary-value');
            const txtMs = document.getElementById('val-ms');
            const txtKmh = document.getElementById('val-kmh');
            const txtMmin = document.getElementById('val-mmin');
            const txtMph = document.getElementById('val-mph');
            const txtOverlayVectorVal = document.getElementById('overlay-vector-val');

            const ctx = canvas.getContext('2d');

            function resizeCanvas() {
                const dpr = window.devicePixelRatio || 1;
                const wrapper = canvas.parentElement;
                if (!wrapper) return;
                
                let targetWidth = wrapper.clientWidth;
                let targetHeight = targetWidth; // strictly maintain 1:1 square aspect ratio
                
                if (targetWidth < 50) {
                    targetWidth = 500;
                    targetHeight = 500;
                }
                
                canvas.width = targetWidth * dpr;
                canvas.height = targetHeight * dpr;
                ctx.scale(dpr, dpr);
            }
            
            // Robust event hook-ups to guarantee rendering size after dynamic WordPress DOM layout settles
            resizeCanvas();
            window.addEventListener('resize', resizeCanvas);
            window.addEventListener('load', resizeCanvas);
            setTimeout(resizeCanvas, 100);
            setTimeout(resizeCanvas, 400);
            setTimeout(resizeCanvas, 1000);

            let state = {
                diameterMeters: 0.5,
                rpm: 1500,
                isPlaying: true,
                direction: 1,
                showParticles: true,
                currentUnit: 'mm',
                angle: 0,
                lastTime: performance.now(),
                particles: []
            };

            const unitConfigs = {
                mm: {
                    label: 'mm',
                    toMeters: (val) => val / 1000,
                    fromMeters: (m) => m * 1000,
                    slider: { min: 1, max: 2000, step: 1 }
                },
                cm: {
                    label: 'cm',
                    toMeters: (val) => val / 100,
                    fromMeters: (m) => m * 100,
                    slider: { min: 0.1, max: 200, step: 0.1 }
                },
                m: {
                    label: 'm',
                    toMeters: (val) => val,
                    fromMeters: (m) => m,
                    slider: { min: 0.001, max: 25, step: 0.001 }
                },
                inch: {
                    label: 'inch',
                    toMeters: (val) => val * 0.0254,
                    fromMeters: (m) => m / 0.0254,
                    slider: { min: 0.1, max: 80, step: 0.1 }
                }
            };

            const presets = {
                bicycle: {
                    name: '자전거 바퀴 (27.5인치)',
                    diameter: 0.7,
                    rpm: 270,
                    unit: 'mm'
                },
                grinder: {
                    name: '초고속 앵글 그라인더',
                    diameter: 0.125,
                    rpm: 11000,
                    unit: 'mm'
                },
                turbine: {
                    name: '초대형 풍력 터빈',
                    diameter: 120,
                    rpm: 15,
                    unit: 'm'
                },
                hdd: {
                    name: '컴퓨터 하드디스크 Platter',
                    diameter: 0.0889,
                    rpm: 7200,
                    unit: 'inch'
                },
                earth: {
                    name: '지구 자전 (적도 둘레)',
                    diameter: 12742000,
                    rpm: 0.0006944,
                    unit: 'm'
                }
            };

            function calculatePhysics() {
                const D = state.diameterMeters;
                const RPM = state.rpm;
                const freqHz = RPM / 60;
                const omega = 2 * Math.PI * freqHz;
                const speedMs = Math.PI * D * freqHz;

                const speedKmh = speedMs * 3.6;
                const speedMmin = speedMs * 60;
                const speedMph = speedMs * 2.23694;

                const R = D / 2;
                let centripetalAccel = 0;
                if (R > 0) {
                    centripetalAccel = (speedMs * speedMs) / R;
                }
                const centripetalG = centripetalAccel / 9.80665;

                return {
                    freqHz,
                    omega,
                    speedMs,
                    speedKmh,
                    speedMmin,
                    speedMph,
                    centripetalG
                };
            }

            function updateOutputs() {
                const physics = calculatePhysics();

                txtFrequency.innerText = physics.freqHz.toFixed(2) + ' Hz';
                txtAngularSpeed.innerText = physics.omega.toFixed(1) + ' rad/s';
                
                if (physics.centripetalG > 100000) {
                    txtCentripetalG.innerText = physics.centripetalG.toExponential(3) + ' G';
                } else {
                    txtCentripetalG.innerText = physics.centripetalG.toLocaleString('ko-KR', { maximumFractionDigits: 1 }) + ' G';
                }

                txtGaugePrimaryValue.innerText = physics.speedMs.toLocaleString('ko-KR', { maximumFractionDigits: 2 });
                txtOverlayVectorVal.innerText = physics.speedMs.toFixed(2) + ' m/s';

                txtMs.innerText = physics.speedMs.toLocaleString('ko-KR', { maximumFractionDigits: 2 });
                txtKmh.innerText = physics.speedKmh.toLocaleString('ko-KR', { maximumFractionDigits: 1 });
                txtMmin.innerText = physics.speedMmin.toLocaleString('ko-KR', { maximumFractionDigits: 0 });
                txtMph.innerText = physics.speedMph.toLocaleString('ko-KR', { maximumFractionDigits: 1 });

                const cardMs = document.getElementById('card-ms');
                if (physics.speedMs > 100) {
                    cardMs.style.boxShadow = '0 0 20px rgba(255, 0, 127, 0.2)';
                    cardMs.style.borderColor = 'rgba(255, 0, 127, 0.4)';
                } else {
                    cardMs.style.boxShadow = '';
                    cardMs.style.borderColor = '';
                }

                const maxGaugeSpeed = 100;
                const percentage = Math.min(physics.speedMs / maxGaugeSpeed, 1);
                const dashoffset = 251.2 * (1 - percentage);
                gaugeProgress.style.strokeDashoffset = dashoffset;

                if (physics.speedMs > 80) {
                    gaugeProgress.style.stroke = '#db2777';
                    gaugeProgress.style.filter = 'drop-shadow(0px 0px 8px rgba(255, 0, 127, 0.7))';
                } else {
                    gaugeProgress.style.stroke = '#0284c7';
                    gaugeProgress.style.filter = 'drop-shadow(0px 0px 6px rgba(0, 242, 254, 0.6))';
                }
            }

            function syncDiameterInputFromSlider() {
                const val = parseFloat(sliderDiameter.value);
                inputDiameter.value = val;
                
                const config = unitConfigs[state.currentUnit];
                state.diameterMeters = config.toMeters(val);
                
                clearActivePresets();
                updateOutputs();
            }

            function syncDiameterSliderFromInput() {
                let val = parseFloat(inputDiameter.value);
                if (isNaN(val) || val <= 0) val = 0.001;
                
                const config = unitConfigs[state.currentUnit];
                const sliderConfig = config.slider;
                
                sliderDiameter.min = sliderConfig.min;
                sliderDiameter.max = sliderConfig.max;
                sliderDiameter.step = sliderConfig.step;
                sliderDiameter.value = Math.min(Math.max(val, sliderConfig.min), sliderConfig.max);
                
                state.diameterMeters = config.toMeters(val);
                
                clearActivePresets();
                updateOutputs();
            }

            function syncRpmInputFromSlider() {
                const val = parseInt(sliderRpm.value);
                inputRpm.value = val;
                state.rpm = val;
                
                clearActivePresets();
                updateOutputs();
            }

            function syncRpmSliderFromInput() {
                let val = parseFloat(inputRpm.value);
                if (isNaN(val) || val < 0) val = 0;
                
                if (val > sliderRpm.max) {
                    sliderRpm.max = Math.ceil(val * 1.2);
                } else if (val < 8000) {
                    if (sliderRpm.max > 8000) {
                        sliderRpm.max = 8000;
                    }
                }

                sliderRpm.value = val;
                state.rpm = val;
                
                clearActivePresets();
                updateOutputs();
            }

            function handleUnitChange() {
                const oldUnit = state.currentUnit;
                const newUnit = selectUnit.value;
                state.currentUnit = newUnit;

                const configNew = unitConfigs[newUnit];
                labelDiameterUnit.innerText = configNew.label;

                sliderDiameter.min = configNew.slider.min;
                sliderDiameter.max = configNew.slider.max;
                sliderDiameter.step = configNew.slider.step;

                const convertedVal = configNew.fromMeters(state.diameterMeters);
                
                inputDiameter.value = Number(convertedVal.toFixed(3));
                sliderDiameter.value = Math.min(Math.max(convertedVal, configNew.slider.min), configNew.slider.max);

                updateOutputs();
            }

            function loadPreset(presetKey) {
                const preset = presets[presetKey];
                if (!preset) return;

                presetButtons.forEach(btn => {
                    if (btn.dataset.preset === presetKey) {
                        btn.classList.add('active');
                    } else {
                        btn.classList.remove('active');
                    }
                });

                state.diameterMeters = preset.diameter;
                state.rpm = preset.rpm;
                state.currentUnit = preset.unit;

                selectUnit.value = preset.unit;
                labelDiameterUnit.innerText = preset.unit;

                const config = unitConfigs[preset.unit];
                sliderDiameter.min = config.slider.min;
                sliderDiameter.max = config.slider.max;
                sliderDiameter.step = config.slider.step;

                const uiDiameterVal = config.fromMeters(preset.diameter);
                inputDiameter.value = Number(uiDiameterVal.toFixed(4));
                sliderDiameter.value = uiDiameterVal;

                if (preset.rpm > sliderRpm.max) {
                    sliderRpm.max = Math.ceil(preset.rpm * 1.1);
                }
                inputRpm.value = preset.rpm;
                sliderRpm.value = preset.rpm;

                updateOutputs();
            }

            function clearActivePresets() {
                presetButtons.forEach(btn => btn.classList.remove('active'));
            }

            class SparkParticle {
                constructor(x, y, angle, linearSpeed) {
                    this.x = x;
                    this.y = y;
                    
                    const speedFactor = Math.min(linearSpeed * 0.08, 4) + Math.random() * 1.5;
                    const directionAngle = angle + (Math.random() - 0.5) * 0.25;
                    
                    this.vx = Math.cos(directionAngle) * speedFactor;
                    this.vy = Math.sin(directionAngle) * speedFactor;
                    
                    this.size = Math.random() * 1.5 + 0.5;
                    this.maxLife = Math.random() * 15 + 15;
                    this.life = this.maxLife;
                    
                    const colors = ['#ffe600', '#ff8400', '#ff007f', '#ffffff'];
                    this.color = colors[Math.floor(Math.random() * colors.length)];
                }

                update() {
                    this.vx *= 0.95;
                    this.vy *= 0.95;
                    this.vy += 0.08;
                    
                    this.x += this.vx;
                    this.y += this.vy;
                    
                    this.life--;
                }

                draw(c) {
                    const alpha = this.life / this.maxLife;
                    c.save();
                    c.globalAlpha = alpha;
                    c.beginPath();
                    c.arc(this.x, this.y, this.size, 0, Math.PI * 2);
                    c.fillStyle = this.color;
                    c.shadowBlur = 4;
                    c.shadowColor = this.color;
                    c.fill();
                    c.restore();
                }
            }

            function animate(currentTime) {
                requestAnimationFrame(animate);

                let dt = (currentTime - state.lastTime) / 1000;
                if (isNaN(dt) || dt < 0 || dt > 1.0) {
                    dt = 0.016; // default to 60fps frame time
                }
                state.lastTime = currentTime;

                const width = canvas.width / (window.devicePixelRatio || 1);
                const height = canvas.height / (window.devicePixelRatio || 1);
                const cx = width / 2;
                const cy = height / 2;

                ctx.clearRect(0, 0, width, height);

                ctx.save();
                ctx.strokeStyle = 'rgba(2, 132, 199, 0.06)';
                ctx.lineWidth = 1;
                for (let x = 0; x < width; x += 30) {
                    ctx.beginPath();
                    ctx.moveTo(x, 0);
                    ctx.lineTo(x, height);
                    ctx.stroke();
                }
                for (let y = 0; y < height; y += 30) {
                    ctx.beginPath();
                    ctx.moveTo(0, y);
                    ctx.lineTo(width, y);
                    ctx.stroke();
                }
                ctx.restore();

                const physics = calculatePhysics();
                if (isNaN(physics.omega)) physics.omega = 0;
                if (isNaN(physics.speedMs)) physics.speedMs = 0;

                if (state.isPlaying) {
                    if (state.rpm > 0) {
                        let clampedDt = Math.min(dt, 0.1);
                        if (isNaN(clampedDt) || clampedDt < 0) clampedDt = 0.016;
                        
                        // Stroboscopic Effect Fix (Wagon-Wheel illusion)
                        const maxVisualOmega = 12;
                        const visualOmega = maxVisualOmega * (1 - Math.exp(-physics.omega / maxVisualOmega));
                        
                        state.angle += state.direction * (isNaN(visualOmega) ? 0 : visualOmega) * clampedDt;
                    }
                }

                if (state.showParticles) {
                    if (state.isPlaying) {
                        if (state.rpm > 0) {
                            const emissionRate = Math.min(Math.floor(physics.speedMs / 12) + 1, 4);
                            const visualRadius = getVisualRadius();

                            for (let i = 0; i < emissionRate; i++) {
                                if (Math.random() > 0.6) {
                                    let emitAngle = Math.random() * Math.PI * 2;
                                    if (physics.speedMs > 150) {
                                        emitAngle = Math.PI / 4 + (Math.random() - 0.5) * 0.15;
                                    }
                                    
                                    const ex = cx + Math.cos(emitAngle) * visualRadius;
                                    const ey = cy + Math.sin(emitAngle) * visualRadius;
                                    const tangAngle = emitAngle + (state.direction * Math.PI / 2);

                                    state.particles.push(new SparkParticle(ex, ey, tangAngle, physics.speedMs));
                                }
                            }
                        }
                    }
                }

                state.particles = state.particles.filter(p => p.life > 0);
                state.particles.forEach(p => {
                    p.update();
                    p.draw(ctx);
                });

                ctx.save();
                ctx.translate(cx, cy);
                const drawRadius = getVisualRadius();
                displayVisualScaleText(physics.speedMs);
                
                if (isNaN(state.angle)) {
                    state.angle = 0;
                }
                ctx.rotate(state.angle);

                // 1. Draw outer tire
                ctx.shadowBlur = 10;
                ctx.shadowColor = 'rgba(0, 242, 254, 0.4)';
                ctx.beginPath();
                ctx.arc(0, 0, drawRadius, 0, Math.PI * 2);
                ctx.strokeStyle = 'rgba(0, 0, 0, 0.05)';
                ctx.lineWidth = 6;
                ctx.stroke();

                ctx.beginPath();
                ctx.arc(0, 0, drawRadius - 3, 0, Math.PI * 2);
                ctx.strokeStyle = 'rgba(0, 242, 254, 0.55)';
                ctx.lineWidth = 2;
                ctx.stroke();

                // 2. Draw interior spokes
                ctx.shadowBlur = 0;
                const spokesCount = 8;
                for (let i = 0; i < spokesCount; i++) {
                    const spokeAngle = (i * Math.PI * 2) / spokesCount;
                    ctx.save();
                    ctx.rotate(spokeAngle);
                    ctx.beginPath();
                    ctx.moveTo(0, 0);
                    ctx.lineTo(0, drawRadius - 10);
                    ctx.strokeStyle = i % 2 === 0 ? 'rgba(0, 242, 254, 0.4)' : 'rgba(138, 43, 226, 0.4)';
                    ctx.lineWidth = i % 2 === 0 ? 4 : 2;
                    ctx.stroke();

                    ctx.beginPath();
                    ctx.moveTo(-4, drawRadius - 12);
                    ctx.lineTo(4, drawRadius - 12);
                    ctx.strokeStyle = 'rgba(15, 23, 42, 0.15)';
                    ctx.lineWidth = 2;
                    ctx.stroke();
                    ctx.restore();
                }

                ctx.beginPath();
                ctx.arc(0, 0, drawRadius * 0.45, 0, Math.PI * 2);
                ctx.strokeStyle = 'rgba(138, 43, 226, 0.25)';
                ctx.lineWidth = 1;
                ctx.stroke();

                // 3. Central Axis Hub
                ctx.beginPath();
                ctx.arc(0, 0, 16, 0, Math.PI * 2);
                ctx.fillStyle = '#ffffff';
                ctx.strokeStyle = '#0284c7';
                ctx.lineWidth = 3;
                ctx.shadowBlur = 8;
                ctx.shadowColor = '#0284c7';
                ctx.fill();
                ctx.stroke();

                ctx.shadowBlur = 0;
                ctx.beginPath();
                ctx.arc(0, 0, 6, 0, Math.PI * 2);
                ctx.fillStyle = '#ffffff';
                ctx.fill();

                ctx.restore();

                // --- BLUEPRINT ANNOTATIONS ---
                ctx.save();
                ctx.translate(cx, cy);
                const config = unitConfigs[state.currentUnit];

                // 1. Radius Line
                ctx.beginPath();
                ctx.moveTo(0, 0);
                ctx.lineTo(0, -drawRadius);
                ctx.strokeStyle = 'rgba(16, 185, 129, 0.5)';
                ctx.lineWidth = 1.5;
                ctx.setLineDash([4, 4]);
                ctx.stroke();
                ctx.setLineDash([]);

                ctx.fillStyle = '#10b981';
                ctx.font = 'bold 10px Inter';
                ctx.textAlign = 'right';
                const uiRadVal = config.fromMeters(state.diameterMeters / 2);
                ctx.fillText(`반경 r = ${uiRadVal.toLocaleString('ko-KR', { maximumFractionDigits: 2 })} ${state.currentUnit}`, -8, -drawRadius / 2);

                // 2. Diameter Line
                const dimAngle = -45 * Math.PI / 180;
                const dx = Math.cos(dimAngle) * drawRadius;
                const dy = Math.sin(dimAngle) * drawRadius;

                ctx.beginPath();
                ctx.moveTo(-dx, -dy);
                ctx.lineTo(dx, dy);
                ctx.strokeStyle = 'rgba(0, 242, 254, 0.45)';
                ctx.lineWidth = 1.2;
                ctx.stroke();

                function drawArrowhead(c, x, y, angle) {
                    c.save();
                    c.translate(x, y);
                    c.rotate(angle);
                    c.beginPath();
                    c.moveTo(0, 0);
                    c.lineTo(-8, -3.5);
                    c.lineTo(-8, 3.5);
                    c.closePath();
                    c.fillStyle = 'rgba(0, 242, 254, 0.7)';
                    c.fill();
                    c.restore();
                }
                drawArrowhead(ctx, dx, dy, dimAngle);
                drawArrowhead(ctx, -dx, -dy, dimAngle + Math.PI);

                const uiDiamVal = config.fromMeters(state.diameterMeters);
                const diamText = `지름 D = ${uiDiamVal.toLocaleString('ko-KR', { maximumFractionDigits: 2 })} ${state.currentUnit}`;
                
                ctx.save();
                ctx.translate(dx * 0.35, dy * 0.35);
                ctx.rotate(dimAngle);
                
                ctx.fillStyle = '#0284c7';
                ctx.font = 'bold 10px Inter';
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                ctx.fillText(diamText, 0, 0);
                ctx.restore();

                // 3. Central Rotation Indicator
                if (state.rpm > 0) {
                    const arcRadius = 34;
                    ctx.beginPath();
                    const startAngle = state.direction > 0 ? -Math.PI * 0.75 : -Math.PI * 0.25;
                    const endAngle = state.direction > 0 ? -Math.PI * 0.25 : -Math.PI * 0.75;
                    ctx.arc(0, 0, arcRadius, startAngle, endAngle, state.direction < 0);
                    ctx.strokeStyle = '#7c3aed';
                    ctx.lineWidth = 2.5;
                    ctx.shadowBlur = 6;
                    ctx.shadowColor = '#7c3aed';
                    ctx.stroke();
                    ctx.shadowBlur = 0;

                    const tipAngle = state.direction > 0 ? endAngle : endAngle;
                    ctx.save();
                    ctx.translate(Math.cos(tipAngle) * arcRadius, Math.sin(tipAngle) * arcRadius);
                    ctx.rotate(tipAngle + (state.direction > 0 ? Math.PI / 2 : -Math.PI / 2));
                    ctx.beginPath();
                    ctx.moveTo(0, 0);
                    ctx.lineTo(-6, -3);
                    ctx.lineTo(-6, 3);
                    ctx.closePath();
                    ctx.fillStyle = '#7c3aed';
                    ctx.fill();
                    ctx.restore();

                    ctx.fillStyle = '#1e293b';
                    ctx.font = '900 10px Outfit';
                    ctx.textAlign = state.direction > 0 ? 'left' : 'right';
                    const rpmX = state.direction > 0 ? arcRadius + 8 : -(arcRadius + 8);
                    ctx.fillText(`${state.direction > 0 ? '↻' : '↺'} ${state.rpm.toLocaleString()} RPM`, rpmX, 4);
                }
                ctx.restore();

                // --- TANGENTIAL SPEED VECTOR ARROW ---
                if (state.rpm > 0) {
                    ctx.save();
                    ctx.translate(cx, cy);

                    const arrowDir = state.direction;
                    const arrowStartY = -drawRadius;
                    
                    ctx.beginPath();
                    ctx.moveTo(-120, arrowStartY);
                    ctx.lineTo(120, arrowStartY);
                    ctx.strokeStyle = 'rgba(15, 23, 42, 0.12)';
                    ctx.lineWidth = 1;
                    ctx.setLineDash([3, 3]);
                    ctx.stroke();
                    ctx.setLineDash([]);

                    const arrowLength = Math.min(physics.speedMs * 1.3 + 25, 170);

                    ctx.shadowBlur = 12;
                    ctx.shadowColor = '#db2777';
                    ctx.strokeStyle = '#db2777';
                    ctx.fillStyle = '#db2777';
                    ctx.lineWidth = 4.5;
                    ctx.lineCap = 'round';

                    ctx.beginPath();
                    ctx.moveTo(0, arrowStartY);
                    ctx.lineTo(arrowDir * arrowLength, arrowStartY);
                    ctx.stroke();

                    ctx.beginPath();
                    ctx.moveTo(arrowDir * arrowLength, arrowStartY);
                    ctx.lineTo(arrowDir * (arrowLength - 10), arrowStartY - 6);
                    ctx.lineTo(arrowDir * (arrowLength - 10), arrowStartY + 6);
                    ctx.closePath();
                    ctx.fill();

                    ctx.shadowBlur = 4;
                    ctx.shadowColor = '#db2777';
                    ctx.fillStyle = '#db2777';
                    ctx.beginPath();
                    ctx.arc(0, arrowStartY, 5, 0, Math.PI * 2);
                    ctx.fill();

                    ctx.shadowBlur = 0;
                    ctx.fillStyle = '#0f172a';
                    ctx.font = 'bold 11px Inter';
                    ctx.textAlign = arrowDir > 0 ? 'left' : 'right';
                    
                    const vectorLabel = `선속도 v = ${physics.speedMs.toFixed(2)} m/s`;
                    ctx.fillText(
                        vectorLabel, 
                        arrowDir * (arrowLength / 2), 
                        arrowStartY - 12
                    );
                    ctx.restore();
                }

                // --- BLUEPRINT SCALE BAR ---
                ctx.save();
                const scaleMeters = getNiceScaleStep(state.diameterMeters / 3.5);
                if (scaleMeters > 0) {
                    const scaleBarPx = scaleMeters / (state.diameterMeters / (drawRadius * 2));
                    const startX = 20;
                    const startY = height - 20;
                    
                    ctx.strokeStyle = 'rgba(15, 23, 42, 0.35)';
                    ctx.lineWidth = 1.5;
                    ctx.beginPath();
                    ctx.moveTo(startX, startY - 4);
                    ctx.lineTo(startX, startY);
                    ctx.lineTo(startX + scaleBarPx, startY);
                    ctx.lineTo(startX + scaleBarPx, startY - 4);
                    ctx.stroke();

                    const uiScaleVal = config.fromMeters(scaleMeters);
                    let displayScaleText = `${uiScaleVal.toLocaleString('ko-KR', { maximumFractionDigits: 2 })} ${state.currentUnit}`;
                    
                    ctx.fillStyle = 'rgba(15, 23, 42, 0.6)';
                    ctx.font = 'bold 9px Inter';
                    ctx.textAlign = 'left';
                    ctx.fillText(displayScaleText, startX + 4, startY - 6);
                    ctx.fillStyle = 'rgba(15, 23, 42, 0.6)'; ctx.fillStyle = 'rgba(15, 23, 42, 0.6)'; ctx.fillText("SCALE", startX + scaleBarPx + 8, startY + 3);
                }
                ctx.restore();
            }

            function getVisualRadius() {
                const D = state.diameterMeters;
                const minVal = 0.01;
                const maxVal = 2.0; 
                
                const dpr = window.devicePixelRatio || 1;
                const height = canvas.height / dpr;
                
                // Dynamically calculate bounds to prevent any text clipping at the top/bottom/sides
                const maxRadius = height / 2 - 60; // 60px padding leaves ample space for tangent labels and scale bar
                const minRadius = Math.max(50, height / 6);
                
                if (D <= minVal) return minRadius;
                if (D >= maxVal) return maxRadius;
                
                const pct = (D - minVal) / (maxVal - minVal);
                return minRadius + pct * (maxRadius - minRadius);
            }

            function displayVisualScaleText(speedMs) {
                const D = state.diameterMeters;
                let text = '화면 스케일: ';
                if (D < 0.01) {
                    text += `마이크로 (1:${(0.1 / D).toFixed(0)} 비율)`;
                } else if (D < 1) {
                    text += `밀리미터급`;
                } else if (D < 10) {
                    text += `인체/자전거급 (1:1 실제 비율)`;
                } else if (D < 1000) {
                    text += `항공기/터빈급 (축소 출력)`;
                } else {
                    text += `행성 스케일 (초고도 축소)`;
                }
                txtCanvasScale.innerText = text;
            }

            function getNiceScaleStep(approxVal) {
                if (approxVal <= 0) return 0;
                const exponent = Math.floor(Math.log10(approxVal));
                const fraction = approxVal / Math.pow(10, exponent);
                let step;
                if (fraction < 1.5) step = 1;
                else if (fraction < 3.5) step = 2;
                else if (fraction < 7.5) step = 5;
                else step = 10;
                return step * Math.pow(10, exponent);
            }

            // Sync Event Bindings
            sliderDiameter.addEventListener('input', syncDiameterInputFromSlider);
            inputDiameter.addEventListener('change', syncDiameterSliderFromInput);
            inputDiameter.addEventListener('input', syncDiameterSliderFromInput);

            sliderRpm.addEventListener('input', syncRpmInputFromSlider);
            inputRpm.addEventListener('change', syncRpmSliderFromInput);
            inputRpm.addEventListener('input', syncRpmSliderFromInput);

            selectUnit.addEventListener('change', handleUnitChange);

            btnPlayPause.addEventListener('click', () => {
                state.isPlaying = !state.isPlaying;
                if (state.isPlaying) {
                    btnPlayPause.innerHTML = '<i class="fa-solid fa-pause"></i> <span>일시정지</span>';
                    btnPlayPause.classList.remove('active');
                } else {
                    btnPlayPause.innerHTML = '<i class="fa-solid fa-play"></i> <span>재생</span>';
                    btnPlayPause.classList.add('active');
                }
            });

            btnReverse.addEventListener('click', () => {
                state.direction *= -1;
                btnReverse.classList.add('press-glow');
                setTimeout(() => btnReverse.classList.remove('press-glow'), 300);
            });

            btnParticles.addEventListener('click', () => {
                state.showParticles = !state.showParticles;
                if (state.showParticles) {
                    btnParticles.classList.add('active');
                } else {
                    btnParticles.classList.remove('active');
                }
            });

            presetButtons.forEach(btn => {
                btn.addEventListener('click', () => {
                    const key = btn.dataset.preset;
                    loadPreset(key);
                });
            });

            loadPreset('bicycle');
            requestAnimationFrame(animate);

            // ── 우클릭 & 소스 복사 방지 (전체 문서 레벨 + 알림창 동기화) ──
            document.addEventListener('contextmenu', function(e) {
                e.preventDefault();
                alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                return false;
            }, { capture: true });

            document.addEventListener('selectstart', function(e) {
                e.preventDefault();
                return false;
            }, { capture: true });

            // 개발자도구 단축키 및 복사 단축키 차단 (F12, Ctrl+U, Ctrl+Shift+I/J/C, Ctrl+C, Ctrl+S)
            // && 문자를 사용하면 WordPress API에서 &amp;&amp;로 강제 치환되므로 nested if로 구현
            document.addEventListener('keydown', function(e) {
                if (e.key === 'F12') {
                    e.preventDefault();
                    alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                    return false;
                }
                if (e.ctrlKey) {
                    if (e.key === 'u' || e.key === 'c' || e.key === 's' || e.key === 'U' || e.key === 'C' || e.key === 'S') {
                        e.preventDefault();
                        alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                        return false;
                    }
                    if (e.shiftKey) {
                        if (['i','I','j','J','c','C'].includes(e.key)) {
                            e.preventDefault();
                            alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                            return false;
                        }
                    }
                }
            }, { capture: true });
        } // <-- CRITICAL: Closed the initRotaspeed() function!

        // Robust ReadyState hook-up with polling to guarantee execution even if DOM elements load late
        let initAttempts = 0;
        function tryInit() {
            initAttempts++;
            initRotaspeed();
            if (!window.__rotaspeed_initialized) {
                if (initAttempts < 50) {
                    setTimeout(tryInit, 100);
                }
            }
        }

        if (document.readyState === 'complete' || document.readyState === 'interactive') {
            tryInit();
        } else {
            document.addEventListener('DOMContentLoaded', tryInit);
            window.addEventListener('load', tryInit);
        }
        })();
    </script>




<div style="background: linear-gradient(135deg, rgba(0,242,254,0.03), rgba(138,43,226,0.03)); border: 1px solid rgba(0,242,254,0.15); border-radius: 12px; padding: 18px 24px; margin: 25px auto 35px auto; font-size: 0.95em; color: #4b5563; line-height: 1.7; font-family: sans-serif;">
    <strong style="color: #1f2937; font-size: 1.05em; display: flex; align-items: center; gap: 8px;">
        <span style="font-size: 1.2em;"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /></span> 간편 사용 설명서
    </strong>
    <ol style="margin: 10px 0 0 0; padding-left: 20px;">
        <li style="margin-bottom: 6px;"><strong>회전체 지름 입력: 슬라이더를 조절하거나 우측 숫자 입력 칸에 회전체의 실제 지름을 입력합니다. (기본 mm단위, cm/m/inch로 단위 변환 지원)</strong></li>
<li style="margin-bottom: 6px;">회전수(RPM) 입력: 아래 슬라이더 또는 숫자 칸에 분당 회전 속도를 직접 타이핑하여 설정합니다.</li>
<li style="margin-bottom: 6px;">실시간 수치 판독: 지름과 RPM을 변경하면 실시간으로 계산된 선속도 파티클 효과와 물리 벡터 화살표가 바뀌며, 실시간 수치(m/s, km/h, m/min) 및 원심 가속도를 확인합니다.</li>
    </ol>
</div>



<details class="premium-seo-accordion" style="border: 1px solid rgba(0,0,0,0.08); border-radius: 12px; background: #fbfbfc; padding: 0; margin: 30px auto; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.01); font-family: sans-serif;">
    <summary style="display: flex; justify-content: space-between; align-items: center; padding: 20px 24px; font-size: 1.1em; font-weight: 700; color: #1f2937; cursor: pointer; user-select: none; outline: none; list-style: none;">
        <span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 상세 기계공학 해설 및 설계 규격 (KS/ISO) 확인하기</span>
        <span class="accordion-arrow" style="font-size: 0.9em; color: #9ca3af; transition: transform 0.2s ease;">▼</span>
    </summary>
    <div style="padding: 0 24px 24px 24px; border-top: 1px solid rgba(0,0,0,0.04); background: #ffffff; border-radius: 0 0 12px 12px; font-size: 0.98em; color: #374151; line-height: 1.8;">
        <div style="margin-top: 20px;">
            <h3>1. 선끝속도(Linear Tip Speed)의 정의 및 공학적 중요성</h3>
<p>선끝속도(접선 속도, Tangential Speed)는 회전하는 강체의 가장 바깥쪽 표면상 한 점이 가지는 순간적인 선형 속도를 의미합니다. 기계공학 및 기계설계 분야에서 선끝속도는 부품의 내구성, 소음, 마찰열, 그리고 가공 정밀도를 결정짓는 극한의 핵심 설계 인자입니다. 대표적으로 원주속도(Peripheral Speed) 혹은 주속이라고도 불리며, 다음과 같은 시스템에서 임계 설계 기준으로 작용합니다.</p><ul><li><strong>절삭가공 (선삭 및 밀링):</strong> 공구 끝단의 절삭 속도(Cutting Speed)가 최적의 범위를 벗어나면 공구 마모가 급격히 촉진되거나 조도가 저하됩니다.</li><li><strong>회전체 역학 (플라이휠, 터빈 플래터):</strong> 고속 회전 시 원심력에 의한 인장 응력이 선끝속도의 제곱에 비례하여 증가하므로 파손 방지를 위한 강도학적 한계 검토가 반드시 필요합니다.</li><li><strong>동력 전달 (풀리, 체인):</strong> 마찰전동 및 물림전동에서 슬립(Slip) 방지 및 소음 임계치 설계를 위해 적정 속도 제어가 요구됩니다.</li></ul>
<h3>2. 핵심 수학적 기초 및 계산 공식 유도</h3>
<p>원운동을 하는 입자의 각속도를 <code>&omega;</code> (rad/s), 회전 반경을 <code>R</code> (m)이라고 하면 접선 방향의 선속도 <code>v</code> (m/s)는 다음과 같이 각속도와 반경의 곱으로 표현됩니다:</p><p style="text-align: center; font-weight: bold; background: #f3f4f6; padding: 12px; border-radius: 8px;">v = R &times; &omega;</p><p>실무 설계 및 현장 계산에서는 각속도 대신 초당 회전수 또는 분당 회전수(RPM, N)와 지름(D, mm)을 주로 사용하므로 공식 변환이 이루어집니다. 각속도 <code>&omega;</code>는 1회전이 <code>2&pi;</code> 라디안이므로 다음과 같이 유도됩니다:</p><p style="text-align: center; font-weight: bold; background: #f3f4f6; padding: 12px; border-radius: 8px;">&omega; = (2&pi; &times; N) / 60</p><p>이를 반경 <code>R = D / 2</code>와 미터 단위 변환(1m = 1000mm)을 적용하여 종합하면 최종적인 선끝속도 공학 계산 공식이 도출됩니다:</p><p style="text-align: center; font-weight: bold; background: #e0f2fe; padding: 16px; border-radius: 8px; font-size: 1.1em; color: #0369a1;">v = (&pi; &times; D &times; N) / (60 &times; 1000) = (&pi; &times; D &times; N) / 60,000 &nbsp;[m/s]</p>
<h3>3. 연관 물리량: 원심가속도 및 원심력</h3>
<p>선끝속도가 증가할 때 회전하는 부품 내부에 발생하는 응력을 분석하기 위해서는 원심가속도(Centripetal Acceleration) <code>a_c</code>를 반드시 고려해야 합니다:</p><p style="text-align: center; font-weight: bold; background: #f3f4f6; padding: 12px; border-radius: 8px;">a_c = v^2 / R = R &times; &omega;^2 &nbsp;[m/s^2]</p><p>원심가속도는 속도의 제곱에 비례하기 때문에 회전 지름보다 회전 속도(RPM)가 미치는 영향이 지배적입니다. 이는 회전체의 질량이 <code>m</code>일 때 관성력인 원심력(Centrifugal Force) <code>F_c = m &times; a_c</code>로 이어지며, 고속 스핀들이나 연삭 숫돌의 파손(Bursting) 설계에 있어 안전계수 결정의 결정적인 척도가 됩니다.</p>

        </div>
    </div>
</details>
<style>
details.premium-seo-accordion[open] summary .accordion-arrow { transform: rotate(180deg); color: #00f2fe; }
details.premium-seo-accordion summary::-webkit-details-marker { display: none; }
details.premium-seo-accordion:hover { border-color: rgba(0,242,254,0.3); }
</style>

]]></content:encoded>
					
					<wfw:commentRss>https://myengnote.com/linear-tip-speed-simulator-calculator/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>벨트-풀리 계산기 &#038; 시뮬레이터</title>
		<link>https://myengnote.com/belt-pulley-speed-calculator-simulator-2/</link>
					<comments>https://myengnote.com/belt-pulley-speed-calculator-simulator-2/#respond</comments>
		
		<dc:creator><![CDATA[동동]]></dc:creator>
		<pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[공학계산기]]></category>
		<category><![CDATA[기계설계]]></category>
		<category><![CDATA[동력전달]]></category>
		<category><![CDATA[물리시뮬레이터]]></category>
		<category><![CDATA[벨트계산기]]></category>
		<category><![CDATA[벨트풀리]]></category>
		<guid isPermaLink="false">https://myengnote.com/belt-pulley-speed-calculator-simulator-2/</guid>

					<description><![CDATA[두 풀리의 지름, 축간거리, 입력 RPM을 조절하여 벨트 길이, 접촉각, 벨트 속도 및 출력 속도를 실시간 계산하고 부드럽게 구동되는 벨트 전동 시스템을 시각화하는 초정밀 2D 시뮬레이터입니다.]]></description>
										<content:encoded><![CDATA[
<h2 style="font-size: 1.6em; font-weight: 800; color: #0c0e25; border-bottom: 2px solid #00f2fe; padding-bottom: 8px; margin-bottom: 20px;"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 벨트-풀리 계산기 &#038; 시뮬레이터</h2>



<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&#038;family=Outfit:wght@400;500;600;700;800&#038;display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* Scoped & Isolated Styles for beltpulley-calculator-wrapper */

        /* Modern Reset and Design Tokens */
        .beltpulley-calculator-wrapper *, .beltpulley-calculator-wrapper *::before, .beltpulley-calculator-wrapper *::after {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }

        .beltpulley-calculator-wrapper {
            --color-bg-dark: #f8f9fd;
            --color-bg-deep: #ffffff;
            --color-panel-bg: rgba(255, 255, 255, 0.85);
            --color-border: rgba(0, 0, 0, 0.06);
            --color-border-hover: rgba(0, 0, 0, 0.12);
            
            --color-text-main: #1e293b;
            --color-text-muted: #475569;
            --color-text-dark: #94a3b8;

            --color-cyan: #0284c7;
            --color-cyan-glow: rgba(2, 132, 199, 0.15);
            --color-magenta: #db2777;
            --color-magenta-glow: rgba(219, 39, 119, 0.15);
            --color-purple: #7c3aed;
            --color-purple-glow: rgba(124, 58, 237, 0.12);
            --color-success: #10b981;

            --gradient-primary: linear-gradient(135deg, var(--color-cyan) 0%, var(--color-purple) 100%);
            --gradient-accent: linear-gradient(135deg, var(--color-magenta) 0%, var(--color-purple) 100%);
            --gradient-panel: linear-gradient(180deg, rgba(255, 255, 255, 0.9) 0%, rgba(248, 250, 252, 0.95) 100%);

            --font-heading: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            --font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
            
            --shadow-neon-cyan: 0 0 15px var(--color-cyan-glow);
            --shadow-neon-magenta: 0 0 15px var(--color-magenta-glow);
            --shadow-card: 0 8px 32px 0 rgba(15, 23, 42, 0.05);
            --blur-glass: blur(16px);
        }

        .beltpulley-calculator-wrapper {
            background-color: var(--color-bg-dark);
            color: var(--color-text-main);
            font-family: var(--font-body);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            overflow-x: hidden;
            position: relative;
        }

        .app-background-glow {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: -1;
            overflow: hidden;
            pointer-events: none;
        }

        .app-background-glow::before, 
        .app-background-glow::after {
            content: '';
            position: absolute;
            width: 600px;
            height: 600px;
            border-radius: 50%;
            filter: blur(140px);
            opacity: 0.15;
        }

        .app-background-glow::before {
            background: var(--color-cyan);
            top: -10%;
            right: -5%;
            animation: pulse-slow 15s infinite alternate;
        }

        .app-background-glow::after {
            background: var(--color-purple);
            bottom: -10%;
            left: -5%;
            animation: pulse-slow 20s infinite alternate-reverse;
        }

        @keyframes pulse-slow {
            0% { transform: scale(1) translate(0, 0); opacity: 0.12; }
            100% { transform: scale(1.2) translate(50px, 50px); opacity: 0.22; }
        }

        .app-container {
            width: 100%;
            max-width: 1440px;
            padding: 24px;
            display: flex;
            flex-direction: column;
            gap: 24px;
            container-type: inline-size;
            container-name: beltpulley-container;
        }

        .app-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 20px 24px;
            background: var(--color-panel-bg);
            backdrop-filter: var(--blur-glass);
            border: 1px solid var(--color-border);
            border-radius: 16px;
            box-shadow: var(--shadow-card);
        }

        .logo-area {
            display: flex;
            align-items: center;
            gap: 16px;
        }

        .logo-icon {
            font-size: 32px;
            background: linear-gradient(135deg, var(--color-cyan), var(--color-magenta));
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .logo-area h1 {
            font-family: var(--font-heading);
            font-weight: 800;
            font-size: 24px;
            letter-spacing: 1.5px;
            background: linear-gradient(90deg, var(--color-text-main), var(--color-text-muted));
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }

        .logo-area .subtitle {
            font-size: 12px;
            color: var(--color-text-muted);
            font-weight: 500;
            letter-spacing: 0.5px;
        }

        .header-badge {
            display: flex;
            align-items: center;
            gap: 10px;
            background: rgba(16, 185, 129, 0.1);
            border: 1px solid rgba(16, 185, 129, 0.2);
            padding: 6px 14px;
            border-radius: 20px;
        }

        .pulse-dot {
            width: 8px;
            height: 8px;
            background-color: var(--color-success);
            border-radius: 50%;
            box-shadow: 0 0 10px var(--color-success);
            animation: pulse-dot-anim 1.5s infinite;
        }

        @keyframes pulse-dot-anim {
            0% { transform: scale(0.9); opacity: 0.6; }
            50% { transform: scale(1.2); opacity: 1; }
            100% { transform: scale(0.9); opacity: 0.6; }
        }

        .badge-text {
            font-size: 11px;
            font-weight: 600;
            color: var(--color-success);
            letter-spacing: 0.5px;
        }

        .app-main-grid {
            display: grid;
            grid-template-columns: 340px 1fr 340px;
            gap: 24px;
            align-items: start;
        }

        .panel {
            background: var(--gradient-panel);
            backdrop-filter: var(--blur-glass);
            border: 1px solid var(--color-border);
            border-radius: 20px;
            padding: 24px;
            box-shadow: var(--shadow-card);
            display: flex;
            flex-direction: column;
            gap: 20px;
            transition: border-color 0.3s ease, box-shadow 0.3s ease;
        }

        .panel:hover {
            border-color: var(--color-border-hover);
        }

        .panel-header {
            display: flex;
            align-items: center;
            gap: 12px;
            border-bottom: 1px solid var(--color-border);
            padding-bottom: 14px;
        }

        .panel-header i {
            font-size: 18px;
        }

        .panel-header h2 {
            font-family: var(--font-heading);
            font-size: 18px;
            font-weight: 600;
            letter-spacing: 0.5px;
        }

        .text-cyan { color: #0284c7; }
        .text-magenta { color: #db2777; }
        .text-purple { color: var(--color-purple); }

        .control-panel {
            grid-column: 1;
        }

        .input-group {
            display: flex;
            flex-direction: column;
            gap: 12px;
        }

        .input-label-row {
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .input-label-row label {
            font-size: 13px;
            font-weight: 600;
            color: var(--color-text-main);
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .helper-text {
            font-size: 10px;
            color: var(--color-text-muted);
            font-weight: 500;
        }

        .number-input-wrapper {
            display: flex;
            position: relative;
            border-radius: 8px;
            overflow: hidden;
            border: 1px solid var(--color-border);
            background: #ffffff;
            transition: all 0.2s ease;
        }

        .number-input-wrapper:focus-within {
            border-color: #0284c7;
            box-shadow: 0 0 10px var(--color-cyan-glow);
        }

        .custom-number-input {
            width: 100%;
            background: #f8fafc !important;
            border: none !important;
            outline: none !important;
            color: #0f172a !important;
            padding: 10px 14px !important;
            font-family: var(--font-body) !important;
            font-size: 15px !important;
            font-weight: 700 !important;
            text-align: right !important;
            padding-right: 60px !important;
            text-shadow: none !important;
            box-shadow: none !important;
            -webkit-appearance: none !important;
            -moz-appearance: textfield !important;
        }

        .custom-number-input::-webkit-outer-spin-button,
        .custom-number-input::-webkit-inner-spin-button {
            -webkit-appearance: none;
            margin: 0;
        }

        .unit-badge {
            position: absolute;
            right: 1px;
            top: 1px;
            bottom: 1px;
            background: rgba(0, 0, 0, 0.03);
            color: var(--color-text-muted);
            font-size: 11px;
            font-weight: 700;
            padding: 0 14px;
            display: flex;
            align-items: center;
            justify-content: center;
            border-left: 1px solid var(--color-border);
            pointer-events: none;
            letter-spacing: 0.5px;
        }

        .custom-slider {
            -webkit-appearance: none;
            width: 100%;
            height: 6px;
            border-radius: 3px;
            background: rgba(0, 0, 0, 0.05);
            outline: none;
            margin: 8px 0;
            transition: background 0.2s ease;
        }

        .custom-slider::-webkit-slider-thumb {
            -webkit-appearance: none;
            appearance: none;
            width: 18px;
            height: 18px;
            border-radius: 50%;
            cursor: pointer;
            transition: transform 0.1s ease, box-shadow 0.2s ease;
        }

        /* D1 Webkit Thumb */
        #slider-d1::-webkit-slider-thumb {
            background: var(--color-cyan);
            box-shadow: 0 0 10px var(--color-cyan);
        }
        #slider-d1::-webkit-slider-thumb:hover {
            transform: scale(1.2);
            box-shadow: 0 0 15px var(--color-cyan), 0 0 5px #fff;
        }
        /* D1 Firefox Thumb */
        #slider-d1::-moz-range-thumb {
            width: 18px;
            height: 18px;
            border: none;
            border-radius: 50%;
            background: var(--color-cyan);
            box-shadow: 0 0 10px var(--color-cyan);
            cursor: pointer;
            transition: transform 0.1s ease, box-shadow 0.2s ease;
        }
        #slider-d1::-moz-range-thumb:hover {
            transform: scale(1.2);
            box-shadow: 0 0 15px var(--color-cyan), 0 0 5px #fff;
        }

        /* D2 Webkit Thumb */
        #slider-d2::-webkit-slider-thumb {
            background: var(--color-magenta);
            box-shadow: 0 0 10px var(--color-magenta);
        }
        #slider-d2::-webkit-slider-thumb:hover {
            transform: scale(1.2);
            box-shadow: 0 0 15px var(--color-magenta), 0 0 5px #fff;
        }
        /* D2 Firefox Thumb */
        #slider-d2::-moz-range-thumb {
            width: 18px;
            height: 18px;
            border: none;
            border-radius: 50%;
            background: var(--color-magenta);
            box-shadow: 0 0 10px var(--color-magenta);
            cursor: pointer;
            transition: transform 0.1s ease, box-shadow 0.2s ease;
        }
        #slider-d2::-moz-range-thumb:hover {
            transform: scale(1.2);
            box-shadow: 0 0 15px var(--color-magenta), 0 0 5px #fff;
        }

        /* C (Center Distance) Webkit Thumb */
        #slider-c::-webkit-slider-thumb {
            background: #ffffff;
            box-shadow: 0 0 10px var(--color-cyan);
        }
        #slider-c::-webkit-slider-thumb:hover {
            transform: scale(1.2);
            box-shadow: 0 0 15px var(--color-cyan), 0 0 5px #fff;
        }
        /* C (Center Distance) Firefox Thumb */
        #slider-c::-moz-range-thumb {
            width: 18px;
            height: 18px;
            border: none;
            border-radius: 50%;
            background: #ffffff;
            box-shadow: 0 0 10px var(--color-cyan);
            cursor: pointer;
            transition: transform 0.1s ease, box-shadow 0.2s ease;
        }
        #slider-c::-moz-range-thumb:hover {
            transform: scale(1.2);
            box-shadow: 0 0 15px var(--color-cyan), 0 0 5px #fff;
        }

        /* RPM Webkit Thumb */
        #slider-rpm::-webkit-slider-thumb {
            background: var(--color-purple);
            box-shadow: 0 0 10px var(--color-purple);
        }
        #slider-rpm::-webkit-slider-thumb:hover {
            transform: scale(1.2);
            box-shadow: 0 0 15px var(--color-purple), 0 0 5px #fff;
        }
        /* RPM Firefox Thumb */
        #slider-rpm::-moz-range-thumb {
            width: 18px;
            height: 18px;
            border: none;
            border-radius: 50%;
            background: var(--color-purple);
            box-shadow: 0 0 10px var(--color-purple);
            cursor: pointer;
            transition: transform 0.1s ease, box-shadow 0.2s ease;
        }
        #slider-rpm::-moz-range-thumb:hover {
            transform: scale(1.2);
            box-shadow: 0 0 15px var(--color-purple), 0 0 5px #fff;
        }

        .control-actions {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 10px;
            margin-top: 10px;
        }

        .presets-section {
            display: flex;
            flex-direction: column;
            gap: 12px;
            border-top: 1px solid var(--color-border);
            padding-top: 18px;
        }

        .presets-section h3 {
            font-size: 13px;
            font-weight: 700;
            color: var(--color-text-main);
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .presets-grid {
            display: grid;
            grid-template-columns: 1fr;
            gap: 8px;
        }

        .preset-btn {
            background: rgba(0, 0, 0, 0.015);
            border: 1px solid var(--color-border);
            border-radius: 10px;
            padding: 8px 12px;
            cursor: pointer;
            color: var(--color-text-main);
            display: flex;
            align-items: center;
            gap: 12px;
            text-align: left;
            transition: all 0.2s ease;
        }

        .preset-btn:hover {
            background: rgba(0, 0, 0, 0.05);
            border-color: var(--color-border);
            transform: translateX(4px);
        }

        .preset-btn.active {
            background: linear-gradient(90deg, var(--color-cyan-glow), var(--color-magenta-glow));
            border-color: #0284c7;
            box-shadow: 0 0 10px var(--color-cyan-glow);
        }

        .preset-icon {
            width: 28px;
            height: 28px;
            background: rgba(0, 0, 0, 0.02);
            border-radius: 8px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 12px;
            color: #0284c7;
            transition: all 0.2s ease;
        }

        .preset-btn:hover .preset-icon, .preset-btn.active .preset-icon {
            background: var(--gradient-primary);
            color: #fff;
            box-shadow: var(--shadow-neon-cyan);
        }

        .preset-name {
            font-size: 12px;
            font-weight: 600;
            letter-spacing: 0.25px;
        }

        .simulation-panel {
            grid-column: 2;
            align-self: stretch;
            justify-content: space-between;
            min-width: 0;
        }

        .simulation-panel .panel-header {
            justify-content: space-between;
        }

        .canvas-scale-indicator {
            font-size: 11px;
            color: var(--color-text-muted);
            background: rgba(0, 0, 0, 0.02);
            padding: 4px 10px;
            border-radius: 6px;
            border: 1px solid var(--color-border);
        }

        .canvas-wrapper {
            position: relative;
            width: 100%;
            background: radial-gradient(circle at center, #ffffff 0%, #f1f5f9 100%);
            border: 1px solid rgba(0, 0, 0, 0.06);
            border-radius: 16px;
            overflow: hidden;
        }

        #physics-canvas {
            display: block;
            width: 100%;
            height: auto;
            aspect-ratio: 16 / 10;
        }

        .canvas-overlay-data {
            position: absolute;
            top: 16px;
            left: 16px;
            pointer-events: none;
            display: flex;
            flex-direction: column;
            gap: 6px;
        }

        .overlay-item {
            background: transparent;
            color: var(--color-text-main);
            padding: 4px 0;
            display: flex;
            align-items: center;
            gap: 6px;
            font-size: 11px;
        }

        .overlay-item .label {
            color: var(--color-text-muted);
            font-weight: 500;
        }

        .overlay-item .value {
            font-weight: 700;
        }

        .simulation-metrics-strip {
            display: flex;
            background: rgba(0, 0, 0, 0.01);
            border: 1px solid var(--color-border);
            border-radius: 12px;
            padding: 12px 20px;
            justify-content: space-around;
            align-items: center;
            gap: 10px;
            margin-top: 10px;
        }

        .mini-metric {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 4px;
            text-align: center;
        }

        .mini-metric .label {
            font-size: 10px;
            font-weight: 600;
            color: var(--color-text-muted);
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }

        .mini-metric .value {
            font-size: 14px;
            font-weight: 700;
            color: var(--color-text-main);
            font-family: var(--font-heading);
        }

        .mini-divider {
            width: 1px;
            height: 24px;
            background: rgba(0, 0, 0, 0.03);
        }

        .results-panel {
            grid-column: 3;
        }

        .ratio-readout-box {
            background: linear-gradient(135deg, rgba(0, 242, 254, 0.08) 0%, rgba(138, 43, 226, 0.03) 100%);
            border: 1px solid rgba(0, 242, 254, 0.25);
            border-radius: 16px;
            padding: 20px;
            text-align: center;
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 6px;
            box-shadow: 0 0 15px rgba(0, 242, 254, 0.05);
        }

        .ratio-title {
            font-size: 11px;
            font-weight: 700;
            color: #0284c7;
            letter-spacing: 1px;
            text-transform: uppercase;
        }

        .ratio-value {
            font-family: var(--font-heading);
            font-weight: 800;
            font-size: 26px;
            background: linear-gradient(90deg, var(--color-text-main), var(--color-cyan));
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }

        .ratio-type {
            font-size: 11px;
            font-weight: 600;
            color: var(--color-text-muted);
        }

        .results-grid {
            display: flex;
            flex-direction: column;
            gap: 12px;
        }

        .result-card {
            background: rgba(255, 255, 255, 0.85); box-shadow: 0 2px 8px rgba(0,0,0,0.03);
            border: 1px solid var(--color-border);
            border-radius: 12px;
            padding: 12px 16px;
            display: flex;
            align-items: center;
            gap: 16px;
            transition: all 0.2s ease;
        }

        .result-card:hover {
            transform: translateY(-2px);
            border-color: rgba(0, 242, 254, 0.2);
            background: rgba(0, 242, 254, 0.03);
        }

        .card-icon {
            width: 38px;
            height: 38px;
            background: rgba(0, 0, 0, 0.015);
            border: 1px solid var(--color-border);
            border-radius: 10px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 14px;
            color: var(--color-text-muted);
            transition: all 0.2s ease;
        }

        .result-card:hover .card-icon {
            color: #0284c7;
            border-color: rgba(0, 242, 254, 0.3);
            background: rgba(0, 242, 254, 0.1);
        }

        .card-content {
            display: flex;
            flex-direction: column;
            gap: 2px;
        }

        .card-unit {
            font-size: 11px;
            font-weight: 600;
            color: var(--color-text-muted);
        }

        .card-value {
            font-family: var(--font-heading);
            font-weight: 700;
            font-size: 17px;
            color: var(--color-text-main);
        }

        .formula-card {
            background: rgba(0, 0, 0, 0.01);
            border: 1px solid var(--color-border);
            border-radius: 12px;
            padding: 16px;
            display: flex;
            flex-direction: column;
            gap: 8px;
            font-size: 12px;
        }

        .formula-card h4 {
            font-weight: 700;
            color: var(--color-text-main);
        }

        .formula-equation {
            font-family: monospace;
            color: #0284c7;
            font-size: 13px;
            background: #f8fafc; border-color: var(--color-border);
            padding: 6px 10px;
            border-radius: 6px;
            border: 1px solid var(--color-border);
            text-align: center;
        }

        /* Container Query for Responsiveness inside WP */
        @container beltpulley-container (max-width: 1050px) {
            .app-main-grid {
                grid-template-columns: 1fr 1fr;
                gap: 20px;
            }
            .control-panel { grid-column: 1; }
            .simulation-panel { grid-column: 2; }
            .results-panel { grid-column: 1 / span 2; }
        }
        @container beltpulley-container (max-width: 580px) {
            .app-main-grid {
                grid-template-columns: 1fr;
            }
            .control-panel, .simulation-panel, .results-panel {
                grid-column: 1;
            }
            .simulation-panel {
                order: -1;
            }
            .simulation-results-section {
                grid-template-columns: 1fr !important;
            }
        }
    
</style>

<div class="beltpulley-calculator-wrapper" style="position: relative; width: 100%; box-sizing: border-box; overflow: hidden; margin: 30px auto; border-radius: 20px;">
    <div class="app-background-glow" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; overflow: hidden;"></div>
    <div style="position: relative; z-index: 2; width: 100%;">
        
    
    
    <div class="app-container">
        <!-- Header -->
        <header class="app-header">
            <div class="logo-area">
                <div class="logo-icon"><i class="fa-solid fa-circle-nodes"></i></div>
                <div>
                    <h1>BELT DRIVE</h1>
                    <div class="subtitle">벨트-풀리 속도 계산기 &#038; 2D 시뮬레이터</div>
                </div>
            </div>
            <div class="header-badge">
                <div class="pulse-dot"></div>
                <div class="badge-text">2D PHYSICS ENGINE ACTIVE</div>
            </div>
        </header>

        <!-- Main Layout Grid -->
        <main class="app-main-grid">
            <!-- Left Panel: Controls -->
            <section class="panel control-panel">
                <div class="panel-header">
                    <i class="fa-solid fa-sliders text-cyan"></i>
                    <h2>시뮬레이션 제어 변수</h2>
                </div>

                <!-- Pulley 1 Diameter D1 -->
                <div class="input-group">
                    <div class="input-label-row">
                        <label><i class="fa-solid fa-circle text-cyan"></i> 구동 풀리 지름 (D₁)</label>
                        <span class="helper-text">구동측 (50 ~ 600 mm)</span>
                    </div>
                    <div class="number-input-wrapper">
                        <input type="number" id="input-d1" class="custom-number-input" min="50" max="600" value="100">
                        <div class="unit-badge">mm</div>
                    </div>
                    <input type="range" id="slider-d1" class="custom-slider" min="50" max="600" value="100">
                </div>

                <!-- Pulley 2 Diameter D2 -->
                <div class="input-group">
                    <div class="input-label-row">
                        <label><i class="fa-solid fa-circle text-magenta"></i> 피동 풀리 지름 (D₂)</label>
                        <span class="helper-text">피동측 (50 ~ 600 mm)</span>
                    </div>
                    <div class="number-input-wrapper">
                        <input type="number" id="input-d2" class="custom-number-input" min="50" max="600" value="200">
                        <div class="unit-badge">mm</div>
                    </div>
                    <input type="range" id="slider-d2" class="custom-slider" min="50" max="600" value="200">
                </div>

                <!-- Center Distance C -->
                <div class="input-group">
                    <div class="input-label-row">
                        <label><i class="fa-solid fa-arrows-left-right"></i> 축간 중심거리 (C)</label>
                        <span class="helper-text">축간거리 (최소 ~ 1200 mm)</span>
                    </div>
                    <div class="number-input-wrapper">
                        <input type="number" id="input-c" class="custom-number-input" min="200" max="1200" value="450">
                        <div class="unit-badge">mm</div>
                    </div>
                    <input type="range" id="slider-c" class="custom-slider" min="200" max="1200" value="450">
                </div>

                <!-- Input RPM N1 -->
                <div class="input-group">
                    <div class="input-label-row">
                        <label><i class="fa-solid fa-gauge-high text-purple"></i> 입력 회전수 (N₁)</label>
                        <span class="helper-text">입력 속도 (0 ~ 2500 RPM)</span>
                    </div>
                    <div class="number-input-wrapper">
                        <input type="number" id="input-rpm" class="custom-number-input" min="0" max="2500" value="240">
                        <div class="unit-badge">RPM</div>
                    </div>
                    <input type="range" id="slider-rpm" class="custom-slider" min="0" max="2500" value="240">
                </div>

                <!-- Simulation Controls -->
                <div class="control-actions">
                    <button id="btn-play-pause" class="action-btn">
                        <i class="fa-solid fa-pause"></i> <span>일시정지</span>
                    </button>
                    <button id="btn-reverse" class="action-btn">
                        <i class="fa-solid fa-backward"></i> <span>방향 전환</span>
                    </button>
                </div>

                <!-- Presets -->
                <div class="presets-section">
                    <h3><i class="fa-solid fa-tags text-cyan"></i> 벨트-풀리 프리셋</h3>
                    <div class="presets-grid">
                        <button class="preset-btn" data-preset="reduction">
                            <div class="preset-icon"><i class="fa-solid fa-down-long"></i></div>
                            <div class="preset-name">2:1 V벨트 감속 시스템</div>
                        </button>
                        <button class="preset-btn" data-preset="mesh1to1">
                            <div class="preset-icon"><i class="fa-solid fa-left-right"></i></div>
                            <div class="preset-name">1:1 등속 동력 전달</div>
                        </button>
                        <button class="preset-btn" data-preset="increase">
                            <div class="preset-icon"><i class="fa-solid fa-up-long"></i></div>
                            <div class="preset-name">1:3 기계식 증속 전동</div>
                        </button>
                    </div>
                </div>
            </section>

            <!-- Right Panel: Physics Simulator & Integrated Results -->
            <section class="panel simulation-panel">
                <div class="panel-header">
                    <div style="display: flex; align-items: center; gap: 12px;">
                        <i class="fa-solid fa-circle-nodes text-cyan"></i>
                        <h2>실시간 벨트 주행 물리 뷰</h2>
                    </div>
                    <div id="txt-canvas-scale" class="canvas-scale-indicator">스케일 자동 조정</div>
                </div>

                <div class="canvas-wrapper">
                    <canvas id="physics-canvas"></canvas>
                    
                    <!-- Overlay Floating Data -->
                    <div class="canvas-overlay-data">
                        <div class="overlay-item">
                            <span class="label">총 벨트 길이 (L):</span>
                            <span class="value text-cyan" id="txt-belt-length-overlay">0.0 mm</span>
                        </div>
                    </div>
                </div>

                <!-- Mini Metrics -->
                <div class="simulation-metrics-strip">
                    <div class="mini-metric">
                        <div class="label">구동 풀리 접촉각 (θ₁)</div>
                        <div id="txt-lap-angle1" class="value">0.0°</div>
                    </div>
                    <div class="mini-divider"></div>
                    <div class="mini-metric">
                        <div class="label">피동 풀리 접촉각 (θ₂)</div>
                        <div id="txt-lap-angle2" class="value">0.0°</div>
                    </div>
                </div>

                <!-- Integrated Results Analysis Section -->
                <div class="simulation-results-section" style="display: grid; grid-template-columns: 1fr 1.2fr; gap: 20px; border-top: 1px solid var(--color-border); padding-top: 20px; margin-top: 10px;">
                    <!-- Left Column: Primary Readout & Formula -->
                    <div style="display: flex; flex-direction: column; gap: 16px;">
                        <div class="ratio-readout-box" style="padding: 16px; min-height: 100px;">
                            <div class="ratio-title">최종 풀리 배율 (i)</div>
                            <div id="txt-pulley-ratio" class="ratio-value" style="font-size: 24px;">2.00 : 1</div>
                            <div id="txt-ratio-type" class="ratio-type" style="font-size: 11px;">2.00배 감속 전동</div>
                        </div>
                        <div class="formula-card" style="padding: 12px; font-size: 11px; background: rgba(0, 0, 0, 0.01);">
                            <h4 style="font-size: 12px; margin-bottom: 6px;"><i class="fa-solid fa-circle-info text-cyan"></i> 벨트 전동 공학식</h4>
                            <div class="formula-equation" style="font-size: 11px; padding: 6px; margin-bottom: 6px;">
                                L ≈ 2C + π(D₁+D₂)/2 + (D₂-D₁)²/4C
                            </div>
                            <div class="formula-equation" style="font-size: 11px; padding: 6px;">
                                θ₁ = π &#8211; 2·sin⁻¹((D₂-D₁)/2C)
                            </div>
                        </div>
                    </div>

                    <!-- Right Column: Quantitative Results Cards -->
                    <div class="results-grid" style="display: flex; flex-direction: column; gap: 10px; justify-content: center;">
                        <!-- Belt Pitch Speed -->
                        <div class="result-card" style="padding: 10px 14px; gap: 12px;">
                            <div class="card-icon" style="width: 32px; height: 32px; font-size: 12px;"><i class="fa-solid fa-gauge-simple text-cyan"></i></div>
                            <div class="card-content">
                                <span class="card-unit" style="font-size: 10px;">벨트 주속도 (v)</span>
                                <span id="txt-belt-speed" class="card-value" style="font-size: 15px;">0.0 m/s</span>
                            </div>
                        </div>

                        <!-- Pulley 2 Output Speed -->
                        <div class="result-card" style="padding: 10px 14px; gap: 12px;">
                            <div class="card-icon" style="width: 32px; height: 32px; font-size: 12px;"><i class="fa-solid fa-rotate text-magenta"></i></div>
                            <div class="card-content">
                                <span class="card-unit" style="font-size: 10px;">피동 풀리 회전수 (N₂)</span>
                                <span id="txt-n2" class="card-value" style="font-size: 15px;">0.0 RPM</span>
                            </div>
                        </div>

                        <!-- Calculated Belt Length -->
                        <div class="result-card" style="padding: 10px 14px; gap: 12px;">
                            <div class="card-icon" style="width: 32px; height: 32px; font-size: 12px;"><i class="fa-solid fa-ruler-horizontal text-purple"></i></div>
                            <div class="card-content">
                                <span class="card-unit" style="font-size: 10px;">계산된 표준 벨트 길이 (L)</span>
                                <span id="txt-belt-length" class="card-value" style="font-size: 15px;">0.0 mm</span>
                            </div>
                        </div>

                        <!-- Torque Factor -->
                        <div class="result-card" style="padding: 10px 14px; gap: 12px;">
                            <div class="card-icon" style="width: 32px; height: 32px; font-size: 12px;"><i class="fa-solid fa-bolt text-success"></i></div>
                            <div class="card-content">
                                <span class="card-unit" style="font-size: 10px;">출력 토크 변환율 (T₂/T₁)</span>
                                <span id="txt-torque-mult" class="card-value" style="font-size: 15px;">100%</span>
                            </div>
                        </div>
                    </div>
                </div>
            </section>
        </main>
    </div>

    <!-- Physics & Animation Engine -->
    <script>
        (function() {
        let isBeltInit = false;

        function initBeltSimulator() {
            if (isBeltInit) return;

            const canvas = document.getElementById('physics-canvas');
            if (!canvas) return;
            const ctx = canvas.getContext('2d');
            if (!ctx) return;

            isBeltInit = true;
            window.__beltpulley_initialized = true;

            // DOM Elements
            const sliderD1 = document.getElementById('slider-d1');
            const inputD1 = document.getElementById('input-d1');
            const sliderD2 = document.getElementById('slider-d2');
            const inputD2 = document.getElementById('input-d2');
            const sliderC = document.getElementById('slider-c');
            const inputC = document.getElementById('input-c');
            const sliderRpm = document.getElementById('slider-rpm');
            const inputRpm = document.getElementById('input-rpm');
            
            const btnPlayPause = document.getElementById('btn-play-pause');
            const btnReverse = document.getElementById('btn-reverse');
            const presetButtons = document.querySelectorAll('.preset-btn');
            
            const txtCenterDistOverlay = document.getElementById('txt-belt-length-overlay');
            const txtLapAngle1 = document.getElementById('txt-lap-angle1');
            const txtLapAngle2 = document.getElementById('txt-lap-angle2');
            
            const txtPulleyRatio = document.getElementById('txt-pulley-ratio');
            const txtRatioType = document.getElementById('txt-ratio-type');
            
            const txtBeltSpeed = document.getElementById('txt-belt-speed');
            const txtN2 = document.getElementById('txt-n2');
            const txtBeltLength = document.getElementById('txt-belt-length');
            const txtTorqueMult = document.getElementById('txt-torque-mult');
            
            const wrapper = document.querySelector('.beltpulley-calculator-wrapper') || document.body;

            // State variables
            const state = {
                d1: 100,
                d2: 200,
                c: 450,
                rpm: 240,
                angle1: 0,
                angle2: 0,
                beltOffset: 0,
                isPlaying: true,
                direction: 1,
                lastTime: 0,
                sparks: []
            };

            // Canvas Resizing (Retina support)
            function resizeCanvas() {
                const rect = canvas.getBoundingClientRect();
                const dpr = window.devicePixelRatio || 1;
                canvas.width = rect.width * dpr;
                canvas.height = rect.height * dpr;
                ctx.scale(dpr, dpr);
            }

            resizeCanvas();
            window.addEventListener('resize', resizeCanvas);
            setTimeout(resizeCanvas, 300);

            // Friction sparks at tangent points
            class BeltSpark {
                constructor(x, y, vx, vy) {
                    this.x = x;
                    this.y = y;
                    this.vx = vx;
                    this.vy = vy;
                    this.size = Math.random() * 1.5 + 0.5;
                    this.life = Math.random() * 20 + 8;
                    this.maxLife = this.life;
                    const colors = ['#00f2fe', '#ff007f', '#ffffff'];
                    this.color = colors[Math.floor(Math.random() * colors.length)];
                }
                update() {
                    this.x += this.vx;
                    this.y += this.vy;
                    this.vx *= 0.96;
                    this.vy *= 0.96;
                    this.life--;
                }
                draw(c) {
                    const alpha = this.life / this.maxLife;
                    c.save();
                    c.globalAlpha = alpha;
                    c.beginPath();
                    c.arc(this.x, this.y, this.size, 0, Math.PI * 2);
                    c.fillStyle = this.color;
                    c.fill();
                    c.restore();
                }
            }

            // Sync Inputs and Sliders
            function syncD1FromSlider() {
                const val = parseInt(sliderD1.value);
                inputD1.value = val;
                state.d1 = val;
                validateCenterDistance();
                clearPresets();
                updateCalculations();
            }
            function syncD1FromInput() {
                let val = parseInt(inputD1.value);
                if (isNaN(val) || val < 50) val = 50;
                if (val > 600) val = 600;
                sliderD1.value = val;
                state.d1 = val;
                validateCenterDistance();
                clearPresets();
                updateCalculations();
            }

            function syncD2FromSlider() {
                const val = parseInt(sliderD2.value);
                inputD2.value = val;
                state.d2 = val;
                validateCenterDistance();
                clearPresets();
                updateCalculations();
            }
            function syncD2FromInput() {
                let val = parseInt(inputD2.value);
                if (isNaN(val) || val < 50) val = 50;
                if (val > 600) val = 600;
                sliderD2.value = val;
                state.d2 = val;
                validateCenterDistance();
                clearPresets();
                updateCalculations();
            }

            function syncCFromSlider() {
                const val = parseInt(sliderC.value);
                inputC.value = val;
                state.c = val;
                validateCenterDistance();
                clearPresets();
                updateCalculations();
            }
            function syncCFromInput() {
                let val = parseInt(inputC.value);
                if (isNaN(val) || val < 200) val = 200;
                if (val > 1200) val = 1200;
                sliderC.value = val;
                state.c = val;
                validateCenterDistance();
                clearPresets();
                updateCalculations();
            }

            function syncRpmFromSlider() {
                const val = parseInt(sliderRpm.value);
                inputRpm.value = val;
                state.rpm = val;
                clearPresets();
                updateCalculations();
            }
            function syncRpmFromInput() {
                let val = parseInt(inputRpm.value);
                if (isNaN(val) || val < 0) val = 0;
                if (val > 2500) val = 2500;
                sliderRpm.value = val;
                state.rpm = val;
                clearPresets();
                updateCalculations();
            }

            // Ensure center distance is physically larger than radius1 + radius2 to avoid overlap
            function validateCenterDistance() {
                const minC = (state.d1 + state.d2) / 2 + 30; // 30mm margin
                if (state.c < minC) {
                    state.c = Math.ceil(minC);
                    inputC.value = state.c;
                    sliderC.value = state.c;
                }
                
                // Set slider range dynamically
                sliderC.min = Math.ceil(minC);
                if (parseInt(inputC.value) < minC) {
                    inputC.value = Math.ceil(minC);
                    sliderC.value = Math.ceil(minC);
                    state.c = Math.ceil(minC);
                }
            }

            // Physics Core Calculations
            function updateCalculations() {
                const D1 = state.d1;
                const D2 = state.d2;
                const C = state.c;
                const N1 = state.rpm;

                // 1. Pulley Ratio
                const ratio = D2 / D1;
                txtPulleyRatio.innerText = ratio.toFixed(2) + ' : 1';
                if (ratio > 1) {
                    txtRatioType.innerText = `${ratio.toFixed(2)}배 감속 전동 (출력 토크 ${ratio.toFixed(2)}배)`;
                } else if (ratio < 1) {
                    const gain = 1 / ratio;
                    txtRatioType.innerText = `${gain.toFixed(2)}배 증속 전동 (출력 토크 ${ratio.toFixed(2)}배)`;
                } else {
                    txtRatioType.innerText = '1:1 동속 동력 전동';
                }

                // 2. Output RPM
                const n2 = N1 / ratio;
                txtN2.innerText = n2.toFixed(1) + ' RPM';
                txtTorqueMult.innerText = (ratio * 100).toFixed(0) + '%';

                // 3. Pitch line velocity (Belt Speed in m/s)
                const pitchSpeed = (Math.PI * D1 * N1) / 60000;
                txtBeltSpeed.innerText = pitchSpeed.toFixed(2) + ' m/s';

                // 4. Belt Length calculation
                const term1 = 2 * C;
                const term2 = (Math.PI * (D1 + D2)) / 2;
                const term3 = Math.pow(D2 - D1, 2) / (4 * C);
                const beltLen = term1 + term2 + term3;
                txtBeltLength.innerText = beltLen.toFixed(1) + ' mm';
                txtCenterDistOverlay.innerText = beltLen.toFixed(1) + ' mm';

                // 5. Lap angles
                // sin(alpha) = (D2 - D1) / 2C
                const sinAlpha = (D2 - D1) / (2 * C);
                const alpha = Math.asin(Math.min(Math.max(sinAlpha, -0.99), 0.99));
                
                const angle1Rad = Math.PI - 2 * alpha;
                const angle2Rad = Math.PI + 2 * alpha;

                const angle1Deg = (angle1Rad * 180) / Math.PI;
                const angle2Deg = (angle2Rad * 180) / Math.PI;

                txtLapAngle1.innerText = angle1Deg.toFixed(1) + '°';
                txtLapAngle2.innerText = angle2Deg.toFixed(1) + '°';

                if (angle1Deg < 120) {
                    txtLapAngle1.style.color = '#db2777';
                } else {
                    txtLapAngle1.style.color = '#10b981';
                }
            }

            // Presets Click Handler
            function loadPreset(key) {
                clearPresets();
                presetButtons.forEach(btn => {
                    if (btn.dataset.preset === key) {
                        btn.classList.add('active');
                    }
                });

                if (key === 'reduction') {
                    state.d1 = 120;
                    state.d2 = 240;
                    state.c = 480;
                    state.rpm = 300;
                } else if (key === 'mesh1to1') {
                    state.d1 = 180;
                    state.d2 = 180;
                    state.c = 450;
                    state.rpm = 200;
                } else if (key === 'increase') {
                    state.d1 = 240;
                    state.d2 = 80;
                    state.c = 450;
                    state.rpm = 100;
                }

                // Sync inputs
                inputD1.value = state.d1;
                sliderD1.value = state.d1;
                inputD2.value = state.d2;
                sliderD2.value = state.d2;
                inputC.value = state.c;
                sliderC.value = state.c;
                inputRpm.value = state.rpm;
                sliderRpm.value = state.rpm;

                validateCenterDistance();
                updateCalculations();
            }

            function clearPresets() {
                presetButtons.forEach(btn => btn.classList.remove('active'));
            }

            // Draw Pulley Wheel
            function drawPulley(c, x, y, radius, angle, themeColor) {
                c.save();
                c.translate(x, y);

                // 1. Pulley Outer Body (dark glass fill)
                c.beginPath();
                c.arc(0, 0, radius, 0, Math.PI * 2);
                c.fillStyle = 'rgba(241, 245, 249, 0.85)';
                c.fill();
                c.strokeStyle = themeColor;
                c.lineWidth = 3;
                c.shadowBlur = 8;
                c.shadowColor = themeColor;
                c.stroke();
                c.shadowBlur = 0;

                // 2. Pulley Inner Rim Ring
                c.beginPath();
                c.arc(0, 0, radius - 6, 0, Math.PI * 2);
                c.strokeStyle = 'rgba(15, 23, 42, 0.06)';
                c.lineWidth = 1.5;
                c.stroke();

                // 3. Rotating Spokes
                c.save();
                c.rotate(angle);
                
                // Central hub
                const hubRad = Math.max(radius * 0.25, 12);
                c.beginPath();
                c.arc(0, 0, hubRad, 0, Math.PI * 2);
                c.strokeStyle = 'rgba(15, 23, 42, 0.15)';
                c.lineWidth = 2;
                c.stroke();

                c.beginPath();
                c.arc(0, 0, 5, 0, Math.PI * 2);
                c.fillStyle = '#ffffff';
                c.fill();

                // 5 spokes
                const spokes = 5;
                for (let s = 0; s < spokes; s++) {
                    const spokeAngle = (s * 2 * Math.PI) / spokes;
                    c.save();
                    c.rotate(spokeAngle);
                    c.beginPath();
                    c.moveTo(0, hubRad);
                    c.lineTo(0, radius - 8);
                    c.strokeStyle = 'rgba(15, 23, 42, 0.08)';
                    c.lineWidth = 3;
                    c.stroke();
                    c.restore();
                }
                
                // Keyway marker (to see rotation clearly)
                c.beginPath();
                c.arc(radius - 12, 0, 4, 0, Math.PI * 2);
                c.fillStyle = '#ffffff';
                c.shadowBlur = 4;
                c.shadowColor = '#ffffff';
                c.fill();

                c.restore();
                c.restore();
            }

            function drawDiameterLeader(c, x, y, r, valStr, labelPrefix, side) {
                c.save();
                c.strokeStyle = 'rgba(71, 85, 105, 0.4)';
                c.lineWidth = 1.2;
                
                // Angle of leader line (45 degrees up-left or up-right)
                const angle = side === -1 ? -Math.PI * 0.75 : -Math.PI * 0.25;
                const px = x + Math.cos(angle) * r;
                const py = y + Math.sin(angle) * r;
                
                // Draw a beautiful small dot on the circle boundary
                c.beginPath();
                c.arc(px, py, 2.5, 0, Math.PI * 2);
                c.fillStyle = side === -1 ? '#0284c7' : '#db2777';
                c.fill();
                
                // Draw leader lines: from circle boundary diagonal outwards, then horizontal
                const lx1 = px + Math.cos(angle) * 20;
                const ly1 = py + Math.sin(angle) * 20;
                const lx2 = lx1 + side * 45;
                const ly2 = ly1;
                
                c.beginPath();
                c.moveTo(px, py);
                c.lineTo(lx1, ly1);
                c.lineTo(lx2, ly2);
                c.stroke();
                
                // Draw text above the horizontal line
                const text = `${labelPrefix} ${valStr}`;
                c.font = 'bold 10px var(--font-body)';
                
                const tx = side === -1 ? lx1 - 4 : lx1 + 4;
                c.fillStyle = '#1e293b';
                c.textAlign = side === -1 ? 'right' : 'left';
                c.textBaseline = 'bottom';
                c.fillText(text, tx, ly2 - 1);
                c.restore();
            }

            // Main Animation Frame
            function animate(currentTime) {
                requestAnimationFrame(animate);

                let dt = (currentTime - state.lastTime) / 1000;
                if (isNaN(dt) || dt < 0 || dt > 1.0) dt = 0.016;
                state.lastTime = currentTime;

                const width = canvas.width / (window.devicePixelRatio || 1);
                const height = canvas.height / (window.devicePixelRatio || 1);
                const cx = width / 2;
                const cy = height / 2;

                ctx.clearRect(0, 0, width, height);

                // 1. Draw Grid
                ctx.save();
                ctx.strokeStyle = 'rgba(2, 132, 199, 0.06)';
                ctx.lineWidth = 1;
                for (let x = 0; x < width; x += 30) {
                    ctx.beginPath();
                    ctx.moveTo(x, 0);
                    ctx.lineTo(x, height);
                    ctx.stroke();
                }
                for (let y = 0; y < height; y += 30) {
                    ctx.beginPath();
                    ctx.moveTo(0, y);
                    ctx.lineTo(width, y);
                    ctx.stroke();
                }
                ctx.restore();

                // 2. Geometry Scale Calculation
                const phys_r1 = state.d1 / 2;
                const phys_r2 = state.d2 / 2;

                const scaleWidth = (width - 190) / (state.c + phys_r1 + phys_r2);
                const scaleHeight = (height - 120) / Math.max(state.d1, state.d2);
                const scale = Math.min(scaleWidth, scaleHeight);
                
                const txtCanvasScale = document.getElementById('txt-canvas-scale');
                if (txtCanvasScale) {
                    txtCanvasScale.innerText = `스케일 비율: 1:${(1/scale * 100).toFixed(0)}급`;
                }

                const r1 = phys_r1 * scale;
                const r2 = phys_r2 * scale;
                const c_vis = state.c * scale;

                // Position centers to perfectly center the entire bounding box
                const x1 = (width - c_vis + r1 - r2) / 2;
                const y1 = cy;
                const x2 = x1 + c_vis;
                const y2 = cy;

                // Physics Rotation
                const ratio = state.d2 / state.d1;
                const pitchSpeed = (Math.PI * state.d1 * state.rpm) / 60000; // m/s

                if (state.isPlaying) {
                    if (state.rpm > 0) {
                        const omega1 = (2 * Math.PI * state.rpm) / 60; // rad/s
                        
                        // Stroboscopic rendering limit
                        const maxOmega = 6;
                        const renderOmega1 = maxOmega * (1 - Math.exp(-omega1 / maxOmega));
                        
                        state.angle1 += state.direction * renderOmega1 * dt;
                        state.angle2 += state.direction * (renderOmega1 / ratio) * dt;

                        // Linear belt move offset
                        state.beltOffset += state.direction * pitchSpeed * 1000 * scale * dt;
                    }
                }

                // 3. Tangent belt geometry calculations
                const dx = x2 - x1;
                const alpha = Math.asin((r2 - r1) / dx);

                // Tangent angles relative to horizontal
                const theta_top1 = -Math.PI / 2 - alpha;
                const theta_bottom1 = Math.PI / 2 + alpha;
                
                const theta_top2 = -Math.PI / 2 - alpha;
                const theta_bottom2 = Math.PI / 2 + alpha;

                // Tangent Points on Pulley 1
                const tp1_top_x = x1 + r1 * Math.cos(theta_top1);
                const tp1_top_y = y1 + r1 * Math.sin(theta_top1);
                const tp1_bottom_x = x1 + r1 * Math.cos(theta_bottom1);
                const tp1_bottom_y = y1 + r1 * Math.sin(theta_bottom1);

                // Tangent Points on Pulley 2
                const tp2_top_x = x2 + r2 * Math.cos(theta_top2);
                const tp2_top_y = y2 + r2 * Math.sin(theta_top2);
                const tp2_bottom_x = x2 + r2 * Math.cos(theta_bottom2);
                const tp2_bottom_y = y2 + r2 * Math.sin(theta_bottom2);

                // 4. Spawn Friction Sparks at Tangent Points
                if (state.isPlaying) {
                    if (state.rpm > 300) {
                        const sparkEmission = Math.min(Math.floor(state.rpm / 600) + 1, 2);
                        for (let s = 0; s < sparkEmission; s++) {
                            if (Math.random() > 0.6) {
                                // Top tangent left sparks
                                const spVx = -state.direction * (2 + Math.random() * 2);
                                const spVy = (Math.random() - 0.5) * 1.5;
                                state.sparks.push(new BeltSpark(tp1_top_x, tp1_top_y, spVx * scale, spVy * scale));
                            }
                        }
                    }
                }

                // Render Sparks
                state.sparks = state.sparks.filter(s => s.life > 0);
                state.sparks.forEach(s => {
                    s.update();
                    s.draw(ctx);
                });

                // 5. Draw Pulley 1 (Drive - Cyan)
                drawPulley(ctx, x1, y1, r1, state.angle1, '#0284c7');

                // Draw Pulley 2 (Driven - Magenta)
                drawPulley(ctx, x2, y2, r2, state.angle2, '#db2777');

                // 6. Draw Snug Belt Around both pulleys (Moving dashed line)
                ctx.save();
                ctx.beginPath();
                
                // Top tangent line
                ctx.moveTo(tp1_top_x, tp1_top_y);
                ctx.lineTo(tp2_top_x, tp2_top_y);
                
                // Clockwise arc around Pulley 2
                ctx.arc(x2, y2, r2, theta_top2, theta_bottom2, false);
                
                // Bottom tangent line
                ctx.lineTo(tp1_bottom_x, tp1_bottom_y);
                
                // Clockwise arc around Pulley 1
                ctx.arc(x1, y1, r1, theta_bottom1, theta_top1, false);

                ctx.closePath();

                // Belt outer shadow/glow
                ctx.shadowBlur = 12;
                ctx.shadowColor = '#0284c7';
                ctx.strokeStyle = '#0284c7';
                ctx.lineWidth = 7;
                ctx.lineCap = 'round';
                ctx.lineJoin = 'round';
                ctx.stroke();
                ctx.shadowBlur = 0;

                // Moving belt textured interior dashes
                ctx.strokeStyle = '#ffffff';
                ctx.lineWidth = 5;
                ctx.setLineDash([16, 12]);
                ctx.lineDashOffset = -state.beltOffset;
                ctx.stroke();
                ctx.setLineDash([]);
                ctx.restore();

                // 7. Dimension annotations
                ctx.save();
                
                // Draw Diameter dimensions on pulleys with professional CAD leader lines
                drawDiameterLeader(ctx, x1, y1, r1, state.d1.toFixed(1) + ' mm', 'd₁ =', -1);
                drawDiameterLeader(ctx, x2, y2, r2, state.d2.toFixed(1) + ' mm', 'd₂ =', 1);

                // Horizontal Center Distance Dimension Line (C)
                ctx.save();
                ctx.strokeStyle = 'rgba(71, 85, 105, 0.3)';
                ctx.lineWidth = 1;
                
                const dimY = y1 + r2 + 25; // 25px below larger pulley
                
                ctx.beginPath();
                ctx.moveTo(x1, y1 + 10);
                ctx.lineTo(x1, dimY + 5);
                ctx.moveTo(x2, y2 + 10);
                ctx.lineTo(x2, dimY + 5);
                ctx.stroke();
                
                ctx.beginPath();
                ctx.moveTo(x1, dimY);
                ctx.lineTo(x2, dimY);
                ctx.stroke();
                
                function drawHorizontalArrowhead(px, py, direction) {
                    ctx.save();
                    ctx.translate(px, py);
                    ctx.fillStyle = 'rgba(71, 85, 105, 0.5)';
                    ctx.beginPath();
                    ctx.moveTo(0, 0);
                    ctx.lineTo(direction * 6, -3);
                    ctx.lineTo(direction * 6, 3);
                    ctx.closePath();
                    ctx.fill();
                    ctx.restore();
                }
                drawHorizontalArrowhead(x1, dimY, 1);
                drawHorizontalArrowhead(x2, dimY, -1);
                
                const cText = `C = ${state.c} mm`;
                ctx.font = 'bold 9px Inter, sans-serif';
                const cTextWidth = ctx.measureText(cText).width;
                const cTextX = (x1 + x2) / 2;
                // Draw white eraser background so text doesn't overlap dimension line
                ctx.fillStyle = '#f1f5f9';
                ctx.fillRect(cTextX - cTextWidth / 2 - 4, dimY - 6, cTextWidth + 8, 12);
                ctx.fillStyle = '#475569';
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                ctx.fillText(cText, cTextX, dimY);
                ctx.restore();

                // Lap angle highlights (colored arcs representing wrap contact)
                ctx.beginPath();
                ctx.arc(x1, y1, r1 + 10, theta_bottom1, theta_top1, false);
                ctx.strokeStyle = 'rgba(0, 242, 254, 0.35)';
                ctx.lineWidth = 3;
                ctx.stroke();

                ctx.beginPath();
                ctx.arc(x2, y2, r2 + 10, theta_top2, theta_bottom2, false);
                ctx.strokeStyle = 'rgba(255, 0, 127, 0.35)';
                ctx.lineWidth = 3;
                ctx.stroke();
                ctx.restore();
            }

            // Bind Event Listeners
            sliderD1.addEventListener('input', syncD1FromSlider);
            inputD1.addEventListener('change', syncD1FromInput);

            sliderD2.addEventListener('input', syncD2FromSlider);
            inputD2.addEventListener('change', syncD2FromInput);

            sliderC.addEventListener('input', syncCFromSlider);
            inputC.addEventListener('change', syncCFromInput);

            sliderRpm.addEventListener('input', syncRpmFromSlider);
            inputRpm.addEventListener('change', syncRpmFromInput);

            btnPlayPause.addEventListener('click', () => {
                state.isPlaying = !state.isPlaying;
                if (state.isPlaying) {
                    btnPlayPause.innerHTML = '<i class="fa-solid fa-pause"></i> <span>일시정지</span>';
                } else {
                    btnPlayPause.innerHTML = '<i class="fa-solid fa-play"></i> <span>재생</span>';
                }
            });

            btnReverse.addEventListener('click', () => {
                state.direction *= -1;
            });

            presetButtons.forEach(btn => {
                btn.addEventListener('click', () => {
                    loadPreset(btn.dataset.preset);
                });
            });

            // Initialize
            loadPreset('reduction');
            requestAnimationFrame(animate);

            // ── 우클릭 & 소스 복사 방지 (전체 문서 레벨 + 알림창 동기화) ──
            document.addEventListener('contextmenu', function(e) {
                e.preventDefault();
                alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                return false;
            }, { capture: true });

            document.addEventListener('selectstart', function(e) {
                e.preventDefault();
                return false;
            }, { capture: true });

            // 개발자도구 단축키 및 복사 단축키 차단 (F12, Ctrl+U, Ctrl+Shift+I/J/C, Ctrl+C, Ctrl+S)
            // 중복 차단 문자를 사용하면 WordPress API에서 오류가 날 수 있으므로 nested if로 구현
            document.addEventListener('keydown', function(e) {
                if (e.key === 'F12') {
                    e.preventDefault();
                    alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                    return false;
                }
                if (e.ctrlKey) {
                    if (e.key === 'u' || e.key === 'c' || e.key === 's' || e.key === 'U' || e.key === 'C' || e.key === 'S') {
                        e.preventDefault();
                        alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                        return false;
                    }
                    if (e.shiftKey) {
                        if (['i','I','j','J','c','C'].includes(e.key)) {
                            e.preventDefault();
                            alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                            return false;
                        }
                    }
                }
            }, { capture: true });
        } // <-- Closed the initBeltSimulator function

        // Robust ReadyState hook-up with polling to guarantee execution even if DOM elements load late
        let initAttempts = 0;
        function tryInit() {
            initAttempts++;
            initBeltSimulator();
            if (!window.__beltpulley_initialized) {
                if (initAttempts < 50) {
                    setTimeout(tryInit, 100);
                }
            }
        }

        if (document.readyState === 'complete' || document.readyState === 'interactive') {
            tryInit();
        } else {
            document.addEventListener('DOMContentLoaded', tryInit);
            window.addEventListener('load', tryInit);
        }
        })();
    </script>

    </div>
</div>

<script>
        (function() {
        let isBeltInit = false;

        function initBeltSimulator() {
            if (isBeltInit) return;

            const canvas = document.getElementById('physics-canvas');
            if (!canvas) return;
            const ctx = canvas.getContext('2d');
            if (!ctx) return;

            isBeltInit = true;
            window.__beltpulley_initialized = true;

            // DOM Elements
            const sliderD1 = document.getElementById('slider-d1');
            const inputD1 = document.getElementById('input-d1');
            const sliderD2 = document.getElementById('slider-d2');
            const inputD2 = document.getElementById('input-d2');
            const sliderC = document.getElementById('slider-c');
            const inputC = document.getElementById('input-c');
            const sliderRpm = document.getElementById('slider-rpm');
            const inputRpm = document.getElementById('input-rpm');
            
            const btnPlayPause = document.getElementById('btn-play-pause');
            const btnReverse = document.getElementById('btn-reverse');
            const presetButtons = document.querySelectorAll('.preset-btn');
            
            const txtCenterDistOverlay = document.getElementById('txt-belt-length-overlay');
            const txtLapAngle1 = document.getElementById('txt-lap-angle1');
            const txtLapAngle2 = document.getElementById('txt-lap-angle2');
            
            const txtPulleyRatio = document.getElementById('txt-pulley-ratio');
            const txtRatioType = document.getElementById('txt-ratio-type');
            
            const txtBeltSpeed = document.getElementById('txt-belt-speed');
            const txtN2 = document.getElementById('txt-n2');
            const txtBeltLength = document.getElementById('txt-belt-length');
            const txtTorqueMult = document.getElementById('txt-torque-mult');
            
            const wrapper = document.querySelector('.beltpulley-calculator-wrapper') || document.body;

            // State variables
            const state = {
                d1: 100,
                d2: 200,
                c: 450,
                rpm: 240,
                angle1: 0,
                angle2: 0,
                beltOffset: 0,
                isPlaying: true,
                direction: 1,
                lastTime: 0,
                sparks: []
            };

            // Canvas Resizing (Retina support)
            function resizeCanvas() {
                const rect = canvas.getBoundingClientRect();
                const dpr = window.devicePixelRatio || 1;
                canvas.width = rect.width * dpr;
                canvas.height = rect.height * dpr;
                ctx.scale(dpr, dpr);
            }

            resizeCanvas();
            window.addEventListener('resize', resizeCanvas);
            setTimeout(resizeCanvas, 300);

            // Friction sparks at tangent points
            class BeltSpark {
                constructor(x, y, vx, vy) {
                    this.x = x;
                    this.y = y;
                    this.vx = vx;
                    this.vy = vy;
                    this.size = Math.random() * 1.5 + 0.5;
                    this.life = Math.random() * 20 + 8;
                    this.maxLife = this.life;
                    const colors = ['#00f2fe', '#ff007f', '#ffffff'];
                    this.color = colors[Math.floor(Math.random() * colors.length)];
                }
                update() {
                    this.x += this.vx;
                    this.y += this.vy;
                    this.vx *= 0.96;
                    this.vy *= 0.96;
                    this.life--;
                }
                draw(c) {
                    const alpha = this.life / this.maxLife;
                    c.save();
                    c.globalAlpha = alpha;
                    c.beginPath();
                    c.arc(this.x, this.y, this.size, 0, Math.PI * 2);
                    c.fillStyle = this.color;
                    c.fill();
                    c.restore();
                }
            }

            // Sync Inputs and Sliders
            function syncD1FromSlider() {
                const val = parseInt(sliderD1.value);
                inputD1.value = val;
                state.d1 = val;
                validateCenterDistance();
                clearPresets();
                updateCalculations();
            }
            function syncD1FromInput() {
                let val = parseInt(inputD1.value);
                if (isNaN(val) || val < 50) val = 50;
                if (val > 600) val = 600;
                sliderD1.value = val;
                state.d1 = val;
                validateCenterDistance();
                clearPresets();
                updateCalculations();
            }

            function syncD2FromSlider() {
                const val = parseInt(sliderD2.value);
                inputD2.value = val;
                state.d2 = val;
                validateCenterDistance();
                clearPresets();
                updateCalculations();
            }
            function syncD2FromInput() {
                let val = parseInt(inputD2.value);
                if (isNaN(val) || val < 50) val = 50;
                if (val > 600) val = 600;
                sliderD2.value = val;
                state.d2 = val;
                validateCenterDistance();
                clearPresets();
                updateCalculations();
            }

            function syncCFromSlider() {
                const val = parseInt(sliderC.value);
                inputC.value = val;
                state.c = val;
                validateCenterDistance();
                clearPresets();
                updateCalculations();
            }
            function syncCFromInput() {
                let val = parseInt(inputC.value);
                if (isNaN(val) || val < 200) val = 200;
                if (val > 1200) val = 1200;
                sliderC.value = val;
                state.c = val;
                validateCenterDistance();
                clearPresets();
                updateCalculations();
            }

            function syncRpmFromSlider() {
                const val = parseInt(sliderRpm.value);
                inputRpm.value = val;
                state.rpm = val;
                clearPresets();
                updateCalculations();
            }
            function syncRpmFromInput() {
                let val = parseInt(inputRpm.value);
                if (isNaN(val) || val < 0) val = 0;
                if (val > 2500) val = 2500;
                sliderRpm.value = val;
                state.rpm = val;
                clearPresets();
                updateCalculations();
            }

            // Ensure center distance is physically larger than radius1 + radius2 to avoid overlap
            function validateCenterDistance() {
                const minC = (state.d1 + state.d2) / 2 + 30; // 30mm margin
                if (state.c < minC) {
                    state.c = Math.ceil(minC);
                    inputC.value = state.c;
                    sliderC.value = state.c;
                }
                
                // Set slider range dynamically
                sliderC.min = Math.ceil(minC);
                if (parseInt(inputC.value) < minC) {
                    inputC.value = Math.ceil(minC);
                    sliderC.value = Math.ceil(minC);
                    state.c = Math.ceil(minC);
                }
            }

            // Physics Core Calculations
            function updateCalculations() {
                const D1 = state.d1;
                const D2 = state.d2;
                const C = state.c;
                const N1 = state.rpm;

                // 1. Pulley Ratio
                const ratio = D2 / D1;
                txtPulleyRatio.innerText = ratio.toFixed(2) + ' : 1';
                if (ratio > 1) {
                    txtRatioType.innerText = `${ratio.toFixed(2)}배 감속 전동 (출력 토크 ${ratio.toFixed(2)}배)`;
                } else if (ratio < 1) {
                    const gain = 1 / ratio;
                    txtRatioType.innerText = `${gain.toFixed(2)}배 증속 전동 (출력 토크 ${ratio.toFixed(2)}배)`;
                } else {
                    txtRatioType.innerText = '1:1 동속 동력 전동';
                }

                // 2. Output RPM
                const n2 = N1 / ratio;
                txtN2.innerText = n2.toFixed(1) + ' RPM';
                txtTorqueMult.innerText = (ratio * 100).toFixed(0) + '%';

                // 3. Pitch line velocity (Belt Speed in m/s)
                const pitchSpeed = (Math.PI * D1 * N1) / 60000;
                txtBeltSpeed.innerText = pitchSpeed.toFixed(2) + ' m/s';

                // 4. Belt Length calculation
                const term1 = 2 * C;
                const term2 = (Math.PI * (D1 + D2)) / 2;
                const term3 = Math.pow(D2 - D1, 2) / (4 * C);
                const beltLen = term1 + term2 + term3;
                txtBeltLength.innerText = beltLen.toFixed(1) + ' mm';
                txtCenterDistOverlay.innerText = beltLen.toFixed(1) + ' mm';

                // 5. Lap angles
                // sin(alpha) = (D2 - D1) / 2C
                const sinAlpha = (D2 - D1) / (2 * C);
                const alpha = Math.asin(Math.min(Math.max(sinAlpha, -0.99), 0.99));
                
                const angle1Rad = Math.PI - 2 * alpha;
                const angle2Rad = Math.PI + 2 * alpha;

                const angle1Deg = (angle1Rad * 180) / Math.PI;
                const angle2Deg = (angle2Rad * 180) / Math.PI;

                txtLapAngle1.innerText = angle1Deg.toFixed(1) + '°';
                txtLapAngle2.innerText = angle2Deg.toFixed(1) + '°';

                if (angle1Deg < 120) {
                    txtLapAngle1.style.color = '#db2777';
                } else {
                    txtLapAngle1.style.color = '#10b981';
                }
            }

            // Presets Click Handler
            function loadPreset(key) {
                clearPresets();
                presetButtons.forEach(btn => {
                    if (btn.dataset.preset === key) {
                        btn.classList.add('active');
                    }
                });

                if (key === 'reduction') {
                    state.d1 = 120;
                    state.d2 = 240;
                    state.c = 480;
                    state.rpm = 300;
                } else if (key === 'mesh1to1') {
                    state.d1 = 180;
                    state.d2 = 180;
                    state.c = 450;
                    state.rpm = 200;
                } else if (key === 'increase') {
                    state.d1 = 240;
                    state.d2 = 80;
                    state.c = 450;
                    state.rpm = 100;
                }

                // Sync inputs
                inputD1.value = state.d1;
                sliderD1.value = state.d1;
                inputD2.value = state.d2;
                sliderD2.value = state.d2;
                inputC.value = state.c;
                sliderC.value = state.c;
                inputRpm.value = state.rpm;
                sliderRpm.value = state.rpm;

                validateCenterDistance();
                updateCalculations();
            }

            function clearPresets() {
                presetButtons.forEach(btn => btn.classList.remove('active'));
            }

            // Draw Pulley Wheel
            function drawPulley(c, x, y, radius, angle, themeColor) {
                c.save();
                c.translate(x, y);

                // 1. Pulley Outer Body (dark glass fill)
                c.beginPath();
                c.arc(0, 0, radius, 0, Math.PI * 2);
                c.fillStyle = 'rgba(241, 245, 249, 0.85)';
                c.fill();
                c.strokeStyle = themeColor;
                c.lineWidth = 3;
                c.shadowBlur = 8;
                c.shadowColor = themeColor;
                c.stroke();
                c.shadowBlur = 0;

                // 2. Pulley Inner Rim Ring
                c.beginPath();
                c.arc(0, 0, radius - 6, 0, Math.PI * 2);
                c.strokeStyle = 'rgba(15, 23, 42, 0.06)';
                c.lineWidth = 1.5;
                c.stroke();

                // 3. Rotating Spokes
                c.save();
                c.rotate(angle);
                
                // Central hub
                const hubRad = Math.max(radius * 0.25, 12);
                c.beginPath();
                c.arc(0, 0, hubRad, 0, Math.PI * 2);
                c.strokeStyle = 'rgba(15, 23, 42, 0.15)';
                c.lineWidth = 2;
                c.stroke();

                c.beginPath();
                c.arc(0, 0, 5, 0, Math.PI * 2);
                c.fillStyle = '#ffffff';
                c.fill();

                // 5 spokes
                const spokes = 5;
                for (let s = 0; s < spokes; s++) {
                    const spokeAngle = (s * 2 * Math.PI) / spokes;
                    c.save();
                    c.rotate(spokeAngle);
                    c.beginPath();
                    c.moveTo(0, hubRad);
                    c.lineTo(0, radius - 8);
                    c.strokeStyle = 'rgba(15, 23, 42, 0.08)';
                    c.lineWidth = 3;
                    c.stroke();
                    c.restore();
                }
                
                // Keyway marker (to see rotation clearly)
                c.beginPath();
                c.arc(radius - 12, 0, 4, 0, Math.PI * 2);
                c.fillStyle = '#ffffff';
                c.shadowBlur = 4;
                c.shadowColor = '#ffffff';
                c.fill();

                c.restore();
                c.restore();
            }

            function drawDiameterLeader(c, x, y, r, valStr, labelPrefix, side) {
                c.save();
                c.strokeStyle = 'rgba(71, 85, 105, 0.4)';
                c.lineWidth = 1.2;
                
                // Angle of leader line (45 degrees up-left or up-right)
                const angle = side === -1 ? -Math.PI * 0.75 : -Math.PI * 0.25;
                const px = x + Math.cos(angle) * r;
                const py = y + Math.sin(angle) * r;
                
                // Draw a beautiful small dot on the circle boundary
                c.beginPath();
                c.arc(px, py, 2.5, 0, Math.PI * 2);
                c.fillStyle = side === -1 ? '#0284c7' : '#db2777';
                c.fill();
                
                // Draw leader lines: from circle boundary diagonal outwards, then horizontal
                const lx1 = px + Math.cos(angle) * 20;
                const ly1 = py + Math.sin(angle) * 20;
                const lx2 = lx1 + side * 45;
                const ly2 = ly1;
                
                c.beginPath();
                c.moveTo(px, py);
                c.lineTo(lx1, ly1);
                c.lineTo(lx2, ly2);
                c.stroke();
                
                // Draw text above the horizontal line
                const text = `${labelPrefix} ${valStr}`;
                c.font = 'bold 10px var(--font-body)';
                
                const tx = side === -1 ? lx1 - 4 : lx1 + 4;
                c.fillStyle = '#1e293b';
                c.textAlign = side === -1 ? 'right' : 'left';
                c.textBaseline = 'bottom';
                c.fillText(text, tx, ly2 - 1);
                c.restore();
            }

            // Main Animation Frame
            function animate(currentTime) {
                requestAnimationFrame(animate);

                let dt = (currentTime - state.lastTime) / 1000;
                if (isNaN(dt) || dt < 0 || dt > 1.0) dt = 0.016;
                state.lastTime = currentTime;

                const width = canvas.width / (window.devicePixelRatio || 1);
                const height = canvas.height / (window.devicePixelRatio || 1);
                const cx = width / 2;
                const cy = height / 2;

                ctx.clearRect(0, 0, width, height);

                // 1. Draw Grid
                ctx.save();
                ctx.strokeStyle = 'rgba(2, 132, 199, 0.06)';
                ctx.lineWidth = 1;
                for (let x = 0; x < width; x += 30) {
                    ctx.beginPath();
                    ctx.moveTo(x, 0);
                    ctx.lineTo(x, height);
                    ctx.stroke();
                }
                for (let y = 0; y < height; y += 30) {
                    ctx.beginPath();
                    ctx.moveTo(0, y);
                    ctx.lineTo(width, y);
                    ctx.stroke();
                }
                ctx.restore();

                // 2. Geometry Scale Calculation
                const phys_r1 = state.d1 / 2;
                const phys_r2 = state.d2 / 2;

                const scaleWidth = (width - 190) / (state.c + phys_r1 + phys_r2);
                const scaleHeight = (height - 120) / Math.max(state.d1, state.d2);
                const scale = Math.min(scaleWidth, scaleHeight);
                
                const txtCanvasScale = document.getElementById('txt-canvas-scale');
                if (txtCanvasScale) {
                    txtCanvasScale.innerText = `스케일 비율: 1:${(1/scale * 100).toFixed(0)}급`;
                }

                const r1 = phys_r1 * scale;
                const r2 = phys_r2 * scale;
                const c_vis = state.c * scale;

                // Position centers to perfectly center the entire bounding box
                const x1 = (width - c_vis + r1 - r2) / 2;
                const y1 = cy;
                const x2 = x1 + c_vis;
                const y2 = cy;

                // Physics Rotation
                const ratio = state.d2 / state.d1;
                const pitchSpeed = (Math.PI * state.d1 * state.rpm) / 60000; // m/s

                if (state.isPlaying) {
                    if (state.rpm > 0) {
                        const omega1 = (2 * Math.PI * state.rpm) / 60; // rad/s
                        
                        // Stroboscopic rendering limit
                        const maxOmega = 6;
                        const renderOmega1 = maxOmega * (1 - Math.exp(-omega1 / maxOmega));
                        
                        state.angle1 += state.direction * renderOmega1 * dt;
                        state.angle2 += state.direction * (renderOmega1 / ratio) * dt;

                        // Linear belt move offset
                        state.beltOffset += state.direction * pitchSpeed * 1000 * scale * dt;
                    }
                }

                // 3. Tangent belt geometry calculations
                const dx = x2 - x1;
                const alpha = Math.asin((r2 - r1) / dx);

                // Tangent angles relative to horizontal
                const theta_top1 = -Math.PI / 2 - alpha;
                const theta_bottom1 = Math.PI / 2 + alpha;
                
                const theta_top2 = -Math.PI / 2 - alpha;
                const theta_bottom2 = Math.PI / 2 + alpha;

                // Tangent Points on Pulley 1
                const tp1_top_x = x1 + r1 * Math.cos(theta_top1);
                const tp1_top_y = y1 + r1 * Math.sin(theta_top1);
                const tp1_bottom_x = x1 + r1 * Math.cos(theta_bottom1);
                const tp1_bottom_y = y1 + r1 * Math.sin(theta_bottom1);

                // Tangent Points on Pulley 2
                const tp2_top_x = x2 + r2 * Math.cos(theta_top2);
                const tp2_top_y = y2 + r2 * Math.sin(theta_top2);
                const tp2_bottom_x = x2 + r2 * Math.cos(theta_bottom2);
                const tp2_bottom_y = y2 + r2 * Math.sin(theta_bottom2);

                // 4. Spawn Friction Sparks at Tangent Points
                if (state.isPlaying) {
                    if (state.rpm > 300) {
                        const sparkEmission = Math.min(Math.floor(state.rpm / 600) + 1, 2);
                        for (let s = 0; s < sparkEmission; s++) {
                            if (Math.random() > 0.6) {
                                // Top tangent left sparks
                                const spVx = -state.direction * (2 + Math.random() * 2);
                                const spVy = (Math.random() - 0.5) * 1.5;
                                state.sparks.push(new BeltSpark(tp1_top_x, tp1_top_y, spVx * scale, spVy * scale));
                            }
                        }
                    }
                }

                // Render Sparks
                state.sparks = state.sparks.filter(s => s.life > 0);
                state.sparks.forEach(s => {
                    s.update();
                    s.draw(ctx);
                });

                // 5. Draw Pulley 1 (Drive - Cyan)
                drawPulley(ctx, x1, y1, r1, state.angle1, '#0284c7');

                // Draw Pulley 2 (Driven - Magenta)
                drawPulley(ctx, x2, y2, r2, state.angle2, '#db2777');

                // 6. Draw Snug Belt Around both pulleys (Moving dashed line)
                ctx.save();
                ctx.beginPath();
                
                // Top tangent line
                ctx.moveTo(tp1_top_x, tp1_top_y);
                ctx.lineTo(tp2_top_x, tp2_top_y);
                
                // Clockwise arc around Pulley 2
                ctx.arc(x2, y2, r2, theta_top2, theta_bottom2, false);
                
                // Bottom tangent line
                ctx.lineTo(tp1_bottom_x, tp1_bottom_y);
                
                // Clockwise arc around Pulley 1
                ctx.arc(x1, y1, r1, theta_bottom1, theta_top1, false);

                ctx.closePath();

                // Belt outer shadow/glow
                ctx.shadowBlur = 12;
                ctx.shadowColor = '#0284c7';
                ctx.strokeStyle = '#0284c7';
                ctx.lineWidth = 7;
                ctx.lineCap = 'round';
                ctx.lineJoin = 'round';
                ctx.stroke();
                ctx.shadowBlur = 0;

                // Moving belt textured interior dashes
                ctx.strokeStyle = '#ffffff';
                ctx.lineWidth = 5;
                ctx.setLineDash([16, 12]);
                ctx.lineDashOffset = -state.beltOffset;
                ctx.stroke();
                ctx.setLineDash([]);
                ctx.restore();

                // 7. Dimension annotations
                ctx.save();
                
                // Draw Diameter dimensions on pulleys with professional CAD leader lines
                drawDiameterLeader(ctx, x1, y1, r1, state.d1.toFixed(1) + ' mm', 'd₁ =', -1);
                drawDiameterLeader(ctx, x2, y2, r2, state.d2.toFixed(1) + ' mm', 'd₂ =', 1);

                // Horizontal Center Distance Dimension Line (C)
                ctx.save();
                ctx.strokeStyle = 'rgba(71, 85, 105, 0.3)';
                ctx.lineWidth = 1;
                
                const dimY = y1 + r2 + 25; // 25px below larger pulley
                
                ctx.beginPath();
                ctx.moveTo(x1, y1 + 10);
                ctx.lineTo(x1, dimY + 5);
                ctx.moveTo(x2, y2 + 10);
                ctx.lineTo(x2, dimY + 5);
                ctx.stroke();
                
                ctx.beginPath();
                ctx.moveTo(x1, dimY);
                ctx.lineTo(x2, dimY);
                ctx.stroke();
                
                function drawHorizontalArrowhead(px, py, direction) {
                    ctx.save();
                    ctx.translate(px, py);
                    ctx.fillStyle = 'rgba(71, 85, 105, 0.5)';
                    ctx.beginPath();
                    ctx.moveTo(0, 0);
                    ctx.lineTo(direction * 6, -3);
                    ctx.lineTo(direction * 6, 3);
                    ctx.closePath();
                    ctx.fill();
                    ctx.restore();
                }
                drawHorizontalArrowhead(x1, dimY, 1);
                drawHorizontalArrowhead(x2, dimY, -1);
                
                const cText = `C = ${state.c} mm`;
                ctx.font = 'bold 9px Inter, sans-serif';
                const cTextWidth = ctx.measureText(cText).width;
                const cTextX = (x1 + x2) / 2;
                // Draw white eraser background so text doesn't overlap dimension line
                ctx.fillStyle = '#f1f5f9';
                ctx.fillRect(cTextX - cTextWidth / 2 - 4, dimY - 6, cTextWidth + 8, 12);
                ctx.fillStyle = '#475569';
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                ctx.fillText(cText, cTextX, dimY);
                ctx.restore();

                // Lap angle highlights (colored arcs representing wrap contact)
                ctx.beginPath();
                ctx.arc(x1, y1, r1 + 10, theta_bottom1, theta_top1, false);
                ctx.strokeStyle = 'rgba(0, 242, 254, 0.35)';
                ctx.lineWidth = 3;
                ctx.stroke();

                ctx.beginPath();
                ctx.arc(x2, y2, r2 + 10, theta_top2, theta_bottom2, false);
                ctx.strokeStyle = 'rgba(255, 0, 127, 0.35)';
                ctx.lineWidth = 3;
                ctx.stroke();
                ctx.restore();
            }

            // Bind Event Listeners
            sliderD1.addEventListener('input', syncD1FromSlider);
            inputD1.addEventListener('change', syncD1FromInput);

            sliderD2.addEventListener('input', syncD2FromSlider);
            inputD2.addEventListener('change', syncD2FromInput);

            sliderC.addEventListener('input', syncCFromSlider);
            inputC.addEventListener('change', syncCFromInput);

            sliderRpm.addEventListener('input', syncRpmFromSlider);
            inputRpm.addEventListener('change', syncRpmFromInput);

            btnPlayPause.addEventListener('click', () => {
                state.isPlaying = !state.isPlaying;
                if (state.isPlaying) {
                    btnPlayPause.innerHTML = '<i class="fa-solid fa-pause"></i> <span>일시정지</span>';
                } else {
                    btnPlayPause.innerHTML = '<i class="fa-solid fa-play"></i> <span>재생</span>';
                }
            });

            btnReverse.addEventListener('click', () => {
                state.direction *= -1;
            });

            presetButtons.forEach(btn => {
                btn.addEventListener('click', () => {
                    loadPreset(btn.dataset.preset);
                });
            });

            // Initialize
            loadPreset('reduction');
            requestAnimationFrame(animate);

            // ── 우클릭 & 소스 복사 방지 (전체 문서 레벨 + 알림창 동기화) ──
            document.addEventListener('contextmenu', function(e) {
                e.preventDefault();
                alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                return false;
            }, { capture: true });

            document.addEventListener('selectstart', function(e) {
                e.preventDefault();
                return false;
            }, { capture: true });

            // 개발자도구 단축키 및 복사 단축키 차단 (F12, Ctrl+U, Ctrl+Shift+I/J/C, Ctrl+C, Ctrl+S)
            // 중복 차단 문자를 사용하면 WordPress API에서 오류가 날 수 있으므로 nested if로 구현
            document.addEventListener('keydown', function(e) {
                if (e.key === 'F12') {
                    e.preventDefault();
                    alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                    return false;
                }
                if (e.ctrlKey) {
                    if (e.key === 'u' || e.key === 'c' || e.key === 's' || e.key === 'U' || e.key === 'C' || e.key === 'S') {
                        e.preventDefault();
                        alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                        return false;
                    }
                    if (e.shiftKey) {
                        if (['i','I','j','J','c','C'].includes(e.key)) {
                            e.preventDefault();
                            alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                            return false;
                        }
                    }
                }
            }, { capture: true });
        } // <-- Closed the initBeltSimulator function

        // Robust ReadyState hook-up with polling to guarantee execution even if DOM elements load late
        let initAttempts = 0;
        function tryInit() {
            initAttempts++;
            initBeltSimulator();
            if (!window.__beltpulley_initialized) {
                if (initAttempts < 50) {
                    setTimeout(tryInit, 100);
                }
            }
        }

        if (document.readyState === 'complete' || document.readyState === 'interactive') {
            tryInit();
        } else {
            document.addEventListener('DOMContentLoaded', tryInit);
            window.addEventListener('load', tryInit);
        }
        })();
    </script>




<div style="background: linear-gradient(135deg, rgba(0,242,254,0.03), rgba(138,43,226,0.03)); border: 1px solid rgba(0,242,254,0.15); border-radius: 12px; padding: 18px 24px; margin: 25px auto 35px auto; font-size: 0.95em; color: #4b5563; line-height: 1.7; font-family: sans-serif;">
    <strong style="color: #1f2937; font-size: 1.05em; display: flex; align-items: center; gap: 8px;">
        <span style="font-size: 1.2em;"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /></span> 간편 사용 설명서
    </strong>
    <ol style="margin: 10px 0 0 0; padding-left: 20px;">
        <li style="margin-bottom: 6px;"><strong>풀리 지름 설정: 슬라이더나 입력 칸을 이용해 구동 풀리(D₁)와 피동 풀리(D₂)의 외경 지름을 입력합니다.</strong></li>
<li style="margin-bottom: 6px;">축간거리 및 입력 속도 설정: 두 풀리 축 사이의 중심거리(C)와 구동 풀리의 RPM(N₁)을 설정합니다.</li>
<li style="margin-bottom: 6px;">실시간 벨트 구동 관찰: 회전 속도에 맞춰 벨트라인이 부드럽게 움직이는 2D 물리 시뮬레이션을 관찰합니다.</li>
<li style="margin-bottom: 6px;">출력 계측치 분석: 총 벨트 길이(L), 풀리별 접촉각(Lap Angle), 벨트 선속도(m/s) 및 피동 풀리 속도(RPM)를 계측 모니터에서 즉시 분석합니다.</li>
    </ol>
</div>



<details class="premium-seo-accordion" style="border: 1px solid rgba(0,0,0,0.08); border-radius: 12px; background: #fbfbfc; padding: 0; margin: 30px auto; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.01); font-family: sans-serif;">
    <summary style="display: flex; justify-content: space-between; align-items: center; padding: 20px 24px; font-size: 1.1em; font-weight: 700; color: #1f2937; cursor: pointer; user-select: none; outline: none; list-style: none;">
        <span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 상세 기계공학 해설 및 벨트 전동 설계 규격 (KS/ISO) 확인하기</span>
        <span class="accordion-arrow" style="font-size: 0.9em; color: #9ca3af; transition: transform 0.2s ease;">▼</span>
    </summary>
    <div style="padding: 0 24px 24px 24px; border-top: 1px solid rgba(0,0,0,0.04); background: #ffffff; border-radius: 0 0 12px 12px; font-size: 0.98em; color: #374151; line-height: 1.8;">
        <div style="margin-top: 20px;">
            <h3>1. 벨트 전동(Belt Drive)의 기본 원리 및 공학적 특징</h3>
<p>벨트 전동은 두 개 이상의 풀리(Pulley)에 유연한 벨트(Belt)를 감아 마찰력 또는 물림력에 의해 동력을 전달하는 대표적인 유연 전동 장치(Flexible Transmitter)입니다. 기어 전동에 비해 축간 거리가 먼 경우에 경제적으로 동력을 전달할 수 있으며, 충격 흡수 성능이 우수하고 운전이 비교적 조용하다는 큰 장점을 지니고 있습니다.</p><ul><li><strong>평벨트 (Flat Belt):</strong> 고속 운전에 적합하며 굴곡성이 좋으나 슬립(Slip)이 발생하기 쉽습니다.</li><li><strong>V벨트 (V-Belt):</strong> 홈과의 쐐기 작용(Wedge Action)에 의한 마찰력 증대로 비교적 큰 토크를 슬립 없이 전달할 수 있어 산업용 동력 전달에 가장 널리 쓰입니다.</li><li><strong>타이밍 벨트 (Timing Belt/정밀 물림 벨트):</strong> 기어처럼 치형이 형성되어 슬립률 0%의 동기 전동(Synchronous Drive)이 가능합니다.</li></ul>
<h3>2. 벨트 길이 및 접촉각 수학적 유도 공식</h3>
<p>벨트 전동 설계의 기본이 되는 기하학적 치수와 역학적 관계식은 다음과 같이 유도됩니다.</p><p><strong>① 평행 벨트 길이 계산 공식 (Open Belt Length):</strong> 두 풀리의 지름을 <code>D1</code>, <code>D2</code>, 축간거리를 <code>C</code>라고 할 때, 기하학적 궤적을 적분하여 얻어지는 표준 근사식입니다.</p><p style="text-align: center; font-weight: bold; background: #f3f4f6; padding: 12px; border-radius: 8px;">L &approx; 2C + &pi;(D1 + D2)/2 + (D2 - D1)^2 / (4C) &nbsp;[mm]</p><p><strong>② 접촉각 (Lap Angle / 접촉 호의 각도, &theta;):</strong> 동력 전달 용량을 결정하는 가장 중요한 인자 중 하나로, 구동 풀리에서의 접촉각 <code>&theta;1</code>이 작을수록 슬립 현상이 일어나기 쉽습니다.</p><p style="text-align: center; font-weight: bold; background: #f3f4f6; padding: 12px; border-radius: 8px;">&theta;1 = &pi; - 2 &times; sin^(-1)((D2 - D1) / 2C) &nbsp;[rad]</p><p style="text-align: center; font-weight: bold; background: #f3f4f6; padding: 12px; border-radius: 8px;">&theta;2 = &pi; + 2 &times; sin^(-1)((D2 - D1) / 2C) &nbsp;[rad]</p><p><strong>③ 벨트의 주속도 (Belt Pitch Line Speed, v):</strong> 동력이 전달되는 벨트의 선형 주행 속도로, 구동 풀리 기준 공식은 다음과 같습니다:</p><p style="text-align: center; font-weight: bold; background: #e0f2fe; padding: 16px; border-radius: 8px; font-size: 1.05em; color: #0369a1;">v = (&pi; &times; D1 &times; N1) / 60,000 &nbsp;[m/s]</p>
<h3>3. 마찰 전동의 오일러 공식 (슬립 및 장력 설계)</h3>
<p>벨트가 풀리에 감겨 회전할 때, 미끄러지기 직전의 긴장측 장력(Tight Side Tension, <code>T_t</code>)과 이완측 장력(Slack Side Tension, <code>T_s</code>)의 비율은 벨트와 풀리 사이의 마찰 계수(<code>&mu;</code>)와 접촉각(<code>&theta;</code>)에 의해 지수적으로 결정됩니다. 이를 **오일러의 벨트 공식(Euler's Belt Formula)**이라고 합니다.</p><p style="text-align: center; font-weight: bold; background: #f3f4f6; padding: 12px; border-radius: 8px;">T_t / T_s = e^(&mu; &times; &theta;)</p><p>이 식은 접촉각 <code>&theta;</code>가 클수록 장력비가 기하급수적으로 증가하여 더 큰 유효 인장력(<code>F_e = T_t - T_s</code>)을 전달할 수 있음을 입증합니다. 따라서 소형 풀리의 접촉각이 120&deg; 이하로 떨어지는 감속 비율이 큰 설계에서는 아이들러 풀리(Idler Pulley)를 추가 장착하여 이완측 접촉각을 강제로 확보하는 보정 설계가 필수로 작용합니다.</p>

        </div>
    </div>
</details>
<style>
details.premium-seo-accordion[open] summary .accordion-arrow { transform: rotate(180deg); color: #00f2fe; }
details.premium-seo-accordion summary::-webkit-details-marker { display: none; }
details.premium-seo-accordion:hover { border-color: rgba(0,242,254,0.3); }
</style>

]]></content:encoded>
					
					<wfw:commentRss>https://myengnote.com/belt-pulley-speed-calculator-simulator-2/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>기어비 계산기 &#038; 시뮬레이터</title>
		<link>https://myengnote.com/gear-ratio-calculator-simulator-3/</link>
					<comments>https://myengnote.com/gear-ratio-calculator-simulator-3/#respond</comments>
		
		<dc:creator><![CDATA[동동]]></dc:creator>
		<pubDate>Tue, 26 May 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[공학계산기]]></category>
		<category><![CDATA[기계설계]]></category>
		<category><![CDATA[기어계산기]]></category>
		<category><![CDATA[기어비]]></category>
		<category><![CDATA[동력전달]]></category>
		<category><![CDATA[물리시뮬레이터]]></category>
		<guid isPermaLink="false">https://myengnote.com/gear-ratio-calculator-simulator-3/</guid>

					<description><![CDATA[기어 잇수와 입력 RPM을 조절하여 기어비, 출력 속도, 토크 변화 및 기하학적 치수를 실시간으로 계산하고 완벽히 맞물려 회전하는 기어열을 시각화하는 초정밀 2D 물리 시뮬레이터입니다.]]></description>
										<content:encoded><![CDATA[
<h2 style="font-size: 1.6em; font-weight: 800; color: #0c0e25; border-bottom: 2px solid #00f2fe; padding-bottom: 8px; margin-bottom: 20px;"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 기어비 계산기 &#038; 시뮬레이터</h2>



<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&#038;family=Outfit:wght@400;500;600;700;800&#038;display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* Scoped & Isolated Styles for gearratio-calculator-wrapper */

        /* Modern Reset and Design Tokens */
        .gearratio-calculator-wrapper *, .gearratio-calculator-wrapper *::before, .gearratio-calculator-wrapper *::after {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }

        .gearratio-calculator-wrapper {
            --color-bg-dark: #f8f9fd;
            --color-bg-deep: #ffffff;
            --color-panel-bg: rgba(255, 255, 255, 0.85);
            --color-border: rgba(0, 0, 0, 0.06);
            --color-border-hover: rgba(0, 0, 0, 0.12);
            
            --color-text-main: #1e293b;
            --color-text-muted: #475569;
            --color-text-dark: #94a3b8;

            --color-cyan: #0284c7;
            --color-cyan-glow: rgba(2, 132, 199, 0.15);
            --color-magenta: #db2777;
            --color-magenta-glow: rgba(219, 39, 119, 0.15);
            --color-purple: #7c3aed;
            --color-purple-glow: rgba(124, 58, 237, 0.12);
            --color-success: #10b981;

            --gradient-primary: linear-gradient(135deg, var(--color-cyan) 0%, var(--color-purple) 100%);
            --gradient-accent: linear-gradient(135deg, var(--color-magenta) 0%, var(--color-purple) 100%);
            --gradient-panel: linear-gradient(180deg, rgba(255, 255, 255, 0.9) 0%, rgba(248, 250, 252, 0.95) 100%);

            --font-heading: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            --font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
            
            --shadow-neon-cyan: 0 0 15px var(--color-cyan-glow);
            --shadow-neon-magenta: 0 0 15px var(--color-magenta-glow);
            --shadow-card: 0 8px 32px 0 rgba(15, 23, 42, 0.05);
            --blur-glass: blur(16px);
        }

        .gearratio-calculator-wrapper {
            background-color: var(--color-bg-dark);
            color: var(--color-text-main);
            font-family: var(--font-body);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            overflow-x: hidden;
            position: relative;
        }

        .app-background-glow {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: -1;
            overflow: hidden;
            pointer-events: none;
        }

        .app-background-glow::before, 
        .app-background-glow::after {
            content: '';
            position: absolute;
            width: 600px;
            height: 600px;
            border-radius: 50%;
            filter: blur(140px);
            opacity: 0.15;
        }

        .app-background-glow::before {
            background: var(--color-cyan);
            top: -10%;
            right: -5%;
            animation: pulse-slow 15s infinite alternate;
        }

        .app-background-glow::after {
            background: var(--color-purple);
            bottom: -10%;
            left: -5%;
            animation: pulse-slow 20s infinite alternate-reverse;
        }

        @keyframes pulse-slow {
            0% { transform: scale(1) translate(0, 0); opacity: 0.12; }
            100% { transform: scale(1.2) translate(50px, 50px); opacity: 0.22; }
        }

        .app-container {
            width: 100%;
            max-width: 1440px;
            padding: 24px;
            display: flex;
            flex-direction: column;
            gap: 24px;
            container-type: inline-size;
            container-name: gearratio-container;
        }

        .app-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 20px 24px;
            background: var(--color-panel-bg);
            backdrop-filter: var(--blur-glass);
            border: 1px solid var(--color-border);
            border-radius: 16px;
            box-shadow: var(--shadow-card);
        }

        .logo-area {
            display: flex;
            align-items: center;
            gap: 16px;
        }

        .logo-icon {
            font-size: 32px;
            background: linear-gradient(135deg, var(--color-cyan), var(--color-magenta));
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .logo-area h1 {
            font-family: var(--font-heading);
            font-weight: 800;
            font-size: 24px;
            letter-spacing: 1.5px;
            background: linear-gradient(90deg, var(--color-text-main), var(--color-text-muted));
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }

        .logo-area .subtitle {
            font-size: 12px;
            color: var(--color-text-muted);
            font-weight: 500;
            letter-spacing: 0.5px;
        }

        .fa-spin-slow {
            animation: fa-spin 15s linear infinite;
        }

        .header-badge {
            display: flex;
            align-items: center;
            gap: 10px;
            background: rgba(16, 185, 129, 0.1);
            border: 1px solid rgba(16, 185, 129, 0.2);
            padding: 6px 14px;
            border-radius: 20px;
        }

        .pulse-dot {
            width: 8px;
            height: 8px;
            background-color: var(--color-success);
            border-radius: 50%;
            box-shadow: 0 0 10px var(--color-success);
            animation: pulse-dot-anim 1.5s infinite;
        }

        @keyframes pulse-dot-anim {
            0% { transform: scale(0.9); opacity: 0.6; }
            50% { transform: scale(1.2); opacity: 1; }
            100% { transform: scale(0.9); opacity: 0.6; }
        }

        .badge-text {
            font-size: 11px;
            font-weight: 600;
            color: var(--color-success);
            letter-spacing: 0.5px;
        }

        .app-main-grid {
            display: grid;
            grid-template-columns: 340px 1fr 340px;
            gap: 24px;
            align-items: start;
        }

        .panel {
            background: var(--gradient-panel);
            backdrop-filter: var(--blur-glass);
            border: 1px solid var(--color-border);
            border-radius: 20px;
            padding: 24px;
            box-shadow: var(--shadow-card);
            display: flex;
            flex-direction: column;
            gap: 20px;
            transition: border-color 0.3s ease, box-shadow 0.3s ease;
        }

        .panel:hover {
            border-color: var(--color-border-hover);
        }

        .panel-header {
            display: flex;
            align-items: center;
            gap: 12px;
            border-bottom: 1px solid var(--color-border);
            padding-bottom: 14px;
        }

        .panel-header i {
            font-size: 18px;
        }

        .panel-header h2 {
            font-family: var(--font-heading);
            font-size: 18px;
            font-weight: 600;
            letter-spacing: 0.5px;
        }

        .text-cyan { color: #0284c7; }
        .text-magenta { color: #db2777; }
        .text-purple { color: var(--color-purple); }

        .control-panel {
            grid-column: 1;
        }

        .input-group {
            display: flex;
            flex-direction: column;
            gap: 12px;
        }

        .input-label-row {
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .input-label-row label {
            font-size: 13px;
            font-weight: 600;
            color: var(--color-text-main);
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .helper-text {
            font-size: 10px;
            color: var(--color-text-muted);
            font-weight: 500;
        }

        .number-input-wrapper {
            display: flex;
            position: relative;
            border-radius: 8px;
            overflow: hidden;
            border: 1px solid var(--color-border);
            background: #ffffff;
            transition: all 0.2s ease;
        }

        .number-input-wrapper:focus-within {
            border-color: #0284c7;
            box-shadow: 0 0 10px var(--color-cyan-glow);
        }

        .custom-number-input {
            width: 100%;
            background: #f8fafc !important;
            border: none !important;
            outline: none !important;
            color: #0f172a !important;
            padding: 10px 14px !important;
            font-family: var(--font-body) !important;
            font-size: 15px !important;
            font-weight: 700 !important;
            text-align: right !important;
            padding-right: 60px !important;
            text-shadow: none !important;
            box-shadow: none !important;
            -webkit-appearance: none !important;
            -moz-appearance: textfield !important;
        }

        .custom-number-input::-webkit-outer-spin-button,
        .custom-number-input::-webkit-inner-spin-button {
            -webkit-appearance: none;
            margin: 0;
        }

        .unit-badge {
            position: absolute;
            right: 1px;
            top: 1px;
            bottom: 1px;
            background: rgba(0, 0, 0, 0.03);
            color: var(--color-text-muted);
            font-size: 11px;
            font-weight: 700;
            padding: 0 14px;
            display: flex;
            align-items: center;
            justify-content: center;
            border-left: 1px solid var(--color-border);
            pointer-events: none;
            letter-spacing: 0.5px;
        }

        .custom-slider {
            -webkit-appearance: none;
            width: 100%;
            height: 6px;
            border-radius: 3px;
            background: rgba(0, 0, 0, 0.05);
            outline: none;
            margin: 8px 0;
            transition: background 0.2s ease;
        }

        .custom-slider::-webkit-slider-thumb {
            -webkit-appearance: none;
            appearance: none;
            width: 18px;
            height: 18px;
            border-radius: 50%;
            cursor: pointer;
            transition: transform 0.1s ease, box-shadow 0.2s ease;
        }

        #slider-z1::-webkit-slider-thumb {
            background: var(--color-cyan);
            box-shadow: 0 0 10px var(--color-cyan);
        }
        #slider-z1::-webkit-slider-thumb:hover {
            transform: scale(1.2);
            box-shadow: 0 0 15px var(--color-cyan), 0 0 5px #fff;
        }

        #slider-z2::-webkit-slider-thumb {
            background: var(--color-magenta);
            box-shadow: 0 0 10px var(--color-magenta);
        }
        #slider-z2::-webkit-slider-thumb:hover {
            transform: scale(1.2);
            box-shadow: 0 0 15px var(--color-magenta), 0 0 5px #fff;
        }

        #slider-rpm::-webkit-slider-thumb {
            background: var(--color-purple);
            box-shadow: 0 0 10px var(--color-purple);
        }
        #slider-rpm::-webkit-slider-thumb:hover {
            transform: scale(1.2);
            box-shadow: 0 0 15px var(--color-purple), 0 0 5px #fff;
        }

        .control-actions {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 10px;
            margin-top: 10px;
        }

        .action-btn {
            background: rgba(0, 0, 0, 0.02);
            border: 1px solid var(--color-border);
            color: var(--color-text-main);
            padding: 10px;
            border-radius: 10px;
            font-size: 12px;
            font-weight: 600;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
            transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
        }

        .action-btn:hover {
            background: rgba(0, 0, 0, 0.03);
            border-color: var(--color-border);
        }

        .action-btn.active {
            background: var(--color-cyan-glow);
            border-color: #0284c7;
            color: #0284c7;
            box-shadow: 0 0 12px var(--color-cyan-glow);
        }

        .presets-section {
            display: flex;
            flex-direction: column;
            gap: 12px;
            border-top: 1px solid var(--color-border);
            padding-top: 18px;
        }

        .presets-section h3 {
            font-size: 13px;
            font-weight: 700;
            color: var(--color-text-main);
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .presets-grid {
            display: grid;
            grid-template-columns: 1fr;
            gap: 8px;
        }

        .preset-btn {
            background: rgba(0, 0, 0, 0.015);
            border: 1px solid var(--color-border);
            border-radius: 10px;
            padding: 8px 12px;
            cursor: pointer;
            color: var(--color-text-main);
            display: flex;
            align-items: center;
            gap: 12px;
            text-align: left;
            transition: all 0.2s ease;
        }

        .preset-btn:hover {
            background: rgba(0, 0, 0, 0.05);
            border-color: var(--color-border);
            transform: translateX(4px);
        }

        .preset-btn.active {
            background: linear-gradient(90deg, var(--color-cyan-glow), var(--color-magenta-glow));
            border-color: #0284c7;
            box-shadow: 0 0 10px var(--color-cyan-glow);
        }

        .preset-icon {
            width: 28px;
            height: 28px;
            background: rgba(0, 0, 0, 0.02);
            border-radius: 8px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 12px;
            color: #0284c7;
            transition: all 0.2s ease;
        }

        .preset-btn:hover .preset-icon, .preset-btn.active .preset-icon {
            background: var(--gradient-primary);
            color: #fff;
            box-shadow: var(--shadow-neon-cyan);
        }

        .preset-name {
            font-size: 12px;
            font-weight: 600;
            letter-spacing: 0.25px;
        }

        .simulation-panel {
            grid-column: 2;
            align-self: stretch;
            justify-content: space-between;
            min-width: 0;
        }

        .simulation-panel .panel-header {
            justify-content: space-between;
        }

        .canvas-scale-indicator {
            font-size: 11px;
            color: var(--color-text-muted);
            background: rgba(0, 0, 0, 0.02);
            padding: 4px 10px;
            border-radius: 6px;
            border: 1px solid var(--color-border);
        }

        .canvas-wrapper {
            position: relative;
            width: 100%;
            background: radial-gradient(circle at center, #ffffff 0%, #f1f5f9 100%);
            border: 1px solid rgba(0, 0, 0, 0.06);
            border-radius: 16px;
            overflow: hidden;
        }

        #physics-canvas {
            display: block;
            width: 100%;
            height: auto;
            aspect-ratio: 16 / 10;
        }

        .canvas-overlay-data {
            position: absolute;
            top: 16px;
            left: 16px;
            pointer-events: none;
            display: flex;
            flex-direction: column;
            gap: 6px;
        }

        .overlay-item {
            background: transparent;
            color: var(--color-text-main);
            padding: 4px 0;
            display: flex;
            align-items: center;
            gap: 6px;
            font-size: 11px;
        }

        .overlay-item .label {
            color: var(--color-text-muted);
            font-weight: 500;
        }

        .overlay-item .value {
            font-weight: 700;
        }

        .simulation-metrics-strip {
            display: flex;
            background: rgba(0, 0, 0, 0.01);
            border: 1px solid var(--color-border);
            border-radius: 12px;
            padding: 12px 20px;
            justify-content: space-around;
            align-items: center;
            gap: 10px;
            margin-top: 10px;
        }

        .mini-metric {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 4px;
            text-align: center;
        }

        .mini-metric .label {
            font-size: 10px;
            font-weight: 600;
            color: var(--color-text-muted);
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }

        .mini-metric .value {
            font-size: 14px;
            font-weight: 700;
            color: var(--color-text-main);
            font-family: var(--font-heading);
        }

        .mini-divider {
            width: 1px;
            height: 24px;
            background: rgba(0, 0, 0, 0.03);
        }

        .results-panel {
            grid-column: 3;
        }

        .ratio-readout-box {
            background: linear-gradient(135deg, rgba(0, 242, 254, 0.08) 0%, rgba(138, 43, 226, 0.03) 100%);
            border: 1px solid rgba(0, 242, 254, 0.25);
            border-radius: 16px;
            padding: 20px;
            text-align: center;
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 6px;
            box-shadow: 0 0 15px rgba(0, 242, 254, 0.05);
        }

        .ratio-title {
            font-size: 11px;
            font-weight: 700;
            color: #0284c7;
            letter-spacing: 1px;
            text-transform: uppercase;
        }

        .ratio-value {
            font-family: var(--font-heading);
            font-weight: 800;
            font-size: 38px;
            background: linear-gradient(90deg, var(--color-text-main), var(--color-cyan));
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }

        .ratio-type {
            font-size: 11px;
            font-weight: 600;
            color: var(--color-text-muted);
        }

        .results-grid {
            display: flex;
            flex-direction: column;
            gap: 12px;
        }

        .result-card {
            background: rgba(255, 255, 255, 0.85); box-shadow: 0 2px 8px rgba(0,0,0,0.03);
            border: 1px solid var(--color-border);
            border-radius: 12px;
            padding: 12px 16px;
            display: flex;
            align-items: center;
            gap: 16px;
            transition: all 0.2s ease;
        }

        .result-card:hover {
            transform: translateY(-2px);
            border-color: rgba(0, 242, 254, 0.2);
            background: rgba(0, 242, 254, 0.03);
        }

        .card-icon {
            width: 38px;
            height: 38px;
            background: rgba(0, 0, 0, 0.015);
            border: 1px solid var(--color-border);
            border-radius: 10px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 14px;
            color: var(--color-text-muted);
            transition: all 0.2s ease;
        }

        .result-card:hover .card-icon {
            color: #0284c7;
            border-color: rgba(0, 242, 254, 0.3);
            background: rgba(0, 242, 254, 0.1);
        }

        .card-content {
            display: flex;
            flex-direction: column;
            gap: 2px;
        }

        .card-unit {
            font-size: 11px;
            font-weight: 600;
            color: var(--color-text-muted);
        }

        .card-value {
            font-family: var(--font-heading);
            font-weight: 700;
            font-size: 18px;
            color: var(--color-text-main);
        }

        .formula-card {
            background: rgba(0, 0, 0, 0.01);
            border: 1px solid var(--color-border);
            border-radius: 12px;
            padding: 16px;
            display: flex;
            flex-direction: column;
            gap: 8px;
            font-size: 12px;
        }

        .formula-card h4 {
            font-weight: 700;
            color: var(--color-text-main);
        }

        .formula-equation {
            font-family: monospace;
            color: #0284c7;
            font-size: 13px;
            background: #f8fafc; border-color: var(--color-border);
            padding: 6px 10px;
            border-radius: 6px;
            border: 1px solid var(--color-border);
            text-align: center;
        }

        /* Container Query for Responsiveness inside WP */
        @container gearratio-container (max-width: 1050px) {
            .app-main-grid {
                grid-template-columns: 1fr 1fr;
                gap: 20px;
            }
            .control-panel { grid-column: 1; }
            .simulation-panel { grid-column: 2; }
            .results-panel { grid-column: 1 / span 2; }
        }
        @container gearratio-container (max-width: 580px) {
            .app-main-grid {
                grid-template-columns: 1fr;
            }
            .control-panel, .simulation-panel, .results-panel {
                grid-column: 1;
            }
            .simulation-panel {
                order: -1;
            }
        }
    
</style>

<div class="gearratio-calculator-wrapper" style="position: relative; width: 100%; box-sizing: border-box; overflow: hidden; margin: 30px auto; border-radius: 20px;">
    <div class="app-background-glow" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; overflow: hidden;"></div>
    <div style="position: relative; z-index: 2; width: 100%;">
        
    
    
    <div class="app-container">
        <!-- Header -->
        <header class="app-header">
            <div class="logo-area">
                <div class="logo-icon"><i class="fa-solid fa-gear fa-spin-slow"></i></div>
                <div>
                    <h1>GEAR MESH</h1>
                    <div class="subtitle">초정밀 기어비 시뮬레이터 &#038; 계산기</div>
                </div>
            </div>
            <div class="header-badge">
                <div class="pulse-dot"></div>
                <div class="badge-text">2D PHYSICS ENGINE ACTIVE</div>
            </div>
        </header>

        <!-- Main Layout Grid -->
        <main class="app-main-grid">
            <!-- Left Panel: Controls -->
            <section class="panel control-panel">
                <div class="panel-header">
                    <i class="fa-solid fa-sliders text-cyan"></i>
                    <h2>시뮬레이션 제어 변수</h2>
                </div>

                <!-- Input Gear Teeth Z1 -->
                <div class="input-group">
                    <div class="input-label-row">
                        <label><i class="fa-solid fa-cog text-cyan"></i> 구동 기어 잇수 (Z₁)</label>
                        <span class="helper-text">구동측 (8 ~ 100 T)</span>
                    </div>
                    <div class="number-input-wrapper">
                        <input type="number" id="input-z1" class="custom-number-input" min="8" max="100" value="12">
                        <div class="unit-badge">T</div>
                    </div>
                    <input type="range" id="slider-z1" class="custom-slider" min="8" max="100" value="12">
                </div>

                <!-- Output Gear Teeth Z2 -->
                <div class="input-group">
                    <div class="input-label-row">
                        <label><i class="fa-solid fa-cog text-magenta"></i> 피동 기어 잇수 (Z₂)</label>
                        <span class="helper-text">피동측 (8 ~ 100 T)</span>
                    </div>
                    <div class="number-input-wrapper">
                        <input type="number" id="input-z2" class="custom-number-input" min="8" max="100" value="24">
                        <div class="unit-badge">T</div>
                    </div>
                    <input type="range" id="slider-z2" class="custom-slider" min="8" max="100" value="24">
                </div>

                <!-- Input Speed RPM -->
                <div class="input-group">
                    <div class="input-label-row">
                        <label><i class="fa-solid fa-gauge-high text-purple"></i> 입력 회전수 (N₁)</label>
                        <span class="helper-text">입력 속도 (0 ~ 3000 RPM)</span>
                    </div>
                    <div class="number-input-wrapper">
                        <input type="number" id="input-rpm" class="custom-number-input" min="0" max="3000" value="120">
                        <div class="unit-badge">RPM</div>
                    </div>
                    <input type="range" id="slider-rpm" class="custom-slider" min="0" max="3000" value="120">
                </div>

                <!-- Gear Module (Pitch sizing) -->
                <div class="input-group">
                    <div class="input-label-row">
                        <label><i class="fa-solid fa-ruler"></i> 기어 모듈 (m)</label>
                        <span class="helper-text">이빨 크기 (1.0 ~ 8.0 mm)</span>
                    </div>
                    <div class="number-input-wrapper">
                        <input type="number" id="input-module" class="custom-number-input" min="1.0" max="8.0" step="0.5" value="3.0">
                        <div class="unit-badge">mm</div>
                    </div>
                </div>

                <!-- Simulation Controls -->
                <div class="control-actions">
                    <button id="btn-play-pause" class="action-btn">
                        <i class="fa-solid fa-pause"></i> <span>일시정지</span>
                    </button>
                    <button id="btn-reverse" class="action-btn">
                        <i class="fa-solid fa-backward"></i> <span>방향 전환</span>
                    </button>
                </div>

                <!-- Presets -->
                <div class="presets-section">
                    <h3><i class="fa-solid fa-tags text-cyan"></i> 하이테크 기어 프리셋</h3>
                    <div class="presets-grid">
                        <button class="preset-btn" data-preset="reduction">
                            <div class="preset-icon"><i class="fa-solid fa-arrow-down-wide-short"></i></div>
                            <div class="preset-name">4:1 강력 감속 시스템</div>
                        </button>
                        <button class="preset-btn" data-preset="mesh1to1">
                            <div class="preset-icon"><i class="fa-solid fa-arrows-left-right"></i></div>
                            <div class="preset-name">1:1 속도 매칭 정밀 기어</div>
                        </button>
                        <button class="preset-btn" data-preset="increase">
                            <div class="preset-icon"><i class="fa-solid fa-arrow-up-wide-short"></i></div>
                            <div class="preset-name">1:3 기계식 증속 장치</div>
                        </button>
                    </div>
                </div>
            </section>

            <!-- Center Panel: Physics Simulator & Integrated Results -->
            <section class="panel simulation-panel">
                <div class="panel-header">
                    <div style="display: flex; align-items: center; gap: 12px;">
                        <i class="fa-solid fa-circle-nodes text-cyan"></i>
                        <h2>실시간 기어 맞물림 물리 뷰</h2>
                    </div>
                    <div id="txt-canvas-scale" class="canvas-scale-indicator">스케일 자동 조정</div>
                </div>

                <div class="canvas-wrapper">
                    <canvas id="physics-canvas"></canvas>
                    
                    <!-- Overlay Floating Data -->
                    <div class="canvas-overlay-data">
                        <div class="overlay-item">
                            <span class="label">축간 중심거리 C:</span>
                            <span class="value text-cyan" id="txt-center-dist">0.0 mm</span>
                        </div>
                    </div>
                </div>

                <!-- Mini Metrics -->
                <div class="simulation-metrics-strip">
                    <div class="mini-metric">
                        <div class="label">선속도 (Pitch Line Speed)</div>
                        <div id="txt-pitch-speed" class="value">0.0 m/s</div>
                    </div>
                    <div class="mini-divider"></div>
                    <div class="mini-metric">
                        <div class="label">접촉점 맞물림 진동수</div>
                        <div id="txt-mesh-frequency" class="value">0.0 Hz</div>
                    </div>
                </div>

                <!-- Integrated Results Analysis Section -->
                <div class="simulation-results-section" style="display: grid; grid-template-columns: 1fr 1.2fr; gap: 20px; border-top: 1px solid var(--color-border); padding-top: 20px; margin-top: 10px;">
                    <!-- Left Column: Primary Readout & Formula -->
                    <div style="display: flex; flex-direction: column; gap: 16px;">
                        <div class="ratio-readout-box" style="padding: 16px; min-height: 100px;">
                            <div class="ratio-title">최종 기어비 (i)</div>
                            <div id="txt-gear-ratio" class="ratio-value" style="font-size: 24px;">2.00 : 1</div>
                            <div id="txt-ratio-type" class="ratio-type" style="font-size: 11px;">2.00배 감속 (토크 2.00배 증대)</div>
                        </div>
                        <div class="formula-card" style="padding: 12px; font-size: 11px; background: rgba(0, 0, 0, 0.01);">
                            <h4 style="font-size: 12px; margin-bottom: 6px;"><i class="fa-solid fa-circle-info text-cyan"></i> 핵심 동역학 공식</h4>
                            <div class="formula-equation" style="font-size: 11px; padding: 6px; margin-bottom: 6px;">
                                i = Z₂ / Z₁ = N₁ / N₂
                            </div>
                            <div class="formula-equation" style="font-size: 11px; padding: 6px;">
                                d = m × Z
                            </div>
                        </div>
                    </div>

                    <!-- Right Column: Quantitative Results Cards -->
                    <div class="results-grid" style="display: flex; flex-direction: column; gap: 10px; justify-content: center;">
                        <!-- Gear 1 Output Diameter -->
                        <div class="result-card" style="padding: 10px 14px; gap: 12px;">
                            <div class="card-icon" style="width: 32px; height: 32px; font-size: 12px;"><i class="fa-solid fa-circle-dot text-cyan"></i></div>
                            <div class="card-content">
                                <span class="card-unit" style="font-size: 10px;">구동 기어 피치원 지름 (d₁)</span>
                                <span id="txt-d1" class="card-value" style="font-size: 15px;">0.0 mm</span>
                            </div>
                        </div>

                        <!-- Gear 2 Output Diameter -->
                        <div class="result-card" style="padding: 10px 14px; gap: 12px;">
                            <div class="card-icon" style="width: 32px; height: 32px; font-size: 12px;"><i class="fa-solid fa-circle-dot text-magenta"></i></div>
                            <div class="card-content">
                                <span class="card-unit" style="font-size: 10px;">피동 기어 피치원 지름 (d₂)</span>
                                <span id="txt-d2" class="card-value" style="font-size: 15px;">0.0 mm</span>
                            </div>
                        </div>

                        <!-- Gear 2 Output Speed -->
                        <div class="result-card" style="padding: 10px 14px; gap: 12px;">
                            <div class="card-icon" style="width: 32px; height: 32px; font-size: 12px;"><i class="fa-solid fa-rotate text-purple"></i></div>
                            <div class="card-content">
                                <span class="card-unit" style="font-size: 10px;">피동 기어 속도 (N₂)</span>
                                <span id="txt-n2" class="card-value" style="font-size: 15px;">0.0 RPM</span>
                            </div>
                        </div>

                        <!-- Torque Multiplier -->
                        <div class="result-card" style="padding: 10px 14px; gap: 12px;">
                            <div class="card-icon" style="width: 32px; height: 32px; font-size: 12px;"><i class="fa-solid fa-bolt text-success"></i></div>
                            <div class="card-content">
                                <span class="card-unit" style="font-size: 10px;">출력 토크 변환배율 (T₂/T₁)</span>
                                <span id="txt-torque-mult" class="card-value" style="font-size: 15px;">100% (동일)</span>
                            </div>
                        </div>
                    </div>
                </div>
            </section>
        </main>
    </div>

    <!-- Physics & Mesh Animation Engine -->
    <script>
        (function() {
        let isGearInit = false;

        function initGearSimulator() {
            if (isGearInit) return;
            
            const canvas = document.getElementById('physics-canvas');
            if (!canvas) return;
            const ctx = canvas.getContext('2d');
            if (!ctx) return;

            isGearInit = true;
            window.__gearratio_initialized = true;

            // DOM Elements
            const sliderZ1 = document.getElementById('slider-z1');
            const inputZ1 = document.getElementById('input-z1');
            const sliderZ2 = document.getElementById('slider-z2');
            const inputZ2 = document.getElementById('input-z2');
            const sliderRpm = document.getElementById('slider-rpm');
            const inputRpm = document.getElementById('input-rpm');
            const inputModule = document.getElementById('input-module');
            
            const btnPlayPause = document.getElementById('btn-play-pause');
            const btnReverse = document.getElementById('btn-reverse');
            const presetButtons = document.querySelectorAll('.preset-btn');
            
            const txtCenterDist = document.getElementById('txt-center-dist');
            const txtPitchSpeed = document.getElementById('txt-pitch-speed');
            const txtMeshFrequency = document.getElementById('txt-mesh-frequency');
            
            const txtGearRatio = document.getElementById('txt-gear-ratio');
            const txtRatioType = document.getElementById('txt-ratio-type');
            
            const txtD1 = document.getElementById('txt-d1');
            const txtD2 = document.getElementById('txt-d2');
            const txtN2 = document.getElementById('txt-n2');
            const txtTorqueMult = document.getElementById('txt-torque-mult');
            
            const wrapper = document.querySelector('.gearratio-calculator-wrapper') || document.body;

            // State variables
            const state = {
                z1: 12,
                z2: 24,
                rpm: 120,
                m: 3.0,
                angle1: 0,
                angle2: 0,
                isPlaying: true,
                direction: 1,
                lastTime: 0,
                sparks: []
            };

            // Canvas Resizing (Retina support)
            function resizeCanvas() {
                const rect = canvas.getBoundingClientRect();
                const dpr = window.devicePixelRatio || 1;
                canvas.width = rect.width * dpr;
                canvas.height = rect.height * dpr;
                ctx.scale(dpr, dpr);
            }

            resizeCanvas();
            window.addEventListener('resize', resizeCanvas);
            // Polling backup to guarantee resize
            setTimeout(resizeCanvas, 300);

            // Spark Particle System
            class GearSpark {
                constructor(x, y, vx, vy) {
                    this.x = x;
                    this.y = y;
                    this.vx = vx;
                    this.vy = vy;
                    this.size = Math.random() * 1.5 + 0.5;
                    this.life = Math.random() * 20 + 10;
                    this.maxLife = this.life;
                    const colors = ['#00f2fe', '#ff007f', '#ffffff'];
                    this.color = colors[Math.floor(Math.random() * colors.length)];
                }
                update() {
                    this.x += this.vx;
                    this.y += this.vy;
                    this.vy += 0.05; // soft gravity
                    this.life--;
                }
                draw(c) {
                    const alpha = this.life / this.maxLife;
                    c.save();
                    c.globalAlpha = alpha;
                    c.beginPath();
                    c.arc(this.x, this.y, this.size, 0, Math.PI * 2);
                    c.fillStyle = this.color;
                    c.shadowBlur = 4;
                    c.shadowColor = this.color;
                    c.fill();
                    c.restore();
                }
            }

            // Sync Z1 Input and Slider
            function syncZ1FromSlider() {
                const val = parseInt(sliderZ1.value);
                inputZ1.value = val;
                state.z1 = val;
                clearPresets();
                updateCalculations();
            }
            function syncZ1FromInput() {
                let val = parseInt(inputZ1.value);
                if (isNaN(val) || val < 8) val = 8;
                if (val > 100) val = 100;
                sliderZ1.value = val;
                state.z1 = val;
                clearPresets();
                updateCalculations();
            }

            // Sync Z2 Input and Slider
            function syncZ2FromSlider() {
                const val = parseInt(sliderZ2.value);
                inputZ2.value = val;
                state.z2 = val;
                clearPresets();
                updateCalculations();
            }
            function syncZ2FromInput() {
                let val = parseInt(inputZ2.value);
                if (isNaN(val) || val < 8) val = 8;
                if (val > 100) val = 100;
                sliderZ2.value = val;
                state.z2 = val;
                clearPresets();
                updateCalculations();
            }

            // Sync RPM Input and Slider
            function syncRpmFromSlider() {
                const val = parseInt(sliderRpm.value);
                inputRpm.value = val;
                state.rpm = val;
                clearPresets();
                updateCalculations();
            }
            function syncRpmFromInput() {
                let val = parseInt(inputRpm.value);
                if (isNaN(val) || val < 0) val = 0;
                if (val > 3000) val = 3000;
                sliderRpm.value = val;
                state.rpm = val;
                clearPresets();
                updateCalculations();
            }

            // Sync Module
            function handleModuleChange() {
                let val = parseFloat(inputModule.value);
                if (isNaN(val) || val < 1.0) val = 1.0;
                if (val > 8.0) val = 8.0;
                state.m = val;
                updateCalculations();
            }

            // Core Math Calculations
            function updateCalculations() {
                const d1 = state.m * state.z1;
                const d2 = state.m * state.z2;
                const centerDist = (d1 + d2) / 2;
                const gearRatio = state.z2 / state.z1;
                const n2 = state.rpm / gearRatio;
                
                // Pitch Line Speed = omega * radius
                // PCD in mm, RPM -> linear velocity in m/s
                const r1_meters = (d1 / 2) / 1000;
                const omega1 = (2 * Math.PI * state.rpm) / 60;
                const pitchSpeed = omega1 * r1_meters;
                
                // Meshing frequency = frequency of gear teeth mesh
                const meshFreq = (state.rpm / 60) * state.z1;

                // Update UI Texts
                txtCenterDist.innerText = centerDist.toFixed(1) + ' mm';
                txtPitchSpeed.innerText = pitchSpeed.toFixed(2) + ' m/s';
                txtMeshFrequency.innerText = meshFreq.toFixed(1) + ' Hz';
                
                txtGearRatio.innerText = gearRatio.toFixed(2) + ' : 1';
                
                if (gearRatio > 1.0) {
                    txtRatioType.innerText = `${gearRatio.toFixed(2)}배 감속 (토크 ${gearRatio.toFixed(2)}배 증대)`;
                } else if (gearRatio < 1.0) {
                    const gain = 1 / gearRatio;
                    txtRatioType.innerText = `${gain.toFixed(2)}배 증속 (토크 ${gearRatio.toFixed(2)}배 감소)`;
                } else {
                    txtRatioType.innerText = '1:1 등속 동력 전달';
                }

                txtD1.innerText = d1.toFixed(1) + ' mm';
                txtD2.innerText = d2.toFixed(1) + ' mm';
                txtN2.innerText = n2.toFixed(1) + ' RPM';
                txtTorqueMult.innerText = (gearRatio * 100).toFixed(0) + '%';
            }

            // Preset Buttons Click Handler
            function loadPreset(key) {
                clearPresets();
                presetButtons.forEach(btn => {
                    if (btn.dataset.preset === key) {
                        btn.classList.add('active');
                    }
                });

                if (key === 'reduction') {
                    state.z1 = 12;
                    state.z2 = 48;
                    state.rpm = 200;
                    state.m = 2.5;
                } else if (key === 'mesh1to1') {
                    state.z1 = 24;
                    state.z2 = 24;
                    state.rpm = 150;
                    state.m = 3.0;
                } else if (key === 'increase') {
                    state.z1 = 36;
                    state.z2 = 12;
                    state.rpm = 100;
                    state.m = 3.0;
                }

                // Sync inputs
                inputZ1.value = state.z1;
                sliderZ1.value = state.z1;
                inputZ2.value = state.z2;
                sliderZ2.value = state.z2;
                inputRpm.value = state.rpm;
                sliderRpm.value = state.rpm;
                inputModule.value = state.m;

                updateCalculations();
            }

            function clearPresets() {
                presetButtons.forEach(btn => btn.classList.remove('active'));
            }

            // Draw Gear Teeth Path
            function drawGearOutline(c, teeth, pcd, mod, angleOffset) {
                const r_pitch = pcd / 2;
                const r_addendum = r_pitch + mod;
                const r_dedendum = r_pitch - 1.25 * mod;

                c.save();
                c.rotate(angleOffset);
                c.beginPath();

                for (let i = 0; i < teeth; i++) {
                    const stepAngle = (i * 2 * Math.PI) / teeth;
                    
                    // Teeth geometry angles
                    const t0 = stepAngle - (2 * Math.PI / teeth) * 0.22;
                    const t1 = stepAngle - (2 * Math.PI / teeth) * 0.10;
                    const t2 = stepAngle + (2 * Math.PI / teeth) * 0.10;
                    const t3 = stepAngle + (2 * Math.PI / teeth) * 0.22;
                    const t4 = stepAngle + (2 * Math.PI / teeth) * 0.50; // valley mid point

                    const p0_x = Math.cos(t0) * r_dedendum;
                    const p0_y = Math.sin(t0) * r_dedendum;
                    
                    const p1_x = Math.cos(t1) * r_addendum;
                    const p1_y = Math.sin(t1) * r_addendum;
                    
                    const p2_x = Math.cos(t2) * r_addendum;
                    const p2_y = Math.sin(t2) * r_addendum;
                    
                    const p3_x = Math.cos(t3) * r_dedendum;
                    const p3_y = Math.sin(t3) * r_dedendum;
                    
                    const p4_x = Math.cos(t4) * r_dedendum;
                    const p4_y = Math.sin(t4) * r_dedendum;

                    if (i === 0) {
                        c.moveTo(p0_x, p0_y);
                    } else {
                        c.lineTo(p0_x, p0_y);
                    }
                    c.lineTo(p1_x, p1_y);
                    c.lineTo(p2_x, p2_y);
                    c.lineTo(p3_x, p3_y);
                    c.lineTo(p4_x, p4_y);
                }

                c.closePath();
                c.restore();
            }

            // Draw Gear Internals (Design details)
            function drawGearDetails(c, x, y, teeth, pcd, mod, angleOffset, themeColor) {
                const r_pitch = pcd / 2;
                
                c.save();
                c.translate(x, y);

                // 1. Draw outer teeth filled path
                drawGearOutline(c, teeth, pcd, mod, angleOffset);
                c.fillStyle = 'rgba(241, 245, 249, 0.85)';
                c.fill();
                c.strokeStyle = themeColor;
                c.lineWidth = 2.5;
                c.shadowBlur = 8;
                c.shadowColor = themeColor;
                c.stroke();
                c.shadowBlur = 0;

                // 2. Draw Pitch Circle (light dotted)
                c.beginPath();
                c.arc(0, 0, r_pitch, 0, Math.PI * 2);
                c.strokeStyle = 'rgba(15, 23, 42, 0.15)';
                c.lineWidth = 1;
                c.setLineDash([4, 4]);
                c.stroke();
                c.setLineDash([]);

                // 3. Draw internal cutouts and spokes
                c.save();
                c.rotate(angleOffset);
                
                // Shaft central hub
                c.beginPath();
                c.arc(0, 0, Math.max(r_pitch * 0.25, 12), 0, Math.PI * 2);
                c.strokeStyle = 'rgba(15, 23, 42, 0.25)';
                c.lineWidth = 2;
                c.stroke();

                c.beginPath();
                c.arc(0, 0, 5, 0, Math.PI * 2);
                c.fillStyle = '#ffffff';
                c.fill();

                // Spokes
                if (teeth >= 12) {
                    const spokes = teeth >= 30 ? 6 : 4;
                    for (let s = 0; s < spokes; s++) {
                        const spokeAngle = (s * 2 * Math.PI) / spokes;
                        c.save();
                        c.rotate(spokeAngle);
                        c.beginPath();
                        c.moveTo(0, Math.max(r_pitch * 0.25, 12));
                        c.lineTo(0, r_pitch - 10);
                        c.strokeStyle = 'rgba(15, 23, 42, 0.1)';
                        c.lineWidth = spokes === 6 ? 3 : 4;
                        c.stroke();
                        c.restore();
                    }
                }
                c.restore();

                // 4. Index marker (draw small dot on one tooth to visualize rotation easily)
                c.save();
                c.rotate(angleOffset);
                c.beginPath();
                c.arc(r_pitch - 6, 0, 3.5, 0, Math.PI * 2);
                c.fillStyle = '#ffffff';
                c.shadowBlur = 4;
                c.shadowColor = '#ffffff';
                c.fill();
                c.restore();

                c.restore();
            }

            function drawDiameterLeader(c, x, y, r, valStr, labelPrefix, side) {
                c.save();
                c.strokeStyle = 'rgba(71, 85, 105, 0.4)';
                c.lineWidth = 1.2;
                
                // Angle of leader line (45 degrees up-left or up-right)
                const angle = side === -1 ? -Math.PI * 0.75 : -Math.PI * 0.25;
                const px = x + Math.cos(angle) * r;
                const py = y + Math.sin(angle) * r;
                
                // Draw a beautiful small dot on the circle boundary
                c.beginPath();
                c.arc(px, py, 2.5, 0, Math.PI * 2);
                c.fillStyle = side === -1 ? '#0284c7' : '#db2777';
                c.fill();
                
                // Draw leader lines: from circle boundary diagonal outwards, then horizontal
                const lx1 = px + Math.cos(angle) * 20;
                const ly1 = py + Math.sin(angle) * 20;
                const lx2 = lx1 + side * 45;
                const ly2 = ly1;
                
                c.beginPath();
                c.moveTo(px, py);
                c.lineTo(lx1, ly1);
                c.lineTo(lx2, ly2);
                c.stroke();
                
                // Draw text above the horizontal line
                const text = `${labelPrefix} ${valStr}`;
                c.font = 'bold 10px var(--font-body)';
                
                const tx = side === -1 ? lx1 - 4 : lx1 + 4;
                c.fillStyle = '#1e293b';
                c.textAlign = side === -1 ? 'right' : 'left';
                c.textBaseline = 'bottom';
                c.fillText(text, tx, ly2 - 1);
                c.restore();
            }

            // Main Animation loop
            function animate(currentTime) {
                requestAnimationFrame(animate);

                let dt = (currentTime - state.lastTime) / 1000;
                if (isNaN(dt) || dt < 0 || dt > 1.0) dt = 0.016;
                state.lastTime = currentTime;

                const width = canvas.width / (window.devicePixelRatio || 1);
                const height = canvas.height / (window.devicePixelRatio || 1);
                const cx = width / 2;
                const cy = height / 2;

                ctx.clearRect(0, 0, width, height);

                // 1. Draw Grid
                ctx.save();
                ctx.strokeStyle = 'rgba(2, 132, 199, 0.06)';
                ctx.lineWidth = 1;
                for (let x = 0; x < width; x += 30) {
                    ctx.beginPath();
                    ctx.moveTo(x, 0);
                    ctx.lineTo(x, height);
                    ctx.stroke();
                }
                for (let y = 0; y < height; y += 30) {
                    ctx.beginPath();
                    ctx.moveTo(0, y);
                    ctx.lineTo(width, y);
                    ctx.stroke();
                }
                ctx.restore();

                // 2. Mesh physics calculations
                const d1 = state.m * state.z1;
                const d2 = state.m * state.z2;
                const r1 = d1 / 2;
                const r2 = d2 / 2;
                const centerDist = r1 + r2;

                // Scale visualization to fit screen dynamically
                // Mathematical bounding model to prevent any dimension label clipping on the left/right
                const totalWidthRequired = 1.707 * centerDist + 210;
                const totalHeightRequired = Math.max(d1, d2) + 120;
                const scale = Math.min(width / totalWidthRequired, height / totalHeightRequired);
                
                // Floating indicator scale update
                const txtCanvasScale = document.getElementById('txt-canvas-scale');
                if (txtCanvasScale) {
                    txtCanvasScale.innerText = `스케일 비율: 1:${(1/scale * 100).toFixed(0)}급`;
                }

                // Centering centers
                const x1 = cx - (centerDist / 2) * scale;
                const y1 = cy;
                const x2 = cx + (centerDist / 2) * scale;
                const y2 = cy;

                const gearRatio = state.z2 / state.z1;

                if (state.isPlaying) {
                    if (state.rpm > 0) {
                        const omega1 = (2 * Math.PI * state.rpm) / 60; // rad/s
                        
                        // Apply stroboscopic limit for fast rotation rendering
                        const maxRenderOmega = 6;
                        const renderOmega1 = maxRenderOmega * (1 - Math.exp(-omega1 / maxRenderOmega));
                        
                        state.angle1 += state.direction * renderOmega1 * dt;
                        state.angle2 = -state.angle1 / gearRatio;
                    }
                }

                // Mesh Point = exact touching tangent point of pitch circles
                const mesh_x = x1 + r1 * scale;
                const mesh_y = cy;

                // 3. Spawning meshing sparks at contact point
                if (state.isPlaying) {
                    if (state.rpm > 0) {
                        const sparkEmission = Math.min(Math.floor(state.rpm / 200) + 1, 3);
                        for (let s = 0; s < sparkEmission; s++) {
                            if (Math.random() > 0.4) {
                                const sparkVx = (Math.random() - 0.5) * 2;
                                const sparkVy = -Math.random() * (state.rpm / 150 + 1.5) - 0.5;
                                state.sparks.push(new GearSpark(mesh_x, mesh_y, sparkVx, sparkVy));
                            }
                        }
                    }
                }

                // Render Sparks
                state.sparks = state.sparks.filter(s => s.life > 0);
                state.sparks.forEach(s => {
                    s.update();
                    s.draw(ctx);
                });

                // 4. Draw Center Distance dimensions
                ctx.save();
                ctx.beginPath();
                ctx.moveTo(x1, y1);
                ctx.lineTo(x2, y2);
                ctx.strokeStyle = 'rgba(0, 0, 0, 0.05)';
                ctx.lineWidth = 1.5;
                ctx.setLineDash([4, 4]);
                ctx.stroke();
                ctx.restore();

                // Draw Gear 1 (Drive - Cyan)
                // Add a small initial tooth phase offset so meshing matches perfectly
                const phase1 = 0;
                drawGearDetails(ctx, x1, y1, state.z1, d1 * scale, state.m * scale, state.angle1 + phase1, '#0284c7');

                // Draw Gear 2 (Driven - Magenta)
                // For perfect tooth meshing: Gear 2 tooth needs to align to Gear 1 valley.
                // An angular phase shift of Math.PI / state.z2 aligns teeth perfectly.
                const phase2 = Math.PI + Math.PI / state.z2;
                drawGearDetails(ctx, x2, y2, state.z2, d2 * scale, state.m * scale, state.angle2 + phase2, '#db2777');

                // 5. Contact pitch point glow (small cyan glowing ring)
                ctx.save();
                ctx.beginPath();
                ctx.arc(mesh_x, mesh_y, 6, 0, Math.PI * 2);
                ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
                ctx.shadowBlur = 10;
                ctx.shadowColor = '#0284c7';
                ctx.fill();
                ctx.restore();

                // 6. Draw Diameter dimensions on gears with professional CAD leader lines
                drawDiameterLeader(ctx, x1, y1, r1 * scale, d1.toFixed(1) + ' mm', 'PCD₁ =', -1);
                drawDiameterLeader(ctx, x2, y2, r2 * scale, d2.toFixed(1) + ' mm', 'PCD₂ =', 1);

                // Horizontal Center Distance Dimension Line (C)
                ctx.save();
                ctx.strokeStyle = 'rgba(71, 85, 105, 0.3)';
                ctx.lineWidth = 1;
                
                const dimY = y1 + r2 * scale + 25; // 25px below larger gear
                
                ctx.beginPath();
                ctx.moveTo(x1, y1 + 10);
                ctx.lineTo(x1, dimY + 5);
                ctx.moveTo(x2, y2 + 10);
                ctx.lineTo(x2, dimY + 5);
                ctx.stroke();
                
                ctx.beginPath();
                ctx.moveTo(x1, dimY);
                ctx.lineTo(x2, dimY);
                ctx.stroke();
                
                function drawHorizontalArrowhead(px, py, direction) {
                    ctx.save();
                    ctx.translate(px, py);
                    ctx.fillStyle = 'rgba(71, 85, 105, 0.5)';
                    ctx.beginPath();
                    ctx.moveTo(0, 0);
                    ctx.lineTo(direction * 6, -3);
                    ctx.lineTo(direction * 6, 3);
                    ctx.closePath();
                    ctx.fill();
                    ctx.restore();
                }
                drawHorizontalArrowhead(x1, dimY, 1);
                drawHorizontalArrowhead(x2, dimY, -1);
                
                const cVal = (d1 + d2) / 2;
                const cText = `C = ${cVal.toFixed(1)} mm`;
                ctx.font = 'bold 9px var(--font-body)';
                const cTw = ctx.measureText(cText).width;
                
                ctx.fillStyle = '#ffffff';
                ctx.fillRect((x1 + x2)/2 - cTw/2 - 4, dimY - 5, cTw + 8, 10);
                
                ctx.fillStyle = '#475569';
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                ctx.fillText(cText, (x1 + x2)/2, dimY);
                ctx.restore();
            }

            // Sync Controls
            sliderZ1.addEventListener('input', syncZ1FromSlider);
            inputZ1.addEventListener('change', syncZ1FromInput);
            
            sliderZ2.addEventListener('input', syncZ2FromSlider);
            inputZ2.addEventListener('change', syncZ2FromInput);

            sliderRpm.addEventListener('input', syncRpmFromSlider);
            inputRpm.addEventListener('change', syncRpmFromInput);

            inputModule.addEventListener('change', handleModuleChange);

            btnPlayPause.addEventListener('click', () => {
                state.isPlaying = !state.isPlaying;
                if (state.isPlaying) {
                    btnPlayPause.innerHTML = '<i class="fa-solid fa-pause"></i> <span>일시정지</span>';
                } else {
                    btnPlayPause.innerHTML = '<i class="fa-solid fa-play"></i> <span>재생</span>';
                }
            });

            btnReverse.addEventListener('click', () => {
                state.direction *= -1;
            });

            presetButtons.forEach(btn => {
                btn.addEventListener('click', () => {
                    loadPreset(btn.dataset.preset);
                });
            });

            // Initialize calculations and presets
            loadPreset('reduction');
            requestAnimationFrame(animate);

            // ── 우클릭 & 소스 복사 방지 (전체 문서 레벨 + 알림창 동기화) ──
            document.addEventListener('contextmenu', function(e) {
                e.preventDefault();
                alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                return false;
            }, { capture: true });

            document.addEventListener('selectstart', function(e) {
                e.preventDefault();
                return false;
            }, { capture: true });

            // 개발자도구 단축키 및 복사 단축키 차단 (F12, Ctrl+U, Ctrl+Shift+I/J/C, Ctrl+C, Ctrl+S)
            // 중복 차단 문자를 사용하면 WordPress API에서 오류가 날 수 있으므로 nested if로 구현
            document.addEventListener('keydown', function(e) {
                if (e.key === 'F12') {
                    e.preventDefault();
                    alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                    return false;
                }
                if (e.ctrlKey) {
                    if (e.key === 'u' || e.key === 'c' || e.key === 's' || e.key === 'U' || e.key === 'C' || e.key === 'S') {
                        e.preventDefault();
                        alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                        return false;
                    }
                    if (e.shiftKey) {
                        if (['i','I','j','J','c','C'].includes(e.key)) {
                            e.preventDefault();
                            alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                            return false;
                        }
                    }
                }
            }, { capture: true });
        } // <-- Closed the initGearSimulator function

        // Robust ReadyState hook-up with polling to guarantee execution even if DOM elements load late
        let initAttempts = 0;
        function tryInit() {
            initAttempts++;
            initGearSimulator();
            if (!window.__gearratio_initialized) {
                if (initAttempts < 50) {
                    setTimeout(tryInit, 100);
                }
            }
        }

        if (document.readyState === 'complete' || document.readyState === 'interactive') {
            tryInit();
        } else {
            document.addEventListener('DOMContentLoaded', tryInit);
            window.addEventListener('load', tryInit);
        }
        })();
    </script>

    </div>
</div>

<script>
        (function() {
        let isGearInit = false;

        function initGearSimulator() {
            if (isGearInit) return;
            
            const canvas = document.getElementById('physics-canvas');
            if (!canvas) return;
            const ctx = canvas.getContext('2d');
            if (!ctx) return;

            isGearInit = true;
            window.__gearratio_initialized = true;

            // DOM Elements
            const sliderZ1 = document.getElementById('slider-z1');
            const inputZ1 = document.getElementById('input-z1');
            const sliderZ2 = document.getElementById('slider-z2');
            const inputZ2 = document.getElementById('input-z2');
            const sliderRpm = document.getElementById('slider-rpm');
            const inputRpm = document.getElementById('input-rpm');
            const inputModule = document.getElementById('input-module');
            
            const btnPlayPause = document.getElementById('btn-play-pause');
            const btnReverse = document.getElementById('btn-reverse');
            const presetButtons = document.querySelectorAll('.preset-btn');
            
            const txtCenterDist = document.getElementById('txt-center-dist');
            const txtPitchSpeed = document.getElementById('txt-pitch-speed');
            const txtMeshFrequency = document.getElementById('txt-mesh-frequency');
            
            const txtGearRatio = document.getElementById('txt-gear-ratio');
            const txtRatioType = document.getElementById('txt-ratio-type');
            
            const txtD1 = document.getElementById('txt-d1');
            const txtD2 = document.getElementById('txt-d2');
            const txtN2 = document.getElementById('txt-n2');
            const txtTorqueMult = document.getElementById('txt-torque-mult');
            
            const wrapper = document.querySelector('.gearratio-calculator-wrapper') || document.body;

            // State variables
            const state = {
                z1: 12,
                z2: 24,
                rpm: 120,
                m: 3.0,
                angle1: 0,
                angle2: 0,
                isPlaying: true,
                direction: 1,
                lastTime: 0,
                sparks: []
            };

            // Canvas Resizing (Retina support)
            function resizeCanvas() {
                const rect = canvas.getBoundingClientRect();
                const dpr = window.devicePixelRatio || 1;
                canvas.width = rect.width * dpr;
                canvas.height = rect.height * dpr;
                ctx.scale(dpr, dpr);
            }

            resizeCanvas();
            window.addEventListener('resize', resizeCanvas);
            // Polling backup to guarantee resize
            setTimeout(resizeCanvas, 300);

            // Spark Particle System
            class GearSpark {
                constructor(x, y, vx, vy) {
                    this.x = x;
                    this.y = y;
                    this.vx = vx;
                    this.vy = vy;
                    this.size = Math.random() * 1.5 + 0.5;
                    this.life = Math.random() * 20 + 10;
                    this.maxLife = this.life;
                    const colors = ['#00f2fe', '#ff007f', '#ffffff'];
                    this.color = colors[Math.floor(Math.random() * colors.length)];
                }
                update() {
                    this.x += this.vx;
                    this.y += this.vy;
                    this.vy += 0.05; // soft gravity
                    this.life--;
                }
                draw(c) {
                    const alpha = this.life / this.maxLife;
                    c.save();
                    c.globalAlpha = alpha;
                    c.beginPath();
                    c.arc(this.x, this.y, this.size, 0, Math.PI * 2);
                    c.fillStyle = this.color;
                    c.shadowBlur = 4;
                    c.shadowColor = this.color;
                    c.fill();
                    c.restore();
                }
            }

            // Sync Z1 Input and Slider
            function syncZ1FromSlider() {
                const val = parseInt(sliderZ1.value);
                inputZ1.value = val;
                state.z1 = val;
                clearPresets();
                updateCalculations();
            }
            function syncZ1FromInput() {
                let val = parseInt(inputZ1.value);
                if (isNaN(val) || val < 8) val = 8;
                if (val > 100) val = 100;
                sliderZ1.value = val;
                state.z1 = val;
                clearPresets();
                updateCalculations();
            }

            // Sync Z2 Input and Slider
            function syncZ2FromSlider() {
                const val = parseInt(sliderZ2.value);
                inputZ2.value = val;
                state.z2 = val;
                clearPresets();
                updateCalculations();
            }
            function syncZ2FromInput() {
                let val = parseInt(inputZ2.value);
                if (isNaN(val) || val < 8) val = 8;
                if (val > 100) val = 100;
                sliderZ2.value = val;
                state.z2 = val;
                clearPresets();
                updateCalculations();
            }

            // Sync RPM Input and Slider
            function syncRpmFromSlider() {
                const val = parseInt(sliderRpm.value);
                inputRpm.value = val;
                state.rpm = val;
                clearPresets();
                updateCalculations();
            }
            function syncRpmFromInput() {
                let val = parseInt(inputRpm.value);
                if (isNaN(val) || val < 0) val = 0;
                if (val > 3000) val = 3000;
                sliderRpm.value = val;
                state.rpm = val;
                clearPresets();
                updateCalculations();
            }

            // Sync Module
            function handleModuleChange() {
                let val = parseFloat(inputModule.value);
                if (isNaN(val) || val < 1.0) val = 1.0;
                if (val > 8.0) val = 8.0;
                state.m = val;
                updateCalculations();
            }

            // Core Math Calculations
            function updateCalculations() {
                const d1 = state.m * state.z1;
                const d2 = state.m * state.z2;
                const centerDist = (d1 + d2) / 2;
                const gearRatio = state.z2 / state.z1;
                const n2 = state.rpm / gearRatio;
                
                // Pitch Line Speed = omega * radius
                // PCD in mm, RPM -> linear velocity in m/s
                const r1_meters = (d1 / 2) / 1000;
                const omega1 = (2 * Math.PI * state.rpm) / 60;
                const pitchSpeed = omega1 * r1_meters;
                
                // Meshing frequency = frequency of gear teeth mesh
                const meshFreq = (state.rpm / 60) * state.z1;

                // Update UI Texts
                txtCenterDist.innerText = centerDist.toFixed(1) + ' mm';
                txtPitchSpeed.innerText = pitchSpeed.toFixed(2) + ' m/s';
                txtMeshFrequency.innerText = meshFreq.toFixed(1) + ' Hz';
                
                txtGearRatio.innerText = gearRatio.toFixed(2) + ' : 1';
                
                if (gearRatio > 1.0) {
                    txtRatioType.innerText = `${gearRatio.toFixed(2)}배 감속 (토크 ${gearRatio.toFixed(2)}배 증대)`;
                } else if (gearRatio < 1.0) {
                    const gain = 1 / gearRatio;
                    txtRatioType.innerText = `${gain.toFixed(2)}배 증속 (토크 ${gearRatio.toFixed(2)}배 감소)`;
                } else {
                    txtRatioType.innerText = '1:1 등속 동력 전달';
                }

                txtD1.innerText = d1.toFixed(1) + ' mm';
                txtD2.innerText = d2.toFixed(1) + ' mm';
                txtN2.innerText = n2.toFixed(1) + ' RPM';
                txtTorqueMult.innerText = (gearRatio * 100).toFixed(0) + '%';
            }

            // Preset Buttons Click Handler
            function loadPreset(key) {
                clearPresets();
                presetButtons.forEach(btn => {
                    if (btn.dataset.preset === key) {
                        btn.classList.add('active');
                    }
                });

                if (key === 'reduction') {
                    state.z1 = 12;
                    state.z2 = 48;
                    state.rpm = 200;
                    state.m = 2.5;
                } else if (key === 'mesh1to1') {
                    state.z1 = 24;
                    state.z2 = 24;
                    state.rpm = 150;
                    state.m = 3.0;
                } else if (key === 'increase') {
                    state.z1 = 36;
                    state.z2 = 12;
                    state.rpm = 100;
                    state.m = 3.0;
                }

                // Sync inputs
                inputZ1.value = state.z1;
                sliderZ1.value = state.z1;
                inputZ2.value = state.z2;
                sliderZ2.value = state.z2;
                inputRpm.value = state.rpm;
                sliderRpm.value = state.rpm;
                inputModule.value = state.m;

                updateCalculations();
            }

            function clearPresets() {
                presetButtons.forEach(btn => btn.classList.remove('active'));
            }

            // Draw Gear Teeth Path
            function drawGearOutline(c, teeth, pcd, mod, angleOffset) {
                const r_pitch = pcd / 2;
                const r_addendum = r_pitch + mod;
                const r_dedendum = r_pitch - 1.25 * mod;

                c.save();
                c.rotate(angleOffset);
                c.beginPath();

                for (let i = 0; i < teeth; i++) {
                    const stepAngle = (i * 2 * Math.PI) / teeth;
                    
                    // Teeth geometry angles
                    const t0 = stepAngle - (2 * Math.PI / teeth) * 0.22;
                    const t1 = stepAngle - (2 * Math.PI / teeth) * 0.10;
                    const t2 = stepAngle + (2 * Math.PI / teeth) * 0.10;
                    const t3 = stepAngle + (2 * Math.PI / teeth) * 0.22;
                    const t4 = stepAngle + (2 * Math.PI / teeth) * 0.50; // valley mid point

                    const p0_x = Math.cos(t0) * r_dedendum;
                    const p0_y = Math.sin(t0) * r_dedendum;
                    
                    const p1_x = Math.cos(t1) * r_addendum;
                    const p1_y = Math.sin(t1) * r_addendum;
                    
                    const p2_x = Math.cos(t2) * r_addendum;
                    const p2_y = Math.sin(t2) * r_addendum;
                    
                    const p3_x = Math.cos(t3) * r_dedendum;
                    const p3_y = Math.sin(t3) * r_dedendum;
                    
                    const p4_x = Math.cos(t4) * r_dedendum;
                    const p4_y = Math.sin(t4) * r_dedendum;

                    if (i === 0) {
                        c.moveTo(p0_x, p0_y);
                    } else {
                        c.lineTo(p0_x, p0_y);
                    }
                    c.lineTo(p1_x, p1_y);
                    c.lineTo(p2_x, p2_y);
                    c.lineTo(p3_x, p3_y);
                    c.lineTo(p4_x, p4_y);
                }

                c.closePath();
                c.restore();
            }

            // Draw Gear Internals (Design details)
            function drawGearDetails(c, x, y, teeth, pcd, mod, angleOffset, themeColor) {
                const r_pitch = pcd / 2;
                
                c.save();
                c.translate(x, y);

                // 1. Draw outer teeth filled path
                drawGearOutline(c, teeth, pcd, mod, angleOffset);
                c.fillStyle = 'rgba(241, 245, 249, 0.85)';
                c.fill();
                c.strokeStyle = themeColor;
                c.lineWidth = 2.5;
                c.shadowBlur = 8;
                c.shadowColor = themeColor;
                c.stroke();
                c.shadowBlur = 0;

                // 2. Draw Pitch Circle (light dotted)
                c.beginPath();
                c.arc(0, 0, r_pitch, 0, Math.PI * 2);
                c.strokeStyle = 'rgba(15, 23, 42, 0.15)';
                c.lineWidth = 1;
                c.setLineDash([4, 4]);
                c.stroke();
                c.setLineDash([]);

                // 3. Draw internal cutouts and spokes
                c.save();
                c.rotate(angleOffset);
                
                // Shaft central hub
                c.beginPath();
                c.arc(0, 0, Math.max(r_pitch * 0.25, 12), 0, Math.PI * 2);
                c.strokeStyle = 'rgba(15, 23, 42, 0.25)';
                c.lineWidth = 2;
                c.stroke();

                c.beginPath();
                c.arc(0, 0, 5, 0, Math.PI * 2);
                c.fillStyle = '#ffffff';
                c.fill();

                // Spokes
                if (teeth >= 12) {
                    const spokes = teeth >= 30 ? 6 : 4;
                    for (let s = 0; s < spokes; s++) {
                        const spokeAngle = (s * 2 * Math.PI) / spokes;
                        c.save();
                        c.rotate(spokeAngle);
                        c.beginPath();
                        c.moveTo(0, Math.max(r_pitch * 0.25, 12));
                        c.lineTo(0, r_pitch - 10);
                        c.strokeStyle = 'rgba(15, 23, 42, 0.1)';
                        c.lineWidth = spokes === 6 ? 3 : 4;
                        c.stroke();
                        c.restore();
                    }
                }
                c.restore();

                // 4. Index marker (draw small dot on one tooth to visualize rotation easily)
                c.save();
                c.rotate(angleOffset);
                c.beginPath();
                c.arc(r_pitch - 6, 0, 3.5, 0, Math.PI * 2);
                c.fillStyle = '#ffffff';
                c.shadowBlur = 4;
                c.shadowColor = '#ffffff';
                c.fill();
                c.restore();

                c.restore();
            }

            function drawDiameterLeader(c, x, y, r, valStr, labelPrefix, side) {
                c.save();
                c.strokeStyle = 'rgba(71, 85, 105, 0.4)';
                c.lineWidth = 1.2;
                
                // Angle of leader line (45 degrees up-left or up-right)
                const angle = side === -1 ? -Math.PI * 0.75 : -Math.PI * 0.25;
                const px = x + Math.cos(angle) * r;
                const py = y + Math.sin(angle) * r;
                
                // Draw a beautiful small dot on the circle boundary
                c.beginPath();
                c.arc(px, py, 2.5, 0, Math.PI * 2);
                c.fillStyle = side === -1 ? '#0284c7' : '#db2777';
                c.fill();
                
                // Draw leader lines: from circle boundary diagonal outwards, then horizontal
                const lx1 = px + Math.cos(angle) * 20;
                const ly1 = py + Math.sin(angle) * 20;
                const lx2 = lx1 + side * 45;
                const ly2 = ly1;
                
                c.beginPath();
                c.moveTo(px, py);
                c.lineTo(lx1, ly1);
                c.lineTo(lx2, ly2);
                c.stroke();
                
                // Draw text above the horizontal line
                const text = `${labelPrefix} ${valStr}`;
                c.font = 'bold 10px var(--font-body)';
                
                const tx = side === -1 ? lx1 - 4 : lx1 + 4;
                c.fillStyle = '#1e293b';
                c.textAlign = side === -1 ? 'right' : 'left';
                c.textBaseline = 'bottom';
                c.fillText(text, tx, ly2 - 1);
                c.restore();
            }

            // Main Animation loop
            function animate(currentTime) {
                requestAnimationFrame(animate);

                let dt = (currentTime - state.lastTime) / 1000;
                if (isNaN(dt) || dt < 0 || dt > 1.0) dt = 0.016;
                state.lastTime = currentTime;

                const width = canvas.width / (window.devicePixelRatio || 1);
                const height = canvas.height / (window.devicePixelRatio || 1);
                const cx = width / 2;
                const cy = height / 2;

                ctx.clearRect(0, 0, width, height);

                // 1. Draw Grid
                ctx.save();
                ctx.strokeStyle = 'rgba(2, 132, 199, 0.06)';
                ctx.lineWidth = 1;
                for (let x = 0; x < width; x += 30) {
                    ctx.beginPath();
                    ctx.moveTo(x, 0);
                    ctx.lineTo(x, height);
                    ctx.stroke();
                }
                for (let y = 0; y < height; y += 30) {
                    ctx.beginPath();
                    ctx.moveTo(0, y);
                    ctx.lineTo(width, y);
                    ctx.stroke();
                }
                ctx.restore();

                // 2. Mesh physics calculations
                const d1 = state.m * state.z1;
                const d2 = state.m * state.z2;
                const r1 = d1 / 2;
                const r2 = d2 / 2;
                const centerDist = r1 + r2;

                // Scale visualization to fit screen dynamically
                // Mathematical bounding model to prevent any dimension label clipping on the left/right
                const totalWidthRequired = 1.707 * centerDist + 210;
                const totalHeightRequired = Math.max(d1, d2) + 120;
                const scale = Math.min(width / totalWidthRequired, height / totalHeightRequired);
                
                // Floating indicator scale update
                const txtCanvasScale = document.getElementById('txt-canvas-scale');
                if (txtCanvasScale) {
                    txtCanvasScale.innerText = `스케일 비율: 1:${(1/scale * 100).toFixed(0)}급`;
                }

                // Centering centers
                const x1 = cx - (centerDist / 2) * scale;
                const y1 = cy;
                const x2 = cx + (centerDist / 2) * scale;
                const y2 = cy;

                const gearRatio = state.z2 / state.z1;

                if (state.isPlaying) {
                    if (state.rpm > 0) {
                        const omega1 = (2 * Math.PI * state.rpm) / 60; // rad/s
                        
                        // Apply stroboscopic limit for fast rotation rendering
                        const maxRenderOmega = 6;
                        const renderOmega1 = maxRenderOmega * (1 - Math.exp(-omega1 / maxRenderOmega));
                        
                        state.angle1 += state.direction * renderOmega1 * dt;
                        state.angle2 = -state.angle1 / gearRatio;
                    }
                }

                // Mesh Point = exact touching tangent point of pitch circles
                const mesh_x = x1 + r1 * scale;
                const mesh_y = cy;

                // 3. Spawning meshing sparks at contact point
                if (state.isPlaying) {
                    if (state.rpm > 0) {
                        const sparkEmission = Math.min(Math.floor(state.rpm / 200) + 1, 3);
                        for (let s = 0; s < sparkEmission; s++) {
                            if (Math.random() > 0.4) {
                                const sparkVx = (Math.random() - 0.5) * 2;
                                const sparkVy = -Math.random() * (state.rpm / 150 + 1.5) - 0.5;
                                state.sparks.push(new GearSpark(mesh_x, mesh_y, sparkVx, sparkVy));
                            }
                        }
                    }
                }

                // Render Sparks
                state.sparks = state.sparks.filter(s => s.life > 0);
                state.sparks.forEach(s => {
                    s.update();
                    s.draw(ctx);
                });

                // 4. Draw Center Distance dimensions
                ctx.save();
                ctx.beginPath();
                ctx.moveTo(x1, y1);
                ctx.lineTo(x2, y2);
                ctx.strokeStyle = 'rgba(0, 0, 0, 0.05)';
                ctx.lineWidth = 1.5;
                ctx.setLineDash([4, 4]);
                ctx.stroke();
                ctx.restore();

                // Draw Gear 1 (Drive - Cyan)
                // Add a small initial tooth phase offset so meshing matches perfectly
                const phase1 = 0;
                drawGearDetails(ctx, x1, y1, state.z1, d1 * scale, state.m * scale, state.angle1 + phase1, '#0284c7');

                // Draw Gear 2 (Driven - Magenta)
                // For perfect tooth meshing: Gear 2 tooth needs to align to Gear 1 valley.
                // An angular phase shift of Math.PI / state.z2 aligns teeth perfectly.
                const phase2 = Math.PI + Math.PI / state.z2;
                drawGearDetails(ctx, x2, y2, state.z2, d2 * scale, state.m * scale, state.angle2 + phase2, '#db2777');

                // 5. Contact pitch point glow (small cyan glowing ring)
                ctx.save();
                ctx.beginPath();
                ctx.arc(mesh_x, mesh_y, 6, 0, Math.PI * 2);
                ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
                ctx.shadowBlur = 10;
                ctx.shadowColor = '#0284c7';
                ctx.fill();
                ctx.restore();

                // 6. Draw Diameter dimensions on gears with professional CAD leader lines
                drawDiameterLeader(ctx, x1, y1, r1 * scale, d1.toFixed(1) + ' mm', 'PCD₁ =', -1);
                drawDiameterLeader(ctx, x2, y2, r2 * scale, d2.toFixed(1) + ' mm', 'PCD₂ =', 1);

                // Horizontal Center Distance Dimension Line (C)
                ctx.save();
                ctx.strokeStyle = 'rgba(71, 85, 105, 0.3)';
                ctx.lineWidth = 1;
                
                const dimY = y1 + r2 * scale + 25; // 25px below larger gear
                
                ctx.beginPath();
                ctx.moveTo(x1, y1 + 10);
                ctx.lineTo(x1, dimY + 5);
                ctx.moveTo(x2, y2 + 10);
                ctx.lineTo(x2, dimY + 5);
                ctx.stroke();
                
                ctx.beginPath();
                ctx.moveTo(x1, dimY);
                ctx.lineTo(x2, dimY);
                ctx.stroke();
                
                function drawHorizontalArrowhead(px, py, direction) {
                    ctx.save();
                    ctx.translate(px, py);
                    ctx.fillStyle = 'rgba(71, 85, 105, 0.5)';
                    ctx.beginPath();
                    ctx.moveTo(0, 0);
                    ctx.lineTo(direction * 6, -3);
                    ctx.lineTo(direction * 6, 3);
                    ctx.closePath();
                    ctx.fill();
                    ctx.restore();
                }
                drawHorizontalArrowhead(x1, dimY, 1);
                drawHorizontalArrowhead(x2, dimY, -1);
                
                const cVal = (d1 + d2) / 2;
                const cText = `C = ${cVal.toFixed(1)} mm`;
                ctx.font = 'bold 9px var(--font-body)';
                const cTw = ctx.measureText(cText).width;
                
                ctx.fillStyle = '#ffffff';
                ctx.fillRect((x1 + x2)/2 - cTw/2 - 4, dimY - 5, cTw + 8, 10);
                
                ctx.fillStyle = '#475569';
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                ctx.fillText(cText, (x1 + x2)/2, dimY);
                ctx.restore();
            }

            // Sync Controls
            sliderZ1.addEventListener('input', syncZ1FromSlider);
            inputZ1.addEventListener('change', syncZ1FromInput);
            
            sliderZ2.addEventListener('input', syncZ2FromSlider);
            inputZ2.addEventListener('change', syncZ2FromInput);

            sliderRpm.addEventListener('input', syncRpmFromSlider);
            inputRpm.addEventListener('change', syncRpmFromInput);

            inputModule.addEventListener('change', handleModuleChange);

            btnPlayPause.addEventListener('click', () => {
                state.isPlaying = !state.isPlaying;
                if (state.isPlaying) {
                    btnPlayPause.innerHTML = '<i class="fa-solid fa-pause"></i> <span>일시정지</span>';
                } else {
                    btnPlayPause.innerHTML = '<i class="fa-solid fa-play"></i> <span>재생</span>';
                }
            });

            btnReverse.addEventListener('click', () => {
                state.direction *= -1;
            });

            presetButtons.forEach(btn => {
                btn.addEventListener('click', () => {
                    loadPreset(btn.dataset.preset);
                });
            });

            // Initialize calculations and presets
            loadPreset('reduction');
            requestAnimationFrame(animate);

            // ── 우클릭 & 소스 복사 방지 (전체 문서 레벨 + 알림창 동기화) ──
            document.addEventListener('contextmenu', function(e) {
                e.preventDefault();
                alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                return false;
            }, { capture: true });

            document.addEventListener('selectstart', function(e) {
                e.preventDefault();
                return false;
            }, { capture: true });

            // 개발자도구 단축키 및 복사 단축키 차단 (F12, Ctrl+U, Ctrl+Shift+I/J/C, Ctrl+C, Ctrl+S)
            // 중복 차단 문자를 사용하면 WordPress API에서 오류가 날 수 있으므로 nested if로 구현
            document.addEventListener('keydown', function(e) {
                if (e.key === 'F12') {
                    e.preventDefault();
                    alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                    return false;
                }
                if (e.ctrlKey) {
                    if (e.key === 'u' || e.key === 'c' || e.key === 's' || e.key === 'U' || e.key === 'C' || e.key === 'S') {
                        e.preventDefault();
                        alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                        return false;
                    }
                    if (e.shiftKey) {
                        if (['i','I','j','J','c','C'].includes(e.key)) {
                            e.preventDefault();
                            alert("이 콘텐츠는 저작권법의 보호를 받습니다. 무단 복제 및 우클릭을 금지합니다.");
                            return false;
                        }
                    }
                }
            }, { capture: true });
        } // <-- Closed the initGearSimulator function

        // Robust ReadyState hook-up with polling to guarantee execution even if DOM elements load late
        let initAttempts = 0;
        function tryInit() {
            initAttempts++;
            initGearSimulator();
            if (!window.__gearratio_initialized) {
                if (initAttempts < 50) {
                    setTimeout(tryInit, 100);
                }
            }
        }

        if (document.readyState === 'complete' || document.readyState === 'interactive') {
            tryInit();
        } else {
            document.addEventListener('DOMContentLoaded', tryInit);
            window.addEventListener('load', tryInit);
        }
        })();
    </script>




<div style="background: linear-gradient(135deg, rgba(0,242,254,0.03), rgba(138,43,226,0.03)); border: 1px solid rgba(0,242,254,0.15); border-radius: 12px; padding: 18px 24px; margin: 25px auto 35px auto; font-size: 0.95em; color: #4b5563; line-height: 1.7; font-family: sans-serif;">
    <strong style="color: #1f2937; font-size: 1.05em; display: flex; align-items: center; gap: 8px;">
        <span style="font-size: 1.2em;"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /></span> 간편 사용 설명서
    </strong>
    <ol style="margin: 10px 0 0 0; padding-left: 20px;">
        <li style="margin-bottom: 6px;"><strong>기어 잇수 조절: 슬라이더나 입력 칸을 통해 구동 기어(Gear 1)와 피동 기어(Gear 2)의 잇수(Z)를 설정합니다.</strong></li>
<li style="margin-bottom: 6px;">입력 회전수(RPM) 및 모듈 조절: 구동 기어의 RPM과 기어의 모듈(m) 값을 입력하여 속도와 물리적 크기를 정합니다.</li>
<li style="margin-bottom: 6px;">실시간 기어 맞물림 관찰: 설정에 따라 두 기어가 완벽히 맞물려 회전하는 물리 애니메이션과 접촉면의 실시간 파티클 스파크를 관찰합니다.</li>
<li style="margin-bottom: 6px;">출력 데이터 분석: 기어비, 피치원 지름(PCD), 중심거리, 출력 RPM 및 토크 배율 변화를 실시간 계측 모니터로 정밀 분석합니다.</li>
    </ol>
</div>



<details class="premium-seo-accordion" style="border: 1px solid rgba(0,0,0,0.08); border-radius: 12px; background: #fbfbfc; padding: 0; margin: 30px auto; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.01); font-family: sans-serif;">
    <summary style="display: flex; justify-content: space-between; align-items: center; padding: 20px 24px; font-size: 1.1em; font-weight: 700; color: #1f2937; cursor: pointer; user-select: none; outline: none; list-style: none;">
        <span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 상세 기계공학 해설 및 기어 설계 규격 (KS/ISO) 확인하기</span>
        <span class="accordion-arrow" style="font-size: 0.9em; color: #9ca3af; transition: transform 0.2s ease;">▼</span>
    </summary>
    <div style="padding: 0 24px 24px 24px; border-top: 1px solid rgba(0,0,0,0.04); background: #ffffff; border-radius: 0 0 12px 12px; font-size: 0.98em; color: #374151; line-height: 1.8;">
        <div style="margin-top: 20px;">
            <h3>1. 기어비(Gear Ratio)의 기본 개념 및 기계설계적 의의</h3>
<p>기어비(Gear Ratio, <code>i</code>)는 두 개의 맞물린 기어 사이에서 입력축과 출력축의 회전수 비율 및 토크 변화율을 결정하는 핵심 설계 인자입니다. 동력전달장치 설계에서 기어비는 모터의 회전 속도를 기계 작동에 적합한 속도로 감속시키거나(감속기), 필요한 작동 토크를 증대시키기 위해 정밀하게 조정됩니다.</p><ul><li><strong>속도비 (Speed Ratio):</strong> 기어비가 1보다 크면 출력 속도는 줄어들며, 감속비가 형성됩니다. 반대로 1보다 작으면 증속이 일어납니다.</li><li><strong>토크 변환 (Torque Multiplier):</strong> 에너지 보존 법칙(동력 일정)에 의해 감속 비율에 비례하여 출력 토크가 증가합니다.</li><li><strong>역학적 효율성:</strong> 기어의 형상(이의 치형)에 따라 동력 전달 효율이 달라지며, KS B ISO 규격에 따른 치형 관리가 중요합니다.</li></ul>
<h3>2. 기어 치수 및 역학 공식 관계식 유도</h3>
<p>기어의 기하학적 형상과 맞물림 동역학을 계산하는 공식들은 다음과 같이 유도됩니다.</p><p><strong>① 피치원 지름 (Pitch Circle Diameter, PCD):</strong> 기어가 만나는 가상의 마찰원 지름으로, 모듈(<code>m</code>)과 잇수(<code>Z</code>)의 곱으로 구합니다.</p><p style="text-align: center; font-weight: bold; background: #f3f4f6; padding: 12px; border-radius: 8px;">d = m &times; Z &nbsp;[mm]</p><p><strong>② 기어비 (Gear Ratio, i) 및 출력 속도 (N2):</strong> 구동기어 잇수를 <code>Z1</code>, 피동기어 잇수를 <code>Z2</code>라 할 때, 맞물림 선속도가 일정하므로 잇수 비율에 역비례하여 회전수가 결정됩니다.</p><p style="text-align: center; font-weight: bold; background: #f3f4f6; padding: 12px; border-radius: 8px;">i = Z2 / Z1 = N1 / N2</p><p style="text-align: center; font-weight: bold; background: #e0f2fe; padding: 16px; border-radius: 8px; font-size: 1.05em; color: #0369a1;">N2 = N1 &times; (Z1 / Z2) &nbsp;[RPM]</p><p><strong>③ 축간 중심거리 (Center Distance, a):</strong> 두 기어의 피치원이 외접할 때의 회전축 간 거리입니다.</p><p style="text-align: center; font-weight: bold; background: #f3f4f6; padding: 12px; border-radius: 8px;">a = (d1 + d2) / 2 = m &times; (Z1 + Z2) / 2 &nbsp;[mm]</p>
<h3>3. 전동 효율 및 기어 파손 설계 (루이스 공식)</h3>
<p>기어설계 시 단순히 치수만 맞추는 것이 아니라, 전달할 동력에 따른 이뿌리 굽힘 강도(Bending Strength)와 이면 접촉 응력(Surface Durability)을 검토해야 합니다. 전통적인 굽힘 강도 계산에는 다음과 같은 <strong>루이스 공식(Lewis Formula)</strong>이 기초가 됩니다.</p><p style="text-align: center; font-weight: bold; background: #f3f4f6; padding: 12px; border-radius: 8px;">&sigma;_b = F_t / (b &times; m &times; Y)</p><p>여기서 <code>F_t</code>는 전달 접선력(N), <code>b</code>는 치폭(mm), <code>m</code>은 모듈, <code>Y</code>는 이의 형상에 따른 루이스 치형 계수(Form Factor)입니다. 현대 기계설계 규격(KS B ISO 6336 등)은 이 Lewis 공식을 한 단계 발전시켜 하중분포 계수, 동하중 계수, 속도 계수 등을 종합적으로 반영하여 피로 파손 한계를 평가합니다.</p>

        </div>
    </div>
</details>
<style>
details.premium-seo-accordion[open] summary .accordion-arrow { transform: rotate(180deg); color: #00f2fe; }
details.premium-seo-accordion summary::-webkit-details-marker { display: none; }
details.premium-seo-accordion:hover { border-color: rgba(0,242,254,0.3); }
</style>

]]></content:encoded>
					
					<wfw:commentRss>https://myengnote.com/gear-ratio-calculator-simulator-3/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
