Обновлено 29.07.2020 Команда электронной коммерции Hangzhou Youzan срочно нуждается в 10+ HC, охватывающих интерфейс, Java, тестирование, если вы заинтересованы, пожалуйста, свяжитесь с ~ lvdada@youzan.com или напрямую свяжитесь с wx: wsldd225
Старый
Наш бизнес связан с электронной коммерцией и образовательной отраслью.Для маркетинговых и функциональных нужд будет много потребностей в отображении карточек (длительное нажатие для сохранения) или необходимости делиться длинными изображениями. И у нас есть ПК для продавцов, и продавец может редактировать и предварительно просматривать стиль карты в режиме реального времени.
Нам нужно поддерживать один и тот же контент карты с двумя фреймворками (vue react) на обоих концах.
Учитывая слишком большой объем зависимостей (разархивировано 160кб+), стабильность, ремонтопригодность, масштабируемость и другие факторы, мы не использовалиhtml2canvasЭто сторонняя библиотека преобразования. Вместо этого рядcanvas-utils
способ рисования на холсте.
Потому что собственный API рисования холста основан на абсолютно позиционированных пикселях, дополненных информацией о размере для рисования.
Например:
ctx.rect(x, y, width, height); // 画矩形
ctx.drawImage(img, destx, desty, destWidth, destHeight); // 画图片
Следовательно, входные параметры canvas-utils, которые мы определяем, также должны содержать эту информацию о положении и размере.
/**
* 绘制圆角矩形
*
* @param {*} ctx 画布
* @param {Number} radius 半径
* @param {Number} x 左上角
* @param {Number} y 左上角
* @param {Number} width 宽度
* @param {Number} height 高度
* @param {String} color 颜色
* @param {String} mode 填充模式
* @param {Function} fn 回调函数
*/
export function drawRoundedRectangle() {}
/**
* 绘制图片(方、圆角、圆)
*
* @param {*} ctx 画布
* @param {*} img load好的img对象
* @param {Number} x 左上角定点 x 轴坐标
* @param {Number} y 左上角定点 y 轴坐标
* @param {Number} w 宽
* @param {Number} h 高
* @param {Number} radius 圆角半径
*/
export function drawImage() {}
/**
* 绘制多行片段
*
* @param {*} ctx 画布
* @param {*} content 内容
* @param {*} x 绘制左下角原点 x 坐标
* @param {*} y 绘制左下角原点 y 坐标
* @param {*} maxWidth 最大宽度
* @param {*} fontSize 字体大小
* @param {*} fontFamily 字体家族
* @param {*} color 字体颜色
* @param {*} textAlign 字体排布
* @param {*} lineHeight 设置行高
* @param {*} maxLine 最大行数
*/
export function drawParagraph() {}
/**
* 创建一个画布
*
* @param {*} width 宽
* @param {*} height 高
* @return {*} canvasAndCtx 画布相关信息
*/
export function initCanvasContext(width, height) {
return [canvas, ctx];
}
Эти четыре основных метода охватывают почти все потребности рисования плакатов, изображений, абзацев текста, фонового контейнера и создания холста. И API-интерфейсы, связанные с холстом, были собраны.Разработчикам не нужно обращать внимание на надоедливые API-интерфейсы холста.Они должны только измерить размер и положение на черновике дизайна, а затем соответствующие элементы могут быть абсолютно позиционированы на холсте.
Вероятно реализация в бизнесе (псевдокод):
Promise.all([
canvasUtils.loadUrlImage(mainCoverImg),
canvasUtils.loadBase64Image(cardInfo.qrCode),
])
.then(([cover, qrCode, shopnameIcon, titleIcon]) => {
const [canvas, ctx] = canvasUtils.initCanvasContext(325, 564);
// 绘制底框
canvasUtils.drawRoundedRectangle(ctx, ...sizeMapValue.base);
// 绘制封面图
canvasUtils.drawImage(ctx, ...sizeMapValue.cover);
// 绘制标题
canvasUtils.drawParagraph(ctx, ...sizeMapValue.title);
// 绘制题数
canvasUtils.drawImage(ctx, ...sizeMapValue.titleIcon);
// ...
return canvas.toDataURL('image/png');
})
Поскольку входным параметром изображения является объект img, ссылку на изображение нужно загрузить первой, а здесь идет асинхронный процесс, поэтому в начале проектирования оговорено, что все изображения Promise.all получают img перед операцией рисования.
Рисование плакатов таким образом может удовлетворить основные потребности, но оно также имеет определенные ограничения.
Например:
- Перед рисованием вам необходимо загрузить адрес изображения, что включает в себя асинхронную операцию, которая является относительно избыточной.
- следите за обновлениями
draw***
метод, передавать аналогичные параметры, это также избыточная операция, не лучше ли использовать параметры конфигурации json? - Нужно ли адаптировать высоту сгенерированного изображения к высоте нескольких дочерних элементов? Это требует написания большого количества дополнительной логики.
- Если два разных стиля текста центрированы по горизонтали? Мы должны считать сумасшедшие, а затем передавать позиционирование X Y. Короче говоря, мы должны часто вычислять в логике, когда речь идет о требованиях адаптивного стиля.
Итак, как решить эти проблемы и более изящно рисовать плакаты на фронтенде?
Как определить схему
Еще одна причина отказа от использования html2canvas заключается в том, что библиотека основана на htmlElement.В текущей ситуации в компании синтаксис шаблонов jsx и vue несовместим, и фрагменты кода нельзя использовать повторно.Еще одна важная причина заключается в том, что апплет нельзя использовать, Итак, какой тип схемы используется?Для конвергенции API и максимальной совместимости на разных платформах?
Здесь форма json используется для настройки параметров генерации изображений.
Основная схема:
{
type: '',
css: {},
custom: null, // 自定义回调
}
предыдущее ядроdrawImage drawParagraph drawRoundedRectangle
Целью метода является отрисовка картинок, текста и контейнеров.Для этих трех типов существуют разные дополнительные конфигурации, и требуются разные более семантические схемы.
картина:
{
type: 'image',
css: {},
url: '',
mode: 'fill | contain',
custom: null,
};
Слово:
{
type: 'text',
css: {},
text: '',
custom: null,
};
контейнер:
{
type: 'div',
css: {},
mode: 'div | line',
children: [],
custom: null,
}
типdiv
Схема типа эквивалентна контейнеру сchildren
Поле, подобное концепции div в html, div может быть вложенным, чтобы нести больше div, текстов, изображений и вместе строить полное дерево узлов.
Псевдокод для описания карты с использованием схемы json:
{
type: 'div',
css: {},
children: [
{
type: 'div',
css: {},
children: [
{
type: 'text',
css: {},
text: '文字一'
},
{
type: 'image',
css: {},
url: 'cdn.image.com/test1',
mode: 'contain'
}
]
},
{
type: 'text',
css: {},
text: '好多文字 好多文字 好多文字'
},
]
}
Используйте схему json для описания представления, которое было решено ранееcanvas-utils
Несколько ограничений программы.
Перед рисованием вам необходимо загрузить адрес изображения, что включает в себя асинхронную операцию, которая является относительно избыточной.
URL-адрес или строка base64, переданная изображению, операция загрузки изображения будет реализована внутри, и внешнему не нужно заботиться.
Продолжайте корректировать метод draw*** и передавать аналогичные параметры. Это тоже избыточная операция. Не лучше ли использовать параметры конфигурации json?
Все вызовы методов заменяются типом, исходным размером и информацией о положении, которую необходимо передать
canvasUtils.drawParagraph(ctx, cardInfo.title, 14, 380, 285, 14, undefined, undefined, undefined, 20, 2);
заменены полями css:
{
type: 'text',
css: {
width: '285px',
height: '14px',
x: '14px',
y: '380px',
...
},
text: cardInfo.title,
custom: null,
};
Подводные камни абсолютно позиционированной системы компоновки
Текущее определение схемы по сути такое же, как и предыдущее Canvas-utils с точки зрения реализации, но упрощает использование позы. Все узлы расположены в соответствии с абсолютным позиционированием. Нам нужно вручную передать информацию о размере (ширина высота) всех узлов и информации о местоположении (xy), почти все библиотеки, такие как jsonToCanvas на рынке, спроектированы таким образом, но это не устраняет некоторые из упомянутых нами ограничений.
- Нужно ли адаптировать высоту сгенерированного изображения к высоте нескольких дочерних элементов? Это требует написания большого количества дополнительной логики.
- Если два разных стиля текста центрированы по горизонтали? Мы должны считать сумасшедшие, а затем передавать позиционирование X Y. Короче говоря, мы должны часто вычислять в логике, когда речь идет о требованиях адаптивного стиля.
Например, стиль следующего рисунка, горизонтальное расположение, имеет разные размеры и стили текста, а количество текстов настраивается:
Мы должны рассчитать эти три узла в режиме реального времени.width height x y
, а потом передать в поле css, нагрузка все равно огромная.
Поскольку наша схема соответствует html при описании структуры изображения (вложенной), почему схема нашего поля css не соответствует реальному css?
с помощьюmargin
Блок-схема потока, с помощьюinline-block
Для горизонтального макета измените предыдущее абсолютное позиционирование на относительное позиционирование CSS по умолчанию, чтобы имитировать возможности CSS.
Что более важно, так это имитировать мощное наследование свойств css, чтобы при определении свойств css определенного узла нам не нужно было снова писать различные свойства и напрямую полагаться на наследование свойств css родительского узла.
Схема, предоставляемая пользователю, должна быть достаточно умной, чтобы учитывать требования расчета спроса внутри компонента.
Оригинальное определение:
{
"type": "div",
"css": {
"width": "200px",
"height": "200px",
"x": "0px",
"y": "0px",
},
"children": [
{
"type": "text",
"css": {
"width": "动态计算",
"height": "动态计算",
"x": "动态计算",
"y": "动态计算",
"fontSize": "12px"
},
"text": "自定义文案:"
},
{
"type": "text",
"css": {
"width": "动态计算",
"height": "动态计算",
"x": "动态计算",
"y": "动态计算",
"fontSize": "16px",
"color": "red"
},
"text": "我后面跟这张图片"
},
{
"type": "image",
"css": {
"width": "15px",
"height": "15px",
},
"url": "https://su.yzcdn.cn/public_files/2018/12/14/61d0dad50c5b2789a0232c120ae5f7fa.jpg",
"mode": "contain"
}
]
}
Более разумное определение:
{
"type": "div",
"css": {
"width": "200px",
"height": "200px",
},
"children": [
{
"type": "text",
"css": {
"display": "inline-block",
"marginTop": "3px",
},
"text": "自定义文案:"
},
{
"type": "text",
"css": {
"display": "inline-block",
"fontSize": "16px",
"color": "red"
},
"text": "我后面跟这张图片"
},
{
"type": "image",
"css": {
"width": "15px",
"height": "15px",
"display": "inline-block"
},
"url": "https://su.yzcdn.cn/public_files/2018/12/14/61d0dad50c5b2789a0232c120ae5f7fa.jpg",
"mode": "contain"
}
]
}
Мы видим, что в оптимизированной версии не нужно указывать ни ширину и высоту текста, ни информацию о положении изображения, как при написании собственного CSS-html.
Оптимизируйте схему css для обработки требований к динамическому размеру.
Поскольку необходимо полагаться на возможности css, определение схемы css также должно относиться кcss2.1Определяемая нами схема css является подмножеством спецификации css2.1.
Затем мы переходим к поиску того, какие наборы в спецификации применимы к нашему случаю.
box model
Woohoo. Я 3.org/TR/CSS2/exploiting…
Включает свойства css, связанные с блочной моделью
export interface IBoxModel {
marginLeft: string;
marginRight: string;
marginTop: string;
marginBottom: string;
borderWidth: string;
borderColor: string;
borderStyle: 'solid' | 'dashed';
borderRadius: string | undefined;
boxShadow: string | undefined;
customVerticalAlign: 'down' | 'top' | 'center';
customAlign: 'left' | 'right' | 'center';
}
visual formatting model
Модель визуального форматирования также является наиболее важной моделью в спецификации CSS помимо блочной модели.Она описывает, как элементы, основанные на блочной модели, располагаются в визуальном окне, например, position для описания абсолютного позиционирования или относительного позиционирования. display: block | inline-block используется для описания вертикального или горизонтального расположения.
Извлеките некоторые из необходимых свойств:
export interface IVisFormatModel {
width: string;
height: string;
maxWidth: string | undefined;
maxHeight: string | undefined;
minWidth: string;
minHeight: string;
position: 'absolute' | 'relative';
top: string | undefined;
left: string | undefined;
right: string | undefined;
bottom: string | undefined;
display: 'block' | 'inline-block';
}
Colors and Backgrounds
Используется для описания цветов и фона
export interface IColorAndBg {
color: string;
backgroundColor: string;
}
Fonts
Woohoo. Я 3.org/TR/CSS2/Faneng…
Используется для описания определенного стиля, размера, шрифта и т. д. отдельного текста.
export interface IFonts {
lineHeight: string | undefined; // line-height 应该属于 visual formatting model,但与传统的 css 不太一样,我们规定在无法在 div 中写文字
fontStyle: string;
fontFamily: string;
fontWeight: number;
fontSize: string;
}
Text
Woohoo. Я 3.org/TR/CSS2/Features…
В отличие от шрифтов, эта спецификация предназначена для описания поведения текста до того, как он будет расположен, например, как он выровнен, есть ли в нем тире и т. д.
export interface IText {
textAlign: 'left' | 'right' | 'center';
lineClamp: number | undefined; // 不在 css2.1 规范内,方便描述几行文字拦截展示 【...】
textDecoration: 'line-through' | undefined;
}
Процесс реализации библиотеки чертежей, расчет блочной модели
Независимо от того, насколько удобна для пользователя наша схема css, нам все равно нужно передать абсолютный размер и позицию позиционирования, когда мы, наконец, вызовем API холста внутри компонента.
После определения схемы типа элемента и схемы css необходимо реализовать расчет размера блочной модели каждого узла в соответствии со свойством css узла внутри компонента, а затем нарисовать окончательный холст из окончательного данные модели коробки.
Общий процесс:
Получение данных блочной модели в соответствии с расчетом CSS — это шаг с наибольшим объемом кода в библиотеке чертежей. Ниже приведен алгоритм вычислений модели блока вычислений.
const defaultConfig = canvasWrap.setDefault(copyConfig);
const inlineBlockConfig = canvasWrap.setInlineBlock(defaultConfig);
const widthConfig = canvasWrap.addWidth(inlineBlockConfig);
const heightConfig = canvasWrap.addHeight(widthConfig);
const originConfig = canvasWrap.addOrigin(heightConfig);
setDefault устанавливает значение по умолчанию
Поскольку схема позволяет не передавать некоторые поля, первым шагом является рекурсивный обход входящего источника данных и присвоение входному параметру значения по умолчанию.
setInlineBlock изменит структуру элементов встроенного блока
Как показано на рисунке, метод setInlineBlockinline-block
Агрегация узлов, создайте новый пустой div и вставьте его в исходное положение, а затем добавьте этиinline-block
Узел вставляется как дочерний, цель которого — облегчить вычисление ширины и высоты позже.
Addwidth вычисляет ширину всех узлов
Пройдите все узлы, и если вы найдете div с дочерними элементами, продолжайте проходить рекурсивно.
Имитация собственной функции css, если для текущего узла задана ширина, берется текущая ширина, в противном случае берется расчетная ширина родительского узла.
Конечно, есть много свойств css, которые будут влиять на окончательный расчет ширины, например, minWidth maxWidth и все ли элементы дочернего узла являются встроенными блоками.
Другой пример - текущий тип текстовый, а ширина не задана, здесь нужно вызывать предоставленный холстctx.measureText(content).width;
чтобы получить ширину.
Вычисленная ширина будет объединена с полем, границей и другими свойствами css для повторного расчета ширины различных моделей блоков.
const sumWidth = calRealdemension(sumWidth, [css.minWidth, css.maxWidth]);
const layerWidth = sumPixels(sumWidth, marginWidth);
const contentWidth = minusPixels(sumWidth, addedBorderWidth);
addBoxWidth(element, sumWidth);
addLayerWidth(element, layerWidth);
addContentWidth(element, contentWidth);
Здесь рассчитанные данные будут напрямую назначены текущему объекту конфигурации, так что ширину родительского узла можно будет использовать непосредственно при рекурсии к следующему слою дочерних элементов.
addHeight вычисляет высоту всех узлов
Он аналогичен расчетной ширине и здесь повторяться не будет.
addOrigin вычисляет положение всех узлов
Теперь, когда информация о размере всех узлов вычислена, все узлы также рекурсивно просматриваются, и информация о положении всех дочерних узлов может быть рассчитана на основе родительского узла.
нарисовать изображение на холсте
const images = canvasWrap.getImages(originConfig);
images.then(imgMap => {
resolve(canvasWrap.drawCanvas(originConfig, imgMap));
})
Получите информацию о положении и размере всех узлов, а затем объедините информацию об изображении единой нагрузки, и, наконец, вы можете использоватьcanvas-utils
В методе рисования рисуется картинка.
пользовательский слот
Наконец, давайте упомянем настраиваемое поле, зарезервированное при определении схемы. В него можно передать функцию обратного вызова. Доступный параметр — ctx, который используется для вызова API рисования холста и данных блочной модели узла, чтобы пользователь может знать область действия текущего узла.
custom(canvas, ctx, config) {
ctx.beginPath();
ctx.moveTo(config.origin.x, config.origin.y);
ctx.lineTo(50, 40);
ctx.stroke();
},
Примечания к рисунку на холсте
Создать проблему размытия изображения
Когда мы напрямую устанавливаем ширину и высоту холста, например
<canvas width="200" height="200"></canvas>
Это на самом деле говорит браузеру создать холст размером 200x200 физических пикселей в виде растрового изображения, которое мы можем непосредственно видеть как изображение.
Если логическая ширина и высота этого холста не указаны с помощью css, браузер по умолчанию имеет размер 200 x 200 пикселей.
Мы можем напрямую представить, что для растрового изображения 200x200 установлено значение css 200x200. Это эквивалентно задаче двукратной оптимизации графа при высоком разрешении, хорошо известной фронтенд-инженерам.
Решение похоже на решение задачи с двукратным увеличением изображения, увеличивая ширину и высоту холста в n раз (n зависит отwindow.devicePixelRatio
), css устанавливается на исходную ширину и высоту.
function initCanvasContext(width: number, height: number): [HTMLCanvasElement, CanvasRenderingContext2D] {
canvas.width = width * window.devicePixelRatio;
canvas.height = height * window.devicePixelRatio;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
return [canvas, ctx];
};
Как рисовать текстовые абзацы с помощью холста
использоватьctx.fillText(content, x, y);
При рисовании абзаца позиция y не ниже текста.
Например, мы рисуем две линии, у которых y равно 10 24 соответственно, а затем рисуем текст, у которого y равно 24:
Причина в том, что текст рисования на холсте имеет свои собственные контрольные правила.
Базовая линия текста по умолчанию находится на нижней стороне.Здесь были проведены эксперименты, и бенчмарки разные на разных системах и устройствах, в том числеbottom ideographic
, Толькоmiddel
Стили одинаковы на разных платформах.
Итак, вот хитрый способ центрировать текст вверх и вниз.
ctx.textBaseline = 'middle'; // 适配安卓 ios 下的文字居中问题
ctx.save();
ctx.translate(0, -(fontSize / 2)); // 适配安卓 ios 下的文字居中问题
ctx.fillText(content, x, y);
ctx.restore();
Сначала центрируйте опорную линию текста, затем измените систему координат при рисовании текста и измените ее на исходную систему координат после рисования.
Further
Эффект от этой галереи на самом деле очень похожhtml2canvas
Эта библиотека классов исчезла, ноjson2canvas
На самом деле есть и другие мыслимые пространства.
Например
- Вы можете напрямую генерировать соответствующие данные json в соответствии со слоем через эскиз, и данные json адаптируются к различным интерфейсным платформам.
- Большая часть реализации этой библиотеки классов заключается в том, как вычислить размер и положение блочной модели каждого узла, которая также не зависит от платформы и может быть быстро перенесена в апплет. Апплет совместим только с API рисования.
- Если вы чувствуете, что настройка json не интуитивно понятна на каждом уровне интерфейса, вы можете создать несколько ключевых компонентов на уровне компонентов.
<Div style={}> <Text style={}> <Image style={}>
, Затем напишите Canvas так же, как напишите HTML. Это также похоже на HTML2CANVAS.