0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

OpenHarmony轻松玩转GIF数据渲染

OpenAtom OpenHarmony 来源:未知 2022-10-18 22:15 次阅读

点击蓝字 ╳ 关注我们

开源项目 OpenHarmony是每个人的 OpenHarmony 9da11e1c-4eee-11ed-a3b6-dac502259ad0.png

周黎生

OpenHarmony知识体系工作组

以下内容来自嘉宾分享,不代表开放原子开源基金会观点 OpenAtom OpenHarmony(以下简称“OpenHarmony”)提供了Image组件支持GIF动图的播放,但是缺乏扩展能力,不支持播放控制等。今天介绍一款三方库——ohos-gif-drawable三方组件,带大家一起玩转GIF的数据渲染,搞定GIF动图的各种需求。

效果演示

9dc7e150-4eee-11ed-a3b6-dac502259ad0.gif  9ec28592-4eee-11ed-a3b6-dac502259ad0.png   本文将从5个小节来带领大家使用ohos-gif-drawable这一款三方库,其中1、2、3这3个小节,主要介绍了ohos-gif-drawable的核心能力、GIF软解码和GIF绘制。4和5小节主要是扩展讨论,如何添加滤镜效果和软解码遇到的耗时问题。  9ee3461a-4eee-11ed-a3b6-dac502259ad0.png  

1.GIF的文件格式理论基础

工欲善其事必先利其器。首先我们需要为自己打下理论基础。了解GIF的数据格式,为后续解码GIF提供理论支持。 9efd9376-4eee-11ed-a3b6-dac502259ad0.png   通过学习GIF的文件格式,我们对于GIF的组成格式有了一定的了解,并且有助于理解后面GIF的解码。 在开始介绍之前,我想让大家了解一下整体的结构思路如下图:  9f396cca-4eee-11ed-a3b6-dac502259ad0.jpg   其中gifuct-js三方库主要完成了解码的工作。 ohos-gif-drawable三方库则是在gifuct-js的三方库之上,进行了封装。并结合了OpenHarmony的Canvas绘制能力,达到了播放和控制GIF的能力。  

2.GIF软解码:gifuct-js三方库介绍

