Вставить в изображение внутри изображения с помощью JavaScript?

Используя JS (или jQuery) и CSS, я пытаюсь создать веб-страницу, которая позволяет размещать одно изображение внутри очень определенной прямоугольной области другого изображения. Подумайте о том, как работает функция «Вставить в» в Photoshop; это концептуально то, чего я пытаюсь достичь.

Я внимательно изучил прекрасную библиотеку ropperjs , думая, что это обеспечит эффективный способ сделать это (без необходимости заново изобретать кучу колес), и поэтому я сделал этот пост на форумеropperjs. Но разработчик cropperjs опубликовал это в качестве ответа:

Я думаю, вам может понадобиться другая библиотека, эта библиотека может не соответствовать вашему цель.

Теперь, чтобы описать мою потребность более подробно:

Начните с ИЗОБРАЖЕНИЯ №1. Это будет изображение на веб-странице, которое не перемещается, не обрезается и не изменяется каким-либо образом. НО он будет иметь прямоугольную область в пределах своих границ (определяемых мной), внутри которой должно быть помещено другое изображение (ИЗОБРАЖЕНИЕ № 2). Это размещение должно вести себя так же, как Paste Into в Photoshop, в том смысле, что ИЗОБРАЖЕНИЕ № 2 не будет каким-либо образом искажено: оно будет помещено в предопределенную область ИЗОБРАЖЕНИЯ № 1, и его можно будет перетаскивать / перемещать (мышью на ПК или пальцем). на телефоне). Большая часть ИЗОБРАЖЕНИЯ № 2 будет обрезана и невидима.

Будет видна только часть внутри определенной прямоугольной области. Внутри этой области ИЗОБРАЖЕНИЕ №2 будет перекрывать/закрывать/закрывать область ИЗОБРАЖЕНИЯ №1. Таким образом, часть ИЗОБРАЖЕНИЯ № 2 будет находиться поверх ИЗОБРАЖЕНИЯ № 1, но ИЗОБРАЖЕНИЕ № 2 может находиться только в заранее определенной области ИЗОБРАЖЕНИЯ № 1. ИЗОБРАЖЕНИЕ № 2 должно быть перетаскиваемым в пределах этой области. Когда желаемое позиционирование завершено, мне нужно разрешить пользователю (через js) захватить и сохранить все составное изображение.

Извините, что утомляю, но если что-то неясно, позвольте мне описать это графически:

ИЗОБРАЖЕНИЕ №1 (зафиксировано на странице):

Обратите внимание на синюю рамку: это моя предопределенная область. Все за пределами этого региона останется прежним. Все внутри этой области будет покрыто частью ИЗОБРАЖЕНИЯ №2.

ИЗОБРАЖЕНИЕ № 2:

Обрезанная версия ИЗОБРАЖЕНИЯ № 2 появится внутри предопределенной (с синей рамкой) области ИЗОБРАЖЕНИЯ № 1, и пользователь сможет перетаскивать ее по своему усмотрению, пока положение не будет удовлетворительным.

ИЗОБРАЖЕНИЕ №3 (финальное соединение):

Наконец, мне нужно, чтобы пользователь мог нажать кнопку, которая (вызывая некоторый JS) захватывает составное изображение и позволяет его сохранить или передать через ajax на мой сервер. Окончательное изображение должно иметь высокое разрешение без какого-либо сжатия или деградации: такое же высокое разрешение, как и исходные изображения на странице.

Я в порядке, используя стороннюю библиотеку. Я также в порядке, делая это с нуля. Но мне нужно некоторое руководство относительно самого простого и наиболее эффективного способа сделать это. Я очень хорошо разбираюсь в HTML, CSS, JavaScript и jQuery. Любые указатели на рабочий пример чего-то подобного или библиотеку, которая это делает, - это все, что мне нужно.

Вы захотите использовать функцию drawImage API холста, чтобы нарисовать изображение поверх другого с правильным размером, кадрированием и положением.

