Sync local files: AGENTS.md update, new projects (browser-os, image-folder-watcher, docker-compose), add .gitignore
This commit is contained in:
@@ -0,0 +1,496 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'>
|
||||
<title>macOS Browser OS</title>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
|
||||
html,body{width:100%;height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Segoe UI',Roboto,Helvetica,Arial,sans-serif;background:#000}
|
||||
#desktop{width:100%;height:100%;position:relative;background-size:cover;background-position:center;transition:background-image .5s}
|
||||
#menubar{position:fixed;top:0;left:0;right:0;height:28px;background:rgba(255,255,255,0.15);backdrop-filter:blur(20px) saturate(180%);-webkit-backdrop-filter:blur(20px) saturate(180%);border-bottom:1px solid rgba(255,255,255,0.1);display:flex;align-items:center;padding:0 16px;z-index:99999;color:#fff;font-size:13px;user-select:none}
|
||||
.apple-logo{font-size:16px;margin-right:14px;cursor:pointer}
|
||||
.menu-items{display:flex;gap:16px;font-weight:500}
|
||||
.menu-item{cursor:pointer;opacity:.9;transition:opacity .2s}
|
||||
.menu-right{margin-left:auto;display:flex;gap:14px;align-items:center}
|
||||
#dock{position:fixed;bottom:12px;left:50%;transform:translateX(-50%);height:60px;background:rgba(255,255,255,0.15);backdrop-filter:blur(20px) saturate(180%);-webkit-backdrop-filter:blur(20px) saturate(180%);border-radius:18px;border:1px solid rgba(255,255,255,0.15);display:flex;align-items:center;padding:0 12px;gap:10px;z-index:99998}
|
||||
.dock-item{width:48px;height:48px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:26px;cursor:pointer;transition:transform .15s,margin .15s;position:relative;background:rgba(255,255,255,0.1);box-shadow:0 2px 8px rgba(0,0,0,0.15)}
|
||||
.dock-item:hover{transform:scale(1.2) translateY(-6px);margin:0 6px}
|
||||
.dock-item::after{content:attr(data-label);position:absolute;top:-28px;left:50%;transform:translateX(-50%) scale(.8);background:rgba(0,0,0,0.7);color:#fff;padding:3px 8px;border-radius:6px;font-size:11px;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .2s,transform .2s}
|
||||
.dock-item:hover::after{opacity:1;transform:translateX(-50%) scale(1)}
|
||||
.dock-indicator{position:absolute;bottom:3px;width:4px;height:4px;background:rgba(255,255,255,0.6);border-radius:50%;left:50%;transform:translateX(-50%);display:none}
|
||||
.dock-item.active .dock-indicator{display:block}
|
||||
.window{position:absolute;background:rgba(30,30,30,0.85);backdrop-filter:blur(30px);-webkit-backdrop-filter:blur(30px);border-radius:12px;border:1px solid rgba(255,255,255,0.08);box-shadow:0 25px 50px -12px rgba(0,0,0,0.5);overflow:hidden;display:flex;flex-direction:column;min-width:280px;min-height:200px;transition:opacity .2s,transform .3s}
|
||||
.window.minimized{opacity:0;pointer-events:none;transform:scale(.9) translateY(20px)}
|
||||
.window.maximized{top:28px!important;left:0!important;width:100%!important;height:calc(100% - 28px - 72px)!important;border-radius:0}
|
||||
.window-header{height:40px;display:flex;align-items:center;padding:0 16px;background:rgba(255,255,255,0.03);border-bottom:1px solid rgba(255,255,255,0.05);cursor:grab;user-select:none;position:relative}
|
||||
.window-controls{display:flex;gap:8px;margin-right:12px}
|
||||
.window-control{width:12px;height:12px;border-radius:50%;cursor:pointer;transition:transform .15s}
|
||||
.window-control:hover{transform:scale(1.15)}
|
||||
.wc-close{background:#ff5f57}
|
||||
.wc-min{background:#febc2e}
|
||||
.wc-max{background:#28c840}
|
||||
.window-title{flex:1;text-align:center;color:rgba(255,255,255,0.85);font-size:13px;font-weight:500;position:absolute;left:50%;transform:translateX(-50%);pointer-events:none}
|
||||
.window-content{flex:1;overflow:auto;position:relative}
|
||||
.resize-handle{position:absolute;bottom:0;right:0;width:15px;height:15px;cursor:se-resize;z-index:10}
|
||||
.resize-handle::after{content:'';position:absolute;bottom:3px;right:3px;width:8px;height:8px;border-right:2px solid rgba(255,255,255,0.2);border-bottom:2px solid rgba(255,255,255,0.2)}
|
||||
#context-menu{position:fixed;background:rgba(40,40,40,0.95);backdrop-filter:blur(20px);border-radius:8px;border:1px solid rgba(255,255,255,0.08);padding:6px 0;min-width:180px;z-index:100000;display:none;box-shadow:0 10px 30px rgba(0,0,0,0.4)}
|
||||
.ctx-item{padding:6px 16px;color:#fff;font-size:13px;cursor:pointer;transition:background .1s}
|
||||
.ctx-item:hover{background:rgba(10,132,255,0.9)}
|
||||
.ctx-sep{height:1px;background:rgba(255,255,255,0.08);margin:4px 0}
|
||||
.calc-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1px;background:rgba(255,255,255,0.05);height:100%}
|
||||
.calc-display{grid-column:1/-1;background:rgba(0,0,0,0.2);color:#fff;font-size:32px;padding:16px;text-align:right;display:flex;align-items:center;justify-content:flex-end}
|
||||
.calc-btn{background:rgba(255,255,255,0.07);border:none;color:#fff;font-size:18px;padding:16px;cursor:pointer;outline:none}
|
||||
.calc-btn:hover{background:rgba(255,255,255,0.12)}
|
||||
.calc-btn:active{background:rgba(255,255,255,0.18)}
|
||||
.calc-btn.op{background:rgba(255,149,0,0.8)}
|
||||
.calc-btn.zero{grid-column:1/3}
|
||||
.notepad-area{width:100%;height:100%;background:rgba(255,255,255,0.95);color:#333;border:none;padding:16px;font-size:14px;line-height:1.6;resize:none;outline:none}
|
||||
.notepad-toolbar{display:flex;gap:8px;padding:8px 12px;background:rgba(255,255,255,0.1);border-bottom:1px solid rgba(255,255,255,0.05)}
|
||||
.np-btn{background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.1);color:#fff;padding:4px 12px;border-radius:6px;cursor:pointer;font-size:12px}
|
||||
.np-btn:hover{background:rgba(255,255,255,0.2)}
|
||||
.term-container{background:rgba(20,20,20,0.95);color:#0f0;font-family:'SF Mono',Monaco,'Courier New',monospace;font-size:13px;padding:12px;height:100%;overflow-y:auto;white-space:pre-wrap;line-height:1.5}
|
||||
.term-input-line{display:flex;align-items:center}
|
||||
.term-prompt{color:#0f0;margin-right:6px;white-space:nowrap}
|
||||
.term-input{background:transparent;border:none;color:#0f0;font-family:inherit;font-size:inherit;flex:1;outline:none;caret-color:#0f0}
|
||||
.term-line{margin:2px 0}
|
||||
.term-error{color:#ff453a}
|
||||
.term-info{color:#64d2ff}
|
||||
.term-success{color:#32d74b}
|
||||
.settings-grid{padding:20px;display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px}
|
||||
.settings-card{background:rgba(255,255,255,0.05);border-radius:12px;padding:16px;border:1px solid rgba(255,255,255,0.06)}
|
||||
.settings-card h3{color:#fff;font-size:14px;margin-bottom:12px;font-weight:600}
|
||||
.wp-thumb{width:100%;height:100px;border-radius:8px;background-size:cover;background-position:center;cursor:pointer;margin-bottom:8px;border:2px solid transparent;transition:border-color .2s}
|
||||
.wp-thumb:hover,.wp-thumb.active{border-color:#0a84ff}
|
||||
.wp-custom{display:flex;gap:8px;margin-top:8px}
|
||||
.wp-custom input{flex:1;background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:6px 10px;color:#fff;font-size:13px;outline:none}
|
||||
.wp-custom button{background:#0a84ff;color:#fff;border:none;border-radius:6px;padding:6px 12px;cursor:pointer;font-size:12px}
|
||||
.fm-toolbar{display:flex;gap:8px;padding:8px 12px;background:rgba(255,255,255,0.05);border-bottom:1px solid rgba(255,255,255,0.06)}
|
||||
.fm-btn{background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.1);color:#fff;padding:4px 10px;border-radius:6px;cursor:pointer;font-size:12px}
|
||||
.fm-path{flex:1;color:rgba(255,255,255,0.7);font-size:12px;display:flex;align-items:center;padding-left:8px}
|
||||
.fm-content{padding:12px;display:grid;grid-template-columns:repeat(auto-fill,minmax(80px,1fr));gap:12px;overflow-y:auto;height:calc(100% - 40px)}
|
||||
.fm-item{display:flex;flex-direction:column;align-items:center;padding:10px 4px;border-radius:8px;cursor:pointer;transition:background .15s}
|
||||
.fm-item:hover{background:rgba(255,255,255,0.08)}
|
||||
.fm-item.selected{background:rgba(10,132,255,0.3)}
|
||||
.fm-icon{font-size:32px;margin-bottom:6px}
|
||||
.fm-name{font-size:11px;color:rgba(255,255,255,0.85);text-align:center;word-break:break-all;line-height:1.3}
|
||||
.game-canvas{display:block;width:100%;height:100%;background:#000}
|
||||
.game-overlay{position:absolute;top:0;left:0;right:0;bottom:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:rgba(0,0,0,0.7);color:#fff;pointer-events:none}
|
||||
.game-overlay.hidden{display:none}
|
||||
.game-overlay button{pointer-events:all;margin-top:16px;padding:10px 24px;background:#0a84ff;color:#fff;border:none;border-radius:8px;font-size:14px;cursor:pointer}
|
||||
.game-hud{position:absolute;top:8px;left:8px;color:#fff;font-size:14px;font-family:monospace;text-shadow:0 1px 3px rgba(0,0,0,0.8);pointer-events:none}
|
||||
.game-controls-hint{position:absolute;bottom:8px;left:50%;transform:translateX(-50%);color:rgba(255,255,255,0.7);font-size:11px;text-align:center;pointer-events:none}
|
||||
@media(max-width:768px){#menubar{height:36px;font-size:14px;padding:0 12px}#dock{height:64px;bottom:8px;gap:6px;padding:0 10px}#dock .dock-item{width:48px;height:48px;font-size:24px}.window{border-radius:10px}.window-header{height:44px}.window.maximized{top:36px!important;height:calc(100% - 36px - 72px)!important}.calc-btn{padding:18px 12px;font-size:20px}.calc-display{font-size:28px;padding:12px}.settings-grid{grid-template-columns:1fr;padding:12px}}
|
||||
::-webkit-scrollbar{width:8px;height:8px}
|
||||
::-webkit-scrollbar-track{background:transparent}
|
||||
::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.15);border-radius:4px}
|
||||
::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.25)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id='desktop'>
|
||||
<div id='menubar'>
|
||||
<div class='apple-logo' id='apple-btn'></div>
|
||||
<div class='menu-items'><div class='menu-item' id='menu-active-app'>Finder</div></div>
|
||||
<div class='menu-right'><span id='clock'></span></div>
|
||||
</div>
|
||||
<div id='dock'>
|
||||
<div class='dock-item' data-app='settings' data-label='Settings'><span>⚙️</span><div class='dock-indicator'></div></div>
|
||||
<div class='dock-item' data-app='finder' data-label='Finder'><span>📁</span><div class='dock-indicator'></div></div>
|
||||
<div class='dock-item' data-app='terminal' data-label='Terminal'><span>💻</span><div class='dock-indicator'></div></div>
|
||||
<div class='dock-item' data-app='notepad' data-label='Notepad'><span>📝</span><div class='dock-indicator'></div></div>
|
||||
<div class='dock-item' data-app='calculator' data-label='Calculator'><span>🖩</span><div class='dock-indicator'></div></div>
|
||||
<div class='dock-item' data-app='snake3d' data-label='Snake 3D'><span>🐍</span><div class='dock-indicator'></div></div>
|
||||
<div class='dock-item' data-app='tetris3d' data-label='Tetris 3D'><span>🧱</span><div class='dock-indicator'></div></div>
|
||||
</div>
|
||||
<div id='context-menu'></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const OS={
|
||||
windows:{},zIndex:100,activeWindow:null,wallpaper:null,nextId:1,
|
||||
init(){
|
||||
this.loadWallpaper();this.updateClock();setInterval(()=>this.updateClock(),1000);
|
||||
document.querySelectorAll('.dock-item').forEach(item=>{item.addEventListener('click',()=>this.openApp(item.dataset.app))});
|
||||
document.getElementById('apple-btn').addEventListener('click',()=>this.toggleAppleMenu());
|
||||
document.getElementById('desktop').addEventListener('contextmenu',e=>{if(e.target.id==='desktop'||(e.target.closest('#desktop')&&!e.target.closest('.window'))){e.preventDefault();this.showContextMenu(e.clientX,e.clientY)}});
|
||||
document.addEventListener('click',e=>{if(!e.target.closest('#context-menu'))document.getElementById('context-menu').style.display='none'});
|
||||
window.addEventListener('keydown',e=>{if(e.metaKey||e.ctrlKey){if(e.key==='n'){e.preventDefault();this.openApp('notepad')}if(e.key==='t'){e.preventDefault();this.openApp('terminal')}}});
|
||||
},
|
||||
updateClock(){const now=new Date();document.getElementById('clock').textContent=now.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})},
|
||||
loadWallpaper(){const wp=localStorage.getItem('os_wallpaper');this.wallpaper=wp||null;if(this.wallpaper){document.getElementById('desktop').style.backgroundImage=this.wallpaper}else{this.setWallpaper(0)}},
|
||||
setWallpaper(indexOrUrl){
|
||||
const wallpapers=['linear-gradient(135deg, #667eea 0%, #764ba2 100%)','linear-gradient(135deg, #f093fb 0%, #f5576c 100%)','linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)','linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)','linear-gradient(135deg, #fa709a 0%, #fee140 100%)','linear-gradient(135deg, #30cfd0 0%, #330867 100%)'];
|
||||
const desktop=document.getElementById('desktop');
|
||||
if(typeof indexOrUrl==='number'){desktop.style.backgroundImage=wallpapers[indexOrUrl%wallpapers.length];this.wallpaper=wallpapers[indexOrUrl%wallpapers.length]}
|
||||
else{desktop.style.backgroundImage='url('+indexOrUrl+')';this.wallpaper='url('+indexOrUrl+')'}
|
||||
localStorage.setItem('os_wallpaper',this.wallpaper);
|
||||
},
|
||||
showContextMenu(x,y){
|
||||
const menu=document.getElementById('context-menu');
|
||||
const actions=[{label:'Change Wallpaper...',app:'settings'},{label:'New Terminal',app:'terminal'},{label:'New Text Document',app:'notepad'},{label:'Refresh',app:'refresh'}];
|
||||
menu.innerHTML='';
|
||||
let sep=false;
|
||||
actions.forEach(a=>{
|
||||
if(a.label==='New Terminal'||a.label==='Refresh'){const s=document.createElement('div');s.className='ctx-sep';menu.appendChild(s)}
|
||||
const div=document.createElement('div');div.className='ctx-item';div.textContent=a.label;div.addEventListener('click',()=>{if(a.app==='refresh')location.reload();else this.openApp(a.app);menu.style.display='none'});menu.appendChild(div);
|
||||
});
|
||||
menu.style.left=Math.min(x,window.innerWidth-200)+'px';
|
||||
menu.style.top=Math.min(y,window.innerHeight-200)+'px';
|
||||
menu.style.display='block';
|
||||
},
|
||||
openApp(appName){
|
||||
if(this.windows[appName]&&!this.windows[appName].closed){this.focusWindow(appName);this.windows[appName].element.classList.remove('minimized');return}
|
||||
this.createWindow(appName);
|
||||
},
|
||||
createWindow(appName){
|
||||
const configs={settings:{title:'Settings',w:640,h:480,content:this.renderSettings()},finder:{title:'Finder',w:600,h:400,content:this.renderFinder()},terminal:{title:'Terminal',w:700,h:450,content:this.renderTerminal()},notepad:{title:'Notepad',w:500,h:400,content:this.renderNotepad()},calculator:{title:'Calculator',w:280,h:420,content:this.renderCalculator(),fixed:true},snake3d:{title:'Snake 3D',w:640,h:480,content:this.renderSnake3D()},tetris3d:{title:'Tetris 3D',w:640,h:480,content:this.renderTetris3D()}};
|
||||
const cfg=configs[appName];
|
||||
const id='win-'+appName+'-'+this.nextId++;
|
||||
const el=document.createElement('div');
|
||||
el.className='window';el.id=id;
|
||||
const startX=60+(Object.keys(this.windows).length*30)%Math.max(1,window.innerWidth-cfg.w-100);
|
||||
const startY=60+(Object.keys(this.windows).length*30)%Math.max(1,window.innerHeight-cfg.h-140);
|
||||
el.style.left=Math.max(0,startX)+'px';
|
||||
el.style.top=Math.max(28,startY)+'px';
|
||||
el.style.width=cfg.w+'px';
|
||||
el.style.height=cfg.h+'px';
|
||||
el.style.zIndex=++this.zIndex;
|
||||
const header=document.createElement('div');header.className='window-header';
|
||||
header.addEventListener('mousedown',e=>this.startDrag(e,id));
|
||||
header.addEventListener('touchstart',e=>this.startDragTouch(e,id));
|
||||
const controls=document.createElement('div');controls.className='window-controls';
|
||||
['wc-close','wc-min','wc-max'].forEach((cls,idx)=>{
|
||||
const btn=document.createElement('div');btn.className='window-control '+cls;
|
||||
const actions=['closeWindow','minimizeWindow','maximizeWindow'];
|
||||
btn.addEventListener('click',()=>this[actions[idx]](appName));
|
||||
controls.appendChild(btn);
|
||||
});
|
||||
const title=document.createElement('div');title.className='window-title';title.textContent=cfg.title;
|
||||
header.appendChild(controls);header.appendChild(title);el.appendChild(header);
|
||||
const content=document.createElement('div');content.className='window-content';content.innerHTML=cfg.content;el.appendChild(content);
|
||||
if(!cfg.fixed){
|
||||
const resize=document.createElement('div');resize.className='resize-handle';
|
||||
resize.addEventListener('mousedown',e=>this.startResize(e,id));
|
||||
resize.addEventListener('touchstart',e=>this.startResizeTouch(e,id));
|
||||
el.appendChild(resize);
|
||||
}
|
||||
document.getElementById('desktop').appendChild(el);
|
||||
this.windows[appName]={element:el,id:id,closed:false,minimized:false,appName:appName};
|
||||
document.querySelectorAll('.dock-item').forEach(item=>{if(item.dataset.app===appName)item.classList.add('active')});
|
||||
el.addEventListener('mousedown',()=>this.focusWindow(appName));
|
||||
el.addEventListener('touchstart',()=>this.focusWindow(appName));
|
||||
if(appName==='settings')Settings.init(id);
|
||||
if(appName==='terminal')Terminal.init(id);
|
||||
if(appName==='notepad')Notepad.init(id);
|
||||
if(appName==='finder')Finder.init(id);
|
||||
if(appName==='calculator')Calculator.init(id);
|
||||
if(appName==='snake3d')Snake3D.init(id);
|
||||
if(appName==='tetris3d')Tetris3D.init(id);
|
||||
this.focusWindow(appName);
|
||||
document.getElementById('menu-active-app').textContent=cfg.title;
|
||||
},
|
||||
closeWindow(appName){
|
||||
const win=this.windows[appName];if(!win)return;
|
||||
win.element.remove();win.closed=true;delete this.windows[appName];
|
||||
if(appName==='snake3d')Snake3D.destroy();
|
||||
if(appName==='tetris3d')Tetris3D.destroy();
|
||||
document.querySelectorAll('.dock-item').forEach(item=>{if(item.dataset.app===appName)item.classList.remove('active')});
|
||||
},
|
||||
minimizeWindow(appName){const win=this.windows[appName];if(!win)return;win.minimized=true;win.element.classList.add('minimized')},
|
||||
maximizeWindow(appName){const win=this.windows[appName];if(!win)return;win.element.classList.toggle('maximized')},
|
||||
focusWindow(appName){
|
||||
const win=this.windows[appName];if(!win||win.closed)return;
|
||||
win.element.style.zIndex=++this.zIndex;this.activeWindow=appName;
|
||||
const titles={settings:'Settings',finder:'Finder',terminal:'Terminal',notepad:'Notepad',calculator:'Calculator',snake3d:'Snake 3D',tetris3d:'Tetris 3D'};
|
||||
document.getElementById('menu-active-app').textContent=titles[appName]||'Finder';
|
||||
},
|
||||
startDrag(e,id){
|
||||
if(e.target.closest('.window-control'))return;
|
||||
const el=document.getElementById(id);if(el.classList.contains('maximized'))return;
|
||||
const rect=el.getBoundingClientRect();
|
||||
const offsetX=e.clientX-rect.left;const offsetY=e.clientY-rect.top;
|
||||
const move=ev=>{el.style.left=(ev.clientX-offsetX)+'px';el.style.top=Math.max(28,ev.clientY-offsetY)+'px'};
|
||||
const up=()=>{document.removeEventListener('mousemove',move);document.removeEventListener('mouseup',up)};
|
||||
document.addEventListener('mousemove',move);document.addEventListener('mouseup',up);
|
||||
},
|
||||
startDragTouch(e,id){
|
||||
if(e.target.closest('.window-control'))return;
|
||||
const el=document.getElementById(id);if(el.classList.contains('maximized'))return;
|
||||
const touch=e.touches[0];const rect=el.getBoundingClientRect();
|
||||
const offsetX=touch.clientX-rect.left;const offsetY=touch.clientY-rect.top;
|
||||
const move=ev=>{const t=ev.touches[0];el.style.left=(t.clientX-offsetX)+'px';el.style.top=Math.max(28,t.clientY-offsetY)+'px'};
|
||||
const up=()=>{document.removeEventListener('touchmove',move);document.removeEventListener('touchend',up)};
|
||||
document.addEventListener('touchmove',move);document.addEventListener('touchend',up);
|
||||
},
|
||||
startResize(e,id){
|
||||
const el=document.getElementById(id);const startX=e.clientX;const startY=e.clientY;
|
||||
const startW=el.offsetWidth;const startH=el.offsetHeight;
|
||||
const move=ev=>{el.style.width=Math.max(280,startW+ev.clientX-startX)+'px';el.style.height=Math.max(200,startH+ev.clientY-startY)+'px'};
|
||||
const up=()=>{document.removeEventListener('mousemove',move);document.removeEventListener('mouseup',up)};
|
||||
document.addEventListener('mousemove',move);document.addEventListener('mouseup',up);
|
||||
},
|
||||
startResizeTouch(e,id){
|
||||
const el=document.getElementById(id);const touch=e.touches[0];
|
||||
const startX=touch.clientX;const startY=touch.clientY;
|
||||
const startW=el.offsetWidth;const startH=el.offsetHeight;
|
||||
const move=ev=>{const t=ev.touches[0];el.style.width=Math.max(280,startW+t.clientX-startX)+'px';el.style.height=Math.max(200,startH+t.clientY-startY)+'px'};
|
||||
const up=()=>{document.removeEventListener('touchmove',move);document.removeEventListener('touchend',up)};
|
||||
document.addEventListener('touchmove',move);document.addEventListener('touchend',up);
|
||||
},
|
||||
renderSettings(){
|
||||
const wp=['linear-gradient(135deg, #667eea 0%, #764ba2 100%)','linear-gradient(135deg, #f093fb 0%, #f5576c 100%)','linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)','linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)','linear-gradient(135deg, #fa709a 0%, #fee140 100%)','linear-gradient(135deg, #30cfd0 0%, #330867 100%)'];
|
||||
return '<div class=\'settings-grid\'><div class=\'settings-card\' style=\'grid-column:1/-1\'><h3>Appearance</h3><div style=\'display:grid;grid-template-columns:repeat(3,1fr);gap:10px\'>'+wp.map((w,i)=>'<div class=\'wp-thumb\' data-index=\''+i+'\' style=\'background:'+w+'\'></div>').join('')+'</div><div class=\'wp-custom\'><input type=\'text\' id=\'wp-url\' placeholder=\'Custom image URL...\' /><button id=\'wp-apply\'>Apply</button></div></div></div>';
|
||||
},
|
||||
renderFinder(){return '<div style=\'height:100%;display:flex;flex-direction:column\'><div class=\'fm-toolbar\'><button class=\'fm-btn\' id=\'fm-up\'>Up</button><div class=\'fm-path\' id=\'fm-path\'>/</div><button class=\'fm-btn\' id=\'fm-mkdir\'>New Folder</button><button class=\'fm-btn\' id=\'fm-newfile\'>New File</button><button class=\'fm-btn\' id=\'fm-del\'>Delete</button></div><div class=\'fm-content\' id=\'fm-content\'></div></div>'},
|
||||
renderTerminal(){return '<div class=\'term-container\' id=\'term-container\'></div>'},
|
||||
renderNotepad(){return '<div style=\'height:100%;display:flex;flex-direction:column\'><div class=\'notepad-toolbar\'><button class=\'np-btn\' id=\'np-open\'>Open</button><button class=\'np-btn\' id=\'np-save\'>Save As...</button><span id=\'np-status\' style=\'margin-left:auto;color:rgba(255,255,255,0.5);font-size:12px\'></span></div><textarea class=\'notepad-area\' id=\'np-area\' placeholder=\'Start typing...\'></textarea></div>'},
|
||||
renderCalculator(){return '<div class=\'calc-grid\'><div class=\'calc-display\' id=\'calc-display\'>0</div><button class=\'calc-btn\' data-key=\'C\'>C</button><button class=\'calc-btn\' data-key=\'pm\'>+/-</button><button class=\'calc-btn\' data-key=\'pct\'>%</button><button class=\'calc-btn op\' data-key=\'/\'>÷</button><button class=\'calc-btn\' data-key=\'7\'>7</button><button class=\'calc-btn\' data-key=\'8\'>8</button><button class=\'calc-btn\' data-key=\'9\'>9</button><button class=\'calc-btn op\' data-key=\'*\'>×</button><button class=\'calc-btn\' data-key=\'4\'>4</button><button class=\'calc-btn\' data-key=\'5\'>5</button><button class=\'calc-btn\' data-key=\'6\'>6</button><button class=\'calc-btn op\' data-key=\'-\'>−</button><button class=\'calc-btn\' data-key=\'1\'>1</button><button class=\'calc-btn\' data-key=\'2\'>2</button><button class=\'calc-btn\' data-key=\'3\'>3</button><button class=\'calc-btn op\' data-key=\'+\'>+</button><button class=\'calc-btn zero\' data-key=\'0\'>0</button><button class=\'calc-btn\' data-key=\'.\'>.</button><button class=\'calc-btn op\' data-key=\'=\'>=</button></div>'},
|
||||
renderSnake3D(){return '<canvas class=\'game-canvas\' id=\'snake-canvas\'></canvas><div class=\'game-hud\' id=\'snake-hud\'>Score: 0</div><div class=\'game-controls-hint\'>Arrow keys or swipe to move | Space to start/pause</div><div class=\'game-overlay\' id=\'snake-overlay\'><h2>Snake 3D</h2><p>Use arrow keys or swipe to control</p><button id=\'snake-start\'>Start Game</button></div>'},
|
||||
renderTetris3D(){return '<canvas class=\'game-canvas\' id=\'tetris-canvas\'></canvas><div class=\'game-hud\' id=\'tetris-hud\'>Score: 0</div><div class=\'game-controls-hint\'>Arrows: Move/Rotate | Down: Soft Drop | Space: Hard Drop</div><div class=\'game-overlay\' id=\'tetris-overlay\'><h2>Tetris 3D</h2><p>Classic Tetris in 3D perspective</p><button id=\'tetris-start\'>Start Game</button></div>'},
|
||||
toggleAppleMenu(){alert('Browser OS v1.0\n7 Apps including 2 functional 3D games\nBuilt with HTML, CSS & JS')}
|
||||
};
|
||||
|
||||
const Settings={
|
||||
init(winId){
|
||||
const w=document.getElementById(winId);
|
||||
if(!w)return;
|
||||
w.querySelectorAll('.wp-thumb').forEach(thumb=>{
|
||||
thumb.addEventListener('click',()=>OS.setWallpaper(parseInt(thumb.dataset.index)));
|
||||
});
|
||||
const btn=w.querySelector('#wp-apply');
|
||||
const inp=w.querySelector('#wp-url');
|
||||
if(btn&&inp){btn.addEventListener('click',()=>OS.setWallpaper(inp.value))}
|
||||
}
|
||||
};
|
||||
|
||||
const VFS={
|
||||
data:null,
|
||||
init(){try{const s=localStorage.getItem('os_vfs');this.data=s?JSON.parse(s):this.createRoot()}catch(e){this.data=this.createRoot()}},
|
||||
save(){localStorage.setItem('os_vfs',JSON.stringify(this.data))},
|
||||
createRoot(){return{type:'dir',name:'/',children:{'Documents':{type:'dir',name:'Documents',children:{}},'Downloads':{type:'dir',name:'Downloads',children:{}},'Welcome.txt':{type:'file',name:'Welcome.txt',content:'Welcome to Browser OS!\n\nThis is a fully functional virtual file system.\nYou can create, edit, and delete files using the Terminal or Finder.'}}}},
|
||||
resolvePath(path){if(!path||path==='/')return{parent:this.data,name:'',target:this.data};const parts=path.replace(/\/+$/, '').split('/').filter(p=>p);let current=this.data;for(let i=0;i<parts.length-1;i++){if(!current.children||!current.children[parts[i]]||current.children[parts[i]].type!=='dir')return null;current=current.children[parts[i]]}const name=parts[parts.length-1];return{parent:current,name:name,target:current.children?current.children[name]:undefined}},
|
||||
getDir(path){const res=this.resolvePath(path);if(!res)return null;if(res.target&&res.target.type==='dir')return res.target;if(!res.target&&res.parent&&res.parent.type==='dir')return res.parent;return null},
|
||||
list(path){const dir=this.getDir(path);return dir?Object.keys(dir.children||{}):null},
|
||||
readFile(path){const res=this.resolvePath(path);if(res&&res.target&&res.target.type==='file')return res.target.content;return null},
|
||||
writeFile(path,content){const parts=path.replace(/\/+$/, '').split('/').filter(p=>p);let current=this.data;for(const part of parts.slice(0,-1)){if(!current.children[part]){current.children[part]={type:'dir',name:part,children:{}};current=current.children[part]}else{current=current.children[part]}}const name=parts[parts.length-1];current.children[name]={type:'file',name:name,content:content};this.save()},
|
||||
mkdir(path){const parts=path.replace(/\/+$/, '').split('/').filter(p=>p);let current=this.data;for(const part of parts){if(!current.children[part]){current.children[part]={type:'dir',name:part,children:{}};current=current.children[part]}else{current=current.children[part]}}this.save()},
|
||||
rm(path){const res=this.resolvePath(path);if(!res||!res.target)return false;if(res.parent&&res.parent.children){delete res.parent.children[res.name];this.save();return true}return false}
|
||||
};
|
||||
VFS.init();
|
||||
|
||||
const Terminal={
|
||||
cwd:'/',history:[],histIndex:0,
|
||||
init(winId){
|
||||
const c=document.querySelector('#'+winId+' .term-container');
|
||||
if(!c)return;
|
||||
this.printLine('Browser OS Terminal v1.0','term-info');
|
||||
this.printLine('Type help for available commands.','term-success');
|
||||
this.addInputLine(c);
|
||||
},
|
||||
printLine(text,cls){
|
||||
const c=document.querySelector('.window .term-container');
|
||||
if(!c)return;
|
||||
const line=document.createElement('div');line.className='term-line '+(cls||'');line.textContent=text;
|
||||
c.appendChild(line);c.scrollTop=c.scrollHeight;
|
||||
},
|
||||
addInputLine(container){
|
||||
const line=document.createElement('div');line.className='term-input-line';
|
||||
const prompt=document.createElement('span');prompt.className='term-prompt';prompt.textContent='user@browser-os:'+this.cwd+'$ ';
|
||||
const input=document.createElement('input');input.className='term-input';input.type='text';input.autocomplete='off';
|
||||
input.addEventListener('keydown',e=>this.handleInput(e,input));
|
||||
line.appendChild(prompt);line.appendChild(input);container.appendChild(line);container.scrollTop=container.scrollHeight;
|
||||
setTimeout(()=>input.focus(),10);
|
||||
},
|
||||
handleInput(e,input){
|
||||
if(e.key==='Enter'){
|
||||
const cmd=input.value.trim();input.disabled=true;
|
||||
if(cmd){this.history.push(cmd);this.histIndex=this.history.length;this.execute(cmd)}
|
||||
const container=input.closest('.term-container');this.addInputLine(container);
|
||||
}else if(e.key==='ArrowUp'){
|
||||
e.preventDefault();if(this.histIndex>0){this.histIndex--;input.value=this.history[this.histIndex]}
|
||||
}else if(e.key==='ArrowDown'){
|
||||
e.preventDefault();if(this.histIndex<this.history.length-1){this.histIndex++;input.value=this.history[this.histIndex]}else{this.histIndex=this.history.length;input.value=''}
|
||||
}
|
||||
},
|
||||
execute(cmdStr){
|
||||
const parts=cmdStr.split(/\s+/);const cmd=parts[0];const args=parts.slice(1);
|
||||
switch(cmd){
|
||||
case 'help':this.printLine('Available commands: ls, cd, pwd, mkdir, touch, rm, cat, echo, edit, clear, neofetch, whoami, date','term-info');break;
|
||||
case 'clear':{const c=document.querySelector('.window .term-container');if(c)c.innerHTML=''}break;
|
||||
case 'whoami':this.printLine('user','term-success');break;
|
||||
case 'date':this.printLine(new Date().toString(),'term-success');break;
|
||||
case 'pwd':this.printLine(this.cwd,'term-success');break;
|
||||
case 'ls':{const p=args[0]?this.resolve(args[0]):this.cwd;const list=VFS.list(p);if(list===null){this.printLine('ls: cannot access '+p+': No such directory','term-error')}else if(list.length===0){this.printLine('(empty)','term-info')}else{this.printLine(list.join(' '))}}break;
|
||||
case 'cd':{if(!args[0]){this.cwd='/';break}const np=this.resolve(args[0]);const dir=VFS.getDir(np);if(dir){this.cwd=np}else{this.printLine('cd: '+args[0]+': No such directory','term-error')}}break;
|
||||
case 'mkdir':{if(!args[0]){this.printLine('mkdir: missing operand','term-error');break}VFS.mkdir(this.resolve(args[0]));this.printLine('Directory created','term-success')}break;
|
||||
case 'touch':{if(!args[0]){this.printLine('touch: missing operand','term-error');break}VFS.writeFile(this.resolve(args[0]),'');this.printLine('File created','term-success')}break;
|
||||
case 'rm':{if(!args[0]){this.printLine('rm: missing operand','term-error');break}if(VFS.rm(this.resolve(args[0]))){this.printLine('Removed','term-success')}else{this.printLine('rm: cannot remove '+args[0],'term-error')}}break;
|
||||
case 'cat':{if(!args[0]){this.printLine('cat: missing operand','term-error');break}const content=VFS.readFile(this.resolve(args[0]));if(content!==null){this.printLine(content)}else{this.printLine('cat: '+args[0]+': No such file','term-error')}}break;
|
||||
case 'echo':{const text=args.join(' ');const idx=text.indexOf('>');if(idx>=0){const t=text.slice(0,idx).trim();const f=text.slice(idx+1).trim();VFS.writeFile(this.resolve(f),t);this.printLine('Written to '+f,'term-success')}else{this.printLine(text)}}break;
|
||||
case 'edit':{if(!args[0]){this.printLine('edit: missing operand','term-error');break}const ep=this.resolve(args[0]);const ec=VFS.readFile(ep);if(ec!==null){OS.openApp('notepad');Notepad.openPath(ep,ec);this.printLine('Opened in Notepad','term-success')}else{this.printLine('edit: '+args[0]+': No such file','term-error')}}break;
|
||||
case 'neofetch':this.printLine('OS: Browser OS 1.0','term-info');this.printLine('Host: Chrome Browser','term-info');this.printLine('Shell: BrowserShell','term-info');this.printLine('Resolution: '+window.innerWidth+'x'+window.innerHeight,'term-info');this.printLine('Terminal: WebTerm','term-info');break;
|
||||
default:if(cmd)this.printLine(cmd+': command not found','term-error');
|
||||
}
|
||||
},
|
||||
resolve(p){if(p.startsWith('/'))return p;if(this.cwd==='/')return '/'+p;return this.cwd+'/'+p}
|
||||
};
|
||||
|
||||
const Notepad={
|
||||
currentPath:null,
|
||||
init(winId){
|
||||
this.currentPath=null;
|
||||
const openBtn=document.querySelector('#'+winId+' #np-open');
|
||||
const saveBtn=document.querySelector('#'+winId+' #np-save');
|
||||
if(openBtn)openBtn.addEventListener('click',()=>this.openFile());
|
||||
if(saveBtn)saveBtn.addEventListener('click',()=>this.saveFile());
|
||||
},
|
||||
openPath(path,content){
|
||||
this.currentPath=path;
|
||||
const area=document.getElementById('np-area');
|
||||
if(area)area.value=content||'';
|
||||
const status=document.getElementById('np-status');
|
||||
if(status)status.textContent=path;
|
||||
},
|
||||
openFile(){
|
||||
const name=prompt('Enter file path to open:');
|
||||
if(!name)return;
|
||||
const path=name.startsWith('/')?name:'/'+name;
|
||||
const content=VFS.readFile(path);
|
||||
if(content!==null){this.openPath(path,content)}else{alert('File not found: '+path)}
|
||||
},
|
||||
saveFile(){
|
||||
const name=prompt('Save as path:');
|
||||
if(!name)return;
|
||||
const path=name.startsWith('/')?name:'/'+name;
|
||||
const area=document.getElementById('np-area');
|
||||
if(area){VFS.writeFile(path,area.value);this.currentPath=path;const status=document.getElementById('np-status');if(status)status.textContent='Saved to '+path}
|
||||
}
|
||||
};
|
||||
|
||||
const Calculator={
|
||||
display:'0',prev:null,op:null,resetNext:false,
|
||||
init(winId){
|
||||
const grid=document.querySelector('#'+winId+' .calc-grid');
|
||||
if(!grid)return;
|
||||
grid.addEventListener('click',e=>{if(e.target.classList.contains('calc-btn')){this.press(e.target.dataset.key)}});
|
||||
},
|
||||
press(key){
|
||||
const disp=document.getElementById('calc-display');
|
||||
if(!disp)return;
|
||||
if(key==='C'){this.display='0';this.prev=null;this.op=null;this.resetNext=false}
|
||||
else if(key==='pm'){this.display=String(parseFloat(this.display)*-1)}
|
||||
else if(key==='pct'){this.display=String(parseFloat(this.display)/100)}
|
||||
else if(['+','-','*','/'].includes(key)){this.prev=parseFloat(this.display);this.op=key;this.resetNext=true}
|
||||
else if(key==='='){
|
||||
if(this.op&&this.prev!==null){
|
||||
const curr=parseFloat(this.display);let res=0;
|
||||
switch(this.op){case '+':res=this.prev+curr;break;case '-':res=this.prev-curr;break;case '*':res=this.prev*curr;break;case '/':res=curr===0?'Error':this.prev/curr;break}
|
||||
this.display=String(res);this.prev=null;this.op=null;this.resetNext=true
|
||||
}
|
||||
}else{
|
||||
if(this.resetNext){this.display=key==='.'?'0.':key;this.resetNext=false}
|
||||
else{this.display=this.display==='0'&&key!=='.'?key:this.display+key}
|
||||
}
|
||||
disp.textContent=this.display;
|
||||
}
|
||||
};
|
||||
|
||||
const Finder={
|
||||
cwd:'/',selected:null,
|
||||
init(winId){
|
||||
this.cwd='/';this.selected=null;
|
||||
const w=document.getElementById(winId);
|
||||
if(!w)return;
|
||||
const upBtn=w.querySelector('#fm-up');
|
||||
const mkdirBtn=w.querySelector('#fm-mkdir');
|
||||
const newfileBtn=w.querySelector('#fm-newfile');
|
||||
const delBtn=w.querySelector('#fm-del');
|
||||
const contentEl=w.querySelector('#fm-content');
|
||||
if(upBtn)upBtn.addEventListener('click',()=>this.goUp());
|
||||
if(mkdirBtn)mkdirBtn.addEventListener('click',()=>this.mkdirPrompt());
|
||||
if(newfileBtn)newfileBtn.addEventListener('click',()=>this.newFilePrompt());
|
||||
if(delBtn)delBtn.addEventListener('click',()=>this.deleteSelected());
|
||||
if(contentEl)contentEl.addEventListener('click',()=>this.deselect());
|
||||
this.render();
|
||||
},
|
||||
render(){
|
||||
const pathEl=document.getElementById('fm-path');
|
||||
const contentEl=document.getElementById('fm-content');
|
||||
if(!pathEl||!contentEl)return;
|
||||
pathEl.textContent=this.cwd;
|
||||
contentEl.innerHTML='';
|
||||
const dir=VFS.getDir(this.cwd);
|
||||
if(!dir||!dir.children)return;
|
||||
Object.values(dir.children).forEach(item=>{
|
||||
const div=document.createElement('div');div.className='fm-item';
|
||||
div.innerHTML='<div class=\'fm-icon\'>'+(item.type==='dir'?'📁':'📄')+'</div><div class=\'fm-name\'>'+item.name+'</div>';
|
||||
div.addEventListener('click',e=>{e.stopPropagation();this.selectItem(div,item)});
|
||||
div.addEventListener('dblclick',()=>{if(item.type==='dir'){this.cwd=this.cwd==='/'?'/'+item.name:this.cwd+'/'+item.name;this.render()}else{OS.openApp('notepad');Notepad.openPath(this.cwd==='/'?'/'+item.name:this.cwd+'/'+item.name,item.content)}});
|
||||
contentEl.appendChild(div);
|
||||
});
|
||||
},
|
||||
selectItem(el,item){
|
||||
document.querySelectorAll('.fm-item').forEach(i=>i.classList.remove('selected'));
|
||||
el.classList.add('selected');this.selected=item;
|
||||
},
|
||||
deselect(){
|
||||
document.querySelectorAll('.fm-item').forEach(i=>i.classList.remove('selected'));
|
||||
this.selected=null;
|
||||
},
|
||||
goUp(){
|
||||
if(this.cwd==='/')return;
|
||||
const parts=this.cwd.split('/').filter(p=>p);parts.pop();
|
||||
this.cwd=parts.length===0?'/':'/'+parts.join('/');
|
||||
this.render();
|
||||
},
|
||||
mkdirPrompt(){
|
||||
const name=prompt('Folder name:');if(!name)return;
|
||||
const path=this.cwd==='/'?'/'+name:this.cwd+'/'+name;
|
||||
VFS.mkdir(path);this.render();
|
||||
},
|
||||
newFilePrompt(){
|
||||
const name=prompt('File name:');if(!name)return;
|
||||
const path=this.cwd==='/'?'/'+name:this.cwd+'/'+name;
|
||||
VFS.writeFile(path,'');this.render();
|
||||
},
|
||||
deleteSelected(){
|
||||
if(!this.selected)return;
|
||||
const path=this.cwd==='/'?'/'+this.selected.name:this.cwd+'/'+this.selected.name;
|
||||
VFS.rm(path);this.selected=null;this.render();
|
||||
}
|
||||
};
|
||||
|
||||
const E3D={proj(x,y,z,c){const ay=c.angleY,ax=c.angleX,x1=x*Math.cos(ay)-z*Math.sin(ay),z1=x*Math.sin(ay)+z*Math.cos(ay),y2=y*Math.cos(ax)-z1*Math.sin(ax),z2=y*Math.sin(ax)+z1*Math.cos(ax),sc=c.fov/(c.fov+z2+c.dist);return{x:x1*sc+c.cx,y:y2*sc+c.cy,scale:sc}},cube(ctx,cx,cy,cz,sz,col,cam){const h=sz/2,v=[{x:cx-h,y:cy-h,z:cz-h},{x:cx+h,y:cy-h,z:cz-h},{x:cx+h,y:cy+h,z:cz-h},{x:cx-h,y:cy+h,z:cz-h},{x:cx-h,y:cy-h,z:cz+h},{x:cx+h,y:cy-h,z:cz+h},{x:cx+h,y:cy+h,z:cz+h},{x:cx-h,y:cy+h,z:cz+h}];const pv=v.map(p=>this.proj(p.x,p.y,p.z,cam));const fs=[{i:[0,1,2,3]},{i:[4,5,6,7]},{i:[0,1,5,4]},{i:[2,3,7,6]},{i:[0,3,7,4]},{i:[1,2,6,5]}];fs.forEach(f=>{f.z=f.i.reduce((s,j)=>s+pv[j].scale,0)/4});fs.sort((a,b)=>a.z-b.z);fs.forEach(f=>{ctx.fillStyle=col;ctx.strokeStyle='rgba(0,0,0,0.3)';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(pv[f.i[0]].x,pv[f.i[0]].y);for(let k=1;k<4;k++)ctx.lineTo(pv[f.i[k]].x,pv[f.i[k]].y);ctx.closePath();ctx.fill();ctx.stroke()})}};
|
||||
|
||||
const Snake3D={c:null,x:null,loop:null,al:null,cam:{dist:400,angleY:0,angleX:.3,cx:0,cy:0,fov:600},gs:15,cs:18,snake:[],food:{},dir:{x:1,y:0},nd:{x:1,y:0},score:0,run:false,over:false,
|
||||
init(){this.c=document.getElementById('snake-canvas');this.x=this.c.getContext('2d');this.resize();window.addEventListener('resize',()=>this.resize());document.getElementById('snake-start').addEventListener('click',()=>this.start());window.addEventListener('keydown',e=>this.key(e));this.c.addEventListener('touchstart',e=>this.touch(e))},
|
||||
resize(){const r=this.c.parentElement.getBoundingClientRect();this.c.width=r.width;this.c.height=r.height;this.cam.cx=this.c.width/2;this.cam.cy=this.c.height/2},
|
||||
start(){this.snake=[{x:2,y:7},{x:1,y:7},{x:0,y:7}];this.food=this.rand();this.dir={x:1,y:0};this.nd={x:1,y:0};this.score=0;this.run=true;this.over=false;document.getElementById('snake-overlay').classList.add('hidden');document.getElementById('snake-hud').textContent='Score: 0';if(this.loop)clearInterval(this.loop);this.loop=setInterval(()=>this.tick(),120);this.frame()},
|
||||
destroy(){if(this.loop)clearInterval(this.loop);if(this.al)cancelAnimationFrame(this.al);this.run=false},
|
||||
rand(){let f;do{f={x:Math.floor(Math.random()*this.gs),y:Math.floor(Math.random()*this.gs)}}while(this.snake.some(s=>s.x===f.x&&s.y===f.y));return f},
|
||||
key(e){if(!this.run)return;const k=e.key;if(k==='ArrowUp'&&this.dir.y===0)this.nd={x:0,y:-1};if(k==='ArrowDown'&&this.dir.y===0)this.nd={x:0,y:1};if(k==='ArrowLeft'&&this.dir.x===0)this.nd={x:-1,y:0};if(k==='ArrowRight'&&this.dir.x===0)this.nd={x:1,y:0};if(k===' '){e.preventDefault();if(this.over)this.start()}},
|
||||
touch(e){if(!this.run)return;e.preventDefault();const t=e.touches[0];const r=this.c.getBoundingClientRect();const mx=t.clientX-r.left;const my=t.clientY-r.top;const cx=r.width/2;const cy=r.height/2;if(Math.abs(mx-cx)>Math.abs(my-cy)){if(mx>cx&&this.dir.x===0)this.nd={x:1,y:0};if(mx<cx&&this.dir.x===0)this.nd={x:-1,y:0}}else{if(my>cy&&this.dir.y===0)this.nd={x:0,y:1};if(my<cy&&this.dir.y===0)this.nd={x:0,y:-1}}},
|
||||
tick(){if(!this.run||this.over)return;this.dir=this.nd;const h={x:this.snake[0].x+this.dir.x,y:this.snake[0].y+this.dir.y};if(h.x<0||h.x>=this.gs||h.y<0||h.y>=this.gs||this.snake.some(s=>s.x===h.x&&s.y===h.y)){this.over=true;this.run=false;document.getElementById('snake-overlay').classList.remove('hidden');document.querySelector('#snake-overlay h2').textContent='Game Over! Score: '+this.score;return}this.snake.unshift(h);if(h.x===this.food.x&&h.y===this.food.y){this.score+=10;this.food=this.rand();document.getElementById('snake-hud').textContent='Score: '+this.score}else{this.snake.pop()}},
|
||||
frame(){if(!this.x)return;this.cam.angleY+=.005;this.x.clearRect(0,0,this.c.width,this.c.height);this.x.fillStyle='#111';this.x.fillRect(0,0,this.c.width,this.c.height);const gs=this.gs;const cs=this.cs;for(let i=0;i<=gs;i++){for(let j=0;j<=gs;j++){const px=(i-gs/2)*cs;const py=80;const pz=(j-gs/2)*cs;const p1=E3D.proj(px-cs/2,py,pz-cs/2,this.cam);const p2=E3D.proj(px+cs/2,py,pz-cs/2,this.cam);const p3=E3D.proj(px+cs/2,py,pz+cs/2,this.cam);const p4=E3D.proj(px-cs/2,py,pz+cs/2,this.cam);this.x.strokeStyle='rgba(255,255,255,0.05)';this.x.lineWidth=1;this.x.beginPath();this.x.moveTo(p1.x,p1.y);this.x.lineTo(p2.x,p2.y);this.x.lineTo(p3.x,p3.y);this.x.lineTo(p4.x,p4.y);this.x.closePath();this.x.stroke()}}const fx=(this.food.x-gs/2+.5)*cs;const fy=80-cs/2;const fz=(this.food.y-gs/2+.5)*cs;E3D.cube(this.x,fx,fy,fz,cs*.6,'#ff453a',this.cam);this.snake.forEach((s,i)=>{const sx=(s.x-gs/2+.5)*cs;const sy=80-cs/2;const sz=(s.y-gs/2+.5)*cs;E3D.cube(this.x,sx,sy,sz,cs*.8,i===0?'#32d74b':'#30d158',this.cam)});if(this.run||this.over)this.al=requestAnimationFrame(()=>this.frame())}
|
||||
};
|
||||
|
||||
const Tetris3D={c:null,x:null,loop:null,al:null,cam:{dist:500,angleY:0,angleX:.4,cx:0,cy:0,fov:600},cols:10,rows:20,cellSize:14,board:[],piece:null,score:0,run:false,over:false,shapes:[[[1,1,1,1]],[[1,1],[1,1]],[[0,1,0],[1,1,1]],[[0,1,1],[1,1,0]],[[1,1,0],[0,1,1]],[[1,0,0],[1,1,1]],[[0,0,1],[1,1,1]]],colors:['#64d2ff','#ffd60a','#bf5af2','#32d74b','#ff453a','#0a84ff','#ff9f0a'],
|
||||
init(){this.c=document.getElementById('tetris-canvas');this.x=this.c.getContext('2d');this.resize();window.addEventListener('resize',()=>this.resize());document.getElementById('tetris-start').addEventListener('click',()=>this.start());window.addEventListener('keydown',e=>this.key(e))},
|
||||
resize(){const r=this.c.parentElement.getBoundingClientRect();this.c.width=r.width;this.c.height=r.height;this.cam.cx=this.c.width/2;this.cam.cy=this.c.height/2},
|
||||
start(){this.board=Array(this.rows).fill(0).map(()=>Array(this.cols).fill(0));this.score=0;this.run=true;this.over=false;this.spawn();document.getElementById('tetris-overlay').classList.add('hidden');document.getElementById('tetris-hud').textContent='Score: 0';if(this.loop)clearInterval(this.loop);this.loop=setInterval(()=>this.tick(),500);this.frame()},
|
||||
destroy(){if(this.loop)clearInterval(this.loop);if(this.al)cancelAnimationFrame(this.al);this.run=false},
|
||||
spawn(){const i=Math.floor(Math.random()*this.shapes.length);this.piece={shape:this.shapes[i],color:this.colors[i],x:Math.floor(this.cols/2)-1,y:0};if(this.col(this.piece.shape,this.piece.x,this.piece.y)){this.over=true;this.run=false;document.getElementById('tetris-overlay').classList.remove('hidden');document.querySelector('#tetris-overlay h2').textContent='Game Over! Score: '+this.score}},
|
||||
col(s,ox,oy){for(let y=0;y<s.length;y++)for(let x=0;x<s[y].length;x++)if(s[y][x]){const nx=ox+x,ny=oy+y;if(nx<0||nx>=this.cols||ny>=this.rows)return true;if(ny>=0&&this.board[ny][nx])return true}return false},
|
||||
merge(){const s=this.piece.shape;for(let y=0;y<s.length;y++)for(let x=0;x<s[y].length;x++)if(s[y][x]){const ny=this.piece.y+y;if(ny>=0)this.board[ny][this.piece.x+x]=this.piece.color}},
|
||||
clear(){let lines=0;for(let y=this.rows-1;y>=0;y--)if(this.board[y].every(c=>c!==0)){this.board.splice(y,1);this.board.unshift(Array(this.cols).fill(0));lines++;y++}if(lines>0){this.score+=lines*100;document.getElementById('tetris-hud').textContent='Score: '+this.score}},
|
||||
rot(){const s=this.piece.shape;const ns=s[0].map((_,i)=>s.map(r=>r[i]).reverse());let nx=this.piece.x;if(this.col(ns,nx,this.piece.y)){nx--;if(this.col(ns,nx,this.piece.y)){nx+=2;if(this.col(ns,nx,this.piece.y))return}}this.piece.shape=ns;this.piece.x=nx},
|
||||
key(e){if(!this.run)return;if(e.key==='ArrowLeft'){if(!this.col(this.piece.shape,this.piece.x-1,this.piece.y))this.piece.x--}else if(e.key==='ArrowRight'){if(!this.col(this.piece.shape,this.piece.x+1,this.piece.y))this.piece.x++}else if(e.key==='ArrowDown'){if(!this.col(this.piece.shape,this.piece.x,this.piece.y+1))this.piece.y++;else{this.merge();this.clear();this.spawn()}}else if(e.key==='ArrowUp'){this.rot()}else if(e.key===' '){e.preventDefault();while(!this.col(this.piece.shape,this.piece.x,this.piece.y+1))this.piece.y++;this.merge();this.clear();this.spawn()}},
|
||||
tick(){if(!this.run||this.over)return;if(!this.col(this.piece.shape,this.piece.x,this.piece.y+1))this.piece.y++;else{this.merge();this.clear();this.spawn()}},
|
||||
frame(){if(!this.x)return;this.cam.angleY+=.003;const w=this.c.width;const h=this.c.height;this.x.clearRect(0,0,w,h);this.x.fillStyle='#111';this.x.fillRect(0,0,w,h);const bw=this.cols*this.cellSize;const bh=this.rows*this.cellSize;const bx=-bw/2;const by=-bh/2+100;for(let y=0;y<this.rows;y++)for(let x=0;x<this.cols;x++)if(this.board[y][x]){const px=bx+x*this.cellSize+this.cellSize/2;const py=by+y*this.cellSize+this.cellSize/2;E3D.cube(this.x,px,py,0,this.cellSize*.9,this.board[y][x],this.cam)}if(this.piece)for(let y=0;y<this.piece.shape.length;y++)for(let x=0;x<this.piece.shape[y].length;x++)if(this.piece.shape[y][x]){const px=bx+(this.piece.x+x)*this.cellSize+this.cellSize/2;const py=by+(this.piece.y+y)*this.cellSize+this.cellSize/2;E3D.cube(this.x,px,py,0,this.cellSize*.9,this.piece.color,this.cam)}const co=[E3D.proj(bx,by,0,this.cam),E3D.proj(bx+bw,by,0,this.cam),E3D.proj(bx+bw,by+bh,0,this.cam),E3D.proj(bx,by+bh,0,this.cam)];this.x.strokeStyle='rgba(255,255,255,0.2)';this.x.lineWidth=2;this.x.beginPath();this.x.moveTo(co[0].x,co[0].y);for(let i=1;i<4;i++)this.x.lineTo(co[i].x,co[i].y);this.x.closePath();this.x.stroke();if(this.run||this.over)this.al=requestAnimationFrame(()=>this.frame())}
|
||||
};
|
||||
|
||||
OS.init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user