Перемещение фигур JS

У меня есть проблемы с перемещением фигур на канвасе. При первом нажатии на прямоугольник все нормально - я кликаю внутри него и перемещаю. После первого перемещения начинаются какие-то проблемы с координатами и прямоугольник уже не нажимается там, где он отрисован. Нажимается чуть ниже. Я где-то ошибся с рассчетом координат, но не могу понять, где.

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Diagram Editor</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    
    <div class="geMenubarContainer">
        <a class="geIcon"></a>
        <div class="geTextDiv">
            <a class="geItem"></a>
        </div>
        <div class="geMenubar">
            <a class="geItem">Файл</a>
            <a class="geItem">Правка</a>
            <a class="geItem">Вид</a>
            <a class="geItem">Положение</a>
            <a class="geItem">Дополнительно</a>
            <a class="geItem">Помощь</a>
        </div>
    </div>
    <div class="geToolbarContainer">
        <div class="geToolbar"></div>
        <a class="geLabel" title="Масштаб (Alt+Mousewheel)">
            100%
            <img src="icons/arrow.gif"></img>
        </a>
        <div class="geSeparator"></div>
        <a class="geLabel" title="Увеличить">
            <img src="icons/glass1.png"></img>
        </a>
        <a class="geLabel" title="Уменьшить">
            <img src="icons/glass2.png"></img>
        </a>
        <div class="geSeparator"></div>
        <a class="geLabel" title="Отменить (Ctrl+Z)">
            <img src="icons/undo.png"></img>
        </a>
        <a class="geLabel" title="Вернуть (Ctrl+Y)">
            <img src="icons/revert.png"></img>
        </a>
        <div class="geSeparator"></div>
        <a class="geLabel" title="Удалить (Delete)">
            <img src="icons/delete.png"></img>
        </a>
        <div class="geSeparator"></div>
        <a class="geLabel" title="Цвет линии">
            <img src="icons/pencil.png"></img>
        </a>
        <a class="geLabel" title="Цвет заливки">
            <img src="icons/filling.png"></img>
        </a>
        <input type="color" id="color-pick-input" title="Цвет линии">
        <input type="range" id="line-width-input" min="1" max="40">
        <div class="geSeparator"></div>
        <a class="geLabel" title="Тип соединения">
            <img src="icons/lines.png"></img>
        </a>
        <a id="text" class="geLabel" title="Добавить текст">
            <img src="icons/text.png"></img>
        </a>
        <div class="geSeparator"></div>
        <a id="clear-button" class="geLabel" title="Добавить текст">
            <img src="icons/clear.png"></img>
        </a>
        <a id="save-button" class="geLabel" title="Добавить текст">
            <img src="icons/save.png"></img>
        </a>
        <div class="geSeparator"></div>
    </div>

    <div id="diagram-container">
        <canvas id="canvas" width="1300" height="800" ></canvas>
    </div>
    <div id="sidebar">
        <img id="btn-toggle-menu" src="icons/next.png"></img>
        <h3>Фигуры</h3>
        <a class="geTitle" >Избранное</a>
        <a class="geTitle" >Общие</a>
            <div  class="geSidebar">
                
                <a class="geItem" title="прямая линия" data-shape="line">
                    <img src="mainFigures/line.png" ></img>
                </a>
                <a class="geItem" title="прямоугольник" data-shape="rectangle">
                    <img src="mainFigures/rectangle.png" ></img>
                </a>
                <a class="geItem" title="круг" data-shape="circle">
                    <img src="mainFigures/circle.png" ></img>
                </a>
                <a class="geItem" title="эллипс">
                    <img src="mainFigures/ellipse.png" ></img>
                </a>
                <a class="geItem" title="треугольник">
                    <img src="mainFigures/triangle.png" ></img>
                </a>
                <a class="geItem" title="трапеция">
                    <img src="mainFigures/trapezium.png" ></img>
                </a>
                <a class="geItem" title="ромб">
                    <img src="mainFigures/rhomb.png" ></img>
                </a>
                <a class="geItem" title="параллелограмм">
                    <img src="mainFigures/parallelogram.png" ></img>
                </a>
                <a class="geItem" title="пятиугольник">
                    <img src="mainFigures/pentagon.png" ></img>
                </a>
                <a class="geItem" title="шестиугольник">
                    <img src="mainFigures/hexagon.png" ></img>
                </a>
                <a class="geItem" title="односторонняя стрелка">
                    <img src="mainFigures/arrows.png" ></img>
                </a>
                <a class="geItem" title="двусторонняя стрелка">
                    <img src="mainFigures/double-arrow.png" ></img>
                </a>
                <a class="geItem" title="криволинейная стрелка" >
                    <img src="mainFigures/curve-arrow.png" ></img>
                </a>
            </div>
        <a class="geTitle" >Блок-схемы</a>
        <a class="geTitle" >UML</a>
    </div>
    <script src="script.js"></script>
    <script src="draw.js"></script>