AKX 10.01.2023 15:37

Если бы это был я, я думаю, что использовал бы это как предлог для изучения WebGL, который, как мне кажется, лучше подходит для того, что вы пытаетесь сделать (хотя я могу ошибаться)

ControlAltDel 10.01.2023 15:45

На самом деле это две вещи в одной, и это делает ответ на этот вопрос гораздо более сложным, чем должен быть. Я рекомендую сначала просто заставить работать перетаскивание и, возможно, опубликовать новый вопрос только по этой проблеме, если вам трудно.

AtomicUs5000 16.01.2023 23:53

В этом вопросе слишком много вопросов. Я предполагаю, что три отрицательных голоса были за то, что это явно не по теме, а три положительных голоса были либо за сочувствие, либо за усилия. В любом случае следует отметить ответ сообщества на вопрос.

halfer 01.02.2023 16:21
Калькулятор CGPA 12 для семестра
Калькулятор CGPA 12 для семестра
Чтобы запустить этот код и рассчитать CGPA, необходимо сохранить код как HTML-файл, а затем открыть его в веб-браузере. Для этого выполните следующие...
Как собрать/развернуть часть вашего приложения Angular
Как собрать/развернуть часть вашего приложения Angular
Вам когда-нибудь требовалось собрать/развернуть только часть вашего приложения Angular или, возможно, скрыть некоторые маршруты в определенных средах?
Оптимизация React Context шаг за шагом в 4 примерах
Оптимизация React Context шаг за шагом в 4 примерах
При использовании компонентов React в сочетании с Context вы можете оптимизировать рендеринг, обернув ваш компонент React в React.memo сразу после...
Интервьюер: Почему '[] instanceof Object' возвращает "true"?
Интервьюер: Почему '[] instanceof Object' возвращает "true"?
Все мы знаем, что [] instanceof Array возвращает true, но почему [] instanceof Object тоже возвращает true?
Абстрактное синтаксическое дерево (AST) и как оно работает с ReactJS
Абстрактное синтаксическое дерево (AST) и как оно работает с ReactJS
Абстрактное синтаксическое дерево (AST) - это древовидная структура данных, которая представляет структуру и иерархию исходного кода на языке...
0
4
261
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вот небольшой отдельный пример того, как сделать что-то подобное с Canvas 2D Context API.

В вашем примере ваш пользователь, вероятно, не выберет базовое изображение самостоятельно, но вы получите его с вашего сервера или еще чего-то.

/**
 * Read an `Image` out of an user-selected file.
 */
function getImageFromInput(input) {
  return new Promise((resolve, reject) => {
    const fr = new FileReader();
    fr.onload = () => {
      const img = new Image();
      img.onload = () => resolve(img);
      img.src = fr.result;
    };
    fr.onerror = reject;
    fr.readAsDataURL(input.files[0]);
  });
}

async function render() {
  let baseImg;
  let overlayImg;
  try {
    baseImg = await getImageFromInput(document.getElementById("base"));
    overlayImg = await getImageFromInput(document.getElementById("overlay"));
  } catch (e) {
    return;
  }
  const canvas = document.getElementById("canvas");
  const ctx = canvas.getContext("2d");
  canvas.width = baseImg.width;
  canvas.height = baseImg.height;
  ctx.drawImage(baseImg, 0, 0, canvas.width, canvas.height);
  // These would be hard-coded according to your base image.
  const x0 = canvas.width * 0.2;
  const y0 = canvas.height * 0.2;
  const x1 = canvas.width * 0.8;
  const y1 = canvas.height * 0.8;
  ctx.drawImage(overlayImg, 0, 0, overlayImg.width, overlayImg.height, x0, y0, x1 - x0, y1 - y0);
}
Choose base image:
<input type = "file" id = "base" accept = "image/*" onchange = "render()">
<br>
Choose overlay image:
<input type = "file" id = "overlay" accept = "image/*" onchange = "render()">
<br>
<canvas width = "800" height = "600" id = "canvas"></canvas>

