/
hamster75
/
Life
Обзор
Документация
Войти
/
hamster75
/
Life
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
6
CI/CD
Аналитика
Безопасность
master
js/camera.js
265 строк
8 KB
Dmitry Smirnov
my collection wip
28 май 2026, 16:34
28 май 2026, 16:34
c2affe5
Код
Авторство
О чём код?
export class Camera { constructor() { this.x = 0; // смещение в пикселях this.y = 0; this.scale = 16; // пикселей на клетку (начальный зум) this._spaceDown = false; this._middleDown = false; this._dragStartX = 0; this._dragStartY = 0; this._dragOriginX = null; this._dragOriginY = null; this._canvas = null; this._onChangeCallbacks = []; this._touchMidX = 0; this._touchMidY = 0; this._touchOriginX = 0; this._touchOriginY = 0; this._touchPinchDist = 0; this._canEditFn = null; this.simRunning = false; // set from main.js via sim.onState this._touch1Active = false; this._touch1StartX = 0; this._touch1StartY = 0; this._touch1OriginX = 0; this._touch1OriginY = 0; } // Привязать камеру к canvas-элементу attach(canvas) { this._canvas = canvas; canvas.addEventListener('wheel', this._onWheel.bind(this), { passive: false }); canvas.addEventListener('mousedown', this._onMouseDown.bind(this)); canvas.addEventListener('mousemove', this._onMouseMove.bind(this)); canvas.addEventListener('mouseup', this._onMouseUp.bind(this)); canvas.addEventListener('mouseleave', this._onMouseUp.bind(this)); canvas.addEventListener('touchstart', this._onCamTouchStart.bind(this), { passive: false }); canvas.addEventListener('touchmove', this._onCamTouchMove.bind(this), { passive: false }); canvas.addEventListener('touchend', this._onCamTouchEnd.bind(this), { passive: false }); window.addEventListener('keydown', this._onKeyDown.bind(this)); window.addEventListener('keyup', this._onKeyUp.bind(this)); } onChange(cb) { this._onChangeCallbacks.push(cb); } setCanEdit(fn) { this._canEditFn = fn; } _notify() { for (const cb of this._onChangeCallbacks) cb(); } zoom(factor) { if (!this._canvas) return; const cx = this._canvas.width / 2; const cy = this._canvas.height / 2; const newScale = Math.min(64, Math.max(1e-6, this.scale * factor)); const ratio = newScale / this.scale; this.x = cx - (cx - this.x) * ratio; this.y = cy - (cy - this.y) * ratio; this.scale = newScale; this._notify(); } // Перевести экранные координаты в координаты клетки screenToCell(sx, sy) { return { x: Math.floor((sx - this.x) / this.scale), y: Math.floor((sy - this.y) / this.scale), }; } // Центрировать поле (width x height клеток) в canvas center(gridWidth, gridHeight, canvasWidth, canvasHeight) { this.x = Math.floor((canvasWidth - gridWidth * this.scale) / 2); this.y = Math.floor((canvasHeight - gridHeight * this.scale) / 2); this._notify(); } // Fit pattern bounding box {minX,maxX,minY,maxY} into canvas with 5% padding. fitView(minX, minY, maxX, maxY, canvasW, canvasH) { const W = maxX - minX + 1; const H = maxY - minY + 1; const scale = Math.max(1e-6, Math.min(64, Math.min(canvasW / W, canvasH / H) * 0.9)); this.scale = scale; const cx = (minX + maxX) / 2; const cy = (minY + maxY) / 2; this.x = Math.floor(canvasW / 2 - cx * scale); this.y = Math.floor(canvasH / 2 - cy * scale); this._notify(); } get isPanning() { return this._spaceDown && this._middleDown === false ? false : this._middleDown || (this._spaceDown && this._dragOriginX !== null); } _startDrag(clientX, clientY) { this._dragStartX = clientX; this._dragStartY = clientY; this._dragOriginX = this.x; this._dragOriginY = this.y; if (this._canvas) this._canvas.classList.add('panning'); } _stopDrag() { this._dragOriginX = null; this._dragOriginY = null; if (this._canvas) this._canvas.classList.remove('panning'); } _isDragging() { return this._dragOriginX !== null; } _onWheel(e) { e.preventDefault(); // Зум: колёсико без зажатых клавиш (не средняя кнопка) if (!this._middleDown) { const rect = this._canvas.getBoundingClientRect(); const mx = e.clientX - rect.left; const my = e.clientY - rect.top; const factor = e.deltaY < 0 ? 1.15 : 1 / 1.15; const newScale = Math.min(64, Math.max(1e-6, this.scale * factor)); const ratio = newScale / this.scale; // Зумируем относительно курсора this.x = mx - (mx - this.x) * ratio; this.y = my - (my - this.y) * ratio; this.scale = newScale; this._notify(); } } _onMouseDown(e) { // Средняя кнопка (button=1) — pan if (e.button === 1) { e.preventDefault(); this._middleDown = true; this._startDrag(e.clientX, e.clientY); } // ЛКМ с зажатым пробелом — pan // Во время симуляции пробел не нужен: редактирование заблокировано, ЛКМ сразу панирует if (e.button === 0 && (this._spaceDown || this.simRunning)) { this._startDrag(e.clientX, e.clientY); } } _onMouseMove(e) { if (!this._isDragging()) return; const dx = e.clientX - this._dragStartX; const dy = e.clientY - this._dragStartY; this.x = this._dragOriginX + dx; this.y = this._dragOriginY + dy; this._notify(); } _onMouseUp(e) { if (e.button === 1) this._middleDown = false; if (e.button === 0 || e.type === 'mouseleave') { if (this._spaceDown || e.button === 1) { // ok } } // Прекращаем drag если ни средняя, ни пробел if (!this._middleDown && !this._spaceDown) { this._stopDrag(); } else if (e.button === 1 && !this._spaceDown) { this._stopDrag(); } } _onKeyDown(e) { const tag = e.target?.tagName; if (tag === 'INPUT' || tag === 'TEXTAREA') return; if (e.code === 'Space' && !e.repeat) { e.preventDefault(); this._spaceDown = true; if (this._canvas) this._canvas.style.cursor = 'grab'; } } _onKeyUp(e) { if (e.code === 'Space') { this._spaceDown = false; this._stopDrag(); if (this._canvas) this._canvas.style.cursor = ''; } } _pinchDist(t0, t1) { const dx = t0.clientX - t1.clientX; const dy = t0.clientY - t1.clientY; return Math.sqrt(dx * dx + dy * dy); } _onCamTouchStart(e) { if (e.touches.length === 2) { e.preventDefault(); this._touch1Active = false; this._touchMidX = (e.touches[0].clientX + e.touches[1].clientX) / 2; this._touchMidY = (e.touches[0].clientY + e.touches[1].clientY) / 2; this._touchOriginX = this.x; this._touchOriginY = this.y; this._touchPinchDist = this._pinchDist(e.touches[0], e.touches[1]); } else if (e.touches.length === 1 && this._canEditFn && !this._canEditFn()) { e.preventDefault(); this._touch1Active = true; this._touch1StartX = e.touches[0].clientX; this._touch1StartY = e.touches[0].clientY; this._touch1OriginX = this.x; this._touch1OriginY = this.y; } } _onCamTouchMove(e) { if (e.touches.length === 1 && this._touch1Active) { e.preventDefault(); this.x = this._touch1OriginX + e.touches[0].clientX - this._touch1StartX; this.y = this._touch1OriginY + e.touches[0].clientY - this._touch1StartY; this._notify(); return; } if (e.touches.length !== 2) return; e.preventDefault(); const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2; const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2; const dist = this._pinchDist(e.touches[0], e.touches[1]); this.x = this._touchOriginX + midX - this._touchMidX; this.y = this._touchOriginY + midY - this._touchMidY; if (this._touchPinchDist > 0) { const factor = dist / this._touchPinchDist; const newScale = Math.min(64, Math.max(1e-6, this.scale * factor)); const ratio = newScale / this.scale; this.x = midX - (midX - this.x) * ratio; this.y = midY - (midY - this.y) * ratio; this.scale = newScale; this._touchPinchDist = dist; this._touchMidX = midX; this._touchMidY = midY; this._touchOriginX = this.x; this._touchOriginY = this.y; } this._notify(); } _onCamTouchEnd(e) { if (e.touches.length < 2) this._touchPinchDist = 0; if (e.touches.length === 0) this._touch1Active = false; } }