</body>
</html>

style.css:

body {
    font-family: Arial, sans-serif;
    display:flex;
    flex-direction: column;
    height: 100vh;
    margin: 0;
    padding: 0;
    background-color: #fbfbfb;
    align-items: center;
    justify-content: center;
}
a:hover {
    color: #FFFF00;
}
a {
     text-decoration: none;
     cursor: pointer;
}
.geMenubarContainer{
  display: inline-flex;
  align-items: center;
  overflow: hidden;
  position: absolute;
  cursor: default;
  top: 0px;
  left: 0px;
  right: 0px;
  height: 64px;
  background-color: #f1f3f4;
}
.geMenubarContainer .geTextDiv{
  position: absolute;
  right: 120px;
  left: 60px;
  top: 9px;
  height: 26px;
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  visibility: visible;
}
.geTextDiv .geItem{
  padding: 2px 8px;
  display: inline;
  font-size: 18px;
  transition: all 0.1s ease-in-out;
  cursor: pointer;
}
.geMenubar{
  display: inline-flex;
  white-space: nowrap;
  align-items: center;
  position: absolute;
  padding-left: 59px;
  box-sizing: border-box;
  top: 34px;
}
.geMenubarContainer .geItem{
    padding: 6px 6px 6px 9px;
    transition: all 0.1s ease-in-out;
}
.geMenubarContainer .geIcon{
    display: block;
    position: absolute;
    top: 12px;
    width: 36px;
    height: 36px;
    margin: 8px 0px 8px 16px;
    opacity: 0.85;
    border-radius: 3px;
    background-position: center center;
    background-size: 90% 90%;
    background-repeat: no-repeat;
    background-image: url('../icons/icon.png')
}
.geToolbarContainer{
  border-width: 1px;
  border-style: none none solid none;
  box-shadow: none;
  overflow: hidden;
  position: absolute;
  cursor: default;
  z-index: 1;
  left: 0px;
  right: 0px;
  top: 64px;
  height: 38px;
  background-color: #f1f3f4;
}
.geToolbar{
    padding-left: 16px;
    border-top: 1px solid #dadce0;
    box-shadow: inset 0 1px 0 0 #fff;
    padding-bottom: 3px;
}
.geSeparator{
  float: left;
  width: 1px;
  height: 20px;
  background: #e5e5e5;
  margin-left: 8px;
  margin-right: 6px;
  margin-top: 4px;
}
.geLabel{
  white-space: nowrap;
  position: relative;
  overflow: hidden;
  width: 50px;
  float: left;
  margin: 2px;
  padding: 3px 5px 3px 5px;
  border: 1px solid transparent;
  transition: all 0.1s ease-in-out;
}
.geToolbar a {
    color: #000000;
    text-decoration: none;
}
.geToolbarContainer img{
    width:20px;
    height:20px;
}
.geButton {
    float: left;
    width: 20px;
    height: 20px;
    padding: 0px 2px 4px 2px;
    margin: 2px;
    border: 1px solid transparent;
    cursor: pointer;
    opacity: 0.6;
    transition: all 0.1s ease-in-out;
}

