<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Live2Dデモ - Kalidokit</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
padding: 40px;
max-width: 800px;
width: 100%;
}
h1 {
color: #667eea;
margin-bottom: 30px;
text-align: center;
font-size: 2em;
}
.demo-section {
background: #f8f9fa;
border-radius: 15px;
padding: 30px;
margin-bottom: 20px;
}
.demo-section h2 {
color: #764ba2;
margin-bottom: 20px;
font-size: 1.5em;
}
.canvas-container {
background: #000;
border-radius: 10px;
overflow: hidden;
margin: 20px 0;
position: relative;
}
canvas {
display: block;
width: 100%;
height: auto;
}
.controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 20px;
}
button {
background: #667eea;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: all 0.3s;
}
button:hover {
background: #764ba2;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
button:active {
transform: translateY(0);
}
.info {
background: #e3f2fd;
border-left: 4px solid #2196f3;
padding: 15px;
margin: 20px 0;
border-radius: 5px;
}
.info p {
color: #1565c0;
line-height: 1.6;
}
.slider-group {
margin: 15px 0;
}
.slider-group label {
display: block;
color: #555;
margin-bottom: 8px;
font-weight: 600;
}
input[type="range"] {
width: 100%;
height: 8px;
border-radius: 5px;
background: #ddd;
outline: none;
-webkit-appearance: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #667eea;
cursor: pointer;
}
.value-display {
display: inline-block;
background: #667eea;
color: white;
padding: 4px 12px;
border-radius: 5px;
font-size: 14px;
margin-left: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>🎭 Live2Dスタイル アバターデモ</h1>
<div class="info">
<p><strong>このデモについて:</strong> Live2D SDKの代わりに、Canvas APIとJavaScriptでシンプルなアバターを作成しました。スライダーで表情や動きをコントロールできます。</p>
</div>
<div class="demo-section">
<h2>アバタープレビュー</h2>
<div class="canvas-container">
<canvas id="avatar-canvas" width="400" height="500"></canvas>
</div>
<div class="slider-group">
<label>
頭の角度 X: <span class="value-display" id="rotX-value">0°</span>
</label>
<input type="range" id="rotX" min="-30" max="30" value="0">
</div>
<div class="slider-group">
<label>
頭の角度 Y: <span class="value-display" id="rotY-value">0°</span>
</label>
<input type="range" id="rotY" min="-30" max="30" value="0">
</div>
<div class="slider-group">
<label>
目の開き具合: <span class="value-display" id="eyeOpen-value">100%</span>
</label>
<input type="range" id="eyeOpen" min="0" max="100" value="100">
</div>
<div class="slider-group">
<label>
口の開き具合: <span class="value-display" id="mouthOpen-value">0%</span>
</label>
<input type="range" id="mouthOpen" min="0" max="100" value="0">
</div>
<div class="controls">
<button onclick="setExpression('normal')">😊 通常</button>
<button onclick="setExpression('happy')">😄 笑顔</button>
<button onclick="setExpression('surprised')">😲 驚き</button>
<button onclick="setExpression('wink')">😉 ウィンク</button>
<button onclick="animate()">🎬 アニメーション</button>
</div>
</div>
<div class="info">
<p><strong>💡 ヒント:</strong> スライダーを動かして表情を変えたり、ボタンをクリックしてプリセット表情を試してみてください!</p>
</div>
</div>
<script>
const canvas = document.getElementById('avatar-canvas');
const ctx = canvas.getContext('2d');
let state = {
rotX: 0,
rotY: 0,
eyeOpen: 1,
mouthOpen: 0,
leftEyeOpen: 1,
rightEyeOpen: 1
};
// スライダーのイベントリスナー
document.getElementById('rotX').addEventListener('input', (e) => {
state.rotX = parseInt(e.target.value);
document.getElementById('rotX-value').textContent = e.target.value + '°';
draw();
});
document.getElementById('rotY').addEventListener('input', (e) => {
state.rotY = parseInt(e.target.value);
document.getElementById('rotY-value').textContent = e.target.value + '°';
draw();
});
document.getElementById('eyeOpen').addEventListener('input', (e) => {
state.eyeOpen = parseInt(e.target.value) / 100;
state.leftEyeOpen = state.eyeOpen;
state.rightEyeOpen = state.eyeOpen;
document.getElementById('eyeOpen-value').textContent = e.target.value + '%';
draw();
});
document.getElementById('mouthOpen').addEventListener('input', (e) => {
state.mouthOpen = parseInt(e.target.value) / 100;
document.getElementById('mouthOpen-value').textContent = e.target.value + '%';
draw();
});
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2);
// 頭の回転を適用
const rotXRad = (state.rotX * Math.PI) / 180;
const rotYRad = (state.rotY * Math.PI) / 180;
// 顔(楕円)
ctx.fillStyle = '#ffd1a3';
ctx.beginPath();
ctx.ellipse(0, 0, 80 + state.rotY * 0.5, 100 - Math.abs(state.rotX) * 0.5, rotXRad, 0, Math.PI * 2);
ctx.fill();
// 髪
ctx.fillStyle = '#4a2c2a';
ctx.beginPath();
ctx.ellipse(0, -40, 90 + state.rotY * 0.5, 70 - Math.abs(state.rotX) * 0.3, rotXRad, 0, Math.PI);
ctx.fill();
// 左目
const leftEyeX = -30 + state.rotY * 0.8;
const leftEyeY = -20 + state.rotX * 0.5;
drawEye(leftEyeX, leftEyeY, state.leftEyeOpen);
// 右目
const rightEyeX = 30 + state.rotY * 0.8;
const rightEyeY = -20 + state.rotX * 0.5;
drawEye(rightEyeX, rightEyeY, state.rightEyeOpen);
// 口
drawMouth(0, 30 + state.rotX * 0.5, state.mouthOpen);
// ほっぺ
ctx.fillStyle = 'rgba(255, 182, 193, 0.5)';
ctx.beginPath();
ctx.arc(-50 + state.rotY * 0.5, 10, 15, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(50 + state.rotY * 0.5, 10, 15, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawEye(x, y, openness) {
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.ellipse(x, y, 12, 16 * openness, 0, 0, Math.PI * 2);
ctx.fill();
if (openness > 0.3) {
ctx.fillStyle = '#4a2c2a';
ctx.beginPath();
ctx.arc(x, y, 6 * openness, 0, Math.PI * 2);
ctx.fill();
}
}
function drawMouth(x, y, openness) {
ctx.strokeStyle = '#8b4513';
ctx.lineWidth = 3;
ctx.beginPath();
if (openness < 0.3) {
// 閉じた口(笑顔)
ctx.arc(x, y, 25, 0.2, Math.PI - 0.2);
} else {
// 開いた口
ctx.ellipse(x, y, 20, 15 * openness, 0, 0, Math.PI * 2);
}
ctx.stroke();
if (openness > 0.5) {
ctx.fillStyle = '#ff6b6b';
ctx.fill();
}
}
function setExpression(type) {
switch(type) {
case 'normal':
state.leftEyeOpen = 1;
state.rightEyeOpen = 1;
state.mouthOpen = 0;
updateSliders(0, 0, 100, 0);
break;
case 'happy':
state.leftEyeOpen = 0.7;
state.rightEyeOpen = 0.7;
state.mouthOpen = 0.5;
updateSliders(0, 0, 70, 50);
break;
case 'surprised':
state.leftEyeOpen = 1.2;
state.rightEyeOpen = 1.2;
state.mouthOpen = 0.8;
updateSliders(0, 0, 100, 80);
break;
case 'wink':
state.leftEyeOpen = 0;
state.rightEyeOpen = 1;
state.mouthOpen = 0.2;
updateSliders(0, 0, 50, 20);
break;
}
draw();
}
function updateSliders(rotX, rotY, eyeOpen, mouthOpen) {
document.getElementById('rotX').value = rotX;
document.getElementById('rotY').value = rotY;
document.getElementById('eyeOpen').value = eyeOpen;
document.getElementById('mouthOpen').value = mouthOpen;
document.getElementById('rotX-value').textContent = rotX + '°';
document.getElementById('rotY-value').textContent = rotY + '°';
document.getElementById('eyeOpen-value').textContent = eyeOpen + '%';
document.getElementById('mouthOpen-value').textContent = mouthOpen + '%';
state.rotX = rotX;
state.rotY = rotY;
}
function animate() {
let frame = 0;
const duration = 120;
function step() {
frame++;
// サインカーブでアニメーション
state.rotY = Math.sin(frame * 0.1) * 20;
state.rotX = Math.sin(frame * 0.05) * 10;
// まばたき
if (frame % 60 === 0) {
state.leftEyeOpen = 0;
state.rightEyeOpen = 0;
} else if (frame % 60 === 5) {
state.leftEyeOpen = 1;
state.rightEyeOpen = 1;
}
draw();
if (frame < duration) {
requestAnimationFrame(step);
} else {
setExpression('normal');
}
}
step();
}
// 初期描画
draw();
</script>
</body>
</html>