|
<!DOCTYPE html> |
|
<html lang="zh-CN"> |
|
<head> |
|
<meta charset="utf-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
|
<title>AR美甲工作室</title> |
|
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script> |
|
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script> |
|
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script> |
|
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script> |
|
<style> |
|
* { |
|
margin: 0; |
|
padding: 0; |
|
box-sizing: border-box; |
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; |
|
} |
|
body { |
|
background-color: #f8f8f8; |
|
overflow: hidden; |
|
position: fixed; |
|
width: 100%; |
|
height: 100%; |
|
touch-action: none; |
|
} |
|
.container { |
|
position: relative; |
|
width: 100%; |
|
height: 100%; |
|
overflow: hidden; |
|
} |
|
.input_video { |
|
position: absolute; |
|
width: 100%; |
|
height: 100%; |
|
left: 0; |
|
top: 0; |
|
object-fit: cover; |
|
opacity: 0; |
|
pointer-events: none; |
|
} |
|
.output_canvas { |
|
position: absolute; |
|
width: 100%; |
|
height: 100%; |
|
left: 0; |
|
top: 0; |
|
object-fit: cover; |
|
} |
|
.controls-container { |
|
position: absolute; |
|
bottom: 0; |
|
left: 0; |
|
width: 100%; |
|
padding: 20px; |
|
background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent); |
|
z-index: 10; |
|
} |
|
.controls-content { |
|
max-width: 500px; |
|
margin: 0 auto; |
|
} |
|
.control-section { |
|
margin-bottom: 25px; |
|
} |
|
.section-title { |
|
color: white; |
|
font-size: 14px; |
|
font-weight: 600; |
|
margin-bottom: 12px; |
|
letter-spacing: 0.5px; |
|
text-transform: uppercase; |
|
} |
|
.design-options { |
|
display: flex; |
|
overflow-x: auto; |
|
padding-bottom: 8px; |
|
gap: 12px; |
|
scrollbar-width: none; |
|
} |
|
.design-options::-webkit-scrollbar { |
|
display: none; |
|
} |
|
.design-option { |
|
width: 60px; |
|
height: 60px; |
|
border-radius: 12px; |
|
overflow: hidden; |
|
position: relative; |
|
border: 2px solid transparent; |
|
transition: all 0.2s ease; |
|
flex-shrink: 0; |
|
} |
|
.design-option.selected { |
|
border-color: #fff; |
|
transform: scale(1.05); |
|
box-shadow: 0 0 15px rgba(255, 255, 255, 0.5); |
|
} |
|
.slider-container { |
|
margin: 15px 0; |
|
color: white; |
|
} |
|
.slider-label { |
|
display: flex; |
|
justify-content: space-between; |
|
font-size: 12px; |
|
margin-bottom: 8px; |
|
} |
|
.slider { |
|
-webkit-appearance: none; |
|
appearance: none; |
|
width: 100%; |
|
height: 4px; |
|
background: rgba(255, 255, 255, 0.3); |
|
border-radius: 2px; |
|
outline: none; |
|
} |
|
.slider::-webkit-slider-thumb { |
|
-webkit-appearance: none; |
|
appearance: none; |
|
width: 18px; |
|
height: 18px; |
|
border-radius: 50%; |
|
background: white; |
|
cursor: pointer; |
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); |
|
} |
|
.toggle-section { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
} |
|
.toggle-container { |
|
display: flex; |
|
align-items: center; |
|
} |
|
.toggle-label { |
|
color: white; |
|
font-size: 14px; |
|
margin-right: 10px; |
|
} |
|
.toggle { |
|
position: relative; |
|
width: 46px; |
|
height: 24px; |
|
border-radius: 12px; |
|
background-color: rgba(255, 255, 255, 0.3); |
|
transition: all 0.3s ease; |
|
} |
|
.toggle.active { |
|
background-color: #3a86ff; |
|
} |
|
.toggle-handle { |
|
position: absolute; |
|
top: 2px; |
|
left: 2px; |
|
width: 20px; |
|
height: 20px; |
|
border-radius: 50%; |
|
background-color: white; |
|
transition: all 0.3s ease; |
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); |
|
} |
|
.toggle.active .toggle-handle { |
|
left: 24px; |
|
} |
|
.snap-button { |
|
position: absolute; |
|
bottom: 240px; |
|
left: 50%; |
|
transform: translateX(-50%); |
|
width: 64px; |
|
height: 64px; |
|
border-radius: 50%; |
|
background-color: white; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); |
|
z-index: 20; |
|
border: none; |
|
outline: none; |
|
} |
|
.snap-button:active { |
|
transform: translateX(-50%) scale(0.95); |
|
} |
|
.snap-button::after { |
|
content: ''; |
|
width: 52px; |
|
height: 52px; |
|
border-radius: 50%; |
|
border: 2px solid #f8f8f8; |
|
} |
|
.camera-flip { |
|
position: absolute; |
|
top: 20px; |
|
right: 20px; |
|
background: rgba(0, 0, 0, 0.5); |
|
color: white; |
|
border: none; |
|
border-radius: 50%; |
|
width: 40px; |
|
height: 40px; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
z-index: 20; |
|
font-size: 18px; |
|
} |
|
.loading-screen { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
background-color: #000; |
|
display: flex; |
|
flex-direction: column; |
|
justify-content: center; |
|
align-items: center; |
|
z-index: 9999; |
|
transition: opacity 0.5s ease; |
|
} |
|
.loading-spinner { |
|
width: 50px; |
|
height: 50px; |
|
border: 3px solid rgba(255, 255, 255, 0.2); |
|
border-radius: 50%; |
|
border-top-color: white; |
|
animation: spin 1s linear infinite; |
|
margin-bottom: 20px; |
|
} |
|
.loading-text { |
|
color: white; |
|
font-size: 18px; |
|
font-weight: 500; |
|
} |
|
@keyframes spin { |
|
to { |
|
transform: rotate(360deg); |
|
} |
|
} |
|
.app-title { |
|
position: absolute; |
|
top: 20px; |
|
left: 20px; |
|
color: white; |
|
font-size: 18px; |
|
font-weight: 600; |
|
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); |
|
z-index: 20; |
|
} |
|
|
|
.color-block { |
|
width: 100%; |
|
height: 100%; |
|
} |
|
|
|
.camera-permission { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
background-color: rgba(0, 0, 0, 0.8); |
|
display: flex; |
|
flex-direction: column; |
|
justify-content: center; |
|
align-items: center; |
|
z-index: 9998; |
|
color: white; |
|
text-align: center; |
|
padding: 20px; |
|
} |
|
|
|
.camera-permission h2 { |
|
margin-bottom: 20px; |
|
font-size: 24px; |
|
} |
|
|
|
.camera-permission p { |
|
margin-bottom: 30px; |
|
font-size: 16px; |
|
max-width: 500px; |
|
} |
|
|
|
.allow-camera-btn { |
|
background-color: #3a86ff; |
|
color: white; |
|
border: none; |
|
padding: 12px 24px; |
|
border-radius: 24px; |
|
font-size: 16px; |
|
font-weight: 500; |
|
cursor: pointer; |
|
} |
|
|
|
.debug-info { |
|
position: fixed; |
|
top: 70px; |
|
left: 20px; |
|
color: white; |
|
background-color: rgba(0,0,0,0.7); |
|
padding: 10px; |
|
border-radius: 5px; |
|
font-size: 12px; |
|
z-index: 100; |
|
max-width: 300px; |
|
display: none; |
|
} |
|
</style> |
|
</head> |
|
|
|
<body> |
|
<div class="loading-screen"> |
|
<div class="loading-spinner"></div> |
|
<div class="loading-text">正在加载AR美甲工作室...</div> |
|
</div> |
|
|
|
<div class="camera-permission" id="cameraPermission"> |
|
<h2>需要摄像头权限</h2> |
|
<p>AR美甲工作室需要使用您的摄像头来实时显示美甲效果。请点击下方按钮允许使用摄像头。</p> |
|
<button class="allow-camera-btn" id="allowCameraBtn">允许使用摄像头</button> |
|
</div> |
|
|
|
<div class="debug-info" id="debugInfo"></div> |
|
|
|
<div class="container"> |
|
<video class="input_video" playsinline></video> |
|
<canvas class="output_canvas"></canvas> |
|
|
|
<div class="app-title">AR美甲工作室</div> |
|
|
|
<button class="camera-flip"> |
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
|
<path d="M9 16V10H15"></path> |
|
<path d="M15 16L9 10"></path> |
|
<path d="M12 3C16.971 3 21 7.029 21 12C21 16.971 16.971 21 12 21C7.029 21 3 16.971 3 12"></path> |
|
<path d="M3 4V8H7"></path> |
|
</svg> |
|
</button> |
|
|
|
<button class="snap-button"></button> |
|
|
|
<div class="controls-container"> |
|
<div class="controls-content"> |
|
<div class="control-section"> |
|
<div class="section-title">美甲设计</div> |
|
<div class="design-options"> |
|
<div class="design-option selected" data-design="pink"> |
|
<div class="color-block" style="background: linear-gradient(45deg, #ff9a9e, #fad0c4);"></div> |
|
</div> |
|
<div class="design-option" data-design="blue"> |
|
<div class="color-block" style="background: linear-gradient(45deg, #2193b0, #6dd5ed);"></div> |
|
</div> |
|
<div class="design-option" data-design="purple"> |
|
<div class="color-block" style="background: linear-gradient(45deg, #c471f5, #fa71cd);"></div> |
|
</div> |
|
<div class="design-option" data-design="gold"> |
|
<div class="color-block" style="background: linear-gradient(45deg, #f6d365, #fda085);"></div> |
|
</div> |
|
<div class="design-option" data-design="green"> |
|
<div class="color-block" style="background: linear-gradient(45deg, #43c6ac, #f8ffae);"></div> |
|
</div> |
|
<div class="design-option" data-design="black"> |
|
<div class="color-block" style="background: linear-gradient(45deg, #232526, #414345);"></div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="control-section"> |
|
<div class="section-title">调整</div> |
|
|
|
<div class="slider-container"> |
|
<div class="slider-label"> |
|
<span>大小</span> |
|
<span id="size-value">100%</span> |
|
</div> |
|
<input type="range" min="50" max="150" value="100" class="slider" id="size-slider"> |
|
</div> |
|
|
|
<div class="slider-container"> |
|
<div class="slider-label"> |
|
<span>长度</span> |
|
<span id="length-value">100%</span> |
|
</div> |
|
<input type="range" min="80" max="200" value="100" class="slider" id="length-slider"> |
|
</div> |
|
|
|
<div class="slider-container"> |
|
<div class="slider-label"> |
|
<span>透明度</span> |
|
<span id="opacity-value">100%</span> |
|
</div> |
|
<input type="range" min="30" max="100" value="100" class="slider" id="opacity-slider"> |
|
</div> |
|
</div> |
|
|
|
<div class="control-section toggle-section"> |
|
<div class="toggle-container"> |
|
<span class="toggle-label">真实阴影</span> |
|
<div class="toggle active" id="shadow-toggle"> |
|
<div class="toggle-handle"></div> |
|
</div> |
|
</div> |
|
|
|
<div class="toggle-container"> |
|
<span class="toggle-label">显示手部轮廓</span> |
|
<div class="toggle" id="lines-toggle"> |
|
<div class="toggle-handle"></div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
|
|
const nailDesigns = { |
|
pink: { |
|
color: 'linear-gradient(45deg, #ff9a9e, #fad0c4)', |
|
shadowColor: 'rgba(255, 154, 158, 0.5)' |
|
}, |
|
blue: { |
|
color: 'linear-gradient(45deg, #2193b0, #6dd5ed)', |
|
shadowColor: 'rgba(33, 147, 176, 0.5)' |
|
}, |
|
purple: { |
|
color: 'linear-gradient(45deg, #c471f5, #fa71cd)', |
|
shadowColor: 'rgba(196, 113, 245, 0.5)' |
|
}, |
|
gold: { |
|
color: 'linear-gradient(45deg, #f6d365, #fda085)', |
|
shadowColor: 'rgba(246, 211, 101, 0.5)' |
|
}, |
|
green: { |
|
color: 'linear-gradient(45deg, #43c6ac, #f8ffae)', |
|
shadowColor: 'rgba(67, 198, 172, 0.5)' |
|
}, |
|
black: { |
|
color: 'linear-gradient(45deg, #232526, #414345)', |
|
shadowColor: 'rgba(35, 37, 38, 0.5)' |
|
} |
|
}; |
|
|
|
|
|
let currentDesign = 'pink'; |
|
let sizeScale = 1.0; |
|
let lengthScale = 1.0; |
|
let opacity = 1.0; |
|
let showShadows = true; |
|
let showLines = false; |
|
let facingMode = 'environment'; |
|
let camera; |
|
let cameraInitialized = false; |
|
|
|
|
|
let hands; |
|
|
|
|
|
const debugInfo = document.getElementById('debugInfo'); |
|
function showDebug(text) { |
|
debugInfo.style.display = 'block'; |
|
debugInfo.textContent = text; |
|
} |
|
|
|
|
|
const videoElement = document.getElementsByClassName('input_video')[0]; |
|
const canvasElement = document.getElementsByClassName('output_canvas')[0]; |
|
const cameraPermissionElement = document.getElementById('cameraPermission'); |
|
const allowCameraButton = document.getElementById('allowCameraBtn'); |
|
|
|
|
|
canvasElement.width = window.innerWidth; |
|
canvasElement.height = window.innerHeight; |
|
const canvasCtx = canvasElement.getContext('2d'); |
|
|
|
|
|
const sizeSlider = document.getElementById('size-slider'); |
|
const sizeValue = document.getElementById('size-value'); |
|
const lengthSlider = document.getElementById('length-slider'); |
|
const lengthValue = document.getElementById('length-value'); |
|
const opacitySlider = document.getElementById('opacity-slider'); |
|
const opacityValue = document.getElementById('opacity-value'); |
|
const shadowToggle = document.getElementById('shadow-toggle'); |
|
const linesToggle = document.getElementById('lines-toggle'); |
|
const flipButton = document.querySelector('.camera-flip'); |
|
const snapButton = document.querySelector('.snap-button'); |
|
const designOptions = document.querySelectorAll('.design-option'); |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
document.querySelector('.loading-screen').style.opacity = 0; |
|
setTimeout(() => { |
|
document.querySelector('.loading-screen').style.display = 'none'; |
|
}, 500); |
|
}); |
|
|
|
|
|
allowCameraButton.addEventListener('click', () => { |
|
initializeARApp(); |
|
}); |
|
|
|
|
|
function applyNailDesign(ctx, fingerTip, fingerBase, design) { |
|
|
|
const tipX = fingerTip.x * canvasElement.width; |
|
const tipY = fingerTip.y * canvasElement.height; |
|
const baseX = fingerBase.x * canvasElement.width; |
|
const baseY = fingerBase.y * canvasElement.height; |
|
|
|
|
|
const fingerAngle = Math.atan2(tipY - baseY, tipX - baseX); |
|
const fingerLength = Math.sqrt(Math.pow(tipX - baseX, 2) + Math.pow(tipY - baseY, 2)); |
|
|
|
|
|
const nailWidth = fingerLength * 0.6 * sizeScale; |
|
const nailLength = fingerLength * 0.7 * lengthScale; |
|
|
|
|
|
ctx.save(); |
|
|
|
|
|
ctx.translate(tipX, tipY); |
|
ctx.rotate(fingerAngle); |
|
|
|
|
|
if (showShadows) { |
|
ctx.save(); |
|
ctx.shadowColor = nailDesigns[design].shadowColor; |
|
ctx.shadowBlur = 15; |
|
ctx.shadowOffsetX = 0; |
|
ctx.shadowOffsetY = 5; |
|
ctx.fillStyle = 'rgba(0,0,0,0)'; |
|
ctx.beginPath(); |
|
ctx.ellipse(0, 0, nailWidth/2, nailLength/2, 0, 0, Math.PI * 2); |
|
ctx.fill(); |
|
ctx.restore(); |
|
} |
|
|
|
|
|
ctx.beginPath(); |
|
ctx.ellipse(0, 0, nailWidth/2, nailLength/2, 0, 0, Math.PI * 2); |
|
ctx.clip(); |
|
|
|
|
|
const gradient = ctx.createLinearGradient(-nailWidth/2, -nailLength/2, nailWidth/2, nailLength/2); |
|
|
|
|
|
const gradientStr = nailDesigns[design].color; |
|
const colorStart = gradientStr.substring( |
|
gradientStr.indexOf('(') + 1, |
|
gradientStr.indexOf(',') |
|
).trim(); |
|
const colorEnd = gradientStr.substring( |
|
gradientStr.lastIndexOf(',') + 1, |
|
gradientStr.lastIndexOf(')') |
|
).trim(); |
|
|
|
gradient.addColorStop(0, colorStart); |
|
gradient.addColorStop(1, colorEnd); |
|
|
|
|
|
ctx.globalAlpha = opacity; |
|
ctx.fillStyle = gradient; |
|
ctx.fillRect(-nailWidth/2, -nailLength/2, nailWidth, nailLength); |
|
|
|
|
|
ctx.restore(); |
|
} |
|
|
|
|
|
function onResults(results) { |
|
|
|
canvasCtx.save(); |
|
canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); |
|
canvasCtx.drawImage( |
|
results.image, 0, 0, canvasElement.width, canvasElement.height |
|
); |
|
|
|
|
|
if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) { |
|
for (const landmarks of results.multiHandLandmarks) { |
|
|
|
if (showLines) { |
|
drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS, |
|
{color: 'rgba(255, 255, 255, 0.5)', lineWidth: 2}); |
|
} |
|
|
|
|
|
|
|
applyNailDesign(canvasCtx, landmarks[4], landmarks[3], currentDesign); |
|
|
|
applyNailDesign(canvasCtx, landmarks[8], landmarks[7], currentDesign); |
|
|
|
applyNailDesign(canvasCtx, landmarks[12], landmarks[11], currentDesign); |
|
|
|
applyNailDesign(canvasCtx, landmarks[16], landmarks[15], currentDesign); |
|
|
|
applyNailDesign(canvasCtx, landmarks[20], landmarks[19], currentDesign); |
|
} |
|
} |
|
|
|
canvasCtx.restore(); |
|
} |
|
|
|
|
|
function initializeARApp() { |
|
try { |
|
cameraPermissionElement.style.display = 'none'; |
|
|
|
if (!cameraInitialized) { |
|
showDebug("正在初始化手部跟踪..."); |
|
|
|
|
|
hands = new Hands({locateFile: (file) => { |
|
return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`; |
|
}}); |
|
|
|
hands.setOptions({ |
|
maxNumHands: 2, |
|
modelComplexity: 1, |
|
minDetectionConfidence: 0.5, |
|
minTrackingConfidence: 0.5 |
|
}); |
|
|
|
hands.onResults(onResults); |
|
|
|
|
|
setupCamera(); |
|
cameraInitialized = true; |
|
|
|
showDebug("手部跟踪已初始化"); |
|
} |
|
} catch (error) { |
|
showDebug("初始化失败: " + error.message); |
|
console.error("初始化AR应用失败:", error); |
|
} |
|
} |
|
|
|
|
|
sizeSlider.addEventListener('input', (e) => { |
|
sizeScale = parseInt(e.target.value) / 100; |
|
sizeValue.textContent = `${e.target.value}%`; |
|
}); |
|
|
|
lengthSlider.addEventListener('input', (e) => { |
|
lengthScale = parseInt(e.target.value) / 100; |
|
lengthValue.textContent = `${e.target.value}%`; |
|
}); |
|
|
|
opacitySlider.addEventListener('input', (e) => { |
|
opacity = parseInt(e.target.value) / 100; |
|
opacityValue.textContent = `${e.target.value}%`; |
|
}); |
|
|
|
shadowToggle.addEventListener('click', () => { |
|
shadowToggle.classList.toggle('active'); |
|
showShadows = shadowToggle.classList.contains('active'); |
|
}); |
|
|
|
linesToggle.addEventListener('click', () => { |
|
linesToggle.classList.toggle('active'); |
|
showLines = linesToggle.classList.contains('active'); |
|
}); |
|
|
|
flipButton.addEventListener('click', () => { |
|
facingMode = facingMode === 'user' ? 'environment' : 'user'; |
|
|
|
if (camera) { |
|
camera.stop(); |
|
} |
|
setupCamera(); |
|
}); |
|
|
|
designOptions.forEach(option => { |
|
option.addEventListener('click', () => { |
|
designOptions.forEach(opt => opt.classList.remove('selected')); |
|
option.classList.add('selected'); |
|
currentDesign = option.getAttribute('data-design'); |
|
}); |
|
}); |
|
|
|
snapButton.addEventListener('click', () => { |
|
const dataUrl = canvasElement.toDataURL('image/png'); |
|
|
|
|
|
const link = document.createElement('a'); |
|
link.href = dataUrl; |
|
link.download = 'AR-美甲设计.png'; |
|
document.body.appendChild(link); |
|
link.click(); |
|
document.body.removeChild(link); |
|
|
|
|
|
snapButton.style.backgroundColor = '#3a86ff'; |
|
setTimeout(() => { |
|
snapButton.style.backgroundColor = 'white'; |
|
}, 300); |
|
}); |
|
|
|
|
|
function setupCamera() { |
|
try { |
|
showDebug("正在初始化摄像头..."); |
|
|
|
camera = new Camera(videoElement, { |
|
onFrame: async () => { |
|
if (hands) { |
|
try { |
|
await hands.send({image: videoElement}); |
|
} catch (error) { |
|
showDebug("手部跟踪出错: " + error.message); |
|
} |
|
} |
|
}, |
|
width: 1280, |
|
height: 720, |
|
facingMode: facingMode |
|
}); |
|
|
|
camera.start() |
|
.then(() => { |
|
showDebug("摄像头已启动"); |
|
setTimeout(() => { |
|
debugInfo.style.display = 'none'; |
|
}, 3000); |
|
}) |
|
.catch(error => { |
|
console.error('摄像头启动失败: ', error); |
|
showDebug("摄像头启动失败: " + error.message); |
|
|
|
cameraPermissionElement.style.display = 'flex'; |
|
}); |
|
} catch (error) { |
|
showDebug("摄像头初始化失败: " + error.message); |
|
console.error("摄像头初始化失败:", error); |
|
} |
|
} |
|
|
|
|
|
window.addEventListener('resize', () => { |
|
canvasElement.width = window.innerWidth; |
|
canvasElement.height = window.innerHeight; |
|
}); |
|
</script> |
|
</body> |
|
</html> |