#diagram-container {
    position: absolute;
    width: calc(100% - 215px); /* Ширина сайдбара */
    height: calc(100% - 64px - 38.8px); /* Высота экрана за вычетом высоты тулбара и сайдбара */
    top: 110px; /* Высота тулбара */
    left: 59px; /* Ширина тулбара */
}

#canvas {
    width: 1300px;
    height: 600px;
    border: 1px solid #ccc;
    background-color: #FFFFFF;
    
}

#sidebar {
    position: fixed;
    overflow-y: scroll;
    z-index: 1;
    top: 100px;
    bottom: 0px;
    right: 0;
    width: 215px;
    height: 100vh;
    background-color: #f1f3f4;
    padding: 20px;
    box-shadow: -5px 0px 5px rgba(0, 0, 0, 0.1);
    transition: right 0.3s ease;
}

#sidebar.open {
    right: -230px;
}

/* Стили для кнопки */
#sidebar h3 {
    position: relative;
    padding-left: 30px; /* Добавляем отступ слева для размещения кнопки */
    text-align: center;
}


#btn-toggle-menu {
    position: absolute;
    width: 20px; /* Размер изображения */
    height: 20px;
    left: 0; /* Положение слева */
    top: 0; /* Положение сверху */
    background-color: transparent;
    border: none;
    padding: 0;
    margin: 0;
    cursor: pointer;
}

#btn-toggle-menu img {
    width: 20px; /* Размер изображения */
    height: 20px;
}
.geTitle{
    display: block;
    background-image: url('../icons/arrow.gif');
    background-repeat: no-repeat;
    background-position: 4px 50%;
    position: relative;
    padding-left: 72px;
    border-bottom: 1px solid #e5e5e5;
    border-top: 1px solid #e5e5e5;
    font-weight: 500;
    font-size: 13px;
    border-color: #e5e5e5;
}
.geSidebar{
    display: block;
    transform-origin: right top 0px;
    border-bottom: 1px solid #e5e5e5;
    padding: 6px;
    overflow: hidden;
}
.geSidebar .geItem{
    display: inline-block;
    background-repeat: no-repeat;
    background-position: 50% 50%;
    border-radius: 8px;
    overflow: hidden;
    width: 34px;
    height: 32px;
    padding: 1px;
}
.geItem img{
    right: 1px;
    top: 1px;
    width: 32px;
    height: 30px;
    display: block;
    position: relative;
    overflow: hidden;
    pointer-events: none;
}
.rectangle {
    border: 3px solid #FF0000;
    position: absolute;
}

draw.js:

const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");

const mouse = {
    x: 0,
    y: 0,
    left: false,
    right: false,
    over: false,
    dragging: false, // флаг для перемещения фигуры
    offset: { x: 0, y: 0 } // смещение относительно координат фигуры
}

canvas.addEventListener("mouseenter", mouseEnterHandler);
canvas.addEventListener("mousemove", mouseMoveHandler);
canvas.addEventListener("mouseleave", mouseLeaveHandler);
canvas.addEventListener("mousedown", mouseDownHandler);
canvas.addEventListener("mouseup", mouseUpHandler);

// Добавляем слушатели событий на ссылки в sidebar
const sidebarLinks = document.querySelectorAll('#sidebar .geItem');
sidebarLinks.forEach(function(link) {
    link.addEventListener('click', function(event) {
        // Получаем значение атрибута 'data-shape' ссылки
        const shape = event.target.getAttribute('data-shape');
        // Рисуем выбранную фигуру сразу по клику на кнопку
        drawShape(shape);
    });
});



function mouseEnterHandler(event) {
    mouse.over = true;
}

function mouseMoveHandler(event) {
    const rect = canvas.getBoundingClientRect();
    mouse.x = event.clientX - rect.left;
    mouse.y = event.clientY - rect.top;
    if (mouse.dragging) { // если мышь зажата и перемещение активно
        // перемещаем фигуру
        moveShape();
    }
}

function mouseLeaveHandler(event) {
    mouse.over = false;
}

