JavaScript 动画与Canvas图形学习笔记

动画与Canvas图形

使用requestAnimationFrame
早期定时动画

早期动画就是使用setInterval()来控制动画的执行

无论是setInterval()还是setTimeout()都是不能保证时间精度的。作为第二个参数的延时只能保证何时将代码添加到任务队列,不能保证添加到队列就会立即执行

时间间隔的问题

浏览器自身计时器会让这个问题雪上加霜,浏览器的计时器精度不足毫秒,以下是浏览器几个计时器精度情况:

​ IE8及更早版本的计时器精度为15.625秒

​ IE9及更晚版本的计时器精度为4毫秒

​ Firefox和Safar的计时器精度为约10毫秒

​ Chrome的计时器精度为4毫秒

即使将时间间隔设到最优,也免不了只能得到近似结果

requestAnimationFrame

该方法接收一个参数,该参数是一个要在重绘屏幕前调用的函数;这个函数就是修改DOM样式以反映下一次重绘有什么变化的地方;为了实现循环,可以把多个requestAnimationFrame()调用串联起来:

function update() {    var div = document.getElementById('status');    div.style.width = (parseInt(div.style.width, 10) + 5 + '%';    if (div.style.left != 100%) {        requestAnimationFrame(update);    }}requestAnimationFrame(update);

在传给该函数的函数中可以接收一个参数,此参数是一个DOMHighResTimeStamp的实例(比如performance.now()返回的值),表示下次重绘的时间

该函数其实把重绘任务安排在了未来一个已知时间点上,而且通过这个参数告诉了开发者,开发者可以根据这个参数优化动画

cancelAnimationFrame

requestAnimationFrame()也返回一个ID,可以通过cancelAnimationFrame()来取消重绘任务

通过requestAnimationFrame节流

支持这个方法的浏览器实际上会暴露作为钩子的回调队列(钩子 hook ,就是浏览器再执行下一次重绘之前的一个点);这个回调队列是一个可修改的函数列表,包含在重绘之前的调用函数,每次调用requestAnimationFrame()函数都会在队列上推入一个回调函数,队列的长度没有限制

这个回调队列的行为不一定和动画有关,不过,每次递归的向队列中加入回调函数,可以保证每次重绘最多只调用一次回调函数,这是一个非常好的节流工具

function test() {    console.log('hello');}window.addEventListener('scroll', () => {    window.requestAnimationFrame(test);})//这样会把每次回调执行集中在钩子,但不会过滤掉多余的调用,所以可以定义一个标志变量作为开关let flag = false;function test() {    console.log('hello');    flag = false;}window.addEventListener('scroll', () => {    if (!flag) {        flag = true;        window.requestAnimationFrame(test);    }})//但是重绘是频繁的操作,所以这个还不能算真正节流,更好的办法是配合使用一个计时器来限制操作执行的频率let flag = true;function test() {    console.log('hello');}window.addEventListener('scroll', () => {    if (flag) {        flag = false;        window.requestAnimationFrame(test);        window.setTimeout(() => flag = true, 50);    }})
基本的画布功能

创建canvas元素时至少要设置其width和height属性,这样才能告诉浏览器在多大面积上绘图;在标签内的内容是后备数据,以防浏览器会不支持canvas元素时显示

元素可以像其他元素一样在DOM节点上设置属性,整个元素还可以通过CSS添加样式,并且元素在添加样式或实际绘图内容前是不可见的

要在画布上绘制图形,首相要取得绘图上下文,使用getContext()方法可以获取绘图上下文的引用,对于平面图形还需要给这个方法传入参数2d

let drawing = document.getElementById('drawing');//确保浏览器支持canvasif (drawing.getContext) {    let context = drawing.getContext('2d');}

可以使用toDataURL方法导出canvas元素上的图像,这个方法接收一个参数:要生成的图像的MIME类型

let drawing = document.getElementById('drawing');//确保浏览器支持canvasif (drawing.getContext) {    //获得图像数据URI    let imgURI = drawing.toDataURL("image/png");    //显示图片    let image = document.createElement("img");    image.src = imgURI;    document.body.appendChild(image);}//较新版本的浏览器还支持“image/jpeg”进行JPEG编码

如果画布的图像是从其它域绘制过来的,toDataURL()方法就会抛出错误,因为跨域问题

2D绘图上下文

该上下文提供了绘制2D图形的方法,包括矩形、弧形和路径;2D上下文坐标原点(0,0)在canvas元素的左上角,x轴坐标向右增长,y轴坐标向下增长,width和height表示两个方向上像素的最大值

填充和描边

可以在2D上下文设置基本的绘画操作:填充和描边,fillStyle和strokeStyle;这两个属性可以接收CSS色彩的任意格式(名称、rgb

、渐变、图案等)

设置完成后,所有的描边和填充都会使用这两种样式,除非再次修改

let context = drawing.getContext('2d');context.strokeStyle = 'red';context.fillStyle = '#0000ff';
绘制矩形

矩形是唯一一个可以直接在2D绘图上下文中绘制的形状,相关方法有三个:fillRect()、strokeRect()、clearRect(),这些方法都接收四个参数:矩形x坐标、矩形y坐标、矩形宽度、矩形高度,单位都为像素

fillRect方法用于在画布上绘制并填充矩形;strokeRect方法用于绘制矩形描边轮廓;clearRect方法可以擦除画布中某个矩形区域

描边的宽度由lineWidth属性控制,可以是任意整数值;lineCap属性控制线条端点形状:butt(平头)、round(出圆头)、square(出方头);lineJoin控制线条交点的形状:round(圆转)、bevel(取平)、miter(出尖)

绘制路径

绘制路径首先要先调用beginPath()方法表示要开始绘制的新路径,然后调用以下方法来绘制路径:

​ arc(x, y, radius, startAngle, endAngle, counterclockwise):以坐标x,y为圆心,以radius为半径绘制一条弧线,起始角度为startAngle,结束角度为endAngle,counterclockwise表示是否逆时针计算起始角度和结束角度(默认顺时针)

​ arcTo(x1, y1, x2, y2, radius):以半径radius,经由(x1, y1)绘制一条从上一点到(x2, y2)的弧线

​ bezierCurveTo(c1x, c1y, c2x, c2y, x, y):以(c1x, c1y)和(c2x, c2y)为控制点,绘制一条从上一点到(x, y)的弧线(三次贝塞尔曲线)

​ lineTo(x, y):绘制一条从上一点到(x, y)的直线

​ moveTo(x, y):不绘制线条,把绘制光标移动到(x, y)

​ quadraticCurveTo(cx, cy, x, y):以(cx, cy)为控制点,绘制一条从上一点到(x, y)的弧线(二次贝塞尔曲线)

​ rect(x, y, width, height):以给定宽度和高度在坐标点(x, y)绘制一个矩形

创建路径后可以使用closePath()方法绘制一条返回起点的线。如果路径已经完成,可以使用fillStyle属性并调用fill()方法来填充路径,也可以指定strokeStyle属性并调用stroke()方法来描画路径,还可以调用clip()方法基于已有路径创建一个新剪切区域

isPointInPath(x, y)方法可以检测指定点是否在路径上,可以在关闭路径前随时调用

绘制文本

可以通过fillText()和strokeText()方法绘制文本,这两个方法都接收四个参数:要绘制的字符串、x坐标、y坐标、可选的最大像素宽度

该方法绘制的结果都取决于以下三个属性:

​ font:以CSS语法指定的字体样式、大小、字体族等,比如“10px Arial”

​ textAlign:指定文本的对齐方式,start、end、left、right、center

​ textBaseLine:指定文本的基线,top、hanging、middle、alphabetic、ideographic、bottom

measureText()方法接受一个参数,即要绘制的文本,然后返回一个TextMetrics对象

变换

上下文变换可以操作绘制在画布上的图像,以下方法可用于改变绘制上下文的变换矩阵:

​ rotate(angle):围绕原点把图像旋转angle弧度

​ scale(scaleX, scaleY):通过在x轴乘以scaleX、在y轴乘以scaleY来缩放图像,默认值都是1.0

​ translate(x, y):把原点移动到(x, y),这个操作执行后,原点坐标就会从(0, 0)变成(x, y)

​ transform(m1_1, m1_2, m2_1, m2_2, dx, dy):像下面这样通过矩阵乘法直接修改矩阵

​ m1_1 m1_2 dx

​ m2_1 m2_2 dy

​ 0 0 1

​ setTransform(m1_1, m1_2, m2_1, m2_2, dx, dy):把矩阵重置为默认值,再以传入的参数调用transform()

如果想记录当前属性和变化状态,可以调用save()方法,调用后,这一时刻所有的设置会被放到一个暂存栈中。保存后可以继续修改上下文,需要恢复时,可以调用restore()方法,这个方法会从栈区中取出之前保存的设置并恢复(save只保存上下文设置和变换,不保存内容)

绘制图像

可以通过drawing()方法绘制图像,该方法接收三组不同参数:

​ 要绘制的图像,x坐标,y坐标

​ 要绘制的图像,x坐标,y坐标,目标宽度,目标高度

​ 要绘制的图像,源图像x坐标,源图像y坐标,源图像宽度、源图像高度、目标区域x坐标、目标区域y坐标、目标区域宽度、目标区域高度

这里要绘制的图像可以是HTML的img元素,还可以是另一个canvas元素

如果绘制的图像来自其它域而非当前页面,则不能获取其数据,此时使用toDataURL()会报错

阴影

可以为已有的形状或路径生成阴影,使用以下方法:

​ shadowColor:CSS颜色值,默认黑色

​ shadowOffsetX:阴影相对于形状或路径的x坐标偏移量,默认为0

​ shadowOffsetY:阴影相对于形状或路径的y坐标偏移量,默认为0

​ shadowBlur:像素,表示模糊量,默认为0

这些属性都可以通过context对象读写

渐变

渐变通过CanvasGradient实例表示,调用上下文的createLinearGradient()创建线性渐变,这个方法接收四个参数:起点x坐标、起点y坐标、终点x坐标、终点y坐标;该方法返回一个CanvasGradient对象并返回实例

然后使用addColorStop()方法为渐变指定色标,这个方法接收两个参数:色标位置、CSS颜色值(色标通过0-1范围内的值表示,0代表第一种颜色,1代表最后一种颜色)

let gradient = context.createLinearGradient(30, 30, 70, 70);gradient.addColorStop(0, "white");gradient.addColorStop(1, "black");context.fillStyle = "#0000ff";context.fillRect(10, 10, 50, 50);context.fillStyle = gradient;context.fillRect(30, 30, 50, 50);

径向渐变(放射性渐变)使用createRadialGradient()方法来创建,该方法接收六个参数:起点圆心x坐标、起点圆心y坐标、起点圆心半径、终点圆心x坐标、终点圆心y坐标、终点圆心半径

图案

图案是用于填充和描画图形的重复图像,通过createPattern()方法创建新图案,传入两个参数:源图案、表示该如何重复图像的字符串(与CSS的background-repeat属性值一样:repeat、repeat-x、repeat-y、no-repeat)

和渐变一样,图案的起点实际上是画布的原点(0, 0);将填充样式设置为图案,表示在指定位置而不是开始绘制的位置显示图案

源图案可以是:HTML的img元素、video元素、另一个canvas元素

图像数据

2D上下文可以使用getImageData()方法获取原始图像数据,方法接收四个参数:左上角像素x坐标、左上角像素y坐标、像素宽度,像素高度;返回一个ImageData实例,每个实例都包含三个属性:width、height、data,data是包含图像原始像素信息的数组

每个像素在data数组中都由四个值表示,分别代表红、绿、蓝和透明度值;例如第一个像素信息包含在第0-3个值中:

let data = imageData.data;red = data[0];green = data[1];blue = data[2];alpha = data[3];

这样四个四个一组表示取得的所有像素的信息

处理好的信息可以通过putImageData()方法,把图像数据再绘制到画布上context.putImageData(imageData, 0, 0);

合成

2D上下文中绘制的所有内容都会应用两个属性:globalAlpha和globalCompositionOperation

globalAlpha是一个在0-1范围的值(包括0和1),用于指定所有内容的透明度;修改只会影响后面的绘制

globalCompositionOperation表示新绘制的形状如何与已有的形状融合,有下列值:

​ source-over:默认值,新图形在原有图形上

​ source-in:新图形只绘制与原有图形重叠的部分,其余部分全部透明

​ source-out:新图形只绘制出不与原有图形重叠的部分,其余部分全部透明

​ source-atop:新图形只绘制出与原有图形重叠的部分,原有图形不受影响

​ destination-over:新图形绘制在原有图形下面,重叠部分只有原图形透明像素下的部分可见

​ destination-in:新图形绘制在原有图形下面,画布上只剩下二者重叠的部分,其余部分完全透明

​ destination-out:新图形与原有图形重叠的部分完全透明,原图形其余部分不受影响

​ destination-atop:新图形绘制在原有图形下面,原有图形与新图形不重叠部分完全透明

​ lighter:新图形与原有图形重叠部分的像素值相加,使该部分变亮

​ copy:新图形将擦除并完全取代原有图形

​ xor:新图形与原有图形重叠部分执行”异或“计算

通过给globalCompositionOperation属性赋上面的值来规定合成方式;这个值在不同浏览器上有差异,用之前先了解

WebGL

WebGL是画布3D上下文,它不是W3C标准,而是Khronos Group的标准,很多WebGL的概念可以从OpenGL照搬过来,可以先去熟悉OpenGL ES2.0

WebGL上下文

在完全支持的浏览器中,WebGL 2.0上下文的名字叫”webgl2“,WebGL 1.0上下文的名字叫”webgl1“,如果浏览器不支持WebGL则会返回null

let drawing = document.getElementById('drawing');if (drawing.getContext) {    let gl = drawing.getContext('webgl');    if (gl) {        //...    }}
WebGL基础

可以在取得WebGL上下文时指定一些选项,通过一个参数对象传入

​ alpha:布尔值,表示是否为上下文创建透明通道缓冲区,默认true

​ depth:布尔值,表示是否使用16位深缓冲区,默认true

​ stencil:布尔值,表示是否使用8位模板缓冲区,默认false

​ antialias:布尔值,表示是否使用默认机制执行抗锯齿操作,默认true

​ premultipliedAlpha:布尔值,表示缓冲区是否预乘透明度值,默认为true

​ preserveDrawingBuffer:布尔值,表示绘图完成后是否保留绘图缓冲区,默认false

let gl = drawing.getContext('webgl', { alpha: false });

某些浏览器可能会在创建WebGL上下文时抛出错误,所以可以把这个方法包装在try/catch中

1、常量

在OpenGL中有各种以GL_开头的常量,假设”GL_COl“,但是在WebGL中需要这样访问:gl.COl;这种方式支持大部分OpenGL常量

2、方法命名

OpenGL中很多方法会包含相关数据类型信息,通常通过后缀来体现;数字(1-4)优先,数据类型(”i“——整数,”f“——浮点数)在后;比如:gl.uniform4f();//表示需要四个浮点数参数 gl.uniform3i();//表示需要三个整数参数

还有方法接收数组,用“v”表示,例如:gl.uniform3iv();//表示接收一个包含三个值的数组参数

3、准备绘图

准备使用WebGL上下文之前,通常要先指定一种实心颜色清除canvas;为此要调用clearColor()方法并传入4个参数:红色值、绿色值、蓝色值、透明度值(都必须是0-1范围内的值)

4、视口与坐标

绘图前还需要定义WebGL视口。默认情况,视口使用整个canvas区域。可以调用viewport()方法并传入视口相对于canvas元素的x、y坐标及宽度和高度;这个方法的x、y坐标原点(0, 0)表示canvas的左下角

在视口中,坐标原点(0, 0)是视口的中心点,左下角是(-1, -1),右上角是(1, 1);如果绘图中坐标超出了,则超出部分不显示,会被剪切

5、缓冲区

在js中,顶点信息保存在定型数组中,要使用这些信息,必须把他们先转换为WebGL缓冲区

创建缓冲区要调用gl.createBuffer()方法,并使用gl.bindBuffer()方法将缓冲区绑定到WebGL上下文

let buffer = gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER, buffer);gl.bufferData(gl.ARRAY_buffer, new Float32Array([0, 0.5, 1]), gl.STATIC_DRAW);

调用gl.bindBuffer()将buffer设置为上下文的当前缓冲区,之后所有的缓冲区操作都在buffer上直接执行;所以gl.bufferData()虽然没有包含对buffer的引用,但是也是在它上面执行的

上面最后一行代码使用了一个Float32Array(通常保存了所有顶点信息)初始化了buffer;如果要输出缓冲区内容,使用drawElement()方法并传入gl.ELEMENT_ARRAY_BUFFER

gl.bufferData()方法的最后一个参数表示如何使用缓冲区,可以是以下常量值:

​ gl.STATIC_DRAW:数据加载一次,可以在多次绘制时使用

​ gl.STREAM_DRAW:数据加载一次,只能在几次绘制中使用

​ gl.DYNAMIC_DRAW:数据可以重复更改,在多次绘制中使用

除非是很有经验的OpenGL程序员,否则我们需要对大多数缓冲区使用gl.STATIC_DRAW

缓冲区会一直驻留在内存中,直到页面卸载;如果不需要缓冲区,最好调用gl.deleteBuffer()方法释放内存:gl.deleteBuffer(buffer);

6、错误

WebGl不会抛出错误,必须调用gl.getError()方法,返回一个常量,表示发生的错误类型,有下列值:

​ gl.NO_ERROR:上一次操作无错误(0值)

​ gl.INVALID_ENUM:上一次操作没有传入WebGL预定的常量

​ gl.INVALID_VALUE:上一次操作需要无符号数值,但是传入了负数

​ gl.INVALID_OPERATION:上一次操作在当前状态下无法完成

​ gl.OUT_OF_MEMORY:上一次操作因内存不足而无法完成

​ gl.CONTEXT_LOST_WEBGL:上一次操作因外部事件(设备掉电)而丢失了WebGL上下文

每次调用都会返回一个错误值;如果有多次错误,则可以重复这个过程,直到返回gl.NO_ERROR

7、着色器

着色器时OpenGL中另一个概念,WebGL中有两种着色器:顶点着色器、片段着色器

着色器用于把3D顶点转换为可以渲染的2D顶点,它是用GLSL(OpenGL Shading Language)语言写的

编写着色器

每个着色器都有一个main()方法,在绘制期间重复运行,给着色器传递数据的方法有两种:attribute和uniform,attribute用于将顶点传入顶点着色器,uniform用于将常量值传入任何着色器;这两个值是在main函数外部定义的,后面紧跟数据类型和变量名

attribute vec2 aVertexPosition;void main() {    gl_Position = vec4(aVertexPosition, 0.0, 1.0);}

定义了一个名为aVertexPosition的attribute,这个attribute是一个包含两项的数组,代表x、y坐标;即使只传两个坐标,顶点着色器返回值也会包含四个元素,存在变量gl_Position中

片段着色器通过uniform传入数据

uniform vec4 uColor;void main() {    gl_FragColor = uColor;}

着色器必须返回一个值,保存在gl_FragColor中,这个值表示绘制时使用的颜色

OpenGl着色器语言比示例要复杂,这里只是简单介绍

创建着色器程序

浏览器并不能理解原生GLSL代码,所以因此GLSL代码字符串必须经过编译并链接到一个着色器程序中

为了便于使用,通常使用带有自定义type属性的script元素把着色器代码包含在网页中,如果type无效则浏览器不会解析script其中的内容,但是我们还是可以读写其中的字符串(type="x-webgl/x-vertex-shader"或type=“x-webgl/x-fragment-shader”)

通过text提取script元素中的内容,然后创建shader对象,需要调用gl.createShader()方法,并传入想创建的着色器类型(gl.VERTEX_SHADER或gl.FRAGMENT_SHADER),然后调用gl.shaderSource(shader对象, GLSL代码)方法把GLSL代码应用到着色器,再调用gl.compileShader(shader对象)编译着色器

接着把这两个对象链接到着色程序:

let program = gl.createProgram();gl.attachShader(program, vertexShader);gl.attachShader(program, fragmentShader);gl.linkProgram(program);

第一行创建一个程序,然后attachShader()添加着色器,调用linkProgram()将两个着色器链接到变量program中,最后就可以通过useProgram()方法让WebGL上下文使用这个程序了:

gl.useProgram(program);

调用这个函数后,所有的后续绘制操作都会使用这个程序

给着色器传值

前面定义的着色器都需要传入一个值,才能完成工作;对于uniform,可以调用gl.getUniformLocation()方法,返回一个对象,表示uniform在内存中的位置,然后使用它来赋值,如:

let uColor = gl.getUniformLocation(program, 'uColor');gl.uniform4fv(uColor, [0, 0, 0, 1]);

attribute也一样,如:

let aVertexPosition = gl.getAttributeLocation(program, 'aVertexPosition');gl.enableVertexAttribArray(aVertexPosition);gl.vertexAttribPointer(aVertexPosition, itemSize, gl.FLOAT, false, 0, 0);

最后创建了一个指向调用gl.bindBuffer()指定的缓冲区的指针,并保存在aVertexPosition中,可以在后面由顶点着色器使用

调试着色器和程序

着色操作可能失败,而且是静默失败,必须手动通过WebGL上下文获取着色器或程序信息

通过gl.getShaderParameter()方法取得编译后的状态,如果着色器编译成功则会返回true,否则返回false,此时可以调用gl.getShaderInfoLog()并传入着色器取得错误,返回一个字符串,表示问题所在(它可以用于顶点着色器或者片段着色器)

通过gl.getShaderParameter()方法还可以检测链接阶段(相关检测查看红宝书p575)

还有getProgramParameter()(与getShaderParameter()方法一样返回true或false),同样也有getProgramInfoLog()方法

GLSL 100升级到GLSL 300

WebGL 2主要是升级到了GLSL 3.00 ES着色器,这个升级有很多新着色器功能暴露,要使用这个升级版着色器,着色器第一行代码必须是:#version 300 es

这个升级包括了一些语法变化:

​ 顶点attribute变量要使用in而不是attribute关键字

​ 使用varying关键字为顶点或片段着色器声明的变量,现在必须根据相应的着色器行为改写为使用in或out

​ 预定义的输出变量gl_FragColor没有了,片段着色器必须为颜色输出声明自己的out变量

​ 纹理查找函数texture2D和textureCube统一成了一个texture函数

8、绘图

WebGl绘图只能绘制三种形状:点、线、三角形

WebGL绘图要使用:drawArrays()和drawElements()方法,前者使用数组缓冲区,后者则操作元素数组缓冲区

这两个函数第一个参数都表示要绘制图形的常量:

​ gl.POINTS:将每个顶点当成一个点来绘制

​ gl.LINES:将数组作为一系列顶点,在这些顶点间绘制直线,每个顶点是起点也是终点,因此数组中的顶点必须是偶数个才能开始绘制

​ gl.LINE_LOOP:将数组作为一系列顶点,从第一个顶点链接第二个顶点,依次链接直到最后一个顶点链接第一个顶点

​ gl.LINE_STRIP:和上一个常量差不多,只是不会从最后一个顶点链接第一个顶点

​ gl.TRIANGLES:将数组作为一系列顶点,在这些顶点间绘制三角形(不特殊指定的话,每个三角形都分开绘制,不共享顶点)

​ gl.TRIANGLES_STRIP:与上一个常量相似,只不过前三个顶点之后的顶点会作为第三个顶点和前两个顶点组成三角形

​ gl.TRIANGLES_FAN:与上一个常量相似,只不过前三个顶点后的顶点会作为第三个顶点与前面的顶点和第一个顶点构成三角形

gl.drawArrays()第二个参数是数组缓冲区的起点索引,第三个参数是数组缓冲区包含的顶点集合数量

9、纹理

WebGL纹理可以使用DOM中的图片,使用gl.createTexture()创建新的纹理,然后再将图片绑定到这个纹理;图片加载后才能使用纹理

gl.pixelStorei()设置了像素储存格式

图片纹理必须跟当前页面同源

例子查看红宝书p578

10、读取像素

可以通过readPixels()方法与OpenGL中方法有相同的参数,只不过最后一个参数必须是定型数组

该方法参数:x、y坐标、宽度、高度、图像格式、类型、定型数组,前四个参数用于指定要读取像素位置,图像格式基本总是gl.RGBA,类型参数指的是要存储在定型数组中的数据类型(gl.UNSIGNED_BYTE——Uint8Array,gl.UNSIGNED_SHORT_5_6_5/gl.UNSIGNED_SHORT_4_4_4_4/gl.UNSIGNED_SHORT_5_5_5_1——Uint16Array)

例子查看红宝书p578

WebGL1与WebGL2

WebGL1代码几乎与WebGL2完全兼容

相关对比查看红宝书p579

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Samuel_luo。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值