GIF解码我们使用了gifuct-js这个库,它是一个纯JavaScript的GIF解码库。首先我们需要了解基础用法。 2.1 参考样例将一个文件ArrayBuffer转换为GIF解码后的帧数据数组。
//javascript
var gif = parseGIF(arraybuffer)
varframes=decompressFrames(gif,true)
2.2 由于OpenHarmony的Image生成PixelMap需要的数据是BGRA数据,而2.1生成的frames所有数组中的patch字段则是RGBA数据,所以我们需要使用
//javascript
var gif = parseGIF(arraybuffer)
varframes=decompressFrames(gif,false)
然后将frame目前还未生成的patch字段数据,通过generatePatch 函数,将RGBA的数据更换为BGRA即可,如下代码所示:
//javascript
const generatePatch = image => {
  const totalPixels = image.pixels.length
  const patchData = new Uint8ClampedArray(totalPixels * 4)
  for (var i = 0; i < totalPixels; i++) {
    const pos = i * 4
    const colorIndex = image.pixels[i]
    const color = image.colorTable[colorIndex] || [0, 0, 0]
    patchData[pos] = color[2] // B
    patchData[pos + 1] = color[1]// G
    patchData[pos + 2] = color[0] // R
    patchData[pos + 3] = colorIndex !== image.transparentIndex ? 255 : 0//A
  }
  return patchData
}
generatePatch函数,在这里会根据颜色表colorTable和基于颜色表的图像数据pixels以及透明度transparentIndex生成BGRA格式的patchData,这个数据和Canvas中getImageData获取的ImageData数据是一致的,都是Uint8ClampedArray类型,可以直接使用putImageData让canvas绘制 最后,生成的patchData赋值给Frame的patch字段。 这里我们并没有直接使用Canvas的putImageData直接绘制。为了提升扩展性,我们使用了Image的能力来生成PixelMap,这样处理为后续滤镜效果提供了可能,也方便后续绘制流程。 好了,到这里我们就基本上把gifuct-js库的基础使用简单介绍完了。 如何使用GIF:ohos-gif-drawable三方库的介绍 我们先来看看整个ohos-gif-drawable组件的模型图,通过模型图,我们可以看到,用户只要关注GIFComponent组件,和GIFComponent.ControllerOptions配置参数以及控制参数autoPlay和resetGif即可,非常简单! 9f50ce6a-4eee-11ed-a3b6-dac502259ad0.jpg  1. 支持的功能列表如下 ● 支持播放GIF图片。 ● 支持控制GIF播放/暂停。 ● 支持重置GIF播放动画。 ● 支持调节GIF播放速率。 ● 支持监听GIF所有帧显示完成后的回调。 ● 支持设置显示大小。 ● 支持7种不同的展示类型。 ● 支持设置显示区域背景颜色。 2. 如何使用ohos-gif-drawable 首先需要使用npm下载ohos-gif-drawable三方库
npminstall@ohos/ohos-gif-drawable--save
接下来我们需要配置一个worker给gifuct-js解码使用 配置worker,在应用工程的entry/src/main/ets/pages目录下新建workers文件夹,并且创建文件 gifParseWorker.ts ,文件内容如下:
import arkWorker from '@ohos.worker';
import { handler } from '@ohos/ohos-gif-drawable/src/main/ets/components/gif/worker/GifWorker'
//handler封装了子线程逻辑,但worker目前只能在entry中进行创建arkWorker.parentPort.onmessage=handler;
然后在entry目录的build-profile.json5文件中,添加如下内容:
"buildOption": {  
"sourceOption": {    
"workers": [     
       "./src/main/ets/pages/workers/gifParseWorker.ts"
]  
}
},
到这里我们worker就配置好了。 下面就到了正式使用环节,我们只要在UI界面需要的地方写上自定义控件GIFComponent, 然后传入GIFComponent.ControllerOptions,gifAutoPlay,gifReset这三个参数就能控制gif动画。
import { GIFComponent, ResourceLoader } from '@ohos/ohos-gif-drawable'
// gif绘制组件用户属性设置
@State model:GIFComponent.ControllerOptions = new GIFComponent.ControllerOptions();
// 是否自动播放
@State gifAutoPlay:boolean = true;
// 重置GIF播放,每次取反都能生效
@State gifReset:boolean = true;
// 在ARKUI的其他容器组件中添加该组件
GIFComponent({model:$model,autoPlay:$gifAutoPlay,resetGif:this.gifReset})
举个简单的例子说明一下
// 创建worker 
let worker = new ArkWorker.Worker('entry/ets/pages/workers/gifParseWorker.ts', {type: 'classic',name: 'loadUrlByWorker'})
// 关闭动画      
this.gifAutoPlay = false;
// 销毁上一次资源
this.model.destroy();
// 新创建一个modelx,用于配置用户参数
let modelx = new GIFComponent.ControllerOptions()
modelx  
// 配置回调动画结束监听,和耗时监听    
.setLoopFinish((loopTime) => {   
this.gifLoopCount++;   
this.loopHint = '当前gif循环了' + this.gifLoopCount + '次,耗时=' + loopTime + 'ms'   
})  
// 设置组件大小    
.setSize({ width: this.compWidth, height: this.compHeight })  
// 设置图像和组件的适配类型  
.setScaleType(this.scaleType)  
// 设置播放速率  
.setSpeedFactor(this.speedFactor)  
// 设置背景  
.setBackgroundColor(Color.Grey)
// 加载网络图片,getContext(this)中的this指向page页面或者组件都可以ResourceLoader.downloadDataWithContext(getContext(this), {   url: 'https://pic.ibaotu.com/gif/18/17/16/51u888piCtqj.gif!fwpaa70/fw/700'   }, (sucBuffer) => {    
// 网络资源sucBuffer返回后处理   
modelx.loadBuffer(sucBuffer, () => {      console.log('网络加载解析成功回调绘制!')    
// 开启自动播放      
this.gifAutoPlay = true;    
// 给组件数据赋新的用户配置参数,达到后续gif动画效果      
this.model = modelx;   }, worker)}, (err) => {   
// 用户根据返回的错误信息,进行业务处理(展示一张失败占位图、再次加载一次、加载其他图片等)
})
这里ResourceLoader内置了加载网络资源GIF,本地工程资源GIF和本地路径资源GIF文件数据的能力。 如果你已经有了GIF文件的arraybuffer数据,也可以直接调用modelx.loadBuffer(buffer: ArrayBuffer, readyRender: (err?) => void, worker: any)进行GIF播放。 甚至你已经生成了GIF解析数据,比如调用了2.2中的解码代码,那么你也可以直接调用modelx.setFrames(images?: GIFFrame[])来进行gif播放 1.控制GIF的播放与暂停:
this.gifAutoPlay = true 开启动画
this.gifAutoPlay=false暂停动画
组件内部会监听该参数的变化,用户只要改变值即可达到控制效果 2.重置GIF的播放
this.gifReset=!this.gifReset 每次变化都会重置gif播放。
由于重置不需要状态管理,所以组件内监听到数据变化就会重置gif播放 3.设置GIF动画播放速度
let modelx = new GIFComponent.ControllerOptions()
modelx.setSpeedFactor(2)//将速率提升到2倍
调用setSpeedFactor(speed: number)即可调整播放速度speed 为对比原始速率的乘积因子,比如设置0.5即为原始速率的0.5倍,设置为2即为原始速率的2倍 4.监听GIF动画播放回调(比如第一次动画结束)和获取动画实际播放总时长
let modelx = new GIFComponent.ControllerOptions()
modelx.setLoopFinish((loopTime?) => {
// loopTime为GIF动画一周期耗时,回调时间为GIF动画一周期结束时间节点
})
调用setLoopFinish(fn: (loopTime?) => void)可以通过回调得到GIF动画运行一周期耗时和一周期结束时间节点 5.显示GIF任意一帧
let modelx = new GIFComponent.ControllerOptions()
modelx.setSeekTo(5)//直接展示该gif第5帧图像
调用setSeekTo(gifPosition: number)可以直接展示该gif的某一帧图像 到这里ohos-gif-drawable三方库的主要能力都介绍完了,是不是很简单呢! 6.适配组件的大小
letmodelx=newGIFComponent.ControllerOptions()
modelx.setScaleType(ScaleType.FIT_CENTER) // 将图像缩放适配组件大小 调用setScaleType(scaletype: ScaleType)可以将图像和组件大小进行适配。 目前支持的类型如下图所示:GIFComponent.ScaleType 9f7d7884-4eee-11ed-a3b6-dac502259ad0.png  