function mouseDownHandler(event) {
    // если кликнули внутрь фигуры, начинаем перемещение
    if (isInsideShape()) {
        mouse.dragging = true;
        // считаем смещение относительно координат фигуры
        mouse.offset.x = mouse.x - shape.x;
        mouse.offset.y = mouse.y - shape.y;
    }
}

function mouseUpHandler(event) {
    mouse.dragging = false; // завершаем перемещение
}

function isInsideShape() {
    return (
        mouse.x >= shape.x &&
        mouse.x <= shape.x + shape.width &&
        mouse.y >= shape.y &&
        mouse.y <= shape.y + shape.height
    );
}

// Перемещаем фигуру на новое место
function moveShape() {
    // вычисляем новые координаты фигуры с учетом смещения
    shape.x = mouse.x - mouse.offset.x;
    shape.y = mouse.y - mouse.offset.y;
    // очищаем холст и рисуем фигуру с новыми координатами
    clearCanvas();
    drawRectangle(shape.x, shape.y);
}

// Функция для рисования выбранной фигуры
function drawShape(shape) {
    switch (shape) {
        case 'line':
            drawLine();
            break;
        case 'rectangle':
            drawRectangle(50,50);
            break;
        case 'circle':
            drawCircle();
            break;
        // Добавьте здесь обработку других фигур, если необходимо
        default:
            console.error('Неизвестная фигура:', shape);
            break;
    }
}

// Функции для рисования различных фигур
function drawLine() {
    // Реализуйте рисование прямой линии
    context.beginPath();
    context.moveTo(50, 50);
    context.lineTo(200, 200);
    context.stroke();
}

function drawRectangle(x,y) {
    // Реализуйте рисование прямоугольника
    context.beginPath();
    context.rect(x, y, shape.width, shape.height);
    context.stroke();
}

function drawCircle() {
    // Реализуйте рисование круга
    context.beginPath();
    context.arc(100, 100, 50, 0, Math.PI * 2);
    context.stroke();
}
// Очищаем холст
function clearCanvas() {
    context.clearRect(0, 0, canvas.width, canvas.height);
}
// Начальные координаты и размеры прямоугольника
const shape = {
    x: 50,
    y: 50,
    width: 100,
    height: 80
};

Ответы (1 шт):

Автор решения: Vladislav G.

Ваша математика расчета координат абсолютно верна! Проблема здесь в самом способе отображения canvas. Обратите внимание на 3 вещи (когда вы выполняете ваш код):

  1. при перемещении прямоугольника вдоль оси Y (по вертикали) вниз - курсор убегает быстрее, чем сам прямоугольник, причем по оси Х такого эффекта не наблюдается. Здесь можно подумать, что есть ошибка в расчетах переноса фигуры, однако это не так, потому что следующий захват, как вы и описали, происходит чуть ниже изображения прямоугольника, т.е. все координаты рассчитаны верно - математически прямоугольник находится ниже, чем отображается. Здесь можно подумать, что прямоугольник отрисовывается с неверными координатами, однако это тоже не так - методы контекста canvas используются верно и с правильными координатами;
  2. а теперь обратите внимание на то, что прямоугольник с начальными координатами 50, 50 по горизонтали чуть дальше от края холста canvas, чем по вертикали, хотя и там, и там по 50px.
  3. затем я изменил начальные размеры прямоугольника на 100, 100 и сделал его квадратом. Сделайте это тоже ради интереса и вы увидите, что на холсте ваш квадрат - вовсе не квадрат, а прямоугольник.

В общем все это - искажение размеров внутренней системы координат canvas (размеров контекста, если это будет удобнее для понимания) относительно самого html-элемента canvas. Можете открыть любые источники и убедиться, что canvas имеет два размера. В то же время система координат курсора работает "над" canvas-ом, в обычной системе координат окна браузера и если масштаб внутренней системы координат canvas отличается от единичной (какая и используется в браузере), то как раз и возникают такие искажения.

А теперь к решению проблемы. Варианта 2:

  1. вводить поправочный коэффициент при расчете координат;
  2. выставлять размеры canvas таким образом, чтобы масштаб внутренней системы координат соответствовал браузерной, т.е. был единичным.

