<!DOCTYPE html>
<html lang="fa">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Mini 3D Animator</title>
<style>
*{
box-sizing:border-box;
-webkit-tap-highlight-color:transparent;
}
html,body{
margin:0;
width:100%;
height:100%;
overflow:hidden;
background:#101114;
color:white;
font-family:Arial,sans-serif;
}
#app{
width:100vw;
height:100vh;
display:grid;
grid-template-columns:135px 1fr 155px;
grid-template-rows:45px 1fr 125px;
}
#top{
grid-column:1/4;
background:#181a1f;
border-bottom:1px solid #333;
display:flex;
align-items:center;
gap:6px;
padding:5px;
overflow-x:auto;
}
button{
border:0;
border-radius:6px;
background:#292c33;
color:white;
padding:8px 10px;
font-size:13px;
white-space:nowrap;
}
button:active{
background:#555;
}
.tool.active{
background:#5965ff;
}
#left{
background:#181a1f;
border-right:1px solid #333;
overflow:auto;
padding:7px;
}
#right{
background:#181a1f;
border-left:1px solid #333;
overflow:auto;
padding:7px;
}
#viewport{
position:relative;
min-width:0;
min-height:0;
overflow:hidden;
}
#canvas{
width:100%;
height:100%;
display:block;
}
#timeline{
grid-column:1/4;
background:#181a1f;
border-top:1px solid #333;
overflow:hidden;
position:relative;
}
#timelineHeader{
height:35px;
display:flex;
align-items:center;
gap:8px;
padding:4px 8px;
background:#202229;
}
#track{
position:absolute;
left:0;
right:0;
bottom:0;
top:35px;
overflow-x:auto;
overflow-y:hidden;
}
#frames{
position:relative;
height:90px;
min-width:1000px;
}
.frame{
position:absolute;
top:0;
height:90px;
border-left:1px solid #30333b;
color:#777;
font-size:10px;
padding-left:3px;
}
.key{
position:absolute;
width:12px;
height:12px;
background:#ffd84d;
transform:rotate(45deg);
border-radius:2px;
top:42px;
}
#playhead{
position:absolute;
top:0;
bottom:0;
width:2px;
background:#ff5555;
z-index:10;
pointer-events:none;
}
h3{
margin:4px 0 8px;
font-size:14px;
}
.item{
background:#25282f;
padding:8px;
border-radius:6px;
margin-bottom:5px;
cursor:pointer;
font-size:13px;
}
.item.selected{
background:#505cff;
}
.row{
display:flex;
gap:4px;
margin-bottom:5px;
}
input{
width:100%;
background:#101216;
color:white;
border:1px solid #393c45;
border-radius:5px;
padding:6px;
font-size:12px;
}
label{
display:block;
font-size:11px;
color:#aaa;
margin:6px 0 3px;
}
.xyz{
display:grid;
grid-template-columns:repeat(3,1fr);
gap:3px;
}
#info{
position:absolute;
left:8px;
top:8px;
background:rgba(0,0,0,.55);
padding:6px 8px;
border-radius:6px;
font-size:11px;
pointer-events:none;
}
#portrait{
display:none;
}
@media(max-width:700px){
#app{
grid-template-columns:100px 1fr 115px;
grid-template-rows:38px 1fr 100px;
}
button{
padding:6px 7px;
font-size:11px;
}
#left,#right{
padding:5px;
}
.item{
padding:6px;
font-size:11px;
}
#timeline{
font-size:11px;
}
}
@media(orientation:portrait){
#app{
display:none;
}
#portrait{
display:flex;
position:fixed;
inset:0;
background:#101114;
align-items:center;
justify-content:center;
flex-direction:column;
gap:15px;
font-size:20px;
z-index:9999;
text-align:center;
}
#portrait span{
font-size:14px;
color:#aaa;
}
}
</style>
</head>
<body>
<div id="portrait">
📱↔️
<b>گوشی را افقی کن</b>
<span>این موتور برای حالت Landscape ساخته شده</span>
</div>
<div id="app">
<div id="top">
<button onclick="addCube()">🧊 مکعب</button>
<button onclick="addSphere()">⚪ کره</button>
<button onclick="addCylinder()">🥫 استوانه</button>
<button onclick="addCharacter()">🤖 کاراکتر</button>
<button class="tool active" id="moveBtn" onclick="setTool('translate')">↔ حرکت</button>
<button class="tool" id="rotateBtn" onclick="setTool('rotate')">🔄 چرخش</button>
<button class="tool" id="scaleBtn" onclick="setTool('scale')">↗ اندازه</button>
<button onclick="addKeyframe()">🔑 Keyframe</button>
<button onclick="togglePlay()" id="play">▶ پخش</button>
<button onclick="resetCamera()">🎥 دوربین</button>
<button onclick="saveProject()">💾 ذخیره</button>
<button onclick="loadProject()">📂 بارگذاری</button>
</div>
<aside id="left">
<h3>🎬 صحنه</h3>
<div id="objects"></div>
<hr>
<button style="width:100%" onclick="duplicateObject()">📑 کپی</button>
<button style="width:100%;margin-top:5px" onclick="deleteObject()">🗑 حذف</button>
</aside>
<main id="viewport">
<canvas id="canvas"></canvas>
<div id="info">انتخاب یک جسم برای نمایش گیزمو</div>
</main>
<aside id="right">
<h3>⚙ ویژگیها</h3>
<div id="properties">
<div style="color:#777;font-size:12px">
جسمی انتخاب نشده
</div>
</div>
</aside>
<div id="timeline">
<div id="timelineHeader">
<button onclick="setFrame(0)">⏮</button>
<button onclick="previousFrame()">◀</button>
<button onclick="nextFrame()">▶</button>
<span>Frame:</span>
<input id="frameInput"
value="0"
style="width:55px"
onchange="setFrame(Number(this.value))">
<span>FPS:</span>
<input id="fps"
value="30"
style="width:45px">
<span id="status">آماده</span>
</div>
<div id="track">
<div id="frames"></div>
</div>
<div id="playhead"></div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/TransformControls.js"></script>
<script>
let scene;
let camera;
let renderer;
let orbit;
let transform;
let selected = null;
let objects = [];
let frame = 0;
let playing = false;
let clock = new THREE.Clock();
const canvas = document.getElementById("canvas");
/* =========================
شروع موتور
========================= */
scene = new THREE.Scene();
scene.background = new THREE.Color(0x101216);
scene.fog = new THREE.Fog(
0x101216,
15,
80
);
camera = new THREE.PerspectiveCamera(
60,
1,
0.1,
1000
);
camera.position.set(
7,
5,
9
);
renderer = new THREE.WebGLRenderer({
canvas:canvas,
antialias:true
});
renderer.setPixelRatio(
Math.min(window.devicePixelRatio,2)
);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type =
THREE.PCFSoftShadowMap;
/* =========================
نور
========================= */
const ambient =
new THREE.HemisphereLight(
0xffffff,
0x222233,
1.2
);
scene.add(ambient);
const sun =
new THREE.DirectionalLight(
0xffffff,
2
);
sun.position.set(
6,
10,
5
);
sun.castShadow = true;
sun.shadow.mapSize.width = 2048;
sun.shadow.mapSize.height = 2048;
scene.add(sun);
/* =========================
زمین
========================= */
const floor =
new THREE.Mesh(
new THREE.PlaneGeometry(60,60),
new THREE.MeshStandardMaterial({
color:0x24262b,
roughness:.8
})
);
floor.rotation.x =
-Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
/* =========================
گرید سه بعدی
========================= */
const grid =
new THREE.GridHelper(
60,
60,
0x666666,
0x333333
);
grid.position.y = .01;
scene.add(grid);
/* =========================
محورهای X Y Z
========================= */
const axes =
new THREE.AxesHelper(4);
scene.add(axes);
/* =========================
کنترل دوربین
========================= */
orbit =
new THREE.OrbitControls(
camera,
renderer.domElement
);
orbit.enableDamping = true;
orbit.dampingFactor = .08;
orbit.enableZoom = true;
orbit.zoomSpeed = 1.2;
orbit.minDistance = 2;
orbit.maxDistance = 80;
/* =========================
Transform Gizmo
========================= */
transform =
new THREE.TransformControls(
camera,
renderer.domElement
);
scene.add(transform);
transform.setMode("translate");
transform.addEventListener(
"dragging-changed",
function(e){
orbit.enabled = !e.value;
}
);
/* =========================
انتخاب
========================= */
const raycaster =
new THREE.Raycaster();
const mouse =
new THREE.Vector2();
canvas.addEventListener(
"pointerdown",
function(e){
if(transform.dragging)
return;
const rect =
canvas.getBoundingClientRect();
mouse.x =
((e.clientX - rect.left) /
rect.width) * 2 - 1;
mouse.y =
-((e.clientY - rect.top) /
rect.height) * 2 + 1;
raycaster.setFromCamera(
mouse,
camera
);
const hits =
raycaster.intersectObjects(
objects,
true
);
if(hits.length){
let obj = hits[0].object;
while(
obj.parent &&
!objects.includes(obj)
){
obj = obj.parent;
}
selectObject(obj);
}
}
);
/* =========================
ساخت متریال
========================= */
function material(color){
return new THREE.MeshStandardMaterial({
color:color,
roughness:.45,
metalness:.15
});
}
/* =========================
اضافه کردن جسم
========================= */
function addObject(obj,name){
obj.name = name;
obj.position.set(
0,
1,
0
);
obj.castShadow = true;
obj.receiveShadow = true;
scene.add(obj);
objects.push(obj);
selectObject(obj);
updateObjectList();
}
/* =========================
مکعب
========================= */
function addCube(){
const geo =
new THREE.BoxGeometry(
2,
2,
2
);
const mesh =
new THREE.Mesh(
geo,
material(0x4f7cff)
);
addObject(
mesh,
"Cube_" + objects.length
);
}
/* =========================
کره
========================= */
function addSphere(){
const geo =
new THREE.SphereGeometry(
1.3,
32,
20
);
const mesh =
new THREE.Mesh(
geo,
material(0xff6666)
);
addObject(
mesh,
"Sphere_" + objects.length
);
}
/* =========================
استوانه
========================= */
function addCylinder(){
const geo =
new THREE.CylinderGeometry(
1,
1,
2.5,
32
);
const mesh =
new THREE.Mesh(
geo,
material(0x63d78b)
);
addObject(
mesh,
"Cylinder_" + objects.length
);
}
/* =========================
کاراکتر سه بعدی
========================= */
function addCharacter(){
const group =
new THREE.Group();
group.name =
"Character_" + objects.length;
const body =
new THREE.Mesh(
new THREE.BoxGeometry(
1.6,
2.2,
1
),
material(0x5967ff)
);
body.position.y = 1.5;
group.add(body);
const head =
new THREE.Mesh(
new THREE.SphereGeometry(
.85,
24,
16
),
material(0xffc48b)
);
head.position.y = 3.2;
group.add(head);
const eye1 =
new THREE.Mesh(
new THREE.SphereGeometry(
.12,
12,
8
),
material(0x111111)
);
eye1.position.set(
-.3,
3.35,
.75
);
group.add(eye1);
const eye2 =
eye1.clone();
eye2.position.x = .3;
group.add(eye2);
const armGeo =
new THREE.BoxGeometry(
.45,
1.8,
.45
);
const arm1 =
new THREE.Mesh(
armGeo,
material(0x5967ff)
);
arm1.position.set(
-1.1,
1.6,
0
);
group.add(arm1);
const arm2 =
arm1.clone();
arm2.position.x = 1.1;
group.add(arm2);
const legGeo =
new THREE.BoxGeometry(
.55,
1.5,
.55
);
const leg1 =
new THREE.Mesh(
legGeo,
material(0x333b99)
);
leg1.position.set(
-.45,
-.25,
0
);
group.add(leg1);
const leg2 =
leg1.clone();
leg2.position.x = .45;
group.add(leg2);
group.position.set(
0,
0,
0
);
group.traverse(
function(o){
if(o.isMesh){
o.castShadow = true;
o.receiveShadow = true;
}
}
);
scene.add(group);
objects.push(group);
selectObject(group);
updateObjectList();
}
/* =========================
انتخاب جسم
========================= */
function selectObject(obj){
selected = obj;
transform.attach(obj);
updateObjectList();
updateProperties();
}
/* =========================
لیست اشیا
========================= */
function updateObjectList(){
const box =
document.getElementById(
"objects"
);
box.innerHTML = "";
objects.forEach(
function(obj,i){
const div =
document.createElement(
"div"
);
div.className =
"item" +
(obj === selected ?
" selected" : "");
div.textContent =
"◈ " + obj.name;
div.onclick =
function(){
selectObject(obj);
};
box.appendChild(div);
}
);
}
/* =========================
ابزارها
========================= */
function setTool(mode){
transform.setMode(mode);
document
.querySelectorAll(".tool")
.forEach(
b => b.classList.remove(
"active"
)
);
if(mode === "translate")
document
.getElementById("moveBtn")
.classList.add("active");
if(mode === "rotate")
document
.getElementById("rotateBtn")
.classList.add("active");
if(mode === "scale")
document
.getElementById("scaleBtn")
.classList.add("active");
}
/* =========================
ویژگی ها
========================= */
function updateProperties(){
const box =
document.getElementById(
"properties"
);
if(!selected){
box.innerHTML =
"جسمی انتخاب نشده";
return;
}
box.innerHTML = `
<label>نام</label>
<input
value="${selected.name}"
onchange="
selected.name=this.value;
updateObjectList();
"
>
<label>موقعیت X Y Z</label>
<div class="xyz">
<input id="px">
<input id="py">
<input id="pz">
</div>
<label>چرخش X Y Z</label>
<div class="xyz">
<input id="rx">
<input id="ry">
<input id="rz">
</div>
<label>اندازه X Y Z</label>
<div class="xyz">
<input id="sx">
<input id="sy">
<input id="sz">
</div>
`;
document.getElementById("px").value =
selected.position.x.toFixed(2);
document.getElementById("py").value =
selected.position.y.toFixed(2);
document.getElementById("pz").value =
selected.position.z.toFixed(2);
document.getElementById("rx").value =
THREE.MathUtils.radToDeg(
selected.rotation.x
).toFixed(1);
document.getElementById("ry").value =
THREE.MathUtils.radToDeg(
selected.rotation.y
).toFixed(1);
document.getElementById("rz").value =
THREE.MathUtils.radToDeg(
selected.rotation.z
).toFixed(1);
document.getElementById("sx").value =
selected.scale.x.toFixed(2);
document.getElementById("sy").value =
selected.scale.y.toFixed(2);
document.getElementById("sz").value =
selected.scale.z.toFixed(2);
[
"px","py","pz",
"rx","ry","rz",
"sx","sy","sz"
].forEach(
function(id){
document
.getElementById(id)
.onchange =
applyProperties;
}
);
}
function applyProperties(){
if(!selected)
return;
selected.position.set(
Number(px.value),
Number(py.value),
Number(pz.value)
);
selected.rotation.set(
THREE.MathUtils.degToRad(
Number(rx.value)
),
THREE.MathUtils.degToRad(
Number(ry.value)
),
THREE.MathUtils.degToRad(
Number(rz.value)
)
);
selected.scale.set(
Number(sx.value),
Number(sy.value),
Number(sz.value)
);
}
/* =========================
کپی
========================= */
function duplicateObject(){
if(!selected)
return;
const copy =
selected.clone(true);
copy.name =
selected.name + "_Copy";
copy.position.x += 2;
scene.add(copy);
objects.push(copy);
selectObject(copy);
updateObjectList();
}
/* =========================
حذف
========================= */
function deleteObject(){
if(!selected)
return;
scene.remove(selected);
objects =
objects.filter(
o => o !== selected
);
transform.detach();
selected = null;
updateObjectList();
updateProperties();
}
/* =========================
Keyframe
========================= */
function addKeyframe(){
if(!selected)
return;
if(!selected.userData.keys)
selected.userData.keys = {};
selected.userData.keys[frame] = {
position:[
selected.position.x,
selected.position.y,
selected.position.z
],
rotation:[
selected.rotation.x,
selected.rotation.y,
selected.rotation.z
],
scale:[
selected.scale.x,
selected.scale.y,
selected.scale.z
]
};
drawTimeline();
document.getElementById(
"status"
).textContent =
"Keyframe ساخته شد";
}
/* =========================
Timeline
========================= */
function drawTimeline(){
const box =⚠️Content was pasted as plain text and auto-formatted as a code block. Use the Code Block button in the editor for proper formatting.