收到這個需求的時候,我的內(nèi)心是崩潰的,腦子里已經(jīng)跑過一萬匹草泥馬,內(nèi)心想這種事為啥不交給ps去做,哪怕是手機里圖片編輯也可以做到吧,專業(yè)的事交給專業(yè)的工具去干不就好了,何必出這種XX需求。后來想想就釋然了,反正拿錢干活,干啥不是干,只要給錢,再XX的需求我也給你寫出來。廢話不多說,請看下方示例
實現(xiàn)效果:
本文案例只負(fù)責(zé)下圖所示組件,上圖中的圖片列表只是為了方便展示
注:本組件使用的是React + AntD,這里樣式就不過多描寫了
思路一
首先,先說一開始錯誤的思路一,給圖片添加文字信息,我一開始想到的是用canvas。先創(chuàng)建畫布,獲取要寫入文字的圖片寬高->再創(chuàng)建一個新的圖片->然后再畫布中畫出剛才創(chuàng)建的新的圖片->輸入框文字發(fā)生改變則,獲取輸入框中輸入的文字相關(guān)信息->在畫布中繪制文字->生成新的圖片渲染到頁面。
但是最后該思路以失敗告終,這條思路只適用于只添加一條文字信息的情況,添加第二條后就會出現(xiàn)添加的文字錯亂的情況,因為新增代碼后我無法保存上條圖片的情況,無法知道上條圖片的文字新增后的狀態(tài),如果是要刪除情況又當(dāng)如何,失敗代碼如下,如果只添加一條文字信息,可以用來參考,各位慢慢研究
import React from 'react'
import { Button } from 'antd'
import ParamsOptionComp from './ParamsOptionComp'
export default class MaterialEdit extends React.Component {
canvas = document.createElement("canvas");
image = React.createRef();
state = {
imgSrc: this.props.src,
paramAttribute: [{
content: '文字內(nèi)容',
color: '#000000',
size: 24,
left: 130,
top: 50
}],
srcList: {
0: this.props.src
}
}
componentDidMount() {
this.setState({
paramAttribute: [{
content: '文字內(nèi)容',
color: '#000000',
size: 24,
left: (this.image.current.clientWidth / 2) - 48,
top: (this.image.current.clientHeight / 2) + 12
}]
}, () => {
this.drawImage()
})
this.drawImage()
}
// 繪制圖片
drawImage = (params, index) => {
// 創(chuàng)建畫布
const ctx = this.canvas.getContext("2d");
// 獲取圖片大小
const imageRect = this.image.current.getBoundingClientRect();
const { width, height } = imageRect;
this.canvas.width = width;
this.canvas.height = height;
// 創(chuàng)建圖片
const image = new Image();
// if (index) {
// console.log(this.state.imgSrc, 123)
// image.src = this.state.imgSrc;
// } else {
image.src = this.props.src;
// }
image.onload = () => {
ctx.drawImage(image, 0, 0, width, height);
// 繪制文字
this.drawText(params ? params : this.state.paramAttribute[0]);
}
}
// 繪制文字
drawText = ({ content, color, size, left, top }) => {
// console.log(content, color, size, left, top)
const ctx = this.canvas.getContext("2d");
ctx.font = `${size}px Arial`;
ctx.fillStyle = color;
ctx.fillText(content, left, top);
this.saveImage()
};
// 當(dāng)文字發(fā)生變化
onValuesChange = (changedValues, allValues) => {
// console.log(changedValues, allValues);
allValues.paramAttribute.forEach((item, index) => {
// if (item && Object.keys(item).length > 0) {
this.drawImage(item)
// } else if (index >= 1) {
// this.drawImage(item, 'index')
// }
})
}
setSrcList = (index) => {
console.log(index)
}
// 保存圖片
saveImage = () => {
const image = this.canvas.toDataURL('image/png');
// console.log(image);
this.setState({ imgSrc: image });
// 根據(jù)需要,將生成的圖片顯示給用戶或保存為新的圖片文件
// 如:window.open(image);
};
// 將圖片轉(zhuǎn)成二進(jìn)制格式
base64ToBlob = (code) => {
let parts = code.split(';base64,')
let contentType = parts[0].split(':')[1]
let raw = window.atob(parts[1])
let rawLength = raw.length
let uint8Array = new Uint8Array(rawLength)
for (let i = 0; i < rawLength; i++) {
uint8Array[i] = raw.charCodeAt(i)
}
return new Blob([uint8Array], { type: contentType })
}
// 下載圖片
download = (dataUrl, fileName) => {
let aLink = document.createElement('a')
let blob = this.base64ToBlob(dataUrl)
let event = document.createEvent('HTMLEvents')
event.initEvent('click', true, true)
aLink.download = fileName + '.' + blob.type.split('/')[1]
aLink.href = URL.createObjectURL(blob)
aLink.click()
}
render() {
const { imgSrc } = this.state
const { src } = this.props
return <>
<div style={{ textAlign: 'center', marginBottom: '30px' }}>
<img src={imgSrc} width={300} ref={this.image} />
</div>
<ParamsOptionComp onValuesChange={this.onValuesChange} paramAttribute={this.state.paramAttribute} setSrcList={this.setSrcList}/>
<Button type="primary" htmlType="submit">
生成新的圖片
</Button>
</>
}
}
思路二
同樣也是與canvas相關(guān),不過這次想到的是,將輸入的文字信息生成節(jié)點添加到頁面中的某個模塊,然后再將這個模塊輸出成canvas然后再轉(zhuǎn)化成圖片下載下來,這里會用到html2canvas這個庫,這個庫可以讓html頁面和canvas相互轉(zhuǎn)換,大致思路如下:
下載html2canvas依賴包->搭建頁面,并創(chuàng)建新增節(jié)點的區(qū)域->初始化新增第一個節(jié)點到頁面中的某個指定模塊->當(dāng)文本框發(fā)生變動,修改節(jié)點信息->實現(xiàn)節(jié)點刪除->利用html2canvas將模塊生成canvas,然后轉(zhuǎn)化成圖片
1. 下載html2canvas依賴包
npm i html2canvas
2. 搭建頁面,并且創(chuàng)建新增節(jié)點的區(qū)域
MaterialEditComp.jsx
import React, { useEffect, useRef, useState } from 'react'
import { Button } from 'antd'
import ParamsOptionComp from './ParamsOptionComp'
import html2canvas from 'html2canvas';
import './index.less'
export default function MaterialEditComp(props) {
const { src, getNewImage } = props;
const [imgSrc, setImgSrc] = React.useState(src);
const contentRef = useRef(null);
const [imageSize, setImageSize] = useState({ width: 0, height: 0 });
const onValuesChange = (changedValues, allValues) => {}
// 新增節(jié)點
const AddNode = ({content, color, size, left, top }, index) => {}
// 刪除節(jié)點
const removeNode = (index) => {}
// 將圖片轉(zhuǎn)換成二進(jìn)制形式
const base64ToBlob = (code) => {}
// 保存圖片
const saveImage = async () => {}
useEffect(() => {
// 坑一
const img = new Image();
img.src = imgSrc
img.onload = () => {
setImageSize({ width: img.width, height: img.height });
};
}, []);
return (
<>
// 新增節(jié)點區(qū)域
<div ref={contentRef} style={{
background: `url(${imgSrc}) no-repeat`,
backgroundSize: 'cover',
backgroundPosition: 'center',
width: imageSize.width + 'px',
height: imageSize.height + 'px',
marginBottom: '30px'
}} className="content">
</div>
// 輸入框組件
<ParamsOptionComp onValuesChange={onValuesChange} imageSize={imageSize} removeNode={removeNode} />
<Button type="primary" htmlType="submit" onClick={saveImage} className='btn'>
生成新的圖片
</Button>
</>
)
}
ParamsOptionComp.jsx
import React, { useEffect, useState } from 'react'
import { Input, Form, Space, Button, InputNumber } from 'antd'
import { PlusOutlined, MinusCircleOutlined, DragOutlined } from '@ant-design/icons'
export default function ParamsOptionComp(props) {
const { onValuesChange, imageSize, removeNode } = props
const [count, setCount] = useState(0)
const [form] = Form.useForm();
// 坑一
useEffect(() => {
form.resetFields()
}, [imageSize])
return <Form form={form} name="dynamic_form_nest_item"
// 坑一
initialValues={{
paramAttribute: [{
left: imageSize.width / 2 - 48,
top: imageSize.height / 2 - 24,
content: '文字內(nèi)容'
}]
}}
onValuesChange={onValuesChange} >
<Form.List name="paramAttribute">
{(fields, { add, remove, move }) => (
<>
{fields.map(({ key, name, ...restField }, index) => (
<Space key={key} style={{ display: 'flex', marginBottom: 0 }} align="baseline">
<Form.Item
style={{ marginBottom: '10px' }}
{...restField}
name={[name, 'content']}
label="文字內(nèi)容"
rules={[{ required: true, message: '請輸入文字內(nèi)容' }]}
>
<Input placeholder="文字內(nèi)容" maxLength={50} />
</Form.Item>
<Form.Item
style={{ marginBottom: '10px' }}
{...restField}
name={[name, 'color']}
label="文字顏色"
rules={[{ required: true, message: '請輸入文字顏色' }]}
initialValue="#000000"
>
<Input placeholder="文字顏色" type="color" style={{ width: '80px' }} />
</Form.Item>
<Form.Item
style={{ marginBottom: '10px' }}
{...restField}
name={[name, 'size']}
label="文字大小"
rules={[{ required: true, message: '請輸入文字大小' }]}
initialValue="24"
>
<InputNumber placeholder="文字大小" min={12} value={13} />
</Form.Item>
<Form.Item
style={{ marginBottom: '10px' }}
{...restField}
name={[name, 'top']}
label="上邊距"
rules={[{ required: true, message: '請輸入上邊距' }]}
initialValue={imageSize.height / 2 - 24}
>
<InputNumber placeholder="上邊距" value={13} />
</Form.Item>
<Form.Item
style={{ marginBottom: '10px' }}
{...restField}
name={[name, 'left']}
label="左邊距"
rules={[{ required: true, message: '請輸入左邊距' }]}
initialValue={imageSize.width / 2 - 48}
>
<InputNumber placeholder="左邊距" value={13} />
</Form.Item>
<MinusCircleOutlined onClick={() => {
if (count === 0) {
return
}
remove(name)
removeNode(index)
setCount(count => count - 1);
}} />
</Space>
))}
<Form.Item>
<Button type="dashed" onClick={async () => {
try {
const values = await form.validateFields()
add();
setCount(count => count + 1);
} catch (errorInfo) {
return;
}
}} block icon={<PlusOutlined />}>添加選項</Button>
</Form.Item>
</>
)}
</Form.List>
</Form>
}
此時頁面如下
上述代碼中有個坑
-
坑
因為產(chǎn)品要求,需要給個初始化數(shù)據(jù),并且文字要在圖片內(nèi)水平垂直居中。但是初始化數(shù)據(jù)時是無法獲取到圖片的寬高的,并且初始化時在useEffect中也無法通過ref獲取圖片的大小,此時只能將接收到的圖片生成新的圖片,然后讀取新圖片的寬高賦值給ImageSize,此時方可獲取到圖片真正的寬高,然后再傳遞給ParamsOptionComp組件
MaterialEditComp.jsx
const [imageSize, setImageSize] = useState({ width: 0, height: 0 }); useEffect(() => { const img = new Image(); img.src = imgSrc img.onload = () => { setImageSize({ width: img.width, height: img.height }); }; }, []); <ParamsOptionComp onValuesChange={onValuesChange} imageSize={imageSize} removeNode= {removeNode} />
ParamsOptionComp.jsx
下方初始化第一條數(shù)據(jù)時,因為
initialValue
就是所謂的defaultValue,只會在第一次賦值的時候改變,無法直接設(shè)置initialValue的值來改變
,所以獲取到的也是第一次初始化的寬高都為0,但是我們可以通過Form的resetFields()方法來解決這個問題,當(dāng)監(jiān)聽到imageSize發(fā)生變化時我們可以調(diào)用resetFields()來重新設(shè)置initialValue的值。
useEffect(() => { form.resetFields() }, [imageSize])
3. 初始化新增第一個節(jié)點到頁面中的某個指定模塊
此時輸入框中已經(jīng)有值了,但是此處圖片中還沒有初始化值,此時,需要在useEffect中調(diào)用AddNode初始化第一個節(jié)點的值。
const AddNode = ({ content, color, size, left, top }, index) => {
const contentNode = contentRef.current;
let newNode = document.createElement('div');
newNode.className = 'node' + index;
// 此處判斷節(jié)點是否已經(jīng)存在
const bool = contentNode?.childNodes[index]
if (bool) {
newNode = contentNode.childNodes[index]
}
newNode.textContent = content
newNode.style.color = color;
newNode.style.fontSize = size + 'px';
newNode.style.top = top + 'px';
newNode.style.left = left + 'px';
newNode.style.position = 'absolute';
// 節(jié)點不存在新增階段
if (!bool) {
contentNode.appendChild(newNode);
} else {
// 節(jié)點存在則替換原來的節(jié)點
contentNode.replaceChild(newNode, contentNode.childNodes[index])
}
}
useEffect(() => {
const img = new Image();
img.src = imgSrc
img.onload = () => {
setImageSize({ width: img.width, height: img.height });
AddNode({
content: '文字內(nèi)容',
color: '#000000',
size: 24,
left: img.width / 2 - 48,
top: img.height / 2 - 24
}, 0);
};
}, []);
4. 當(dāng)文本框發(fā)生變動,修改節(jié)點信息
當(dāng)文本框發(fā)生變動通過表單的onValuesChange 進(jìn)行監(jiān)聽,遍歷表單中的數(shù)據(jù)新增節(jié)點信息
const onValuesChange = (changedValues, allValues) => {
// index標(biāo)記當(dāng)前是第幾個節(jié)點
allValues.paramAttribute.forEach((item, index) => {
item && AddNode(item, index)
})
}
5. 實現(xiàn)節(jié)點刪除
當(dāng)進(jìn)行節(jié)點刪除時,調(diào)用傳遞給ParamsOptionComp組件的removeNode方法獲取到刪除的節(jié)點
// 刪除節(jié)點
const removeNode = (index) => {
const contentNode = contentRef.current;
const bool = contentNode?.childNodes[index]
if (bool) {
contentNode.removeChild(contentNode.childNodes[index])
}
}
6. 利用html2canvas將模塊生成canvas,然后轉(zhuǎn)化成圖片
此時需要利用html2canvas將模塊生成canvas,然后轉(zhuǎn)化成圖片,如果需要調(diào)用接口將圖片保存下來,此處還需將圖片轉(zhuǎn)換成二進(jìn)制,如果不需要則直接下載就好
// 將圖片轉(zhuǎn)換成二進(jìn)制形式
const base64ToBlob = (code) => {
let parts = code.split(';base64,')
let contentType = parts[0].split(':')[1]
let raw = window.atob(parts[1])
let rawLength = raw.length
let uint8Array = new Uint8Array(rawLength)
for (let i = 0; i < rawLength; i++) {
uint8Array[i] = raw.charCodeAt(i)
}
return new Blob([uint8Array], { type: contentType })
}
// 保存圖片
const saveImage = async () => {
const contentNode = contentRef.current;
const canvas = await html2canvas(contentNode, {
useCORS: true,
allowTaint: true,//允許污染
backgroundColor: '#ffffff',
// toDataURL: src
})
const imgData = canvas.toDataURL('image/png');
let blob = base64ToBlob(imgData)
const link = document.createElement('a');
link.href = imgData;
// link.href = URL.createObjectURL(blob);
getNewImage(link.href)
// console.log(blob, 11)
link.download = 'page-image.' + blob.type.split('/')[1];
link.click();
}
注意:此處使用html2canvas時, 必須配置以下信息,否則圖片信息無法進(jìn)行轉(zhuǎn)換
{
useCORS: true,
allowTaint: true,//允許污染
backgroundColor: '#ffffff',
// toDataURL: src
}
完整代碼
MaterialEditComp.jsx
import React, { useEffect, useRef, useState } from 'react'
import { Button } from 'antd'
import ParamsOptionComp from './ParamsOptionComp'
import html2canvas from 'html2canvas';
import './index.less'
export default function MaterialEditComp(props) {
const { src, getNewImage } = props;
const [imgSrc, setImgSrc] = React.useState(src);
const contentRef = useRef(null);
const [imageSize, setImageSize] = useState({ width: 0, height: 0 });
/*
const divStyle = {
background: `url(${imgSrc}) no-repeat`,
backgroundSize: 'cover',
backgroundPosition: 'center',
width: imageSize.width + 'px',
height: imageSize.height + 'px',
marginBottom: '30px'
}; */
const onValuesChange = (changedValues, allValues) => {
// console.log(changedValues, allValues, 11)
allValues.paramAttribute.forEach((item, index) => {
item && AddNode(item, index)
})
}
// 新增節(jié)點
const AddNode = ({ content, color, size, left, top }, index) => {
const contentNode = contentRef.current;
let newNode = document.createElement('div');
newNode.className = 'node' + index;
// 此處判斷節(jié)點是否已經(jīng)存在
const bool = contentNode?.childNodes[index]
if (bool) {
newNode = contentNode.childNodes[index]
}
newNode.textContent = content
newNode.style.color = color;
newNode.style.fontSize = size + 'px';
newNode.style.top = top + 'px';
newNode.style.left = left + 'px';
newNode.style.position = 'absolute';
// 節(jié)點不存在新增階段
if (!bool) {
contentNode.appendChild(newNode);
} else {
// 節(jié)點存在則替換原來的節(jié)點
contentNode.replaceChild(newNode, contentNode.childNodes[index])
}
}
// 刪除節(jié)點
const removeNode = (index) => {
const contentNode = contentRef.current;
const bool = contentNode?.childNodes[index]
if (bool) {
contentNode.removeChild(contentNode.childNodes[index])
}
}
// 將圖片轉(zhuǎn)換成二進(jìn)制形式
const base64ToBlob = (code) => {
let parts = code.split(';base64,')
let contentType = parts[0].split(':')[1]
let raw = window.atob(parts[1])
let rawLength = raw.length
let uint8Array = new Uint8Array(rawLength)
for (let i = 0; i < rawLength; i++) {
uint8Array[i] = raw.charCodeAt(i)
}
return new Blob([uint8Array], { type: contentType })
}
// 保存圖片
const saveImage = async () => {
const contentNode = contentRef.current;
const canvas = await html2canvas(contentNode, {
useCORS: true,
allowTaint: true,//允許污染
backgroundColor: '#ffffff',
// toDataURL: src
})
const imgData = canvas.toDataURL('image/png');
let blob = base64ToBlob(imgData)
const link = document.createElement('a');
link.href = imgData;
// link.href = URL.createObjectURL(blob);
getNewImage(link.href)
// console.log(blob, 11)
link.download = 'page-image.' + blob.type.split('/')[1];
link.click();
}
useEffect(() => {
const img = new Image();
img.src = imgSrc
img.onload = () => {
setImageSize({ width: img.width, height: img.height });
AddNode({
content: '文字內(nèi)容',
color: '#000000',
size: 24,
left: img.width / 2 - 48,
top: img.height / 2 - 24
}, 0);
};
}, []);
return (
<>
<div ref={contentRef} style={{
background: `url(${imgSrc}) no-repeat`,
backgroundSize: 'cover',
backgroundPosition: 'center',
width: imageSize.width + 'px',
height: imageSize.height + 'px',
marginBottom: '30px'
}} className="content">
</div>
<ParamsOptionComp onValuesChange={onValuesChange} imageSize={imageSize} removeNode={removeNode} />
<Button type="primary" htmlType="submit" onClick={saveImage} className='btn'>
生成新的圖片
</Button>
</>
)
}
ParamsOptionComp.jsx文章來源:http://www.zghlxwxcb.cn/news/detail-739739.html
import React, { useEffect, useState } from 'react'
import { Input, Form, Space, Button, InputNumber } from 'antd'
import { PlusOutlined, MinusCircleOutlined, DragOutlined } from '@ant-design/icons'
export default function ParamsOptionComp(props) {
const { onValuesChange, imageSize, removeNode } = props
const [count, setCount] = useState(0)
const [form] = Form.useForm();
// 坑
useEffect(() => {
form.resetFields()
}, [imageSize])
return <Form form={form} name="dynamic_form_nest_item"
// 坑
initialValues={{
paramAttribute: [{
left: imageSize.width / 2 - 48,
top: imageSize.height / 2 - 24,
content: '文字內(nèi)容'
}]
}}
onValuesChange={onValuesChange} >
<Form.List name="paramAttribute">
{(fields, { add, remove, move }) => (
<>
{fields.map(({ key, name, ...restField }, index) => (
<Space key={key} style={{ display: 'flex', marginBottom: 0 }} align="baseline">
<Form.Item
style={{ marginBottom: '10px' }}
{...restField}
name={[name, 'content']}
label="文字內(nèi)容"
rules={[{ required: true, message: '請輸入文字內(nèi)容' }]}
>
<Input placeholder="文字內(nèi)容" maxLength={50} />
</Form.Item>
<Form.Item
style={{ marginBottom: '10px' }}
{...restField}
name={[name, 'color']}
label="文字顏色"
rules={[{ required: true, message: '請輸入文字顏色' }]}
initialValue="#000000"
>
<Input placeholder="文字顏色" type="color" style={{ width: '80px' }} />
</Form.Item>
<Form.Item
style={{ marginBottom: '10px' }}
{...restField}
name={[name, 'size']}
label="文字大小"
rules={[{ required: true, message: '請輸入文字大小' }]}
initialValue="24"
>
<InputNumber placeholder="文字大小" min={12} value={13} />
</Form.Item>
<Form.Item
style={{ marginBottom: '10px' }}
{...restField}
name={[name, 'top']}
label="上邊距"
rules={[{ required: true, message: '請輸入上邊距' }]}
initialValue={imageSize.height / 2 - 24}
>
<InputNumber placeholder="上邊距" value={13} />
</Form.Item>
<Form.Item
style={{ marginBottom: '10px' }}
{...restField}
name={[name, 'left']}
label="左邊距"
rules={[{ required: true, message: '請輸入左邊距' }]}
initialValue={imageSize.width / 2 - 48}
>
<InputNumber placeholder="左邊距" value={13} />
</Form.Item>
<MinusCircleOutlined onClick={() => {
if (count === 0) {
return
}
remove(name)
removeNode(index)
setCount(count => count - 1);
}} />
</Space>
))}
<Form.Item>
<Button type="dashed" onClick={async () => {
try {
const values = await form.validateFields()
add();
setCount(count => count + 1);
} catch (errorInfo) {
return;
}
}} block icon={<PlusOutlined />}>添加選項</Button>
</Form.Item>
</>
)}
</Form.List>
</Form>
}
使用:文章來源地址http://www.zghlxwxcb.cn/news/detail-739739.html
const src=''
const getNewImage = (image) => {
console.log(image)
page.close()
}
return <MaterialEditComp src={src} getNewImage={getNewImage} />
到了這里,關(guān)于React實現(xiàn)文本框輸入文字內(nèi)容動態(tài)給圖片添加文字信息(多個)并生成新的圖片的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!