Скретч-карты — это своего рода элемент взаимодействия с веб-страницей, с которым все хорошо знакомы. Чтобы добиться эффекта скретч-покрытия, для его достижения нужно использовать холст, и его должен знать каждый фронтенд-инженер. Внедрить скретч-карту несложно, но для этого требуется много очков знаний, овладение которыми поможет нам глубже понять принцип и очень полезно улучшить способность делать выводы о других вещах. В этом выпуске в качестве примера рассматривается реализация скретч-карт, рассказывается, как инкапсулировать функции с научной точки зрения и разумно, и объясняются соответствующие вопросы знаний.
Давайте посмотрим на окончательный эффект:
Какие очки знаний задействованы в реализации скретч-карт?
Точка знаний 1: размер элемента холста и размер холста
Точка знаний 2: прототип, __proto__, конструктор
Точка знаний 3: globalCompositeOperation холста
Пункт знаний 4: пассивный атрибут третьего параметра addEventListener
Точка знаний 5: ImageData холста
Введите официальный контент, которым поделились в этом выпуске ниже.
1 Анализ спроса
Чтобы удовлетворить потребности большего количества сценариев, мы предоставляем как можно больше параметров для удобства пользователей. Сначала подумайте, какие параметры конфигурации могут потребоваться скретч-карте с точки зрения продукта и пользовательского интерфейса.
- Тип покрытия (рисунок или сплошной цвет)
- Радиус кисти
- При нанесении на процент прямо соскребите все покрытие
- Эффект соскабливания всего покрытия (затухание или сразу)
Затем добавьте параметры технической конфигурации:
- элемент холста
- Отображение нескольких пикселей на экране (адаптировано для экранов с большим увеличением, таких как Retina)
- Время анимации перехода для эффекта затухания
ОК, после подтверждения вышеуказанных параметров конфигурации можно официально начинать работу.
2-страничная конструкция
Структура каталогов проекта выглядит следующим образом:
|- award.jpg <--刮刮卡底层结果页图片
|- index.html
|- scratch-2x.jpg <--刮刮卡涂层图片
|- scratchcard.js
Структура страницы очень проста, фон div отображает результаты, а холст в div используется для покрытия.
Создайте новый index.html и добавьте следующий код (код шаблона HTML пропущен):
HTML-код:
<div class="card">
<canvas id="canvas" width="750" height="280"></canvas>
</div>
CSS-код:
.card {
width: 375px;
height: 140px;
background: url('award.jpg');
background-size: 375px 140px;
}
.card canvas {
width: 375px;
height: 140px;
}
award.jpg использует изображение, увеличенное в 2 раза, поэтому используйте background-size, чтобы уменьшить размер экрана до 1x.
Здесь можно обнаружить, что ширина и высота холста в HTML несовместимы с шириной и высотой в CSS. Причина в адаптации к экрану Retina 2x. Это включает в себя очки знаний размера холста холста.
Страница теперь выглядит так, с отображаемым результирующим изображением:
Точка знаний 1: размер элемента холста и размер холста
Ширина и высота холста в HTML — это размер холста, вообще говоря, это «размер области рисования» холста, который следует отличать от отображаемого размера элемента.
Наш результирующий материал изображения имеет размер 750x280, поэтому для того, чтобы холст полностью отрисовывал это изображение, размер холста также должен быть 750x280.
Тогда размер элемента является «размером отображения» холста на странице. Установите ширину и высоту элемента холста с помощью CSS, чтобы он отображался правильно.
3 Прототип строительного класса
Создайте скретч-карту.js.
В сочетании с анализом требований в главе 1 прототип класса выглядит следующим образом:
function ScratchCard(config) {
// 默认配置
this.config = {
// canvas元素
canvas: null,
// 直接全部刮开的百分比
showAllPercent: 65,
// 图片图层
coverImg: null,
// 纯色图层,如果图片图层值不为null,则纯色图层无效
coverColor: null,
// 全部刮开回调
doneCallback: null,
// 擦除半径
radius: 20,
// 屏幕倍数
pixelRatio: 1,
// 展现全部的淡出效果时间(ms)
fadeOut: 2000
}
Object.assign(this.config, config);
}
Использование эталонного воплощения объекта имеет много преимуществ перед передаточной функцией:
- Семантические параметры для легкого понимания
- Не заботиться о порядке параметров
- Добавление, удаление и корректировка порядка параметров не повлияют на использование бизнес-кодов.
Используйте метод Object.assign, чтобы переопределить параметры по умолчанию переданными параметрами конфигурации. Для свойств, которых нет в переданной конфигурации, используется конфигурация по умолчанию.
Вставьте файлcratchcard.js в index.html и вставьте код скрипта внизу тела:
new ScratchCard({
canvas: document.getElementById('canvas'),
coverImg: 'scratch-2x.jpg',
pixelRatio: 2,
doneCallback: function() {
console.log('done')
}
});
Класс скретч-карты очень удобен в использовании, просто передайте значения, которые не используют конфигурацию по умолчанию.
4 Внедрение скретч-карты
4.1 Создание прототипа ScratchCard
Идем дальше и пишем Scratchcard.js:
function ScratchCard(config) {
this.config = {
...(略)
}
Object.assign(this.config, config)
+ this._init();
}
+ ScratchCard.prototype = {
+ constructor: ScratchCard,
+ // 初始化
+ _init: function() {}
+ }
Конструктор: ScratchCard установлен здесь, просто для большей строгости, и нет проблем с пропуском этой строки.
по кодуprototype
иconstructor
Ведите ко второй точке знаний.
Точка знаний 2: прототип, __proto__, конструктор
Сначала запомните две вещи:
-
__proto__
иconstructor
Свойства уникальны для объектов (функции тоже являются объектами). -
prototype
Свойства уникальны для функций.
※Поскольку функция в JS также является объектом, функция также имеет
__proto__
иconstructor
Атрибуты.
【__прото__】
__proto__
Свойства указываются объектом на объект, то есть на их объект-прототип (который также можно понимать как родительский объект).
Его функция заключается в том, что при доступе к свойствам объекта, если свойство не существует в объекте, оно перейдет к его__proto__
Найти в объекте (родительском объекте), на который указывает атрибут.Если родительский объект не имеет этого атрибута, продолжить поиск в родительском объекте.__proto__
Посмотрите на объект, на который указывает атрибут (объект дедушки).Если он не найден, продолжайте поиск, пока вершина цепочки прототипов не станет нулевой. null — это конечная точка цепочки прототипов.
вышеизложенным__proto__
Цепочка свойств для соединения объектов до нуля называется цепочкой прототипов.
【прототип】
prototype
Объекты уникальны для функций, это указатель на объект из функции. Его смысл — объект-прототип функции, то есть объект-прототип экземпляра, созданного этой функцией.
// 示例代码
var demo = new Demo()
function Demo(config) { ... }
Поэтому в приведенном выше коде demo.__proto__ === Demo.prototype.
prototype
Роль атрибутов заключается в следующем:prototype
Содержит свойства и методы, общие для всех созданных им экземпляров.
【конструктор】
constructor
Свойство также зависит от объекта, оно относится к функции объекта. Его смысл в том, чтобы указать на конструктор объекта. Конечный конструктор всех функций указывает на Function.
Когда функция создается, ееprototype
объект, этот объект также будет автоматически полученconstructor
атрибут и указывает на себя.
Итак, почему мы должны вручную устанавливать здесь конструктор: ScratchCard?
Причина в том, что мы используем этот синтаксис:
ScratchCard.prototype = {}
Приводит к перезаписи автоматически установленного значения свойства конструктора. В этом случае, если мы не установим конструктор: ScratchCard преднамеренно, конструктор будет указывать на Object.
4.2 Выполнение покрытия холста
Сначала добавьте следующий код:
function ScratchCard(config) {
this.config = {
...(略)
}
Object.assign(this.config, config);
+ this.canvas = this.config.canvas;
+ this.ctx = null;
+ this.offsetX = null;
+ this.offsetY = null;
this._init();
}
ScratchCard.prototype = {
constructor: ScratchCard,
// 初始化
_init: function() {
+ var that = this;
+ this.ctx = this.canvas.getContext('2d');
+ this.offsetX = this.canvas.offsetLeft;
+ this.offsetY = this.canvas.offsetTop;
+ if (this.config.coverImg) {
+ // 如果设置的图片涂层
+ var coverImg = new Image();
+ coverImg.src = this.config.coverImg;
+ // 读取图像
+ coverImg.onload = function() {
+ // 绘制图像
+ that.ctx.drawImage(coverImg, 0, 0);
+ that.ctx.globalCompositeOperation = 'destination-out';
+ }
+ } else {
+ // 如果没设置图片涂层,则使用纯色涂层
+ this.ctx.fillStyle = this.config.coverColor;
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
+ this.ctx.globalCompositeOperation = 'destination-out';
+ }
}
}
Код инициализации должен реализовать наложение покрытия. Ключевая логика здесь такова: если установлено покрытие изображения, сплошное покрытие цвета игнорируется.
Задействованы два API холста:
drawImage
для нанесения изображений.
fillRect
Используется для рисования прямоугольника.Перед рисованием необходимо установить кисть, то есть черезfillStyle
свойство устанавливает цвет.
Что означает этот код?
this.ctx.globalCompositeOperation = 'destination-out';
globalCompositeOperation — третья точка знаний.
Точка знаний 3: globalCompositeOperation холста
Подробное описание этого атрибута можно найти на w3school:
ценность | описывать |
---|---|
source-over | дефолт. Отображение исходного изображения поверх целевого изображения. |
source-atop | Отображение исходного изображения поверх целевого изображения. Части исходного изображения за пределами целевого изображения невидимы. |
source-in | Отображение исходного изображения в целевом изображении. Отображается только часть исходного изображения в целевом изображении, целевое изображение прозрачно. |
source-out | Отображение исходного изображения в дополнение к целевому изображению. Будет отображаться только часть исходного изображения за пределами целевого изображения, а целевое изображение будет прозрачным. |
destination-over | Отображает целевое изображение над исходным изображением. |
destination-atop | Отображает целевое изображение поверх исходного изображения. Части целевого изображения за пределами исходного изображения отображаться не будут. |
destination-in | Отображение целевого изображения в исходном изображении. Будет отображаться только часть целевого изображения внутри исходного изображения, исходное изображение прозрачно. |
destination-out | Отображает целевое изображение вне исходного изображения. Будет отображаться только часть целевого изображения за пределами исходного изображения, исходное изображение прозрачно. |
lighter | Показать исходное изображение + целевое изображение. |
copy | Отображение исходного изображения. Игнорировать целевое изображение. |
xor | Объедините исходное изображение с целевым изображением с помощью операции XOR. |
Это кажется немного запутанным и трудным для понимания, но на самом деле это похоже на указание того, как объединяются два слоя в фотошопе, например, кто кого маскирует, устранение пересечения, слияние цветов пересечения и так далее.
Вы можете обратиться к иллюстрации w3school, синий — это целевое изображение, а красный — исходное изображение.
Вернемся к скретч-карте, покрытие изображения - это целевое изображение. В настоящее время исходное изображение не установлено, поэтому исходное изображение полностью прозрачно (непрозрачная часть исходного изображения используется для вырезания целевого изображения и рендеринга). он прозрачный), поэтому отображается целевое изображение (слои покрытия изображения).
Теперь эффект такой, как показано ниже, покрытие было закрыто.
4.3 Добавить событие смазывания
Событие smear на самом деле использует события touchstart, touchmove и touchend.Для совместимости с операциями мыши также включены mousedown, mousemove и mouseup.
Измените код:
function ScratchCard(config) {
this.config = {
...(略)
}
Object.assign(this.config, config);
this.canvas = this.config.canvas;
this.ctx = null;
this.offsetX = null;
this.offsetY = null;
+ // 是否在画布上处于按下状态
+ this.isDown = false;
+ // 是否已完成刮刮卡
+ this.done = false;
this._init();
}
ScratchCard.prototype = {
constructor: ScratchCard,
// 初始化
_init: function() {
...(略)
this.offsetY = this.canvas.offsetTop;
+ this._addEvent();
if (this.config.coverImg) { ...(略) }
},
+ // 添加事件
+ _addEvent: function() {
+ this.canvas.addEventListener('touchstart', this._eventDown.bind(this), { passive: false });
+ this.canvas.addEventListener('touchend', this._eventUp.bind(this), { passive: false });
+ this.canvas.addEventListener('touchmove', this._scratch.bind(this), { passive: false });
+ this.canvas.addEventListener('mousedown', this._eventDown.bind(this), { passive: false });
+ this.canvas.addEventListener('mouseup', this._eventUp.bind(this), { passive: false });
+ this.canvas.addEventListener('mousemove', this._scratch.bind(this), { passive: false });
+ },
+ _eventDown: function(e) {
+ e.preventDefault();
+ this.isDown = true;
+ },
+ _eventUp: function(e) {
+ e.preventDefault();
+ this.isDown = false;
+ },
+ // 刮涂层
+ _scratch: function(e) {
+ }
}
Код прост для понимания, просто добавьте прослушиватели событий. При нажатии установите для isDown значение true, а при поднятии установите для isDown значение false.
Вы можете видеть третий параметр addEventListener {passive: false}, что это, черт возьми, такое? Это четвертая точка знания.
Пункт знаний 4: пассивный атрибут третьего параметра addEventListener
Первоначально соглашение о параметрах addEventListener() выглядит следующим образом:
el.addEventListener(type, listener, useCapture)
- эл: объект события
- тип: тип события, щелчок, наведение мыши и т. д.
- слушатель: обработчик события, то есть обратный вызов после срабатывания события
- useCapture: логическое значение, независимо от того, является ли это типом захвата, по умолчанию — false (пузырьковое)
В конце 2015 года спецификация DOM была пересмотрена и дополнена новыми параметрами:
el.addEventListener(type, listener, {
capture: false, // useCapture
once: false, // 是否设置单次监听
passive: false // 是否让阻止默认行为preventDefault()失效
})
Значение по умолчанию для всех трех свойств — false.
Зачем дополнительный пассивный атрибут?
Чтобы предотвратить прокрутку страницы, многие мобильные страницы будут прослушивать сенсорные события, такие как touchmove, например:
document.addEventListener("touchmove", function(e){
e.preventDefault()
})
Поскольку свойство cancelable объекта события touchmove имеет значение true, прослушиватель может предотвратить его поведение по умолчанию с помощью метода preventDefault(). Итак, каково его поведение по умолчанию, обычно прокручивание текущей страницы (и, возможно, масштабирование страницы), если его поведение по умолчанию заблокировано, страница должна оставаться неподвижной. Но браузер не может знать заранее, будет ли обработчик вызывать preventDefault(), все, что он может сделать, это дождаться выполнения обработчика перед выполнением поведения по умолчанию, а выполнение обработчика занимает много времени, а некоторые даже занимает много времени, что приведет к зависанию страницы. Даже если слушателем является пустая функция, это вызовет определенное отставание, ведь выполнение пустой функции займет время.
Когда для пассивного режима задано значение true, функция preventDefault() в коде будет игнорироваться, поэтому страница станет более гладкой. Как показано ниже, страница мобильного телефона справа устанавливает значение true для пассивного режима.
Хорошо, так вот вопрос? Поскольку по умолчанию пассивно: false, зачем вам снова писать это в коде?
Ответ здесь, посмотрите официальное описание хрома:woohoo. chrome status.com/feature/509…
Оригинальный текст выглядит следующим образом:
AddEventListenerOptions defaults passive to false. With this change touchstart and touchmove listeners added to the document will default to passive:true (so that calls to preventDefault will be ignored)..
Значение: в опции addEventListener пассив по умолчанию равен false. Но если событием является touchstart или touchmove, пассивное значение по умолчанию станет истинным (так что preventDefault будет проигнорировано).
Хорошо, принцип закончился, мы не заблокировали поведение страницы по умолчанию. Если она не заблокирована, при считывании скретч-карты страница также будет прокручиваться.
4.4 Запрет прокрутки страницы
После прочтения раздела 4.3 очень просто предотвратить прокрутку страницы. Добавьте следующий код в скрипт index.html:
+ window.addEventListener('touchmove', function(e) {
+ e.preventDefault();
+ }, {passive: false});
new ScratchCard({
...(略)
});
4.5 Реализуйте эффект стирания
Здесь, чтобы улучшить метод _scratch, код выглядит следующим образом:
_scratch: function(e) {
e.preventDefault();
var that = this;
if (!this.done && this.isDown) {
if (e.changedTouches) {
e = e.changedTouches[e.changedTouches.length - 1];
}
var x = (e.clientX + document.body.scrollLeft || e.pageX) - this.offsetX || 0,
y = (e.clientY + document.body.scrollTop || e.pageY) - this.offsetY || 0;
with(this.ctx) {
beginPath()
arc(x * that.config.pixelRatio, y * that.config.pixelRatio, that.config.radius * that.config.pixelRatio, 0, Math.PI * 2);
fill();
}
}
}
Логика примерно такая:
- Оценивается, что скретч-карта не поцарапана (this.done равно false) и находится в нажатом состоянии (this.isDown равно true).
- Если контактов несколько, используется последний. Получите последний штрих с e.changedTouches.
- Вычислите относительные координаты точки касания на холсте.
- Рисует круг в точке касания на холсте.
Следует отметить, что умножение pixelRatio — это адаптация к нескольким экранам. В этом примере размер холста в 2 раза больше размера, а координаты рассчитываются по размеру элемента страницы, что ровно удваивает разницу, поэтому умножается на pixelRatio (pixelRatio = 2).
Помните globalCompositeOperation в разделе 4.2? когда установлено наdestination-out
Когда , непрозрачная часть исходного изображения будет вырезана из целевого изображения, таким образом реализуя эффект скретч-карты.
4.6 Процент прозрачных частей детекторного покрытия
Несмотря на то, что достигается эффект скретч-покрытия, необходимо в режиме реального времени определить, сколько царапин, чтобы судить о том, завершена ли скретч-карта.
Продолжайте изменять код:
_scratch: function(e) {
...(略)
if (!this.done && this.isDown) {
...(略)
with(this.ctx) {
...(略)
}
+ if (this._getFilledPercentage() > this.config.showAllPercent) {
+ this._scratchAll()
+ }
}
}
+ // 刮开全部涂层
+ _scratchAll() {
+ var that = this;
+ this.done = true;
+ if (this.config.fadeOut > 0) {
+ // 先使用CSS opacity清除,再使用canvas清除
+ this.canvas.style.transition = 'all ' + this.config.fadeOut / 1000 + 's linear';
+ this.canvas.style.opacity = '0';
+ setTimeout(function() {
+ that._clear();
+ }, this.config.fadeOut)
+ } else {
+ // 直接使用canvas清除
+ that._clear();
+ }
+ // 执行回调函数
+ this.config.doneCallback && this.config.doneCallback();
+ },
+ // 清除全部涂层
+ _clear() {
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
+ },
+ // 获取刮开区域百分比
+ _getFilledPercentage: function() {
+ var imgData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
+ // 存储当前cavnas画布的全部像素点信息
+ var pixels = imgData.data;
+ // 存储当前canvas画布的透明像素信息
+ var transPixels = [];
+ // 遍历全部像素点信息
+ for (var i = 0; i < pixels.length; i += 4) {
+ // 把透明的像素点添加到transPixels里
+ if (pixels[i + 3] < 128) {
+ transPixels.push(pixels[i + 3]);
+ }
+ }
+ // 计算透明像素点的占比
+ return (transPixels.length / (pixels.length / 4) * 100).toFixed(2)
+ }
}
Добавлено 3 новых метода:
_scratchAll
: Очистить покрытие (соскрести все). Если задано fadeOut (время затухания), холст будет затухать с помощью CSS-анимации, а затем покрытие будет удалено. Если fadeOut равен 0, покрытие удаляется напрямую.
_clear
: Очистите покрытие. Это очень просто, просто нарисуйте прямоугольник, закрывающий холст.
_getFilledPercentage
: вычисляет процент поцарапанной области. Вычислить долю полностью прозрачных пикселей путем обхода каждого пикселя холста.
А вот и пятая точка знания.
Точка знаний 5: ImageData холста
Используйте метод холста getImageData(), чтобы получить всю информацию о пикселях и вернуть формат массива. В массиве не каждый элемент представляет информацию о пикселе, но каждые 4 элемента являются информацией о пикселе. Например:
data[0] = значение R пикселя 1, красный (0-255)
data[1] = значение G пикселя 1, зеленый (0-255)
data[2] = значение B пикселя 1, синий (0-255)
data[3] = значение пикселя 1, альфа-канал (0-255; 0 прозрачный, 255 полностью видимый)
data[4] = значение R пикселя 2, красный (0-255)
...
В этом примере нет промежуточного значения прозрачности, поэтому можно считать, что альфа меньше 128, чтобы быть прозрачным.
4.7 Примечания
Из-за ограничений безопасности браузера Image не может читать локальные изображения, поэтому его необходимо развернуть на сервере для просмотра этого проекта по протоколу http.
Выше представлен весь контент, опубликованный в этом выпуске. Пожалуйста, перейдите на GitHub для получения полного кода:GitHub.com/Y UE слово 32/Сычуаньцы…
Казалось бы, простая скретч-карта скрывает так много очков знаний, вы все это освоили?
Добро пожаловать в мой личный общедоступный аккаунт WeChat, чтобы получать последние статьи в любое время ^_^