AKX, спасибо... но мне нужно иметь возможность щелкнуть и удерживать "внутреннее" изображение (ИЗОБРАЖЕНИЕ №2) и перемещать его, позиционируя его по мере необходимости внутри ограниченной области. Это, кажется, не происходит для меня.

HerrimanCoder 10.01.2023 20:29

Конечно, но это другая забота. Это просто показывает вам, как объединить два изображения в одно изображение в заданном диапазоне координат (x0, y0, x1, y1), о чем вы изначально просили.

AKX 11.01.2023 07:45

Еще раз спасибо АКХ. Но я поместил это в свой исходный пост: «пользователь сможет перетаскивать его по своему усмотрению, пока положение не будет удовлетворительным». Можете ли вы указать мне какие-либо направления о том, как добиться перетаскивания? Я смотрю на перетаскиваемые элементы пользовательского интерфейса jQuery, но я надеялся, что уже существует структура для перетаскивания, позиционирования, обрезки и захвата результата в виде нового изображения.

HerrimanCoder 11.01.2023 16:33

Голосование за то, что вы не ответили на все части чрезмерно широкого вопроса, не относящегося к теме. Я проголосовал за то, чтобы закрыть его как Needs Focus.

halfer 01.02.2023 16:47
Ответ принят как подходящий

Как вы упомянули в своем комментарии, вы можете использовать функцию перетаскивания из пользовательского интерфейса jQuery, чтобы включить позиционирование для второго изображения. Вы также можете использовать html2canvas для захвата вашего монтажа.

Поскольку у меня возникла проблема со встроенным фрагментом кода в Stack Overflow, взгляните на этот JSFiddle. Я также помещаю код ниже:

<script src = "https://cdn.jsdelivr.net/npm/jquery@3.6.3/dist/jquery.min.js"></script>
<script src = "https://cdn.jsdelivr.net/npm/jquery-ui@1.13.2/dist/jquery-ui.min.js"></script>
<script src = "https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>

<p>Copy a first image and paste it here (Ctrl-V):</p>
<div id = "frame"></div>
<div id = "container">
  <div></div>
  <div></div>
</div>
#frame,
#container div,
#container img,
button {
  position: absolute;
}

#frame {
  width: 300px;
  height: 200px;
  border: solid 5px #22bbf6;
  background-color: transparent;
  z-index: 3;
  pointer-events: none;
}

#container div:first-child {
  z-index: 1;
}

#container div:last-child {
  width: 310px;
  height: 210px;
  overflow: hidden;
  z-index: 2;
  pointer-events: none;
}

#container img {
  z-index: -1;
  pointer-events: auto;
}

button {
  top: 270px;
  cursor: pointer;
}
$(document).ready(() => {

  let counter = 0,
      $body = $('body'),
      $p = $('p'),
      $container = $('#container'),
      frame = document.getElementById('frame');

  document.onpaste = e => {
    let fileReader = new FileReader(),
        clipItems = e.clipboardData.items;

    fileReader.onload = e => {
      counter++;

      let img = document.createElement('img');

      if (counter === 1 || counter === 2) {
        img.src = e.target.result;
        img.height = frame.offsetHeight;
      }

      if (counter === 1) {
        $p.text('Copy a second image and paste it here (Ctrl-V):');
        $container.children().first().append(img);
      } else if (counter === 2) {
        $p.text('You can adjust the position of your second image and capture your montage:');
        $container.children().last().append(img);
        $(img).draggable();
        $(img).css('cursor', 'grab');
        $body.append('<button type = "button">Capture</button>');
      }
    };

    fileReader.readAsDataURL(clipItems[clipItems.length - 1].getAsFile());
  }

  $body.on('click', 'button', () => {
    $container.css({ width: '310px', height: '210px' });

    $(frame).css('border', 'none');

    html2canvas($container[0], { scale: 2 }).then(canvas => {
      $p.text('Result:');
      $('#container, button').hide();
      $body.append(canvas);
    });
  });
  
});