为什么要配置worker

在具体实践过程中我们会发现,当我们按下解码按钮的时候,主界面会有一点卡顿的情况。特别是大的GIF文件进行解码的时候效果更明显。这是因为我们在主线程中进行了CPU的密集型计算,这是一个耗时且占用CPU的操作。主线程中是不能执行耗时操作的。但是JavaScript只有一个线程啊?那么解码这一块操作该如何处理会比较好呢?带着疑惑,我去查阅了资料发现JavaScript虽然属于单线程环境。但是通过引入Worker的能力,引入子线程worker,可以实现JavaScript的“多线程”技术。

OpenHarmony如何在子线程中处理耗时任务

为了争取良好的用户体验,我们需要将耗时操作封装至子线程中。 这里简单描述一下worker的能力: 能够让主页面运行的JavaScript线程中加载运行另外单独的一个或者多个JavaScript线程,但是它的多线程编程能力区别于传统意义上的多线程编程。主线程和Worker线程之间,不会共享任何作用域和资源,他们的通信方式是基于事件监听机制的 message。 接下来我们参考OpenHarmony文档下的worker能力 1.OpenHarmony环境下Worker的API接口列表 2.Worker的使用简单案例 经过了解之后,我们可以把解码的耗时封装到worker中处理,避免主线程耗时操作占用CPU导致卡顿问题。提升用户体验。 这也是使用ohos-gif-drawable三方库需要配置worker的原因。

扩展部分

GIF的滤镜效果 1. 灰白滤镜
//javascript
// 重点代码更改  
  let avg = (color[0] + color[1] + color[2]) / 3
  patchData[pos] = avg;
  patchData[pos + 1] = avg;
  patchData[pos + 2] = avg;
patchData[pos+3]=colorIndex!==image.transparentIndex?255:0;
2. 反转滤镜
//javascript
// 重点代码更改
  patchData[pos] = 255 - color[0];
  patchData[pos + 1] = 255 - color[1];
  patchData[pos + 2] = 255 - color[2];