Первый вариант даже рассматривать не буду - сложная математика и не понятно зачем так извращаться.

Второй вариант - лично я для себя взял за правило следующий алгоритм работы с canvas:

  1. помещаем canvas в контейнер, допустим div;
  2. в стилях для canvas обязательно указываю: dispaly: block; width: 100%: height: 100%. Теперь на странице canvas будет занимать родителя и управлять размерами на странице будем только через родителя. Так можно получить абсолютно адаптивный canvas;
  3. в JS canvas должен подстроить размеры своей внутренней системы координат под размеры родительского контейнера, а т.к. сам canvas занимает 100% ширины и высоты родителя, то и получается единичный масштаб внутренней системы координат canvas относительно html-элемента canvas. Это легко сделать: canvas.height = canvasContainer.offsetHeight; canvas.width = canvasContainer.offsetWidth;
  4. !!!ВАЖНО: если сделать это единожды (как и написано в моем решении), то это сработает только при первичной инициализации и будет работать до тех пор, пока вы не измените размеры страницы. Как только контейнер, в котором расположен canvas, изменит свои размеры - все сломается. Вам остается решить эту проблему и тут есть 2 варианта: отслеживать изменения размеров контейнера и соответствующим образом обрабатывать их, обновляя значение размеров внутренней системы координат canvas, либо использовать что-то типа requestAnimationFrame(). Когда я делал проект на canvas, использовал второй вариант.

PS: кстати, если вы запустите мой код здесь, то в маленьком окошке он будет работать корректно. А если вы развернете на весь экран, то все сломается. Это как раз то, о чем я писал чуть выше в п.4.

PPS: добавил отслеживание изменения размеров родительского контейнера canvas-элемента.

const canvasContainer = document.querySelector('#diagram-container')
const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");

const mouse = {
    x: 0,
    y: 0,
    left: false,
    right: false,
    over: false,
    dragging: false, // флаг для перемещения фигуры
    offset: { x: 0, y: 0 } // смещение относительно координат фигуры
}

canvas.addEventListener("mouseenter", mouseEnterHandler);
canvas.addEventListener("mousemove", mouseMoveHandler);
canvas.addEventListener("mouseleave", mouseLeaveHandler);
canvas.addEventListener("mousedown", mouseDownHandler);
canvas.addEventListener("mouseup", mouseUpHandler);

//установка размеров внутреннего размера canvas в соответствии с размерами родительского контейнера
function canvasInitSize() {
    canvas.height = canvasContainer.offsetHeight;
    canvas.width = canvasContainer.offsetWidth;
}

//прослушиваем изменение размеров контейнера, в котором хранится canvas
new ResizeObserver(() => canvasInitSize()).observe(canvasContainer);

// Добавляем слушатели событий на ссылки в sidebar
const sidebarLinks = document.querySelectorAll('#sidebar .geItem');
sidebarLinks.forEach(function(link) {
    link.addEventListener('click', function(event) {
        // Получаем значение атрибута 'data-shape' ссылки
        const shape = event.target.getAttribute('data-shape');
        // Рисуем выбранную фигуру сразу по клику на кнопку
        drawShape(shape);
    });
});



function mouseEnterHandler(event) {
    mouse.over = true;
}

function mouseMoveHandler(event) {
    const rect = canvas.getBoundingClientRect();
    mouse.x = event.clientX - rect.left;
    mouse.y = event.clientY - rect.top;
    if (mouse.dragging) { // если мышь зажата и перемещение активно
        // перемещаем фигуру
        moveShape();
    }  
}

function mouseLeaveHandler(event) {
    mouse.over = false;
}

function mouseDownHandler(event) {
    // если кликнули внутрь фигуры, начинаем перемещение
    if (isInsideShape()) {
        mouse.dragging = true;
        // считаем смещение относительно координат фигуры
        mouse.offset.x = mouse.x - shape.x;
        mouse.offset.y = mouse.y - shape.y;
    }
}

