Web互动涂鸦:基于HTML5 Canvas的简易实现
魔都财观 2024-10-11 10:33:01 阅读 94
本文还有配套的精品资源,点击获取
简介:简单涂鸦DOM利用HTML5 Canvas元素实现网页上的自由绘画功能。用户通过鼠标操作进行绘画,该项目通过JavaScript控制Canvas进行像素级绘图,提供了基本的绘画、擦除、保存和分享功能。开发者可以在此基础上扩展更多绘图工具和功能。
1. Canvas绘图基础
1.1 HTML5 Canvas元素简介
Canvas是HTML5中提供的一个用于绘制图形的API,通过简单的标签,开发者可以在网页上绘制图形、动画等。Canvas的核心是一张位图,允许JavaScript脚本进行像素级操作,提供了2D绘图的无限可能性。
<code><canvas id="myCanvas" width="200" height="100"></canvas>code>
1.2 Canvas与SVG的比较
尽管SVG也提供了图形绘制的能力,但它基于XML,更擅长于静态图形的展示。而Canvas是基于JavaScript的,适合用于动态图形和交互式内容的创建,其渲染过程完全由JavaScript控制,因此更加灵活和强大。
1.3 Canvas绘图上下文
Canvas绘图依赖于一个叫做“绘图上下文”(context)的对象。通过获取Canvas元素的绘图上下文,可以执行绘图命令。通常使用的是2D上下文,通过 getContext('2d')
方法获得。
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
本章节主要介绍了Canvas的定义、特点以及如何在HTML中使用Canvas元素。同时,区分了Canvas与SVG的不同之处,帮助读者理解各自的应用场景。最后,介绍了Canvas的绘图上下文概念,为后续章节中复杂的绘图操作打下了基础。
2. 鼠标事件监听实现绘图
2.1 Canvas事件监听基础
2.1.1 鼠标事件类型详解
Canvas提供了多种鼠标事件,这些事件使得我们能够根据用户的动作来执行相应的绘图操作。常见的鼠标事件包括:
click
: 当鼠标点击画布时触发。 mousedown
和 mouseup
: 当鼠标按钮被按下或释放时触发。 mousemove
: 当鼠标在画布上移动时不断触发。 mouseover
和 mouseout
: 当鼠标指针移动到画布上或移出画布时触发。 mouseenter
和 mouseleave
: 类似于 mouseover
和 mouseout
,但不冒泡,即不会在子元素触发。
2.1.2 事件监听器的绑定与响应
为了响应上述事件,我们需要在JavaScript中为Canvas元素绑定事件监听器。通常,我们使用 addEventListener
方法来添加监听器。例如:
// 获取Canvas元素
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 绑定点击事件监听器
canvas.addEventListener('click', function(event) {
const rect = canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = ***;
// 根据点击位置进行绘图操作
// 例如:绘制一个点
ctx.beginPath();
ctx.arc(x, y, 5, 0, Math.PI * 2);
ctx.fill();
});
在上述代码中,我们首先获取了Canvas元素及其绘图上下文,然后为它绑定了一个点击事件监听器。当用户点击Canvas时,事件对象 event
会被传递给监听函数,其中 event.clientX
和 event.clientY
表示点击位置的坐标。通过计算,我们可以得到相对于Canvas左上角的坐标,并在该位置绘制一个圆点。
2.2 通过鼠标绘制图形
2.2.1 绘制线条与曲线
利用鼠标事件,我们可以让用户自由地绘制线条和曲线。以下是使用 mousedown
, mousemove
, 和 mouseup
事件绘制线条的示例代码:
let isDrawing = false;
let startX, startY;
canvas.addEventListener('mousedown', function(e) {
startX = e.clientX - canvas.offsetLeft;
startY = e.clientY - canvas.offsetTop;
isDrawing = true;
});
canvas.addEventListener('mousemove', function(e) {
if (isDrawing) {
const endX = e.clientX - canvas.offsetLeft;
const endY = e.clientY - canvas.offsetTop;
// 开始绘制线条
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(endX, endY);
ctx.stroke();
startX = endX;
startY = endY;
}
});
canvas.addEventListener('mouseup', function(e) {
if (isDrawing) {
// 绘制结束
isDrawing = false;
}
});
在这段代码中, mousedown
事件负责记录起始点, mousemove
事件负责绘制线条,而 mouseup
事件则负责标记绘制过程的结束。用户在画布上按下鼠标并移动时,就可以绘制连续的线条。
2.2.2 绘制多边形和矩形
通过监听不同的鼠标事件,我们还可以让用户绘制多边形和矩形。以下是如何根据用户点击来绘制矩形的代码示例:
let rectX, rectY, rectWidth, rectHeight;
canvas.addEventListener('mousedown', function(e) {
rectX = e.clientX - canvas.offsetLeft;
rectY = e.clientY - canvas.offsetTop;
});
canvas.addEventListener('mousemove', function(e) {
const mouseX = e.clientX - canvas.offsetLeft;
const mouseY = e.clientY - canvas.offsetTop;
// 计算矩形的位置和大小
rectWidth = Math.abs(mouseX - rectX);
rectHeight = Math.abs(mouseY - rectY);
// 重绘画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.strokeRect(rectX, rectY, rectWidth, rectHeight);
});
canvas.addEventListener('mouseup', function(e) {
// 此处可以执行矩形绘制结束的逻辑
});
在这个例子中,当用户按下鼠标时记录起始点,而 mousemove
事件用来更新矩形的位置和大小。当用户释放鼠标时,可以执行一些结束绘制的逻辑,比如在画布上填充矩形。
2.3 鼠标控制下的交互绘图
2.3.1 实现图形的选择与移动
在交互式绘图应用中,用户往往需要对已经绘制的图形进行选择和移动。这通常需要记录每个图形的位置信息,并在鼠标事件触发时进行更新。
// 假设有一个圆形对象的数组
let circles = [];
function createCircle(x, y) {
const circle = {
x: x,
y: y,
radius: 10,
};
circles.push(circle);
}
// 鼠标点击事件监听,用于选择并移动图形
canvas.addEventListener('mousedown', function(e) {
const x = e.clientX - canvas.offsetLeft;
const y = e.clientY - canvas.offsetTop;
circles.forEach((circle, index) => {
const dx = x - circle.x;
const dy = y - circle.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < circle.radius) {
// 选中圆形,设置isMoving状态
selectedCircleIndex = index;
isMoving = true;
}
});
});
// 鼠标移动事件监听,用于移动图形
canvas.addEventListener('mousemove', function(e) {
if (isMoving) {
const x = e.clientX - canvas.offsetLeft;
const y = e.clientY - canvas.offsetTop;
circles[selectedCircleIndex].x = x;
circles[selectedCircleIndex].y = y;
drawAllCircles();
}
});
// 绘制所有圆形
function drawAllCircles() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
circles.forEach(circle => {
ctx.beginPath();
ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2);
ctx.stroke();
});
}
在上述示例代码中,我们定义了一个 createCircle
函数来创建圆形对象,并将它们保存在数组 circles
中。通过监听 mousedown
和 mousemove
事件,我们可以实现选择圆形并随着鼠标移动来拖动圆形的功能。
2.3.2 鼠标悬停效果与动态绘制
动态绘制指的是当鼠标悬停在画布上时,根据鼠标的移动轨迹绘制图形。常见的动态绘制效果包括颜色变化、线条粗细变化等。
let prevX, prevY;
canvas.addEventListener('mousemove', function(e) {
const x = e.clientX - canvas.offsetLeft;
const y = e.clientY - canvas.offsetTop;
if (prevX !== undefined && prevY !== undefined) {
// 绘制线条,实现动态效果
ctx.beginPath();
ctx.moveTo(prevX, prevY);
ctx.lineTo(x, y);
// 随着鼠标的移动改变线条颜色
ctx.strokeStyle = `hsl(${Math.random() * 360}, 50%, 50%)`;
ctx.lineWidth = Math.random() * 10 + 1;
ctx.stroke();
}
// 更新上一个坐标点
prevX = x;
prevY = y;
});
这段代码使用 mousemove
事件监听器来不断绘制线条,并通过随机函数生成不同的颜色和线条宽度,从而实现动态绘制效果。每次移动鼠标时,都会根据当前鼠标的坐标和之前记录的坐标来绘制线条。
下一章节将介绍如何在Canvas中实现颜色与线条宽度的调整。
3. 颜色与线条宽度调整
3.1 颜色选择与填充
颜色是任何视觉作品的基础元素之一,它能够极大地影响最终的视觉效果。在Canvas中,颜色的处理和应用非常灵活,开发者可以根据需求进行多种颜色相关的操作。
3.1.1 颜色模型与选择技巧
在Canvas中,颜色通常使用RGB模型来定义。RGB是红、绿、蓝三种颜色的首字母缩写,通过调整这三种颜色的亮度值(0-255)可以混合出几乎所有的颜色。
// 设置颜色为红色
ctx.fillStyle = 'rgb(255, 0, 0)';
开发者还可以使用十六进制颜色代码来定义颜色,如下所示:
// 设置颜色为蓝色
ctx.fillStyle = '#0000FF';
此外,Canvas还支持使用RGBA和HSLA模型,其中A表示透明度,可以用来制作半透明的视觉效果。
在选择颜色时,可以利用颜色选择器工具来帮助确定具体的颜色代码,或者使用在线颜色库来寻找灵感。考虑到颜色对比和视觉舒适度也非常重要,例如WCAG(Web Content Accessibility Guidelines)为web内容提供了颜色对比度的指导标准,以确保色盲用户也能正常使用。
3.1.2 设置图形与画布的背景色
在Canvas中设置颜色不仅可以用于填充图形,还可以用于设置画布的背景色。
// 设置画布背景色为白色
document.getElementById('myCanvas').getContext('2d').fillnaStyle = '#FFFFFF';
为图形填充颜色的基本方法是使用 fillStyle
属性来设置所需的颜色,然后调用 fill()
方法:
// 设置圆形的颜色为黄色,并填充到Canvas中
ctx.beginPath();
ctx.arc(75, 75, 50, 0, Math.PI * 2, true); // 创建圆形
ctx.fillStyle = 'yellow';
ctx.fill();
此外,可以通过线性渐变或径向渐变来创建更为复杂和动态的背景效果。这将在3.3节进行详细讨论。
3.2 线条样式定制
Canvas不仅提供了填充颜色的功能,还允许开发者定制线条的样式。线条样式对于创建轮廓和边框非常重要,可以显著提升图形的可读性和美观度。
3.2.1 线条宽度与端点样式的设置
在Canvas中,线条的宽度可以通过 lineWidth
属性进行设置:
// 设置线条宽度为5像素
ctx.lineWidth = 5;
线条的端点样式定义了线条的结束端点外观,有三种端点样式可供选择:
butt
:端点是平的,与线条的最后一个像素形成垂直线。 round
:端点是圆的,其半径等于线条宽度的一半。 square
:端点是方的,其外延等于线条宽度的一半。
// 设置线条端点样式为圆形
ctx.lineCap = 'round';
3.2.2 虚线样式的应用与自定义
虚线的样式在很多设计中都有应用,可以用来表示不连续的线条。在Canvas中,可以通过设置 lineDash
属性来定义虚线样式:
// 设置虚线样式为[10, 5]
ctx.setLineDash([10, 5]);
在上面的代码中,数字数组定义了交替的虚线段和间隔长度,单位是像素。
此外,如果需要实现自定义的虚线样式,可以利用Canvas的绘图循环和条件判断来手动绘制特定的线条模式。这种方式可以创造出无限可能的虚线效果,比如虚线、点线等。
3.3 高级绘图特性
Canvas绘图库不仅包含基础的填充和线条绘制,还有更多高级特性,如渐变和图案的填充、文字与图像的绘制等。
3.3.1 渐变与图案的填充技术
渐变填充可以使图形从一个颜色平滑过渡到另一个颜色,适用于创建阴影效果、高光效果等。
在Canvas中,创建渐变可以通过以下步骤:
使用 createLinearGradient()
或 createRadialGradient()
创建渐变对象。 使用 addColorStop()
方法指定渐变的起始和结束颜色以及它们在渐变路径上的位置。 将渐变对象赋值给图形的 fillStyle
或 strokeStyle
属性。 使用 fill()
或 stroke()
方法绘制图形。
// 创建线性渐变
var gradient = ctx.createLinearGradient(0, 0, 0, 170);
gradient.addColorStop(0, '#00F');
gradient.addColorStop(1, '#0F0');
// 应用渐变
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 170, 170);
图案的填充使用的是 createPattern()
方法,它接受一个图像(Image)、视频(Video)、Canvas或另一个 <img>
元素作为参数,并且可以设置图像重复的模式,例如 repeat
、 repeat-x
、 repeat-y
和 no-repeat
。
3.3.2 文字与图像在Canvas上的绘制
Canvas提供了 fillText()
和 strokeText()
方法来在Canvas上绘制文字,支持设置字体样式、大小、颜色等。
// 设置文字样式并绘制到Canvas上
ctx.font = 'bold 48px arial';
ctx.fillText("Hello Canvas", 10, 50);
绘制图像时,可以使用 drawImage()
方法将图片绘制到Canvas中。这个方法非常强大,它允许开发者对图像进行缩放、裁剪等操作:
// 绘制图像到Canvas中
var img = new Image();
img.onload = function() {
ctx.drawImage(img, 0, 0, 300, 200);
};
img.src = 'path_to_image.jpg';
通过这些高级特性,开发者可以创作出视觉效果丰富的Canvas应用,从而满足各种创意需求和业务场景。
4. 橡皮擦和画布清空功能
在前几章中,我们已经探讨了Canvas绘图的基础知识,学习了如何利用鼠标事件实现绘图功能,并深入到颜色和线条样式的调整。现在,我们将注意力转向如何在Canvas上实现橡皮擦和清空画布的功能。这些功能对于提供一个完整的绘图体验至关重要,允许用户修正错误或从头开始。
4.1 实现橡皮擦功能
橡皮擦工具是数字绘图软件中不可或缺的一部分。与传统橡皮擦一样,它能够移除画布上的内容,为用户提供重新绘制或修正错误的机会。
4.1.1 橡皮擦工具的原理与实现
橡皮擦功能的基本原理是将画布上的像素恢复到初始状态,或者根据背景色覆盖掉相应的部分。在Canvas中,这通常意味着通过清除特定区域的像素来实现。
代码示例1:基本橡皮擦功能实现
function erase(ctx, x, y, size) {
ctx.globalCompositeOperation = 'destination-out'; // 设置合成模式为destination-out,实现橡皮擦效果
ctx.fillStyle = 'rgba(0,0,0,1)'; // 设置橡皮擦颜色,这里为黑色
ctx.fillRect(x - size/2, y - size/2, size, size); // 绘制一个覆盖目标区域的矩形
ctx.globalCompositeOperation = 'source-over'; // 恢复合成模式为默认值
}
4.1.2 橡皮擦的大小和形状定制
橡皮擦并不总是圆形的,用户可能希望根据需要来定制橡皮擦的大小和形状。下面的代码展示了如何实现一个可调整大小的圆形橡皮擦,并留出定制形状的可能性。
代码示例2:可定制大小和形状的橡皮擦
let eraserSize = 20; // 初始橡皮擦大小
let eraserShape = 'round'; // 初始橡皮擦形状
canvas.addEventListener('mousedown', startErase, false);
canvas.addEventListener('mousemove', erase, false);
canvas.addEventListener('mouseup', stopErase, false);
canvas.addEventListener('mouseleave', stopErase, false);
function startErase(e) {
if (e.which === 1) { // 左键开始绘制
isDrawing = true;
}
}
function erase(e) {
if (!isDrawing) return;
ctx.globalCompositeOperation = 'destination-out';
ctx.lineCap = 'round';
ctx.strokeStyle = 'rgba(0,0,0,1)';
ctx.lineWidth = eraserSize;
let pos = getMousePos(canvas, e);
if (eraserShape === 'round') {
ctx.beginPath();
ctx.arc(pos.x, pos.y, eraserSize / 2, 0, Math.PI * 2);
ctx.fill();
} else if (eraserShape === 'square') {
// 实现方形橡皮擦的逻辑代码
}
ctx.globalCompositeOperation = 'source-over';
}
function stopErase(e) {
if (e.which === 1) { // 左键停止绘制
isDrawing = false;
}
}
function getMousePos(canvas, evt) {
// 获取鼠标在Canvas元素上的位置
}
在上述代码中,我们使用了 mousedown
, mousemove
, mouseup
, 和 mouseleave
事件来追踪鼠标动作,并应用了Canvas的 globalCompositeOperation
属性来实现橡皮擦效果。通过改变 lineCap
和 lineWidth
属性,我们可以模拟不同的笔刷形状和大小。
4.2 清空画布技巧
在用户希望去除画布上所有内容,或者准备以全新的画布开始时,清空画布是一个必要的功能。
4.2.1 清空整个画布的操作方法
清空画布是一个简单但重要的操作。可以使用 .clearRect
方法来实现。
代码示例3:清除整个画布
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
4.2.2 局部清除与重绘技巧
有时,用户可能只希望清除画布的一部分。利用 globalCompositeOperation
和 drawImage
方法可以实现非破坏性的局部清除和重绘。
代码示例4:局部清除与重绘
function clearPartOfCanvas(x, y, width, height) {
// 保存当前画布内容
ctx.save();
// 使用destination-out模式来擦除指定区域
ctx.globalCompositeOperation = 'destination-out';
ctx.fillRect(x, y, width, height);
// 恢复画布内容
ctx.restore();
}
4.3 高级擦除功能实现
橡皮擦功能的高级实现不仅包括基本的擦除操作,还可以扩展到更复杂的交互,例如擦除效果和撤销操作的集成。
4.3.1 可配置的擦除区域选择
让用户能够选择擦除的区域可以大大增强橡皮擦的灵活性。我们可以通过绘制一个自定义的形状或选择框来实现这一功能。
代码示例5:可配置擦除区域的橡皮擦
let selectionStartX, selectionStartY;
let isSelecting = false;
canvas.addEventListener('mousedown', function(e) {
isSelecting = true;
selectionStartX = e.offsetX;
selectionStartY = e.offsetY;
});
canvas.addEventListener('mouseup', function(e) {
if (isSelecting) {
let selectionEndX = e.offsetX;
let selectionEndY = e.offsetY;
clearPartOfCanvas(selectionStartX, selectionStartY, selectionEndX - selectionStartX, selectionEndY - selectionStartY);
}
isSelecting = false;
});
4.3.2 擦除效果与撤销操作的集成
一个成熟的绘图应用还应该提供撤销操作的能力,允许用户撤销最近的擦除或绘图动作。这通常涉及到维护一个命令的历史堆栈。
代码示例6:集成擦除效果与撤销操作
const commandStack = [];
function executeCommand(command) {
command.execute();
commandStack.push(command);
}
function undo() {
if (commandStack.length === 0) return;
const command = commandStack.pop();
command.undo();
}
// 命令模式
class Command {
constructor(ctx) {
this.ctx = ctx;
this.properties = { ... };
}
execute() {
// 执行命令的逻辑代码
}
undo() {
// 执行撤销命令的逻辑代码
}
}
在上述示例中,我们使用了命令模式,这是实现撤销功能的一种常见方法。 Command
类定义了基本操作, execute
方法用于执行当前命令,而 undo
方法用于撤销该命令。我们通过一个数组 commandStack
来保存所有操作,以支持撤销功能。
这一章,我们深入了解了Canvas上的橡皮擦功能和画布清空技术。我们从基础的橡皮擦实现到高级的擦除效果和撤销操作的集成,每一步都细致地分析了相关的技术细节和实现方法。这些功能对于提供一个流畅且无限制的绘图体验是必不可少的。
5. 画作保存与分享实现
在本章节中,我们将探讨如何实现Canvas中完成的画作的保存和分享。这不仅包括将画布内容导出为数据格式,并保存到本地存储,还会涉及到分享给其他用户或通过社交媒体进行分享的过程。这一章节将分为三个主要部分:
5.1 画作的数据保存方法 5.2 实现画作的本地保存 5.3 推广与分享机制
5.1 画作的数据保存方法
Canvas绘图的保存通常涉及到将画布状态导出为某种数据格式。最常用的数据格式是PNG或JPEG,但也可以导出为PDF,SVG,甚至是二进制的图片数据。
5.1.1 将画布状态导出为数据格式
要将Canvas的状态导出为图片数据,我们可以使用 toDataURL()
方法。这个方法会返回一个包含图片表示的data URL,可被用作 <img>
标签的 src
属性。这个过程不涉及服务器端的操作,所有的处理都在客户端完成。
// 获取当前canvas元素
const canvas = document.getElementById('myCanvas');
// 将画布内容导出为PNG格式的数据URL
const pngData = canvas.toDataURL('image/png');
// 可以将pngData用于创建下载链接或显示图片
document.getElementById('downloadLink').href = pngData;
5.1.2 压缩画布数据与存储优化
导出的数据可能很大,特别是当画布尺寸较大或细节丰富时。为了优化存储和传输,我们可以在导出前对数据进行压缩。Web Workers可以用来进行后台压缩,从而不会影响到主线程的响应性。
// 使用Worker来压缩画布数据
const worker = new Worker('compressWorker.js');
// 发送画布数据到Worker进行压缩
worker.postMessage({ canvasData: pngData });
// 在Worker中处理压缩
// compressWorker.js
self.addEventListener('message', (e) => {
const compressedData = compress(e.data.canvasData);
self.postMessage(compressedData);
});
5.2 实现画作的本地保存
除了数据导出,用户还希望能够直接将他们的作品保存到本地文件系统。HTML5提供了File APIs,它允许我们通过用户触发的事件保存文件到本地。
5.2.1 画作的下载与本地存储
为了提供画作的下载和存储,我们可以创建一个 <a>
元素,并设置其 href
属性为Canvas导出的数据URL,并使用 download
属性直接触发下载。
<!-- HTML示例 -->
<a id="downloadLink" href="" download="myDrawing.png">下载画作</a>code>
// JavaScript代码示例
document.getElementById('downloadLink').href = pngData;
5.2.2 文件格式支持与兼容性处理
并非所有浏览器都支持 toDataURL()
方法返回的所有图像格式。为了解决这个问题,我们可能需要检测浏览器是否支持特定格式,并且可能需要回退到一种更兼容的格式,例如JPEG。
5.3 推广与分享机制
为了增加用户互动和画作的曝光率,实现分享功能至关重要。用户应能够生成分享链接或二维码,这些链接和二维码将指向他们创建的画作。
5.3.1 创建分享链接与二维码
分享链接可以是一个指向Canvas画作的URL,而二维码则可以生成为方便用户扫描的图片格式。
// 生成分享链接
const shareLink = '***' + encodeURIComponent(pngData);
// 使用第三方库来生成分享用的二维码
const qrCode = qrcode(0, 'M');
qrCode.makeImage({
text: shareLink,
width: 256,
height: 256,
color: {
dark: '#000000ff',
light: '#ffffffff'
}
}, function (img) {
document.getElementById('qrImage').appendChild(img);
});
5.3.2 社交媒体集成与第三方平台分享
为了使分享更加容易,可以集成社交媒体分享按钮,允许用户直接将链接或内容分享到Facebook、Twitter等平台。同时,也可以集成第三方平台API,例如Email、WhatsApp等,让用户选择分享方式。
<!-- 社交媒体分享按钮示例 -->
<!-- Facebook -->
<a href="***{ {shareLink}}" target="_blank">分享到Facebook</a>code>
<!-- Twitter -->
<a href="***{ {shareLink}}" target="_blank">分享到Twitter</a>code>
以上就是关于如何实现画作的保存与分享的详细介绍。通过这些方法,用户不仅能够将他们的Canvas艺术作品保存为数据文件,还能通过各种方式与他人分享他们的创作。
本文还有配套的精品资源,点击获取
简介:简单涂鸦DOM利用HTML5 Canvas元素实现网页上的自由绘画功能。用户通过鼠标操作进行绘画,该项目通过JavaScript控制Canvas进行像素级绘图,提供了基本的绘画、擦除、保存和分享功能。开发者可以在此基础上扩展更多绘图工具和功能。
本文还有配套的精品资源,点击获取
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。