patchData[pos+3]=colorIndex!==image.transparentIndex?255:0;
3. 高级滤镜效果 假设我们这边已经拿到了patch: Uint8ClampedArray像素数据,这里我需要先将其变换为一张PixelMap数据,参考GIFComponent中patch数据转换为PixelMap的代码。
//typescript
import image from "@ohos.multimedia.image"
let colorBuffer = patch.buffer
let pixelmap = await image.createPixelMap(colorBuffer, {
  'size': {
    'height': frame.dims.height as number,
    'width': frame.dims.width as number
  }
})
4. 高斯模糊 然后对PixelMap像素数据进行高斯模糊, 调用 `blur(pixelmap,10,true, (outPixelMap)=>{ // 模糊后的pixelmap数据})`在回调中获取模糊后的pixelmap。以下是模糊处理的算法
export async function blur(bitmap: any, radius: number, canReuseInBitmap: boolean, func: AsyncTransform) {
  if (radius < 1) {
    func("error,radius must be greater than 1 ", null);
    return;
  }


  let imageInfo = await bitmap.getImageInfo();
  let size = {
    width: imageInfo.size.width,
    height: imageInfo.size.height
  }


  if (!size) {
    func(new Error("fastBlur The image size does not exist."), null)
    return;
  }


  let w = size.width;
  let h = size.height;
  var pixEntry: Array = new Array()
  var pix: Array = new Array()




  let bufferData = new ArrayBuffer(bitmap.getPixelBytesNumber());
  await bitmap.readPixelsToBuffer(bufferData);
  let dataArray = new Uint8Array(bufferData);


  for (let index = 0; index < dataArray.length; index+=4) {
    const r = dataArray[index];
    const g = dataArray[index+1];
    const b = dataArray[index+2];
    const f = dataArray[index+3];


    let entry = new PixelEntry();
    entry.a = 0;
    entry.b = b;
    entry.g = g;
    entry.r = r;
    entry.f = f;
    entry.pixel = ColorUtils.rgb(entry.r, entry.g, entry.b);
    pixEntry.push(entry);
    pix.push(ColorUtils.rgb(entry.r, entry.g, entry.b));
  }


  let wm = w - 1;
  let hm = h - 1;
  let wh = w * h;
  let div = radius + radius + 1;


  let r = CalculatePixelUtils.createIntArray(wh);
  let g = CalculatePixelUtils.createIntArray(wh);
  let b = CalculatePixelUtils.createIntArray(wh);


  let rsum, gsum, bsum, x, y, i, p, yp, yi, yw: number;
  let vmin = CalculatePixelUtils.createIntArray(Math.max(w, h));


  let divsum = (div + 1) >> 1;
  divsum *= divsum;
  let dv = CalculatePixelUtils.createIntArray(256 * divsum);
  for (i = 0; i < 256 * divsum; i++) {
    dv[i] = (i / divsum);
  }


  yw = yi = 0;
  let stack = CalculatePixelUtils.createInt2DArray(div, 3);
  let stackpointer, stackstart, rbs, routsum, goutsum, boutsum, rinsum, ginsum, binsum: number;
  let sir: Array;
  let r1 = radius + 1;
  for (y = 0; y < h; y++) {
    rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
    for (i = -radius; i <= radius; i++) {
      p = pix[yi + Math.min(wm, Math.max(i, 0))];
      sir = stack[i + radius];
      sir[0] = (p & 0xff0000) >> 16;
      sir[1] = (p & 0x00ff00) >> 8;
      sir[2] = (p & 0x0000ff);
      rbs = r1 - Math.abs(i);
      rsum += sir[0] * rbs;
      gsum += sir[1] * rbs;
      bsum += sir[2] * rbs;
      if (i > 0) {
        rinsum += sir[0];
        ginsum += sir[1];
        binsum += sir[2];
      } else {
        routsum += sir[0];
        goutsum += sir[1];
        boutsum += sir[2];
      }
    }
    stackpointer = radius;


    for (x = 0; x < w; x++) {


      r[yi] = dv[rsum];
      g[yi] = dv[gsum];
      b[yi] = dv[bsum];


      rsum -= routsum;
      gsum -= goutsum;
      bsum -= boutsum;


      stackstart = stackpointer - radius + div;
      sir = stack[stackstart % div];


      routsum -= sir[0];
      goutsum -= sir[1];
      boutsum -= sir[2];


      if (y == 0) {
        vmin[x] = Math.min(x + radius + 1, wm);
      }
      p = pix[yw + vmin[x]];


      sir[0] = (p & 0xff0000) >> 16;
      sir[1] = (p & 0x00ff00) >> 8;
      sir[2] = (p & 0x0000ff);


      rinsum += sir[0];
      ginsum += sir[1];
      binsum += sir[2];


      rsum += rinsum;
      gsum += ginsum;
      bsum += binsum;


      stackpointer = (stackpointer + 1) % div;
      sir = stack[(stackpointer) % div];


      routsum += sir[0];
      goutsum += sir[1];
      boutsum += sir[2];


      rinsum -= sir[0];
      ginsum -= sir[1];
      binsum -= sir[2];


      yi++;
    }
    yw += w;
  }
  for (x = 0; x < w; x++) {
    rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
    yp = -radius * w;
    for (i = -radius; i <= radius; i++) {
      yi = Math.max(0, yp) + x;


      sir = stack[i + radius];


      sir[0] = r[yi];
      sir[1] = g[yi];
      sir[2] = b[yi];


      rbs = r1 - Math.abs(i);


      rsum += r[yi] * rbs;
      gsum += g[yi] * rbs;
      bsum += b[yi] * rbs;


      if (i > 0) {
        rinsum += sir[0];
        ginsum += sir[1];
        binsum += sir[2];
      } else {
        routsum += sir[0];
        goutsum += sir[1];
        boutsum += sir[2];
      }


      if (i < hm) {
        yp += w;
      }
    }
    yi = x;
    stackpointer = radius;
    for (y = 0; y < h; y++) {
      // Preserve alpha channel: ( 0xff000000 & pix[yi] )
      pix[yi] = (0xff000000 & pix[Math.round(yi)]) | (dv[Math.round(rsum)] << 16) | (dv[
      Math.round(gsum)] << 8) | dv[Math.round(bsum)];


      rsum -= routsum;
      gsum -= goutsum;
      bsum -= boutsum;


      stackstart = stackpointer - radius + div;
      sir = stack[stackstart % div];


      routsum -= sir[0];
      goutsum -= sir[1];
      boutsum -= sir[2];


      if (x == 0) {
        vmin[y] = Math.min(y + r1, hm) * w;
      }
      p = x + vmin[y];


      sir[0] = r[p];
      sir[1] = g[p];
      sir[2] = b[p];


      rinsum += sir[0];
      ginsum += sir[1];
      binsum += sir[2];


      rsum += rinsum;
      gsum += ginsum;
      bsum += binsum;


      stackpointer = (stackpointer + 1) % div;
      sir = stack[stackpointer];


      routsum += sir[0];
      goutsum += sir[1];
      boutsum += sir[2];


      rinsum -= sir[0];
      ginsum -= sir[1];
      binsum -= sir[2];


      yi += w;
    }
  }


  let bufferNewData = new ArrayBuffer(bitmap.getPixelBytesNumber());
  let dataNewArray = new Uint8Array(bufferNewData);
  let index = 0;


  for (let i = 0; i < dataNewArray.length; i += 4) {
    dataNewArray[i] = ColorUtils.red(pix[index]);
    dataNewArray[i+1] = ColorUtils.green(pix[index]);
    dataNewArray[i+2] = ColorUtils.blue(pix[index]);
    dataNewArray[i+3] = pixEntry[index].f;
    index++;
  }
  await bitmap.writeBufferToPixels(bufferNewData);
  if (func) {
    func("success", bitmap);
  }
}
如果需要高级滤镜效果可以参考ImageKnife组件的transform部分,这里仅仅展示模糊效果。 由于滤镜效果目前ohos-gif-drawable三方库并没有开发接口提供出来,所以开发者可以根据实际需求重写自定义组件GIFComponent.,只需要在生成PixelMap的代码片段中加入滤镜代码,即可利用滤镜效果开发更多精彩的应用。