function mouseUpHandler(event) {
    mouse.dragging = false; // завершаем перемещение
}

function isInsideShape() {
    return (
        (mouse.x >= shape.x) &&
        (mouse.x <= shape.x + shape.width) &&
        (mouse.y >= shape.y) &&
        (mouse.y <= shape.y + shape.height)
    );
}

// Перемещаем фигуру на новое место
function moveShape() {
    // вычисляем новые координаты фигуры с учетом смещения
    shape.x = mouse.x - mouse.offset.x;
    shape.y = mouse.y - mouse.offset.y;
    // очищаем холст и рисуем фигуру с новыми координатами
    clearCanvas();
    drawRectangle(shape.x, shape.y); 
}

// Функция для рисования выбранной фигуры
function drawShape(shape) {
    switch (shape) {
        case 'line':
            drawLine();
            break;
        case 'rectangle':
            drawRectangle(50,50);
            break;
        case 'circle':
            drawCircle();
            break;
        // Добавьте здесь обработку других фигур, если необходимо
        default:
            console.error('Неизвестная фигура:', shape);
            break;
    }
}

// Функции для рисования различных фигур
function drawLine() {
    // Реализуйте рисование прямой линии
    context.beginPath();
    context.moveTo(50, 50);
    context.lineTo(200, 200);
    context.stroke();
}

function drawRectangle(x,y) {
    // Реализуйте рисование прямоугольника
    context.beginPath();
    context.rect(x, y, shape.width, shape.height);
    context.stroke();
}

function drawCircle() {
    // Реализуйте рисование круга
    context.beginPath();
    context.arc(100, 100, 50, 0, Math.PI * 2);
    context.stroke();
}
// Очищаем холст
function clearCanvas() {
    context.clearRect(0, 0, canvas.width, canvas.height);
}
// Начальные координаты и размеры прямоугольника
const shape = {
    x: 50,
    y: 50,
    width: 100,
    height: 100
};
body {
    font-family: Arial, sans-serif;
    display:flex;
    flex-direction: column;
    height: 100vh;
    margin: 0;
    padding: 0;
    background-color: #fbfbfb;
    align-items: center;
    justify-content: center;
}
a:hover {
    color: #FFFF00;
}
a {
     text-decoration: none;
     cursor: pointer;
}
.geMenubarContainer{
  display: inline-flex;
  align-items: center;
  overflow: hidden;
  position: absolute;
  cursor: default;
  top: 0px;
  left: 0px;
  right: 0px;
  height: 64px;
  background-color: #f1f3f4;
}
.geMenubarContainer .geTextDiv{
  position: absolute;
  right: 120px;
  left: 60px;
  top: 9px;
  height: 26px;
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  visibility: visible;
}
.geTextDiv .geItem{
  padding: 2px 8px;
  display: inline;
  font-size: 18px;
  transition: all 0.1s ease-in-out;
  cursor: pointer;
}
.geMenubar{
  display: inline-flex;
  white-space: nowrap;
  align-items: center;
  position: absolute;
  padding-left: 59px;
  box-sizing: border-box;
  top: 34px;
}
.geMenubarContainer .geItem{
    padding: 6px 6px 6px 9px;
    transition: all 0.1s ease-in-out;
}
.geMenubarContainer .geIcon{
    display: block;
    position: absolute;
    top: 12px;
    width: 36px;
    height: 36px;
    margin: 8px 0px 8px 16px;
    opacity: 0.85;
    border-radius: 3px;
    background-position: center center;
    background-size: 90% 90%;
    background-repeat: no-repeat;
    background-image: url('../icons/icon.png')
}
.geToolbarContainer{
  border-width: 1px;
  border-style: none none solid none;
  box-shadow: none;
  overflow: hidden;
  position: absolute;
  cursor: default;
  z-index: 1;
  left: 0px;
  right: 0px;
  top: 64px;
  height: 38px;
  background-color: #f1f3f4;
}
.geToolbar{
    padding-left: 16px;
    border-top: 1px solid #dadce0;
    box-shadow: inset 0 1px 0 0 #fff;
    padding-bottom: 3px;
}
.geSeparator{
  float: left;
  width: 1px;
  height: 20px;
  background: #e5e5e5;
  margin-left: 8px;
  margin-right: 6px;
  margin-top: 4px;
}
.geLabel{
  white-space: nowrap;
  position: relative;
  overflow: hidden;
  width: 50px;
  float: left;
  margin: 2px;
  padding: 3px 5px 3px 5px;
  border: 1px solid transparent;
  transition: all 0.1s ease-in-out;
}
.geToolbar a {
    color: #000000;
    text-decoration: none;
}
.geToolbarContainer img{
    width:20px;
    height:20px;
}
.geButton {
    float: left;
    width: 20px;
    height: 20px;
    padding: 0px 2px 4px 2px;
    margin: 2px;
    border: 1px solid transparent;
    cursor: pointer;
    opacity: 0.6;
    transition: all 0.1s ease-in-out;
}