Я использовал там немного Vanilla JS, особенно когда я нашел его более удобным, чем jQuery.

Обратите внимание, что вы можете изменить качество изображения с помощью параметра scale (см. параметры html2canvas ).


РЕДАКТИРОВАТЬ 1

Как объясняется в моем комментарии, я изначально установил очень ограниченную высоту, чтобы избежать больших изображений для моей демонстрации. Ваш тест показывает, что ваше первое изображение было слишком сильно уменьшено, поэтому вы не видите, что его ширина на самом деле может быть больше, чем рамка. Это потому, что это зависит от соотношения сторон...

Чтобы дать вам более содержательный ответ, я отредактировал мой JSFiddle. Теперь вы можете выбрать область (используя возможности перетаскивания и изменения размера из пользовательского интерфейса jQuery) перед вставкой второго изображения. Я также определил менее ограничительную высоту и добавил параметры html2canvas, чтобы заставить его работать.

Вот обновленный код:

<link rel = "stylesheet" href = "https://cdn.jsdelivr.net/npm/jquery-ui@1.13.2/dist/themes/base/jquery-ui.min.css">

<script src = "https://cdn.jsdelivr.net/npm/jquery@3.6.3/dist/jquery.min.js"></script>
<script src = "https://cdn.jsdelivr.net/npm/jquery-ui@1.13.2/dist/jquery-ui.min.js"></script>
<script src = "https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>

<p>Copy a first image and paste it here (Ctrl-V):</p>
<div id = "frame"></div>
<div id = "container">
  <div></div>
  <div></div>
</div>
#frame,
#container,
#container div,
#container img {
  position: absolute;
}

#frame {
  width: 300px;
  height: 200px;
  border: solid 5px #22bbf6;
  background-color: transparent;
  z-index: 3;
}

#container div:first-child {
  z-index: 1;
}

#container div:last-child {
  overflow: hidden;
  z-index: 2;
  pointer-events: none;
}

#container img {
  z-index: -1;
  pointer-events: auto;
}

button {
  margin-left: 5px;
  cursor: pointer;
}
$(document).ready(() => {

  let counter = 0,
      $body = $('body'),
      $p = $('p'),
      $container = $('#container'),
      $lastChild = $container.children().last(),
      frame = document.getElementById('frame');
      
  const verticalOffset = 3;

  function dragResizeCallback(event) {
    $lastChild.width(event.target.offsetWidth);
    $lastChild.height(event.target.clientHeight + verticalOffset);
    $lastChild.offset({
      top: event.target.offsetTop,
      left: event.target.offsetLeft
    });
  }

  document.onpaste = e => {
    let fileReader = new FileReader(),
        clipItems = e.clipboardData.items;

    fileReader.onload = e => {
      counter++;

      let img = document.createElement('img');

      if (counter === 1 || counter === 2) {
        img.src = e.target.result;
        img.height = 350;
      }

      if (counter === 1) {
        $p.text('Drag/resize the frame, then copy a second image and paste it here (Ctrl-V):');
        $container.children().first().append(img);
        $(frame).css('cursor', 'move');
        
        $(frame).draggable({
          stop: (e) => dragResizeCallback(e)
        });
        
        $(frame).resizable({
          stop: (e) => dragResizeCallback(e)
        });
      } else if (counter === 2) {
        $p.text('You can adjust the position of your second image and capture your montage:');
        $container.children().last().append(img);
        $(img).draggable();
        $(img).css('cursor', 'grab');
        $(frame).css('pointer-events', 'none');
        $p.append('<button type = "button">Capture</button>');
      }
    };

    fileReader.readAsDataURL(clipItems[clipItems.length - 1].getAsFile());
  }

  $body.on('click', 'button', () => {
    $(frame).resizable('destroy');
    $(frame).css('border', 'none');

    html2canvas($container[0], {
      width: $lastChild.width(),
      height: $lastChild.height(),
      x: $lastChild.position().left,
      y: $lastChild.position().top,
      scale: 2
    }).then(canvas => {
      $p.text('Result:');
      $('#container, button').hide();
      $body.append(canvas);
    });
  });
  
});