参考资料

1.《GIF文件格式解析》

https://segmentfault.com/a/1190000022866045

2.GIF解码库gifuct-js

https://github.com/matt-way/gifuct-js

3.GIF解码库底层逻辑jsBinarySchemaParser

https://github.com/matt-way/jsBinarySchemaParser

4.高级滤镜算法借鉴

https://gitee.com/openharmony-tpc/ImageKnife/tree/master/imageknife/src/main/ets/components/imageknife/transform

5.OpenHarmony环境下Worker的API接口列表

https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/apis/js-apis-worker.md

6.Worker的使用简单案例

https://gitee.com/wang_zhaoyong/js_worker_module/wikis/Worker%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8

7.Web Worker API参考

https://developer.mozilla.org/zh-CN/docs/Web/API/Worker

8.OpenHarmony的Canvas文档

https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/arkui-ts/ts-components-canvas-canvas.md

9.OpenHarmony的CanvasRenderingContext2D对象文档

https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/arkui-ts/ts-canvasrenderingcontext2d.md


原文标题:OpenHarmony轻松玩转GIF数据渲染

文章出处:【微信公众号:OpenAtom OpenHarmony】欢迎添加关注!文章转载请注明出处。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • OpenHarmony
    +关注

    关注

    23

    文章

    3259

    浏览量

    15159