#diagram-container {
    position: absolute;
    width: calc(100% - 215px); /* Ширина сайдбара */
    height: calc(100% - 64px - 38.8px); /* Высота экрана за вычетом высоты тулбара и сайдбара */
    top: 110px; /* Высота тулбара */
    left: 59px; /* Ширина тулбара */
}

#canvas {
    display: block;
    width: 100%;
    height: 100%;
    border: 1px solid #ccc;
    background-color: #FFFFFF;
    
}

#sidebar {
    position: fixed;
    overflow-y: scroll;
    z-index: 1;
    top: 100px;
    bottom: 0px;
    right: 0;
    width: 215px;
    height: 100vh;
    background-color: #f1f3f4;
    padding: 20px;
    box-shadow: -5px 0px 5px rgba(0, 0, 0, 0.1);
    transition: right 0.3s ease;
}

#sidebar.open {
    right: -230px;
}

/* Стили для кнопки */
#sidebar h3 {
    position: relative;
    padding-left: 30px; /* Добавляем отступ слева для размещения кнопки */
    text-align: center;
}


#btn-toggle-menu {
    position: absolute;
    width: 20px; /* Размер изображения */
    height: 20px;
    left: 0; /* Положение слева */
    top: 0; /* Положение сверху */
    background-color: transparent;
    border: none;
    padding: 0;
    margin: 0;
    cursor: pointer;
}

#btn-toggle-menu img {
    width: 20px; /* Размер изображения */
    height: 20px;
}
.geTitle{
    display: block;
    background-image: url('../icons/arrow.gif');
    background-repeat: no-repeat;
    background-position: 4px 50%;
    position: relative;
    padding-left: 72px;
    border-bottom: 1px solid #e5e5e5;
    border-top: 1px solid #e5e5e5;
    font-weight: 500;
    font-size: 13px;
    border-color: #e5e5e5;
}
.geSidebar{
    display: block;
    transform-origin: right top 0px;
    border-bottom: 1px solid #e5e5e5;
    padding: 6px;
    overflow: hidden;
}
.geSidebar .geItem{
    display: inline-block;
    background-repeat: no-repeat;
    background-position: 50% 50%;
    border-radius: 8px;
    overflow: hidden;
    width: 34px;
    height: 32px;
    padding: 1px;
}
.geItem img{
    right: 1px;
    top: 1px;
    width: 32px;
    height: 30px;
    display: block;
    position: relative;
    overflow: hidden;
    pointer-events: none;
}
.rectangle {
    border: 3px solid #FF0000;
    position: absolute;
}
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Diagram Editor</title>
    <link rel="stylesheet" href="style.css">
</head>

