• 博文
  • 归档
  • 前端基础滤镜

    2021-08-23

    次访问

    前言

    前文带大家看过3D LUT滤镜如何实现影片级别的滤镜效果,这种滤镜最为强大,可以实现任何滤镜效果。

    通过photoshop导出处理文件,即可通过程序实现对应的滤镜效果,而且由于是颜色查找,即颜色A,通过LUT文件,直接映射成颜色B,所以处理过程只是像素点的颜色映射,不存在计算,速度非常快。

    然后也带大家看过如何操纵图片的像素,通过canvas获取页面上图片的像素,然后对像素点直接进行计算,再输出新的颜色,就能得到处理后的图片。

    现在,带大家看看第二种方式的滤镜,到底可以实现哪些效果。由于这种方式是对像素点进行运算,而这些算法有限,所以能实现的效果也是有限的。

    filter

    初始化页面

    首先,我们初始化一个简单的页面,提供一个上传图片按钮,一个canvas显示图片,并用css控制下图片显示的大小,还有一个点击使用滤镜的按钮。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <!DOCTYPE html>
    <html>
    <head>
    <title>基础滤镜</title>
    <style type="text/css">
    canvas {
    width: 300px;
    }
    </style>
    </head>

    <body>
    <input type="file" id="fileInput" name="选择图片" />
    <div class="wrap-image">
    <canvas id="imageUpload"></canvas>
    </div>
    <div onclick="useFilter()">点击使用滤镜</div>
    <script type="text/javascript"></script>
    </body>
    </html>

    封装图片处理

    应用滤镜,我们会重复使用getImageData、putImageData和遍历像素点的逻辑,这些我们都可以封装起来。

    下面我们创建一个CanvasImage类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class CanvasImage {
    constructor(img, context) {
    this.image = img;
    this.context = context;
    }

    getData() {
    return this.context.getImageData(0, 0, this.image.width, this.image.height);
    }
    setData(data) {
    this.context.putImageData(data, 0, 0);
    }
    transform() {
    // TODO 后文再完善
    }
    }

    然后我们在上传图片时创建一个CanvasImage的实例,接下来我们看看上传图片。

    上传图片

    用户点击选择图片按钮后,选择了一张图片。
    我们通过new Image()创建一个img,并将src设置为用户选择的本地图片的临时地址,在图片加载完成后,给页面中的canvas设置宽高,然后drawImage把图片显示到canvas上,最后new CanvasImage(img, context)来创建一个CanvasImage的实例filter。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    let filter
    const fileInput = document.getElementById('fileInput');

    fileInput.addEventListener('change', (e) => {
    const imgElement = document.getElementById('imageUpload');
    const img = new Image();
    img.src = URL.createObjectURL(e.target.files[0]);
    img.onload = function () {
    imgElement.width = img.width
    imgElement.height = img.height
    const context = imgElement.getContext("2d");
    context.drawImage(img, 0, 0);
    filter = new CanvasImage(img, context);
    }
    }, false);

    封装滤镜处理函数

    现在我们来完善上面空着的transform:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    transform(fn, factor) {
    const imageData = this.getData()
    const { data } = imageData
    for (let i = 0; i < data.length; i += 4) {
    const [r, g, b, a] = fn.call(this, data[i], data[i + 1], data[i + 2], data[i + 3], factor, i);
    data[i] = r;
    data[i + 1] = g;
    data[i + 2] = b;
    data[i + 3] = a;
    }
    this.setData(imageData)
    }

    transform接受2个参数,fn是滤镜算法函数,这个函数输入像素相关数据,输出r,g,b,a。factor代表滤镜算法需要的控制因子。

    这样我们就只需要关注滤镜算法函数fn怎么写,然后执行filter.transform(fn, factor)就可以对图片做处理,并把处理后的图片数据更新到页面。

    接下来我们看看几个简单的滤镜算法。

    滤镜算法

    灰度

    比如我们先实现一个灰度算法,取r、g、b的加权平均值:

    1
    2
    3
    4
    function Greyscale(r, g, b) {
    const avg = 0.3 * r + 0.59 * g + 0.11 * b;
    return [avg, avg, avg, 255];
    }

    filter_Greyscale

    然后在点击使用滤镜绑定的函数useFilter中可以这样写:

    1
    2
    3
    function useFilter() {
    filter.transform(Greyscale)
    }

    怀旧

    1
    2
    3
    4
    function Sepia(r, g, b) {
    const avg = 0.3 * r + 0.59 * g + 0.11 * b;
    return [avg + 100, avg + 50, avg, 255];
    }

    filter_Sepia

    负片

    1
    2
    3
    function invert(r, g, b) {
    return [255 - r, 255 - g, 255 - b, 255];
    }

    filter_invert

    噪点

    1
    2
    3
    4
    function noise(r, g, b, a, factor) {
    var rand = (0.5 - Math.random()) * factor;
    return [r + rand, g + rand, b + rand, 255];
    }

    调用时需传入噪点控制因子,比如传45:

    1
    2
    3
    function useFilter() {
    filter.transform(noise, 45)
    }

    下图是原图、噪点传45和100的对比:

    filter_noise

    亮度

    1
    2
    3
    function brightness(r, g, b, a, factor) {
    return [r + factor, g + factor, b + factor, 255];
    }

    调用时,控制因子factor是亮度。
    下图是原图、亮度传45和100的对比:

    filter_brightness

    饱和度

    1
    2
    3
    4
    5
    6
    7
    8
    function saturation(r, g, b, a, factor) {
    // factor取值[-1, 1]
    const max = Math.max(r, g, b);
    r += max !== r ? (max - r) * (-factor) : 0;
    g += max !== g ? (max - g) * (-factor) : 0;
    b += max !== b ? (max - b) * (-factor) : 0;
    return [r, g, b, 255];
    }

    下图是饱和度传-0.5,原图,饱和度传0.5的对比:

    filter_saturation

    对比度

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function contrast(r, g, b, a, factor) {
    // factor取值[-1, 1]
    const contrast = Math.floor(factor * 255);
    const contrastF = 259 * (contrast + 255) / (255 * (259 - contrast));
    r = contrastF * (r - 128) + 128;
    g = contrastF * (g - 128) + 128;
    b = contrastF * (b - 128) + 128;
    return [r, g, b, 255];
    }

    下图是对比度传-0.2,原图,对比度传0.2的对比:
    filter_contrast

    色温

    1
    2
    3
    4
    5
    6
    7
    function temperature(r, g, b, a, factor) {
    // factor取值[-1, 1]
    const temperature = Math.round(factor * 255)
    r = Math.min(Math.max(r + temperature, 0), 255)
    b = Math.min(Math.max(b - temperature, 0), 255)
    return [r, g, b, 255];
    }

    下图是色温传-0.2,原图,色温传0.2的对比:
    filter_temperature

    总结

    上面写了这些示例,现在轮到你发挥想象力了,只需要在滤镜函数中编写你的颜色处理程序:

    1
    2
    3
    4
    function noise(r, g, b, a, factor) {
    // 在这里发挥你的想象力
    return [r, g, b, a];
    }

    比如,调换r、g、b的顺序,或者把b都变成255……然后实现别的滤镜效果,比如色调、模糊等等,而且这些基础滤镜效果还可以叠加,从而创建出更多的效果哦。

    ...more
  • 尤雨溪带你实现一个mini vue

    2021-08-20

    次访问

    mini_vue_1

    前言

    本篇文章主要是解析尤雨溪在codepen上实现的一段mini-vue(solution)代码。

    前面的文章,我们学习了怎么把数据变成响应式,怎么用虚拟dom局部刷新页面,现在我们在这两者的基础上,看怎么实现一个mini vue。

    代码解析

    先把所有的代码折叠起来看一下:

    mini_vue_2

    可以看到,在响应式、虚拟dom的实现上,和前文一模一样,只是在这两个功能上,增加了1个createApp函数,那么接下来我们主要看看createApp函数干了什么。

    首先我们初始化了一个叫Component的常量,这个常量内部有data和render函数,data返回了{ count: 0 },render返回一个虚拟dom,渲染这个虚拟dom就会向页面中添加一个div,div中显示count的值,并绑定了点击事件,点击div就让count自增1。(虚拟dom渲染不清楚的话,可以看看上文)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const Component = {
    data() {
    return {
    count: 0
    }
    },
    render() {
    return h(
    'div',
    {
    onClick: () => {
    this.count++
    }
    },
    String(this.count)
    )
    }
    }

    然后我们来看看createApp函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    function createApp(Component, container) {
    // implement this
    const state = reactive(Component.data())
    let isMount = true
    let prevTree
    watchEffect(() => {
    const tree = Component.render.call(state)
    if (isMount) {
    mount(tree, container)
    isMount = false
    } else {
    patch(prevTree, tree)
    }
    prevTree = tree
    })
    }

    // calling this should actually mount the component.
    createApp(Component, document.getElementBy('app'))

    进入函数后,首先将Component.data()即{ count: 0 }变成响应式后赋值给state,然后申明两个变量isMount和prevTree,然后调用watchEffect:

    1
    2
    3
    4
    5
    function watchEffect(effect) {
    activeEffect = effect
    effect()
    activeEffect = null
    }

    那么就执行了effect函数,申明并初始化tree常量,Component.render.call(state)的意思是执行Component中的render函数,并把render函数内的this指向state。

    初次createApp时,isMount是true,所以接下来就直接渲染tree,并将isMount变成false,后面count数据变化时,触发effect函数再次走到if (isMount)这个判断时,isMount值就都是false,从而patch(prevTree, tree),用新的tree对前一个tree打补丁,即局部刷新页面。

    每次渲染或局部刷新完页面,就把当前的tree赋值给prevTree。

    现在一个mini vue就实现了,点击页面上的0,0就会变成1,每次点击页面上的数字,数字就自增1。
    (响应式和虚拟节点相关代码这里就不贴了,有需要去前文看,或者直接去codepen)

    总结

    简单来说,之前实现的数据响应式,在使用时,只是在数据变化时把数据打印出来:

    1
    2
    3
    watchEffect(() => {
    console.log(state.count)
    })

    现在只是把watchEffect的传参effect的功能改一下,从简单的打印,改成渲染或局部更新页面,并且运用虚拟dom提高页面性能。从而实现数据变化时,页面自动局部刷新,也就是数据驱动视图。

    ...more
  • 尤雨溪带你实现虚拟节点

    2021-08-20

    次访问

    Virtual_DOM_1

    前言

    本篇文章主要是解析尤雨溪在codepen上实现的一段vdom代码。这段代码实现了一个简易的Virtual DOM。

    Virtual DOM的作用是减少浏览器对页面的重绘,从而让用户体验更好,通过js把页面渲染相关的东西存起来,在数据更新的时候,不直接用新数据来重绘整个页面,而是先对比数据差异,在数据变化的地方局部渲染。接下来,我们就看看尤雨溪会怎么实现一个简易版本的Virtual DOM,从而让大家更好的理解Virtual DOM算法

    代码解析
    核心代码就两个函数,mount和patch,mount的字面意思是安装、镶嵌,嵌入,这里我们可以理解成渲染,patch的字面意思是补丁,这里我们可以理解成用新数据对页面打补丁,即局部更新页面。

    首先我们看看mount函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    function h(tag, props, children) {
    return { tag, props, children };
    }

    function mount(vnode, container, anchor) {
    const el = document.createElement(vnode.tag);
    vnode.el = el;
    // props
    if (vnode.props) {
    for (const key in vnode.props) {
    el.setAttribute(key, vnode.props[key]);
    }
    }
    if (vnode.children) {
    if (typeof vnode.children === "string") {
    el.textContent = vnode.children;
    } else {
    vnode.children.forEach(child => {
    mount(child, el);
    });
    }
    }
    if (anchor) {
    container.insertBefore(el, anchor)
    } else {
    container.appendChild(el);
    }
    }

    const tree1 = h("div", { class: "red" }, [
    h("span", null, "hello"),
    h("span", null, "world")
    ]);

    mount(tree1, document.querySelector("#app"));

    上面这段代码,在页面中的#app元素中添加了tree1:

    Virtual_DOM_2

    mount函数接受3个参数:vnode虚拟节点,container容器,anchor锚。
    执行mount(tree1, document.querySelector(“#app”));,即渲染节点树tree1,tree1通过h函数包装后,返回的是一个对象,有tag, props, children3个属性。

    在mount中,首先tree1的tag属性做处理,用createElement创建节点,然后把这个创建的节点el赋值给vnode的el属性。
    然后对tree1的props属性做处理,如果props存在,就遍历props的key,通过setAttribute把props设置到el上。
    然后对tree1的children属性做处理,如果children存在,就看看children的类型,是字符串的话,就把值赋值给el.textContent,否则就遍历children,对每个遍历到的值child执行mount(child, el)。所以对于children,最后都会走到类型是字符串那一步,从而更新到el的textContent中。
    最后对anchor做处理,anchor的字面意思是锚,所以这里用来控制要渲染的节点树的位置。如果anchor存在,就在已有的节点anchor前面插入el节点,否则就把el节点添加到container中。

    简单来说,mount函数根据虚拟节点来操作HTML页面,实现页面的更新。
    有props时将props中的属性加到标签上;有children时,是字符串的children就直接把值写入父元素,否则就对children中的元素迭代mount函数;有anchor时就把创建的el加到anchor前面,否则就默认加到父元素container中。

    接下来我们看看patch函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    function patch(n1, n2) {
    // Implement this
    // 1. check if n1 and n2 are of the same type
    if (n1.tag !== n2.tag) {
    // 2. if not, replace
    const parent = n1.el.parentNode
    const anchor = n1.el.nextSibling
    parent.removeChild(n1.el)
    mount(n2, parent, anchor)
    return
    }

    const el = n2.el = n1.el

    // 3. if yes
    // 3.1 diff props
    const oldProps = n1.props || {}
    const newProps = n2.props || {}
    for (const key in newProps) {
    const newValue = newProps[key]
    const oldValue = oldProps[key]
    if (newValue !== oldValue) {
    if (newValue != null) {
    el.setAttribute(key, newValue)
    } else {
    el.removeAttribute(key)
    }
    }
    }
    for (const key in oldProps) {
    if (!(key in newProps)) {
    el.removeAttribute(key)
    }
    }
    // 3.2 diff children
    const oc = n1.children
    const nc = n2.children
    if (typeof nc === 'string') {
    if (nc !== oc) {
    el.textContent = nc
    }
    } else if (Array.isArray(nc)) {
    if (Array.isArray(oc)) {
    // array diff
    const commonLength = Math.min(oc.length, nc.length)
    for (let i = 0; i < commonLength; i++) {
    patch(oc[i], nc[i])
    }
    if (nc.length > oc.length) {
    nc.slice(oc.length).forEach(c => mount(c, el))
    } else if (oc.length > nc.length) {
    oc.slice(nc.length).forEach(c => {
    el.removeChild(c.el)
    })
    }
    } else {
    el.innerHTML = ''
    nc.forEach(c => mount(c, el))
    }
    }
    }

    const tree2 = h("div", { class: "green" }, [
    h("span", null, "this has "),
    h("span", null, "changed")
    ]);

    patch(tree1, tree2);

    patch函数接受2个虚拟节点的参数。

    首先判断老节点和新节点的标签是否相同,不同的话就直接用新节点替换掉老节点,也不用继续往下走了。比如老节点是div,新节点是span,那么这2个节点就完全不一样,就需要直接替换掉整个节点。
    替换也很简单,获取老节点的父元素parent和紧跟的节点anchor,然后删除老节点,然后把新节点渲染到anchor前面。

    如果类型一样,比如都是div,那么接着找差异。
    先初始化el,老节点的el就是在mount渲染是赋值的tag,因为类型一样,所以我们将这个值也赋值给新节点。

    接下来寻找props的差异,因为props是设置在el上的,所以我们由外而内逐步寻找差异。
    首先初始化新修props,取虚拟节点传入的props,没传就是{}。
    然后遍历新props中的key,通过key取新就props对于的value,一旦新旧value不同,就要做处理,新value存在就setAttribute,不存在就removeAttribute。比如这里遍历到的key是class,那么newValue就是green,而oldValue是red,所以将class设置成green,如果我们的tree2没有传{ class: “green” },那newValue就不存在,那就会移除el上的class属性。

    newProps遍历完了,这一步将newProps中不为空的属性设置到了el上。
    那有的属性oldProps有,而newProps没有呢?
    所以接下来就是遍历oldProps,如果oldProps中的key,在newProps中没有,就移除这个属性。

    处理完props的差异,接下来处理children的差异。

    首先把新旧节点的children分别赋值给oc和nc,代表oldChildren和newChildren。

    如果nc是字符串,还跟oc不一样,就把nc直接赋值给el.textContent。
    否则,看nc是不是数组,是的话,就继续往下看。
    如果oc不是数组,那就说明nc和oc开始变得不一样了,这时直接将el中的内容清空,遍历nc数组,对nc中的内容逐个mount渲染到页面。
    oc也是数组的话,就需要继续寻找差异,这时我们先去oc和nc数组长度中较小的值,在这个长度内,逐个迭代patch函数来打补丁,最后迭代到nc是字符串,直接更新el.textContent。这样就处理完了oc和nc长度相同的部分,比如oc长3,nc长5,那么oc和nc的前3个就处理完了,接下来就处理剩下的2个。
    如果nc比oc长,我们只需要把剩下的2个渲染;
    如果nc比oc短,比如nc长1,oc长7,我们把nc和oc的第一个处理了,oc还剩后6个没处理,这时我们只需要截取oc的后6个,然后逐个删除。

    到这里,我们的children也找出了差异,并把差异部分更新到页面,实现了局部更新。

    总结

    虚拟节点的原理是不是很简单?我们不谈高大上的diff算法,Virtual DOM,局部更新等名词,就只是简单的实现一个通过js来局部刷新dom的功能,看起来就简单多了。

    Virtual_DOM_3

    ...more
  • 尤雨溪教你写进阶版响应式

    2021-08-19

    次访问

    Reactivity_defineProperty

    前言

    上一篇,我们解析了尤雨溪的初级版响应式代码,一定觉得初级版的响应式和我们平常用的vue3相差太远了,定义一个响应式对象,竟然还要手写get和set,而我们日常使用vue3来定义一个响应式对象只需要用ref或者reactive包起来。

    那么,这一篇我们看看尤雨溪对初级版做了哪些升级。

    封装reactive

    上一版中,定义响应式变量需手写get和set:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const state = {
    get count() {
    dep.depend()
    return actualCount
    },
    set count(newCount) {
    actualCount = newCount
    dep.notify()
    }
    }

    那么这一版首先就将这一步封装起来。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    function reactive(raw) {
    // use Object.defineProperty
    // 1. iterate over the existing keys
    Object.keys(raw).forEach(key => {
    // 2. for each key: create a corresponding dep
    const dep = new Dep()

    // 3. rewrite the property into getter/setter
    let realValue = raw[key]
    Object.defineProperty(raw, key, {
    get() {
    // 4. call dep methods inside getter/setter
    dep.depend()
    return realValue
    },
    set(newValue) {
    realValue = newValue
    dep.notify()
    }
    })
    })
    return raw
    }

    封装后,定义响应式变量只需要:

    1
    2
    3
    const state = reactive({
    count: 0
    })

    现在看来,是不是和vue3的写法一样了呢?

    接下来我们解读下这个优化。

    首先翻译几个名词,为什么想翻译一下呢?因为我觉得尤雨溪写的代码命名非常好,一段代码读下来,就像看小说,即使不懂编程,也会大概知道是做什么。

    reactive: 反应的
    raw: 未加工的、不成熟的

    这两个变量名取的好呀,一眼看过去,函数reactive接受一个未加工的变量raw,然后返回了raw。咱们不看内容,就可以猜到reactive函数做了什么。

    下面我们来看看reactive函数的内容(代码中的英文注释也是尤雨溪写的哦)。

    传进来的raw对象,我们对key进行遍历,取得key对应的value值后,立即重写raw。

    这里用到Object.defineProperty(obj, prop, descriptor),这个方法会直接在一个对象obj上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。其中obj是要定义属性的对象,prop是要定义或修改的属性的key,descriptor是更新或定义的属性的描述。

    所以这里重写时,给key值对应的value绑定了get和set函数,实现了上一版手动绑定的过程。

    然后优化watchEffect,执行effect后立刻将activeEffect置空,防止不必要的订阅行为。

    1
    2
    3
    4
    5
    function watchEffect(effect) {
    activeEffect = effect
    effect()
    activeEffect = null
    }

    再次进阶

    上面的优化比较小,只是简化了响应式变量的定义,接下来我们看看尤雨溪还能做什么优化。

    首先,重写了Dep类。
    之前的Dep类比较简单,只有一个发布订阅模式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let activeEffect
    class Dep {
    subscribers = new Set()
    depend() {
    if (activeEffect) {
    this.subscribers.add(activeEffect)
    }
    }
    notify() {
    this.subscribers.forEach(effect => effect())
    }
    }

    优化后:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    let activeEffect
    class Dep {
    // imeplement this
    subscribers = new Set()

    constructor(value) {
    this._value = value
    }

    get value() {
    this.depend()
    return this._value
    }

    set value(value) {
    this._value = value
    this.notify()
    }

    depend() {
    if (activeEffect) {
    this.subscribers.add(activeEffect)
    }
    }

    notify() {
    this.subscribers.forEach((effect) => {
    effect()
    })
    }
    }

    多了一个叫_value的私有变量,并且这个_value是响应式的。然后写了一个reactiveHandlers函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // proxy version
    const reactiveHandlers = {
    get(target, key) {
    // how do we get the dep for this key?
    const value = getDep(target, key).value
    if (value && typeof value === 'object') {
    return reactive(value)
    } else {
    return value
    }
    },
    set(target, key, value) {
    getDep(target, key).value = value
    }
    }

    接着是getDep函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const targetToHashMap = new WeakMap()

    function getDep(target, key) {
    let depMap = targetToHashMap.get(target)
    if (!depMap) {
    depMap = new Map()
    targetToHashMap.set(target, depMap)
    }

    let dep = depMap.get(key)
    if (!dep) {
    dep = new Dep(target[key])
    depMap.set(key, dep)
    }

    return dep
    }

    最后就是用Proxy替代defineProperty重写reactive函数:

    1
    2
    3
    function reactive(obj) {
    return new Proxy(obj, reactiveHandlers)
    }

    这里补充下Proxy知识点:

    Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

    语法是const p = new Proxy(target, handler),
    target是要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
    handler通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const handler = {
    get: function(obj, prop) {
    return prop in obj ? obj[prop] : 37;
    }
    };

    const p = new Proxy({ a: 1 }, handler);

    console.log(p.a, p.b); // 1, 37

    补充完Proxy知识点,我们再去看reactive。

    我们用reactiveHandlers代理obj,读写obj时会进入reactiveHandlers,读的时候,通过getDep获取value值,如果value是object类型,就将value变成响应式,然后返回value;写的时候就把新值写到getDep获取的value上。

    在getDep函数中,用到了Map和WeakMap。
    Map对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者原始值) 都可以作为一个键或一个值。
    WeakMap对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

    getDep中,首先去targetToHashMap中获取target,并赋值给depMap,没有获取到的话,就将target作为键,new Map()生成的depMap作为值,存到targetToHashMap中。

    然后在depMap中取key的值,取不到的话,就new Dep(target[key])作为value和key一起,组成键值对,加到depMap中。最后返回dep。

    使用还是一样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const state = reactive({
    count: 0
    })

    watchEffect(() => {
    console.log(state.count)
    })

    state.count++

    在上面的代码中加上一些打印,使代码流程看得更加清楚:

    reactivity_proxy_code

    可见getDep最后返回了一个Dep的实例。

    还有什么不清楚的地方,自行断点调试。

    结语

    所以,看到这里,你明白Proxy比起defineProperty,在实现vue的响应式时,有什么好处吗?

    后期后空,我们继续学习尤雨溪会怎么实现一个mini vue。

    ...more
  • 尤雨溪教你写初级版响应式

    2021-08-18

    次访问

    Reactivity_Dep

    本篇文章主要是解析尤雨溪在codepen上实现的一段Reactivity(Dep)代码,我们拆成2部分来看。

    Part 1 实现响应式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    let activeEffect

    class Dep {
    subscribers = new Set()
    depend() {
    if (activeEffect) {
    this.subscribers.add(activeEffect)
    }
    }
    notify() {
    this.subscribers.forEach(effect => effect())
    }
    }

    function watchEffect(effect) {
    activeEffect = effect
    effect()
    }

    首先,定义了一个全局变量activeEffect。

    然后写了一个Dep类,它有一个subscribers属性,depend和notify方法。
    subscribers是Set对象,Set对象允许存储任何类型的唯一值,无论是原始值或者是对象引用。

    Set

    调用depend时,如果activeEffect存在,就将其加到subscribers中。
    调用notify时,遍历subscribers,执行遍历到的对象。

    watchEffect函数接收一个effect参数,调用时将effect参数赋值给全局变量activeEffect,然后执行effect。

    从上面的解析可以看到,代码实现的非常简陋,没有做类型判断,所以调用时注意传参的类型,watchEffect传参须是函数。

    现在功能实现好了,怎么用呢?我们继续往下看。

    Part 2 使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // usage -----------------------

    const dep = new Dep()

    let actualCount = 0
    const state = {
    get count() {
    dep.depend()
    return actualCount
    },
    set count(newCount) {
    actualCount = newCount
    dep.notify()
    }
    }

    watchEffect(() => {
    console.log(state.count)
    }) // 0

    state.count++ // 1

    我们在适当的地方加上打印,那么程序的走向将更加明了:

    watchEffect

    上面的打印能看懂,就不用看下面这些了。

    首先创建一个Dep对象类型的实例dep。

    然后创建一个actualCount变量,并初始化,值为0。

    然后创建一个state变量,state变量内部使用get和set语法。
    其中get语法和set语法分别将对象属性和一个函数绑定,在获取或设置该属性时,触发绑定的函数执行。
    上面创建并初始化state的代码意思是获取state.count时,执行get后面的函数,调用dep.depend,然后返回actualCount。
    设置state.count时,比如state.count = 1,就执行set后面的函数,把1赋值给actualCount,然后调用dep.notify。

    然后调用watchEffect函数,传入的参数是匿名函数() => { console.log(state.count) }。
    watchEffect函数中,将传入的匿名函数赋值给全局变量activeEffect,然后执行这个匿名函数,于是打印state.count的值,而打印state.count的值,就需要获取state.count,就会触发get绑定的函数,于是执行dep.depend,由于此时的activeEffect值是匿名函数,所以将该匿名函数加到subscribers中,然后返回actualCount,所以得到的值是0,打印了0。

    最后state.count++,使state.count自增1,这里先获取再自增,所以会触发get和set,由于全局变量activeEffect一直存在,所以get时会将activeEffect加入subscribers,而subscribers内的值不会重复,所以一直就只有一个值,那就是最开始加入的匿名函数。
    触发set时就执行和set绑定的函数,把1赋值给actualCount,然后执行dep.notify,notify中遍历subscribers,遍历到了之前加入的匿名函数,就执行了该匿名函数,于是打印了1。

    总结

    首先我们翻译几个名词。

    • subscribers: 订阅者,用户
    • depend: 依赖
    • notify: 通知,公布
    • actual: 目前的
    • effect: 达到目的

    从这些名词中,你是不是已经想到了一些javascript的设计模式?这里就不做讨论,感兴趣的话自行学习。

    所以,上面的代码,翻译成白话,调用watchEffect来执行一个函数,这个函数获取了一个值count,于是把这个函数加到监听者列表,当count变化时,就会遍历监听者列表,于是遍历到刚刚加入的函数,于是执行了它,于是就获取到了当前的count值(actualCount)。

    打个比方,同学小明,查询(watchEffect)了一下iphone12(state)的价格(count),获取价格时系统就把小明加入订阅者,在价格变化时,通知小明同学最新的价格。

    这一期的代码比较简陋,但是循序渐进,由简入深,下期我们继续讲解进阶版。

    ...more
  • 前端如何在像素级别操纵图片

    2021-08-12

    次访问

    前言

    前端也可以做图像处理?
    是的,可以。
    今天我们要介绍一种在前端处理图像的方式。

    对于一般的图片,我们用专业的软件(比如PhotoShop、mark man等等)放大到最后,就会看到很多小格子,这一个一个的格子,就是像素,每个格子,即每个像素,都代表不同的颜色。
    而颜色都可以用rgba的形式表示,比如白色rgba(255, 255, 255, 1),其中r代表红色,g代表绿色,b代表蓝色,a代表透明度,rgb代表的色值取值范围是[0, 255],a代表的透明度取值范围是[0, 1]。

    px

    获取像素数据

    下面将带领大家一步一步来看怎么获取像素数据。

    首先,我们创建一个页面。提供选择图片功能。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <!DOCTYPE html>
    <html>
    <head>
    <title>test</title>
    <style type="text/css">
    canvas {
    width: 300px;
    }
    </style>
    </head>

    <body>
    <input type="file" id="fileInput" name="选择图片"/>
    <div class="wrap-image">
    <canvas id="canvas"></canvas>
    </div>
    <script type="text/javascript">

    </script>
    </body>
    </html>

    选择图片后,需要将图片显示到canvas中,我们在上面的script标签中加入下面的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const fileInput = document.getElementById('fileInput');
    const canvas = document.getElementById('canvas');
    let context
    fileInput.addEventListener('change', (e) => {
    let img = new Image
    img.src = URL.createObjectURL(e.target.files[0]);
    img.onload = function(){
    canvas.width = img.width
    canvas.height = img.height
    context = canvas.getContext("2d");
    context.drawImage(img, 0, 0);
    }
    }, false);

    showImage

    现在图片显示出来了,那要怎么获取图片像素数据呢?这里我们用到canvas的getImageData对象,他的用法如下:

    1
    let myImageData = context.getImageData(left, top, width, height);

    那么在本例中,我们在drawImage后可以接着getImageData,在img.onload中继续加入下面的代码:

    1
    let imageData = context.getImageData(0, 0, img.width, img.height);

    将imageData打印出来,可以看到:

    imageData

    现在,我们就获取到了图片的像素数据,它在imageData对象的data中。

    读取像素点

    上面我们获取到了像素数据,但要怎么读取像素点呢?
    首先,我们来看看imageData对象的data对象是什么。

    data是Uint8ClampedArray类型的一维数组,包含着RGBA格式的整型数据。每个部份被分配到一个在数组内连续的索引,左上角像素的红色部份在数组的索引0位置。像素从左到右被处理,然后往下。data包含width × height × 4 bytes数据,索引值从0到(width × height × 4) - 1。

    例如,要读取图片中位于第50行,第200列的像素的蓝色部份,你会写以下代码:

    1
    let b = imageData.data[((50 * (img.width * 4)) + (200 * 4)) + 2];

    那么根据行(row)、列(col)读取一个像素点的r/g/b/a值的公式是:

    1
    2
    3
    4
    5
    let rIndex = row * (img.width * 4) + col * 4 // data中r值的索引
    let r = imageData.data[rIndex];
    let g = imageData.data[(rIndex + 1];
    let b = imageData.data[(rIndex + 2];
    let a = imageData.data[(rIndex + 3];

    操作像素

    上面我们获取到了图片数据,并读取到了像素点,能读取像素点,是不是也可以设置像素点呢?
    是的,下面我们将用canvas的putImageData来更新像素。
    这里我们将实现一个invert的效果,在获取imageData之后调用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function invert(imageData) {
    const { data } = imageData
    for (let i = 0; i < data.length; i += 4) {
    data[i] = 255 - data[i]; // red
    data[i + 1] = 255 - data[i + 1]; // green
    data[i + 2] = 255 - data[i + 2]; // blue
    }
    context.putImageData(imageData, 0, 0);
    }

    invertImage

    总结
    1、前端的canvas不仅可以显示图片,还可以获取到图片数据。
    2、数据中有图片的宽和高,还有像素信息,它对应着图片从左到右,再从上到下的像素点的rgba值。
    3、我们可以读到某行某列的像素数据,同样的我们也可以写。
    4、对不同的颜色通道及透明度做一系列运算,就可以得到颜色变化了的新图片。
    5、改变图片的颜色,主要应用在图片滤镜上,前端的基础滤镜,都是这个原理。

    ...more
  • 前端如何通过LUT实现图片滤镜

    2021-08-09

    次访问

    前言

    说到图片滤镜,相信大家都不陌生,比如什么晨光效果、小清新效果,但大家都清楚滤镜效果是怎么实现的吗?比如下面两个好看的滤镜效果:

    film-emulation-lut-21613370085
    lut-film-emulation-271613371435

    在前端,一般的图像处理库,都是基于算法来实现的,获取图片的像素,解析成R、G、B,然后对这3个颜色一通操作,比如R都变成255,G都减25…,或者复杂点,进行个卷积运算,比如fabric.js的滤镜效果。
    但是这些算法实现的滤镜效果不仅数量少,效果也不够丰富,只能实现些简单的效果,比如反色、灰阶、怀旧等等。如果设计师在PS中对图片进行了风格调色,比如相机校正、曲线、色彩分割、HSL调整等等项目,在程序上我们又要如何实现呢?
    本文要介绍的图片滤镜实现方式就可以解决上面的问题,只要是设计师实现的滤镜效果,我们都可以实现,而且,还有很多免费的滤镜效果可以使用,不一定需要设计师输出,滤镜效果好,并且处理速度快,这种方式就是3D LUT(look up table),即3D颜色查找表。

    准备工作

    3D LUT资源文件

    设计师输出.CUBE文件,或者网上找免费资源。
    图片的风格处理大都比较复杂,对设计师来说,Photoshop中内建了几个影片的3D LUT电影调色档,也可以用外部导入的电影调色档,所以我们也可以直接用设计师们用到的外部电影调色档,资源的格式一般是.CUBE文件。

    3D LUT资源文件格式处理

    如果前端直接使用.CUBE文件,不仅文件体积大,而且需要做文件解析,但是如果把.CUBE文件转成png,不仅体积小很多,也不用解析文本,直接解析png图片中的像素即可,这里推荐一个cube转png小工具,处理后的png一般是下面这样,宽高尺寸是512x512。

    Going-for-a-walk

    前端实现基于3D LUT的滤镜

    下面我们写一个简单的页面,实现用户上传原图和lut图,就可以得到处理后的图,并且可以点击下载处理后的图。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    <!DOCTYPE html>
    <html>
    <head>
    <title>LUT</title>
    <style type="text/css">
    canvas {
    width: 300px;
    }
    </style>
    </head>

    <body>
    <a href="#" id="downloadButton">下载</a>
    <input type="file" id="fileInput" name="选择图片"/>
    <input type="file" id="lutInput"/>
    <div class="wrap-image">
    <canvas id="imageUpload"></canvas>
    <canvas id="lutUpload"></canvas>
    <canvas id="canvasOutput"></canvas>
    </div>
    <script type="text/javascript">
    var imgElement = document.getElementById('imageUpload');
    var lutElement = document.getElementById('lutUpload');
    var outputElement = document.getElementById('canvasOutput');

    var fileInput = document.getElementById('fileInput');
    var lutInput = document.getElementById('lutInput');

    fileInput.addEventListener('change', (e) => {
    var img = new Image
    img.src = URL.createObjectURL(e.target.files[0]);
    img.onload = function(){
    imgElement.width = img.width
    imgElement.height = img.height
    outputElement.width = img.width
    outputElement.height = img.height
    var context = imgElement.getContext("2d");
    context.drawImage(img, 0, 0);
    }
    }, false);
    lutInput.addEventListener('change', (e) => {
    var img = new Image
    img.src = URL.createObjectURL(e.target.files[0]);
    img.onload = function(){
    lutElement.width = img.width
    lutElement.height = img.height
    var context = lutElement.getContext("2d");
    context.drawImage(img, 0, 0);
    applyLUT("canvasOutput");
    }
    }, false);
    function applyLUT(resultID) {
    var imageContext = imgElement.getContext("2d");
    var lutContext = lutElement.getContext("2d");
    var imageData = imageContext.getImageData(0, 0, imgElement.width, imgElement.height);
    var lutData = lutContext.getImageData(0, 0, lutElement.width, lutElement.height);
    for (var i = 0; i < imageData.data.length; i += 4) {
    var r = Math.floor(imageData.data[i] / 4);
    var g = Math.floor(imageData.data[i + 1] / 4);
    var b = Math.floor(imageData.data[i + 2] / 4);
    var lutX = (b % 8) * 64 + r;
    var lutY = Math.floor(b / 8) * 64 + g;
    var lutIndex = (lutY * lutElement.width + lutX) * 4;
    imageData.data[i] = lutData.data[lutIndex];
    imageData.data[i + 1] = lutData.data[lutIndex + 1];;
    imageData.data[i + 2] = lutData.data[lutIndex + 2];;
    }
    document.getElementById(resultID).getContext("2d").putImageData(imageData, 0, 0);
    };
    document.getElementById('downloadButton').onclick = function() {
    this.href = document.getElementById('canvasOutput').toDataURL();
    this.download = 'image.png';
    };
    </script>
    </body>
    </html>

    效果如下,左边是原图,中间是3D LUT文件,右边是处理后的图,效果不错吧!

    goingforawalk

    怎么样,效果是不是特别好?而且3D LUT不仅可以处理图片,还可以处理视频哦。

    参考

    • A function that helps to apply LUT to image. Make sure to change the canvas IDs or to create temporary canvases
    • Playing with JavaScript, photos and 3D LUTS (lookup tables)
    • free-luts
    • film-emulation-luts
    ...more
  • 前端如何使用openCV

    2021-08-05

    次访问

    简介

    OpenCV(Open Source Computer Vision Library),是一个在图像处理和识别上很强大的库,最开始只有C++版本,但现在构建了各种不同语言的版本,比如Python和Java,甚至是JavaScript,本文要介绍的就是JavaScript版本的OpenCV.js。

    如何获取OpenCV.js

    1、官方编译好的版本下载地址为:
    https://docs.opencv.org/_VERSION_/opencv.js
    其中 VERSION 换成你想要的版本。
    目前最新的為 4.5.3 版本,那么下载地址就是https://docs.opencv.org/4.5.3/opencv.js

    2、参照官网自行构建OpenCV.js

    实现一个简单的处理

    图片灰度处理效果:

    color_gray

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    <!DOCTYPE html>
    <html>
    <head>
    <title>OpenCV.js</title>
    <style type="text/css">
    .wrap-image {
    display: flex;
    flex-direction: row;
    margin-top: 10px;
    }
    .wrap-image img,
    .wrap-image canvas {
    width: 300px;
    margin-right: 10px;
    }
    </style>
    </head>

    <body>
    <h3 id="status">Loading the Opencv ...</h3>
    <input type="file" id="fileInput"/>
    <div class="wrap-image">
    <img id="imageUpload" alt="No Image" />
    <canvas id="canvasOutput"></canvas>
    </div>
    <script type="text/javascript">
    let imgElement = document.getElementById('imageUpload');
    let inputElement = document.getElementById('fileInput');

    inputElement.onchange = function() {
    imgElement.src = URL.createObjectURL(event.target.files[0]);
    }

    imgElement.onload =function() {
    let src = cv.imread('imageUpload');
    let dst = new cv.Mat();
    cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY, 0);
    cv.imshow('canvasOutput', dst);
    src.delete(); dst.delete();
    };
    function onOpenCvReady() {
    document.getElementById('status').remove();
    }
    </script>
    <script async src="js/opencv.js" onload="onOpenCvReady();" type="text/javascript"></script>
    </body>
    </html>

    代码解析:
    1、opencv.js很大,载入时需要加上async,并设定onload来检测是否载入完成。

    1
    <script async src="js/opencv.js" onload="onOpenCvReady();" type="text/javascript"></script>

    2、记得及时清理Mat对象,释放内存。

    1
    2
    src.delete();
    dst.delete();

    3、imread和imshow 必须传入 <img /> 或 <canvas /> 的 id 或是 DOM。

    下载图片

    图片处理好了,用户想要下载,那就再html中加上下载链接:

    1
    <a href="#" id="downloadButton">下载</a>

    然后把下面的JavaScript加到之前的script标签中:

    1
    2
    3
    4
    document.getElementById('downloadButton').onclick = function() {
    this.href = document.getElementById('canvasOutput').toDataURL();
    this.download = 'image.png';
    };

    总结
    一旦你习惯了将图像作为Mat对象来操作,你就可以做更多的事情了,你可以在OpenCV的网站上找到更多的教程,包括人脸识别和模板匹配等等。

    参考

    • 如何在 Nodejs 或前端使用 OpenCV(免安裝). 在本機使用 OpenCV 很簡單,在伺服器端使用 OpenCV… | by Up Chen | Medium
    • An Introduction to Computer Vision in JavaScript using OpenCV.js | DigitalOcean
    ...more
  • SVG Path实现tooltips

    2020-11-25

    次访问

    原文地址

    在地图指针之后,让我们来尝试下更多有趣的SVG Path形状:一个tooltip。点击查看例子

    svgTooltip

    tooltip形状的path路径是由5个变量决定的:width,height,pointer offset,corner radius和placement(left,top,right或bottom)。

    简单来说,我们先来完成一个没有圆角的向上的tooltip,我们需要这么做:
    1、移动到点A:M aX,aY
    2、连线到点B:L bX,bY
    3、垂直连线到点C:H cX
    4、水平连线到点D:V dY
    5、垂直连线到点E:H eX
    6、水平连线到点F:V fY
    7、垂直连线到点G:H gX
    8、连线到点A:L aX,aY
    9、结束路径:z

    点A的坐标是(0,0),其他点的坐标由width,height和offset计算出来:

    1
    2
    3
    4
    5
    6
    bx =  -offset
    by = -offset // cy,fy,gy也是一样的
    cx = -width / 2 // dx也一样
    dy = -offset - height // ey也一样
    ex = width / 2 // fx也一样
    gx = offset

    整个路径实现出来就会是这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function topTooltipPath(width, height, offset) {
    const left = -width / 2
    const right = width / 2
    const top = -offset - height
    const bottom = -offset
    return `M 0,0
    L ${-offset},${bottom}
    H ${left}
    V ${top}
    H ${right}
    V ${bottom}
    H ${offset}
    L 0,0 z`
    }

    上面的代码解释下就是:画笔移动到点M,再画直线到点B,再水平画直线到点C,再垂直画直线到点D,再水平画直线到点E,再垂直画直线到点F,再水平画直线到点G,再画直线到点M,并结束。 需要注意的是坐标系是左上角为原点,x轴正方向朝右,y轴正方向朝下。

    实现圆角,我们有2个方案:弧(Arc)和二次贝塞尔曲线(Quadratic Bezier curve)。
    弧需要使用7个变量,但是二次贝塞尔曲线就要简单得多:它在当前路径上取得起点,另外2个点是顶点(vX,vY)和终点(tX,tY)。在SVG中,二次贝塞尔曲线的写法是Q vX,vY tX,tY。

    拿圆角C来举例,设圆角半径是r,圆角的起点在B到C的水平直线上,,故起点坐标是(cX+r, cY),终点在出圆角去点D的垂直直线上,故终点坐标是(cX,cY-r),顶点就是点C(cX,cY),把上面例子中的直角变成圆角,我们只需要把H cX V dY换成H cX+r Q cX, cY cX, (cY - r) V dY,这样就得到了半径是r的圆角C,下面是优化后的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function topTooltipPath(width, height, offset, radius) {
    const left = -width / 2
    const right = width / 2
    const top = -offset - height
    const bottom = -offset
    return `M 0,0
    L ${-offset},${bottom}
    H ${left + radius}
    Q ${left},${bottom} ${left},${bottom - radius}
    V ${top + radius}
    Q ${left},${top} ${left + radius},${top}
    H ${right - radius}
    Q ${right},${top} ${right},${top + radius}
    V ${bottom - radius}
    Q ${right},${bottom} ${right - radius},${bottom}
    H ${offset}
    L 0,0 z`
    }

    获得圆角的向下的tooltip,我们只需要把路径中的Y坐标全部取反:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function bottomTooltipPath(width, height, offset, radius) {
    const left = -width / 2
    const right = width / 2
    const bottom = offset + height
    const top = offset
    return `M 0,0
    L ${-offset},${top}
    H ${left + radius}
    Q ${left},${top} ${left},${top + radius}
    V ${bottom - radius}
    Q ${left},${bottom} ${left + radius},${bottom}
    H ${right - radius}
    Q ${right},${bottom} ${right},${bottom - radius}
    V ${top + radius}
    Q ${right},${top} ${right - radius},${top}
    H ${offset}
    L 0,0 z`
    }

    同样的可以得到朝左和朝右的tooltip:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    function leftTooltipPath(width, height, offset, radius) {
    const left = -offset - width
    const right = -offset
    const top = -height / 2
    const bottom = height / 2
    return `M 0,0
    L ${right},${-offset}
    V ${top + radius}
    Q ${right},${top} ${right - radius},${top}
    H ${left + radius}
    Q ${left},${top} ${left},${top + radius}
    V ${bottom - radius}
    Q ${left},${bottom} ${left + radius},${bottom}
    H ${right - radius}
    Q ${right},${bottom} ${right},${bottom - radius}
    V ${offset}
    L 0,0 z`
    }
    function rightTooltipPath(width, height, offset, radius) {
    const left = offset
    const right = offset + width
    const top = -height / 2
    const bottom = height / 2
    return `M 0,0
    L ${left},${-offset}
    V ${top + radius}
    Q ${left},${top} ${left + radius},${top}
    H ${right - radius}
    Q ${right},${top} ${right},${top + radius}
    V ${bottom - radius}
    Q ${right},${bottom} ${right - radius},${bottom}
    H ${left + radius}
    Q ${left},${bottom} ${left},${bottom - radius}
    V ${offset}
    L 0,0 z`
    }

    参考

    • SVG Paths | MDN
    ...more
  • lottie-web应用优化之Base64

    2019-04-28

    次访问

    前文的优化中,主要把设计输出的多张图片合并成1张雪碧图,减小了图片请求次数和资源大小。
    每次新的动效出来,还需要开发介入处理。

    本文的优化主要解决2个问题:
    1、资源文件多,不方便发布。
    前文优化后,有3个文件,1个html,1个json,1个png的雪碧图。
    本文优化后,只有1个html文件。

    2、需要开发介入。
    前文优化后,需要开发处理雪碧图生成、json数据改造、雪碧图压缩等等。
    本文优化后,不需要开发介入。设计用AE导出资源后,通过前端提供的工具,自动生成html代码,这个html文件直接发布即可。

    优化方案:通过Electron,开发出一个桌面应用,这个应用将自动处理所有需要开发介入的事情。

    Electron

    Electron 是一个使用 JavaScript, HTML 和 CSS 等 Web 技术创建原生程序的框架。
    【打造你的第一个 Electron 应用】一文可以让你快速上手Electron。

    动效生成器

    界面

    启动Electron应用,会打开一个界面,本文叫做HTML动效生成器,界面中收集3个信息:
    1、AE导数的demo文件夹;
    2、动效尺寸宽
    3、动效尺寸高

    比如demo文件在桌面,那么在demo文件夹路径的输入框中直接填入地址:C:\Users\win\Desktop\demo
    因为默认导出的数据尺寸宽高是100%,大小不固定,所以还需收集尺寸信息。动效宽高填设计元素的宽高即可(单位px)。

    electron

    生成逻辑

    逻辑写在renderer.js中。
    设计输出资源的目录demo文件夹的结构是这样的:
    .
    ├── images
    │ ├── img_0.png
    │ ├── img_1.png
    │ ├── img_2.png
    │ ├── …
    ├── demo.html
    ├── lottie.js
    └── data.json

    点击生成资源按钮时,
    1、获取images文件夹下的图片资源。主要用到node文件系统fs的readdirSync。
    2、将图片转成Base64。主要用到readFileSync来读取文件、Buffer.from将读取到的文件转成Base64。
    3、更改data.json中的assets字段,将代表图片相对路径的p字段置空,将代表图片的u字段改成Base64。这样png图片文件就Base64编码整个进data.json文件了。更改字段主要是先readFileSync读取utf8格式文件,然后将读取的数据转成json格式,重新将要更改的字段赋值,然后转成字符串writeFileSync写入data.json。
    然后点击创建按钮,
    1、重写index.html文件,把动效尺寸宽高重写,把data.json文件的数据整个进html。
    2、把重写后的index.html文件输出到demo文件夹下,这个index.html就是我们最终想要的文件。

    1、注意本地文件相对路径的写法,需要用path来拼接,如果直接写相对路径,在打包后相对路径发生变化会导致文件找不到

    2、重写,复制等文件操作其实主要就是node fs文件系统的读和写,必要的时候做一个格式转换。

    static/index.html是index.html的模版文件,主要是删除设计输出的demo.html文件的多余信息,lottie库通过script引入而不是直接在html中,animationData置空,生成的时候会重写。

    打包

    参考官网的应用程序打包,选择electron-packager来打包,打包后生成.exe可执行文件,双击即可启动该桌面应用。
    至此,自动生成代码的小工具就完成啦。

    总结

    数据对比:

    webp格式动图 HTML HTML格式雪碧图优化 HTML格式Base64优化
    总流量 2M 258.2k 139.3k 161k
    图片请求次数 1 18 1 0
    总请求次数 1 21 3 2
    ...more

分类归档

  • ai3
  • css2
  • doc2
  • git1
  • js15
  • node2
  • 动效6
  • 图像处理13

标签云

最近文章

  • js遍历那些事儿
  • 详解CSS Grid布局
  • Shutterstock Editor关于Canvas和WebGL图片滤镜的实现
  • javascript异步error
  • win10编译opencvjs
PREVNEXT

© 2018 - 2024 Lovelyun