РЕДАКТИРОВАТЬ 2

Если вы не хотите игнорировать то, что находится за пределами области обрезки в вашем экспорте, вы можете определить переменную $firstChild, содержащую $container.children().first() (выше объявления и инициализации $lastChild), а затем настроить html2canvas следующим образом:

html2canvas($container[0], {
  width: $firstChild.find('img').width(),
  height: $firstChild.find('img').height(),
  x: $firstChild.position().left,
  y: $firstChild.position().top,
  scale: 2
})

Badacadabra, спасибо за подробное и продуманное решение. Я попробовал ваш jsfiddle, но кажется, что все, что он сделал, это разрешить 1 изображение поверх другого, а затем перетащить, а затем зафиксировать результат. Это примерно половина того, что мне нужно, но действительно важная часть, которую мне не хватает, - это возможность вставить 2-е изображение в область/область кадрирования 1-го изображения, а затем иметь возможность перемещаться по 2-му изображению в пределах области обрезки. , при этом внешние части внутреннего изображения обрезаны (скрыты). Это важная часть, которая мне нужна. Есть идеи?

HerrimanCoder 16.01.2023 03:36

Я чувствую, что мы не видим одного и того же при запуске моего JSFiddle. Это странно, потому что я успешно протестировал его в Firefox (Linux/Windows), Chrome (Windows) и Edge (Windows). Какой у вас браузер? Какая у вас операционная система? Какие изображения вы использовали для своего теста? Проблема может возникнуть из-за img.height = frame.offsetHeight;, который я поставил специально, чтобы избежать больших изображений для этой демонстрации...

Badacadabra 16.01.2023 22:38

Badac: извините за задержку, я использую последнюю версию Chrome (он автоматически обновляется примерно раз в неделю). Я только что попробовал еще раз, и снова получил то же самое. Я постараюсь сделать запись экрана и разместить ссылку здесь.

HerrimanCoder 19.01.2023 18:57

Badacadabra, я сделал для вас это видео на YouTube, демонстрируя свой опыт использования вашего JSFiddle: youtu.be/CBB3s0Wh1wM - с нетерпением жду вашего ответа.

HerrimanCoder 19.01.2023 19:16

Спасибо за видео! Я отредактировал свой ответ с некоторыми улучшениями. Теперь все должно быть в порядке... ;)

Badacadabra 21.01.2023 16:06

Бадакадабра, мы прошли 99% пути. Единственное, что мне нужно, это то, что когда я нажимаю «Захват», мне нужно, чтобы он захватывал оба изображения. Таким образом, граница исходного изображения № 1 должна быть областью захваченного изображения, и она также будет включать внутреннее изображение № 2 (которое теперь перемещено в правильное место). Другими словами, окончательный захват должен включать в себя всю область изображения №1, а также перемещенное/обрезанное изображение №2 внутри него. Вот новое видео на YouTube, которое я сделал, если непонятно: youtu.be/hAJxYYQ4BVc - СПАСИБО!

HerrimanCoder 23.01.2023 15:22

Я думаю, вы не видели мою вторую правку... На самом деле, я предвидел эту просьбу, показывая вам, как настроить код. Мой JSFiddle демонстрирует, как захватить составное изображение внутри области обрезки. Но после моего первого редактирования я снова прочитал ваш вопрос и понял, что вам нужна и внешняя часть. ;)

Badacadabra 23.01.2023 21:49

Я обновил свой JSFiddle, на всякий случай...

Badacadabra 23.01.2023 22:11

СПАСИБО ОГРОМНОЕ за развернутый ответ. Это действительно помогает мне!

HerrimanCoder 24.01.2023 14:44

Другие вопросы по теме