<body>

    <div class="geMenubarContainer">
        <a class="geIcon"></a>
        <div class="geTextDiv">
            <a class="geItem"></a>
        </div>
        <div class="geMenubar">
            <a class="geItem">Файл</a>
            <a class="geItem">Правка</a>
            <a class="geItem">Вид</a>
            <a class="geItem">Положение</a>
            <a class="geItem">Дополнительно</a>
            <a class="geItem">Помощь</a>
        </div>
    </div>
    <div class="geToolbarContainer">
        <div class="geToolbar"></div>
        <a class="geLabel" title="Масштаб (Alt+Mousewheel)">
            100%
            <img src="icons/arrow.gif"></img>
        </a>
        <div class="geSeparator"></div>
        <a class="geLabel" title="Увеличить">
            <img src="icons/glass1.png"></img>
        </a>
        <a class="geLabel" title="Уменьшить">
            <img src="icons/glass2.png"></img>
        </a>
        <div class="geSeparator"></div>
        <a class="geLabel" title="Отменить (Ctrl+Z)">
            <img src="icons/undo.png"></img>
        </a>
        <a class="geLabel" title="Вернуть (Ctrl+Y)">
            <img src="icons/revert.png"></img>
        </a>
        <div class="geSeparator"></div>
        <a class="geLabel" title="Удалить (Delete)">
            <img src="icons/delete.png"></img>
        </a>
        <div class="geSeparator"></div>
        <a class="geLabel" title="Цвет линии">
            <img src="icons/pencil.png"></img>
        </a>
        <a class="geLabel" title="Цвет заливки">
            <img src="icons/filling.png"></img>
        </a>
        <input type="color" id="color-pick-input" title="Цвет линии">
        <input type="range" id="line-width-input" min="1" max="40">
        <div class="geSeparator"></div>
        <a class="geLabel" title="Тип соединения">
            <img src="icons/lines.png"></img>
        </a>
        <a id="text" class="geLabel" title="Добавить текст">
            <img src="icons/text.png"></img>
        </a>
        <div class="geSeparator"></div>
        <a id="clear-button" class="geLabel" title="Добавить текст">
            <img src="icons/clear.png"></img>
        </a>
        <a id="save-button" class="geLabel" title="Добавить текст">
            <img src="icons/save.png"></img>
        </a>
        <div class="geSeparator"></div>
    </div>

    <div id="diagram-container">
        <canvas id="canvas"></canvas>
    </div>
    <div id="sidebar">
        <img id="btn-toggle-menu" src="icons/next.png"></img>
        <h3>Фигуры</h3>
        <a class="geTitle">Избранное</a>
        <a class="geTitle">Общие</a>
        <div class="geSidebar">

            <a class="geItem" title="прямая линия" data-shape="line">
                <img src="mainFigures/line.png"></img>
            </a>
            <a class="geItem" title="прямоугольник" data-shape="rectangle">
                <img src="mainFigures/rectangle.png"></img>
            </a>
            <a class="geItem" title="круг" data-shape="circle">
                <img src="mainFigures/circle.png"></img>
            </a>
            <a class="geItem" title="эллипс">
                <img src="mainFigures/ellipse.png"></img>
            </a>
            <a class="geItem" title="треугольник">
                <img src="mainFigures/triangle.png"></img>
            </a>
            <a class="geItem" title="трапеция">
                <img src="mainFigures/trapezium.png"></img>
            </a>
            <a class="geItem" title="ромб">
                <img src="mainFigures/rhomb.png"></img>
            </a>
            <a class="geItem" title="параллелограмм">
                <img src="mainFigures/parallelogram.png"></img>
            </a>
            <a class="geItem" title="пятиугольник">
                <img src="mainFigures/pentagon.png"></img>
            </a>
            <a class="geItem" title="шестиугольник">
                <img src="mainFigures/hexagon.png"></img>
            </a>
            <a class="geItem" title="односторонняя стрелка">
                <img src="mainFigures/arrows.png"></img>
            </a>
            <a class="geItem" title="двусторонняя стрелка">
                <img src="mainFigures/double-arrow.png"></img>
            </a>
            <a class="geItem" title="криволинейная стрелка">
                <img src="mainFigures/curve-arrow.png"></img>
            </a>
        </div>
        <a class="geTitle">Блок-схемы</a>
        <a class="geTitle">UML</a>
    </div>
    <script src="script.js"></script>
    <script src="draw.js"></script>

</body>

</html>

→ Ссылка