原文标题:OpenHarmony轻松玩转GIF数据渲染

文章出处:【微信号:gh_e4f28cfa3159,微信公众号:OpenAtom OpenHarmony】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    OpenHarmony Sheet 表格渲染引擎

    基于 Canvas 实现的高性能 Excel 表格引擎组件 [OpenHarmonySheet]。 由于大部分前端项目渲染层是使用框架根据排版模型树结构逐层渲染的,整棵渲染树也是与排版
    发表于 01-05 16:32

    轻松玩转AVR C语言 资料很好

    轻松玩转AVR资料很好
    发表于 07-26 22:44

    第13章 GIF图片显示

    Service开发出了GIF文件格式(图形交换格式)。 它设计用于跨数据网络传输图像。GIF标准支持隔行扫描、透明、应用定义数据、动画以及原始文本
    发表于 10-13 08:18

    如何轻松玩转adc?

    如何轻松玩转adc?
    发表于 01-21 06:28

    OpenHarmony小型系统有什么方案可以加载gif的图片?

    OpenHarmony 小型系统中提供基础组件image,但是只能加载png或者jpg类型的图片,有什么方案可以加载gif的图片?
    发表于 04-19 10:13

    OpenHarmony轻松玩转GIF数据渲染

    OpenHarmony轻松玩转GIF数据渲染OpenAtom
    发表于 10-20 10:57

    OpenHarmony 3.2 Beta Audio——音频渲染

    └── audio_server.cpp三、音频渲染总体流程四、Native接口使用在OpenAtom OpenHarmony(以下简称“OpenHarmony”)系统中,音频模块提供了功能测试代码,本文
    发表于 03-02 14:28

    GIF Decoder

    GIF Decoder GIF Decoder GIF Decoder GIF Decoder
    发表于 05-24 10:53 2次下载

    轻松玩转STM32Cube资料包下载(2)

    轻松玩转STM32Cube资料包下载(2)
    发表于 09-28 09:41 0次下载

    轻松玩转STM32Cube资料包下载(下)

    轻松玩转STM32Cube资料包下载(下)
    发表于 09-28 09:46 0次下载

    轻松玩转AVR单片机C语言》1.pfd

    轻松玩转AVR单片机C语言》1.pfd
    发表于 09-30 16:47 0次下载

    轻松玩转AVR单片机C语言》2.pfd

    轻松玩转AVR单片机C语言》2.pfd
    发表于 09-30 16:49 0次下载

    轻松玩转AVR单片机C语言》3.pfd

    轻松玩转AVR单片机C语言》3.pfd
    发表于 09-30 16:51 0次下载

    轻松玩转AVR单片机C语言》4.pfd

    轻松玩转AVR单片机C语言》4.pfd
    发表于 09-30 16:53 0次下载

    轻松玩转AVR单片机C语言》5.pfd

    轻松玩转AVR单片机C语言》5.pfd
    发表于 09-30 16:54 0次下载