<!DOCTYPE html>
<html lang="fa">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Mini Blender</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#171717;color:#ddd;font-family:Arial,sans-serif}
button,select,input{background:#292929;color:#ddd;border:1px solid #444;border-radius:4px}
button{padding:6px 10px;cursor:pointer}
button:hover{background:#3b3b3b}
#top{
height:42px;background:#222;border-bottom:1px solid #111;
display:flex;align-items:center;gap:6px;padding:5px
}
#top .logo{font-weight:bold;color:#f0a020;font-size:17px;margin:0 12px}
#top select{height:30px}
#main{height:calc(100% - 42px);display:flex}
#left{
width:220px;background:#202020;border-right:1px solid #111;
display:flex;flex-direction:column
}
#center{flex:1;position:relative;background:#111}
#right{
width:280px;background:#202020;border-left:1px solid #111;
overflow:auto
}
#timeline{
position:absolute;left:0;right:0;bottom:0;height:155px;
background:#191919;border-top:1px solid #050505
}
.panel-title{
padding:8px;background:#292929;border-bottom:1px solid #111;font-weight:bold
}
.panel{padding:9px;border-bottom:1px solid #111}
.row{display:flex;gap:5px;margin:5px 0;align-items:center}
.row label{width:80px;font-size:12px;color:#aaa}
.row input{width:90px;padding:5px}
#outliner{overflow:auto;flex:1}
.obj{
padding:6px 10px;cursor:pointer;white-space:nowrap
}
.obj:hover{background:#303030}
.obj.selected{background:#454545;color:white}
#tools{
position:absolute;left:10px;top:10px;display:flex;flex-direction:column;gap:4px
}
#tools button{width:38px;height:38px;padding:0;font-size:17px}
#mode{
position:absolute;left:65px;top:10px;
background:#252525;padding:5px;border-radius:5px
}
#status{
position:absolute;left:10px;bottom:165px;background:#222c;
padding:7px;border-radius:4px;font-size:12px
}
#help{
position:absolute;right:10px;top:10px;background:#222c;
padding:10px;border-radius:5px;font-size:12px;line-height:1.7
}
#timelineHead{height:35px;display:flex;align-items:center;gap:5px;padding:4px}
#timelineCanvas{position:absolute;left:0;right:0;bottom:0;top:35px}
.section{
padding:8px;border-bottom:1px solid #111
}
.section h4{margin:0 0 8px;color:#f0a020}
.color{
width:55px;height:28px;padding:0;border:0
}
</style>
</head>
<body>
<div id="top">
<div class="logo">MINI BLENDER</div>
<select id="modeSelect">
<option>Object Mode</option>
<option>Edit Mode</option>
<option>Pose Mode</option>
</select>
<button onclick="addCube()">+ Cube</button>
<button onclick="addSphere()">+ Sphere</button>
<button onclick="addCylinder()">+ Cylinder</button>
<button onclick="addCone()">+ Cone</button>
<button onclick="duplicate()">Duplicate</button>
<button onclick="deleteSelected()">Delete</button>
<button onclick="saveProject()">Save</button>
<button onclick="loadProject()">Load</button>
<span style="flex:1"></span>
<button onclick="toggleGrid()">Grid</button>
</div>
<div id="main">
<aside id="left">
<div class="panel-title">OUTLINER</div>
<div id="outliner"></div>
<div class="panel-title">TOOLS</div>
<div class="panel">
<button onclick="setTool('translate')">Move</button>
<button onclick="setTool('rotate')">Rotate</button>
<button onclick="setTool('scale')">Scale</button>
<button onclick="setTool('none')">Select</button>
</div>
</aside>
<main id="center">
<div id="tools">
<button onclick="setTool('translate')">✥</button>
<button onclick="setTool('rotate')">↻</button>
<button onclick="setTool('scale')">⤢</button>
</div>
<div id="mode">
<select id="selectMode">
<option value="vertex">Vertex</option>
<option value="edge">Edge</option>
<option value="face">Face</option>
</select>
</div>
<div id="help">
<b>Shortcuts</b><br>
G = Move<br>
R = Rotate<br>
S = Scale<br>
Tab = Mode<br>
A = Select All<br>
X = Delete<br>
Shift+D = Duplicate<br>
I = Keyframe
</div>
<div id="status">Ready</div>
<div id="timeline">
<div id="timelineHead">
<button onclick="play()">▶</button>
<button onclick="stop()">■</button>
<button onclick="prevFrame()">◀</button>
<button onclick="nextFrame()">▶</button>
<span>Frame:</span>
<input id="frameInput" type="number" value="1" min="1" max="1000"
style="width:70px">
<button onclick="insertKeyframe()">Insert Keyframe</button>
</div>
<canvas id="timelineCanvas"></canvas>
</div>
</main>
<aside id="right">
<div class="panel-title">PROPERTIES</div>
<div class="section">
<h4>Transform</h4>
<div class="row">
<label>Location X</label>
<input id="px" type="number" step=".1">
</div>
<div class="row">
<label>Location Y</label>
<input id="py" type="number" step=".1">
</div>
<div class="row">
<label>Location Z</label>
<input id="pz" type="number" step=".1">
</div>
<div class="row">
<label>Rotation X</label>
<input id="rx" type="number" step=".1">
</div>
<div class="row">
<label>Rotation Y</label>
<input id="ry" type="number" step=".1">
</div>
<div class="row">
<label>Rotation Z</label>
<input id="rz" type="number" step=".1">
</div>
<div class="row">
<label>Scale X</label>
<input id="sx" type="number" step=".1">
</div>
<div class="row">
<label>Scale Y</label>
<input id="sy" type="number" step=".1">
</div>
<div class="row">
<label>Scale Z</label>
<input id="sz" type="number" step=".1">
</div>
</div>
<div class="section">
<h4>Material</h4>
<div class="row">
<label>Color</label>
<input id="color" class="color" type="color" value="#4b82ff">
</div>
<div class="row">
<label>Metallic</label>
<input id="metallic" type="range" min="0" max="1" step=".01" value="0">
</div>
<div class="row">
<label>Roughness</label>
<input id="roughness" type="range" min="0" max="1" step=".01" value=".5">
</div>
<div class="row">
<label>Opacity</label>
<input id="opacity" type="range" min="0" max="1" step=".01" value="1">
</div>
</div>
<div class="section">
<h4>Mesh</h4>
<button onclick="enterEdit()">Edit Mode</button>
<button onclick="extrude()">Extrude</button>
<button onclick="mergeVertices()">Merge</button>
</div>
<div class="section">
<h4>Animation</h4>
<button onclick="insertKeyframe()">Insert Location Keyframe</button>
<button onclick="clearKeyframes()">Clear Keyframes</button>
</div>
</aside>
</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,camera,renderer,orbit,gizmo,raycaster,mouse;
let selected=null;
let tool='none';
let grid=true;
let playing=false;
let timer=null;
const objects=[];
const keyframes={};
init();
animate();
function init(){
scene=new THREE.Scene();
scene.background=new THREE.Color(0x111111);
camera=new THREE.PerspectiveCamera(
50,
(window.innerWidth-500)/window.innerHeight,
.1,
5000
);
camera.position.set(7,5,8);
renderer=new THREE.WebGLRenderer({
antialias:true
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(
document.getElementById('center').clientWidth,
document.getElementById('center').clientHeight
);
document.getElementById('center').appendChild(renderer.domElement);
orbit=new THREE.OrbitControls(camera,renderer.domElement);
orbit.enableDamping=true;
gizmo=new THREE.TransformControls(camera,renderer.domElement);
gizmo.addEventListener('dragging-changed',e=>{
orbit.enabled=!e.value;
});
gizmo.addEventListener('change',()=>{
updateProperties();
});
scene.add(gizmo);
raycaster=new THREE.Raycaster();
mouse=new THREE.Vector2();
createGrid();
const ambient=new THREE.HemisphereLight(0xffffff,0x333333,1.5);
scene.add(ambient);
const light=new THREE.DirectionalLight(0xffffff,2);
light.position.set(5,10,5);
scene.add(light);
addCube();
window.addEventListener('resize',resize);
renderer.domElement.addEventListener('pointerdown',selectObject);
setupProperties();
drawTimeline();
}
function createGrid(){
const helper=new THREE.GridHelper(40,40,0x444444,0x292929);
helper.name='Grid';
scene.add(helper);
const axes=new THREE.AxesHelper(4);
axes.name='Axes';
scene.add(axes);
}
function toggleGrid(){
grid=!grid;
scene.getObjectByName('Grid').visible=grid;
scene.getObjectByName('Axes').visible=grid;
}
function addCube(){
const geo=new THREE.BoxGeometry(2,2,2);
const mat=new THREE.MeshStandardMaterial({
color:0x4b82ff,
roughness:.5,
metalness:0
});
const mesh=new THREE.Mesh(geo,mat);
mesh.name='Cube';
mesh.position.set(0,1,0);
addObject(mesh);
}
function addSphere(){
const geo=new THREE.SphereGeometry(1,32,16);
const mat=new THREE.MeshStandardMaterial({
color:0x4b82ff
});
const mesh=new THREE.Mesh(geo,mat);
mesh.name='Sphere';
mesh.position.set(0,1,0);
addObject(mesh);
}
function addCylinder(){
const geo=new THREE.CylinderGeometry(1,1,2,32);
const mat=new THREE.MeshStandardMaterial({
color:0x4b82ff
});
const mesh=new THREE.Mesh(geo,mat);
mesh.name='Cylinder';
mesh.position.set(0,1,0);
addObject(mesh);
}
function addCone(){
const geo=new THREE.ConeGeometry(1,2,32);
const mat=new THREE.MeshStandardMaterial({
color:0x4b82ff
});
const mesh=new THREE.Mesh(geo,mat);
mesh.name='Cone';
mesh.position.set(0,1,0);
addObject(mesh);
}
function addObject(obj){
let base=obj.name;
let n=1;
while(scene.getObjectByName(obj.name)){
n++;
obj.name=base+'.'+String(n).padStart(3,'0');
}
scene.add(obj);
objects.push(obj);
select(obj);
updateOutliner();
}
function select(obj){
selected=obj;
gizmo.detach();
if(obj){
gizmo.attach(obj);
}
updateOutliner();
updateProperties();
document.getElementById('status').textContent=
obj ? 'Selected: '+obj.name : 'Nothing selected';
}
function selectObject(e){
if(tool!=='none' && gizmo.dragging)return;
const rect=renderer.domElement.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 o=hits[0].object;
while(o.parent && !objects.includes(o)){
o=o.parent;
}
if(objects.includes(o)){
select(o);
}
}
}
function updateOutliner(){
const box=document.getElementById('outliner');
box.innerHTML='';
objects.forEach(o=>{
const div=document.createElement('div');
div.className='obj'+(o===selected?' selected':'');
let icon='◼';
if(o.type==='Mesh')icon='▣';
if(o.type==='Light')icon='💡';
if(o.type==='Camera')icon='📷';
div.textContent=icon+' '+o.name;
div.onclick=()=>select(o);
box.appendChild(div);
});
}
function setTool(t){
tool=t;
if(t==='none'){
gizmo.detach();
}else{
gizmo.attach(selected);
if(t==='translate')gizmo.setMode('translate');
if(t==='rotate')gizmo.setMode('rotate');
if(t==='scale')gizmo.setMode('scale');
}
document.getElementById('status').textContent=
'Tool: '+t;
}
function updateProperties(){
if(!selected)return;
setValue('px',selected.position.x);
setValue('py',selected.position.y);
setValue('pz',selected.position.z);
setValue('rx',THREE.MathUtils.radToDeg(selected.rotation.x));
setValue('ry',THREE.MathUtils.radToDeg(selected.rotation.y));
setValue('rz',THREE.MathUtils.radToDeg(selected.rotation.z));
setValue('sx',selected.scale.x);
setValue('sy',selected.scale.y);
setValue('sz',selected.scale.z);
if(selected.material){
document.getElementById('color').value=
'#'+selected.material.color.getHexString();
document.getElementById('metallic').value=
selected.material.metalness ?? 0;
document.getElementById('roughness').value=
selected.material.roughness ?? .5;
document.getElementById('opacity').value=
selected.material.opacity ?? 1;
}
}
function setValue(id,v){
document.getElementById(id).value=
Number(v).toFixed(3);
}
function setupProperties(){
const ids=[
'px','py','pz',
'rx','ry','rz',
'sx','sy','sz'
];
ids.forEach(id=>{
document.getElementById(id).addEventListener('change',()=>{
if(!selected)return;
selected.position.x=Number(document.getElementById('px').value);
selected.position.y=Number(document.getElementById('py').value);
selected.position.z=Number(document.getElementById('pz').value);
selected.rotation.x=
THREE.MathUtils.degToRad(Number(document.getElementById('rx').value));
selected.rotation.y=
THREE.MathUtils.degToRad(Number(document.getElementById('ry').value));
selected.rotation.z=
THREE.MathUtils.degToRad(Number(document.getElementById('rz').value));
selected.scale.x=Number(document.getElementById('sx').value);
selected.scale.y=Number(document.getElementById('sy').value);
selected.scale.z=Number(document.getElementById('sz').value);
});
});
document.getElementById('color').addEventListener('input',e=>{
if(selected && selected.material){
selected.material.color.set(e.target.value);
}
});
document.getElementById('metallic').addEventListener('input',e=>{
if(selected && selected.material){
selected.material.metalness=Number(e.target.value);
}
});
document.getElementById('roughness').addEventListener('input',e=>{
if(selected && selected.material){
selected.material.roughness=Number(e.target.value);
}
});
document.getElementById('opacity').addEventListener('input',e=>{
if(selected && selected.material){
selected.material.opacity=Number(e.target.value);
selected.material.transparent=
selected.material.opacity<1;
}
});
document.getElementById('modeSelect').addEventListener('change',e=>{
if(e.target.value==='Edit Mode'){
enterEdit();
}
if(e.target.value==='Object Mode'){
exitEdit();
}
});
}
function enterEdit(){
if(!selected || !selected.isMesh)return;
document.getElementById('modeSelect').value='Edit Mode';
document.getElementById('status').textContent=
'Edit Mode: '+selected.name;
}
function exitEdit(){
document.getElementById('modeSelect').value='Object Mode';
document.getElementById('status').textContent=
'Object Mode';
}
function extrude(){
if(!selected || !selected.isMesh)return;
const old=selected.geometry;
const geo=old.clone();
const pos=geo.attributes.position;
const arr=[];
for(let i=0;i<pos.count;i++){
arr.push(
pos.getX(i),
pos.getY(i),
pos.getZ(i)
);
}
const newGeo=new THREE.BufferGeometry();
const verts=[];
for(let i=0;i<arr.length;i+=3){
verts.push(
arr[i],
arr[i+1],
arr[i+2]
);
}
const offset=1;
for(let i=0;i<arr.length;i+=3){
verts.push(
arr[i],
arr[i+1]+offset,
arr[i+2]
);
}
newGeo.setAttribute(
'position',
new THREE.Float32BufferAttribute(verts,3)
);
newGeo.computeVertexNormals();
selected.geometry.dispose();
selected.geometry=newGeo;
document.getElementById('status').textContent=
'Extrude executed';
}
function mergeVertices(){
if(!selected || !selected.isMesh)return;
const geo=selected.geometry;
const pos=geo.attributes.position;
const seen=[];
const out=[];
for(let i=0;i<pos.count;i++){
const x=pos.getX(i);
const y=pos.getY(i);
const z=pos.getZ(i);
let found=-1;
for(let j=0;j<seen.length;j++){
const p=seen[j];
if(
Math.abs(p.x-x)<.001 &&
Math.abs(p.y-y)<.001 &&
Math.abs(p.z-z)<.001
){
found=j;
break;
}
}
if(found===-1){
seen.push({x,y,z});
out.push(x,y,z);
}
}
const ng=new THREE.BufferGeometry();
ng.setAttribute(
'position',
new THREE.Float32BufferAttribute(out,3)
);
ng.computeVertexNormals();
selected.geometry.dispose();
selected.geometry=ng;
}
function duplicate(){
if(!selected)return;
const copy=selected.clone();
copy.name=selected.name+'.copy';
copy.position.x+=1;
addObject(copy);
}
function deleteSelected(){
if(!selected)return;
scene.remove(selected);
const i=objects.indexOf(selected);
if(i!==-1)objects.splice(i,1);
gizmo.detach();
selected=null;
updateOutliner();
}
function insertKeyframe(){
if(!selected)return;
const frame=
Number(document.getElementById('frameInput').value);
if(!keyframes[selected.uuid])
keyframes[selected.uuid]=[];
keyframes[selected.uuid].push({
frame:frame,
x:selected.position.x,
y:selected.position.y,
z:selected.position.z,
rx:selected.rotation.x,
ry:selected.rotation.y,
rz:selected.rotation.z,
sx:selected.scale.x,
sy:selected.scale.y,
sz:selected.scale.z
});
drawTimeline();
document.getElementById('status').textContent=
'Keyframe inserted at '+frame;
}
function clearKeyframes(){
if(!selected)return;
delete keyframes[selected.uuid];
drawTimeline();
}
function play(){
if(playing)return;
playing=true;
timer=setInterval(()=>{
let f=
Number(document.getElementById('frameInput').value)+1;
if(f>250)f=1;
document.getElementById('frameInput').value=f;
evaluateFrame(f);
},40);
}
function stop(){
playing=false;
clearInterval(timer);
}
function prevFrame(){
let f=
Number(document.getElementById('frameInput').value)-1;
if(f<1)f=1;
document.getElementById('frameInput').value=f;
evaluateFrame(f);
}
function nextFrame(){
let f=
Number(document.getElementById('frameInput').value)+1;
if(f>250)f=1;
document.getElementById('frameInput').value=f;
evaluateFrame(f);
}
function evaluateFrame(frame){
objects.forEach(o=>{
const keys=keyframes[o.uuid];
if(!keys || keys.length===0)return;
let before=null;
let after=null;
keys.forEach(k=>{
if(k.frame<=frame &&
(!before || k.frame>before.frame))
before=k;
if(k.frame>=frame &&
(!after || k.frame<after.frame))
after=k;
});
if(!before && after)before=after;
if(!after && before)after=before;
if(!before || !after)return;
if(before.frame===after.frame){
applyKey(o,before);
return;
}
const t=
(frame-before.frame)/
(after.frame-before.frame);
o.position.x=
THREE.MathUtils.lerp(before.x,after.x,t);
o.position.y=
THREE.MathUtils.lerp(before.y,after.y,t);
o.position.z=
THREE.MathUtils.lerp(before.z,after.z,t);
o.rotation.x=
THREE.MathUtils.lerp(before.rx,after.rx,t);
o.rotation.y=
THREE.MathUtils.lerp(before.ry,after.ry,t);
o.rotation.z=
THREE.MathUtils.lerp(before.rz,after.rz,t);
});
}
function applyKey(o,k){
o.position.set(k.x,k.y,k.z);
o.rotation.set(k.rx,k.ry,k.rz);
o.scale.set(k.sx,k.sy,k.sz);
}
function drawTimeline(){
const c=document.getElementById('timelineCanvas');
const box=c.getBoundingClientRect();
c.width=Math.max(400,box.width);
c.height=Math.max(70,box.height);
const ctx=c.getContext('2d');
ctx.clearRect(0,0,c.width,c.height);
ctx.fillStyle='#191919';
ctx.fillRect(0,0,c.width,c.height);
const step=50;
for(let x=0;x<c.width;x+=step){
ctx.strokeStyle='#303030';
ctx.beginPath();
ctx.moveTo(x,0);
ctx.lineTo(x,c.height);
ctx.stroke();
ctx.fillStyle='#888';
ctx.fillText(
Math.round(x/step*10),
x+2,
14
);
}
objects.forEach((o,row)=>{
const keys=keyframes[o.uuid];
if(!keys)return;
keys.forEach(k=>{
const x=k.frame/10*step;
ctx.fillStyle='#e0a020';
ctx.beginPath();
ctx.moveTo(x,25+row*18);
ctx.lineTo(x+6,31+row*18);
ctx.lineTo(x,37+row*18);
ctx.lineTo(x-6,31+row*18);
ctx.closePath();
ctx.fill();
});
});
}
function saveProject(){
const data={
objects:objects.map(o=>({
name:o.name,
type:
o.geometry.type,
position:o.position.to⚠️Content was pasted as plain text and auto-formatted as a code block. Use the Code Block button in the editor for proper formatting.