如何画出一只皮卡丘(基于css)并用代码演示过程(完整思路版)

1,933 阅读11分钟

效果

静态皮卡丘效果:♥Pikachu (yuyuanw.github.io)

代码演示的效果:♥Pikachu代码展示版 (yuyuanw.github.io)

静态皮卡丘源码:看index2和css2就行了,1是我做失败的

动态皮卡丘源码:加了展示代码和倍速播放的功能面向对象实现

如何找到模仿对象?

首先找一个简单些的卡通人物,搜索到英文单词,例如:皮卡丘----> Pikachu 随后可以去浏览图片。找自己满意的简单一下的卡通图。 这里使用的是codepen平台,找到的一个适合模仿的网页。 Pikachu.png 我的目标就是做出一只卡哇伊的皮卡丘,如上图所示。

如何画出静态的皮卡丘?

css是层叠样式表,就是说我要用一个个盒子做出Pikachu的眼睛、鼻子、嘴巴、脸颊……
首先在,html中写入元素,一张皮skin,两只眼睛eye left/right,一个鼻子nose,一个嘴巴mouth,两个脸颊cheek left/right,做好布局后,接下来写css

<body>
    <div class="skin">
        <div class="eye left">
            <div class="eyeball left"></div>
        </div>
        <div class="eye right">
            <div class="eyeball right"></div>
        </div>
        <div class="nose"></div>
        <div class="mouth">
            <div class="lip left"></div>
            <div class="lip right"></div>
            <div class="lipdown"><div class="cop"></div></div>
            <div class="x1"></div>
            <div class="x2"></div>
        </div>
        <div class="cheek left"><img src="../img/pikachu-flash.gif"></div>
        <div class="cheek right"><img src="../img/pikachu-flash.gif"></div>
    </div>
</body>

css写的思路是:

首先给定盒模型和基础设定
*{
    box-sizing: border-box;
    margin: 0;
}
::after,::before{
    box-sizing: border-box;
    margin: 0;
}
然后来写黄黄的皮肤。

首先是给皮肤这个盒子设定大小,设置height和width,然后刷上颜色,background,这个我是用qq截图的识色复制的颜色。定位和布局,定位,因为是其他的元素都会在skin上,所以采用相对定位,relative,布局,一张皮有什么好布局的....

.skin{
    background: rgb(255,230,0);
    min-height: 100vh;
    width: 100vw;
    position: relative;
}
接下来画鼻子。

脸颊腮红和鼻子是相对而言简单一些的,但是因为要做定位,所以先画鼻子会好一些。
开始画鼻子咯。
我确定这个可以用一个盒子做出来。
首先给这个盒子一个大小,让它显示出来,设置width和heigth还有border,随后做定位,因为skin是relative,所以鼻子可以用absolute来定位,接下来给它放到中间去,居中显示,left和top的50%,然后负marigin实现整体居中。接下来就是变形了。变形考验操作一点。 鼻子的下面是一个三角形,上面是一个扇形,有点类似半圆。三角形可以用border-color显示1/4的颜色,其余颜色transparent透明,就可以显示一个三角了,但是需要border很宽。半圆border-radius来设置相邻的两条边。最后绘色,鼻子的颜色就是border-color未被设置成透明的那个颜色。

.nose{
    border: 20px solid red;
    width:40px;
    height: 40px;

    position: absolute;
    left:50%;
    top:50%;
    margin-left:-20px;
    margin-top: -20px;

    border-radius: 50% 50% 0 0 ;
    border-color: rgb(0,0,0) transparent transparent transparent;
}
画眼睛

两只眼睛除了位置不一样,其余的布局,大小,颜色,形状都是一样的,因此统一设置。 首先定位元素eye,然后给两个盒子一样的大小width和height,之后做定位,也是absolute,居中left,top50%,移动的话,再分开移动给位置。形状,都是圆,用border-radius来做,颜色,去色写成background就行了。

.eye{
    border:3px solid black;
    width: 84px;
    height: 84px;
    
    position: absolute;
    left:50%;
    top:50%;

    border-radius: 50%;
    background: rgb(46,46,46);
}

两只眼睛的位置,都是要往上移,一只往左移,一只往右移,用负margin来移动。 虽然transform也能移动,但我还是优先考虑margin,因为transfrom涉及定位。

.eye.left{
    margin-left: -160px;
    margin-top: -64px;
}

.eye.right{
    margin-left:76px;
    margin-top:-64px;
}

画完眼睛外部了,还有眼球没有画。首先是设置html,在eye left和eye right的div里面再写盒子 eyeball left和eyeball right。 两只眼球相对于外面的眼睛而言,设置都是一样的,无论是在眼睛的位置,还是大小,形状,颜色。 因此选定eyeball进行统一设置,先给盒子大小,width和height,border,然后是定位,它是eye中的元素,定位也不用写了,写位置吧,用margin给它移到左上角的位置,然后是形状,圆形,border-radius,最后是绘色,直接设置背景色background-color就行了。

.eyeball{
    border:3px solid black;
    width: 36px;
    height: 36px;

    margin-left: 14px;
    margin-top: 2px;

    border-radius: 50%;
    background-color: white;
}
画脸颊腮红

为什么画脸颊腮红,不先画嘴巴,因为嘴巴最复杂。。。 又是两个除了位置不一样其余都一样的盒子,对cheek left和cheek right 一起设置cheek。 首先是给两个盒子大小,width和height还有border,然后是定位,absolute,居中位置,left,top50%,然后是形状,border-radius设置两个圆,最后绘色,background-color写上取到的颜色。

.cheek{
    border:4px solid black;
    width:128px;
    height: 128px;

    position: absolute;
    left:50%;
    top:50%;

    border-radius: 50%;
    background-color: #FF0000;
}

接下来就是分别给left 和right 位置了。 和眼睛一样,用margin来做。

.cheek.left{
    margin-left: -250px;
    margin-top:64px;
}

.cheek.right{
    margin-left:120px;
    margin-top:64px;
}
最后,画嘴巴了。

嘴巴一看需要用五个盒子来画,总体的一个盒子mouth作为总的容器,然后上嘴唇用两个盒子,下嘴唇用一个盒子,舌头用一个盒子。先把html中的div盒子写了。

<div class="mouth">
        <div class="lip left"></div>
        <div class="lip right"></div>
        <div class="lipdown">
            <div class="cop"></div>
        </div>
</div>

先把容器mouth的位置放好。 给大小,居中显示,负margin,

.mouth{
  
    width:200px;
    height:250px;

    position: absolute;
    left:50%;
    margin-left: -100px;
    top:50%;
    margin-top: 16px;  

画上嘴唇,这个形状像是圆角矩形盒子截一部分。先把大小位置弄好,

.lip{
    border:4px solid  black;
    width:100px;
    height:30px;

    position: absolute;
    margin-left: 50px;
    margin-top: -6px;
    
    background-color: #FFE600;
}

然后就是嘴唇的形状了,

.lip.left{
    transform: translateX(-48px) rotate(-15deg);
    border-radius: 0px 0px 0px 100px;
    border-color: transparent transparent transparent black;
}

.lip.right{
    transform: translateX(48px) rotate(15deg);
    border-radius: 0px 0px 100px 0px;
    border-color: transparent black transparent transparent;
}

发现有瑕疵,上嘴唇的中间颜色显示有明显的bug,遮住!

<div class="mouth">
            <div class="lip left"></div>
            <div class="lip right"></div>
            <div class="lipdown"><div class="cop"></div></div>
            <div class="x1"></div>
            <div class="x2"></div>
        </div>

用x1遮住嘴唇拼接处的瑕疵用x2把外面细线的瑕疵遮住

.x1{
    width:8px;
    height:4px;
    background-color: black;

    border-radius: 50% 0 0  0;
    position: absolute;
    left:50%;
    margin-left: -5px;
    margin-top: 8px;

    z-index: 5;
}

.x2{
    width:180px;
    height:0px;
    margin-left: 8px;

    border:4px solid #FFE600;
    z-index: 8;
    position: absolute;

    background-color: #FFE600;

}

画下嘴唇 下嘴唇是一个很长很尖的圆角矩形截取一部分,并且外面那一部分要隐藏,overflow。

.mouth > .lipdown{
    border: 4px solid black;
    width: 150px;
    height: 400px;
    margin-left: 25px;

    position: absolute;
    bottom: 0;

    border-radius: 150px/400px;

    background-color: rgb(155,0,10);
    
}

隐藏的话,要在mouth元素的位置加上overflow: hidden;为了不让其他元素被遮住,skin,nose,x1都加上z-index: 5

画舌头

舌头像是一个小点的圆角矩形或者圆,截取上面一部分,

.lipdown > .cop{
    border:1px solid red;
    width:150px;
    height:180px;

    position: absolute;
    bottom:0px;
    margin-left: -4px;

    border-radius: 150px/180px;
    background-color: rgb(255,72,95);
}

超出的部分隐藏,在.mouth > .lipdown里面写overflow: hidden;

这样一只皮卡丘就画好了

pikachu.png

如何用代码演示过程?

1. 如何让屏幕一个一个动态地敲出字?

先让屏幕呈现出内容。

<div id = 'test1'></div>
test1.innerHTML = 'halo'

然后让屏幕呈现出我们想展示的内容。

const string = '我是Yuyuan,接下来我要展示我的作品了。'
test1.innerHTML = string

然后让屏幕一个字一个字呈现出我们想展示的内容。借助延时函数。

const string = '我是Yuyuan,接下来我要展示我的作品了。'
let n = 0

let step = ()=>{
    setInterval(()=>{ 
        if(n<string.length){
            test1.innerHTML = string[n]
            console.log(string[n])
            n=n+1
            step()
        }
        
    },1000)
}
step()

效果如下:

image.png

接下来,让屏幕展示的内容连贯起来,借助substring。

const string = '我是Yuyuan,接下来我要展示我的作品了。'
let n = 0

let step = ()=>{
    setInterval(()=>{ 
        if(n<string.length){
            test1.innerHTML = string.substring(0,n)
            console.log(string[n])
            n=n+1
            step()
        }
        
    },1000)
}
step()

image.png

这样我们就实现了第一个小目标:让字一个一个跳动入场屏幕。

这个要点就是,借助延时函数,并且,利用递归,在本函数中调用本函数,实现循环,并且可以通过条件判断来终结循环。

2.如何同时展示css和text的内容?

翻译一下的意思就是,我想要一边打字显示出我写代码的过程,一边还能画出皮卡丘。

如何让css生效?

当我将string的内容换成css呢?背景能不能让我写的样式生效?

const string = '我是Yuyuan,接下来我要展示我的作品了。<style>body{background:pink}</style>'
let n = 0

let step = ()=>{
    setInterval(()=>{ 
        if(n<string.length){
            test1.innerHTML = string.substring(0,n)
            console.log(string[n])
            n=n+1
            step()
        }
        
    },1000)
}
step()

image.png

它居然正确地识别了我写的废话和我要加的样式,这就很nice。也就是innerHTML可以识别样式。那我就用innerText来加文字,innerHTML来加样式画图了咯。

<div id="test1"></div>
<style id="test2"></style>
const string = '我是Yuyuan,接下来我要展示我的作品了。<style>body{background:pink}</style>'
let n = 0

let step = ()=>{
    setInterval(()=>{ 
        if(n<string.length){
            console.log(string.substring(0,n))
            test1.innerText = string.substring(0,n)
            test2.innerHTML = string.substring(0,n)
            n=n+1
            step()
        }
    },1000)
}

step()

image.png

3.如何一边打印css的内容,一边将皮卡丘在屏幕上画出来?

①打印css内容——模拟写代码过程
将原本的css内容放入string中就可以了
②画皮卡丘 css要生效,就要将HTML中的布局标签都写好。

<body>
    <div id="test1"></div>
    <style id="test2"></style>

    <div id="html">
        <div class="skin">
            <div class="eye left">
                <div class="eyeball left"></div>
            </div>
            <div class="eye right">
                <div class="eyeball right"></div>
            </div>
            <div class="nose"></div>
            <div class="mouth">
                <div class="lip left"></div>
                <div class="lip right"></div>
                <div class="lipdown"><div class="cop"></div></div>
                <div class="x1"></div>
                <div class="x2"></div>
            </div>
            <div class="cheek left"></div>
            <div class="cheek right"></div>
        </div>
    </div>
    <script src="yanshi.js"></script>
    
</body>

拷贝原先css的内容到string中

 string =   `
*{
    box-sizing: border-box;
    margin: 0;
}
::after,::before{
    box-sizing: border-box;
    margin: 0;
}
.skin{
    background: rgb(255,230,0);
    min-height: 50vh;
    width: 100vw;

    position: relative;

    z-index: 1;
}



.nose{
    border: 20px solid red;
    width:40px;
    height: 40px;

    position: absolute;
    left:50%;
    top:140px;
    margin-left:-20px;
    margin-top: -20px;

    border-radius: 50% 50% 0 0 ;
    border-color: rgb(0,0,0) transparent transparent transparent;

    z-index: 4;
}

@keyframes wave {
    0%{
        transform: rotate(0deg);
    }
    25%{
        transform: rotate(10deg);      
    }
    50%{
        transform: rotate(0deg);      
    }
    75%{
        transform: rotate(-10deg);
    }
    100%{
        transform: rotate(0deg);
    }
}

.nose:hover{
    animation: wave 300ms infinite;
}

.eye{
    border:3px solid black;
    width: 84px;
    height: 84px;
    
    position: absolute;
    left:50%;
    top:140px;

    border-radius: 50%;
    background: rgb(46,46,46);
}

.eye.left{
    margin-left: -160px;
    margin-top: -64px;
}

.eye.right{
    margin-left:76px;
    margin-top:-64px;
}

.eyeball{
    border:3px solid black;
    width: 36px;
    height: 36px;

    margin-left: 14px;
    margin-top: 2px;

    border-radius: 50%;
    background-color: white;
}

.cheek{
    border:4px solid black;
    width:128px;
    height: 128px;

    position: absolute;
    left:50%;
    top:140px;

    border-radius: 50%;
    background-color: #FF0000;
}

.cheek.left{
    margin-left: -250px;
    margin-top:64px;
}

.cheek.right{
    margin-left:120px;
    margin-top:64px;
}


.mouth{
  
    width:200px;
    height:250px;

    position: absolute;
    left:50%;
    margin-left: -100px;
    top:140px;
    margin-top: 16px;

    overflow: hidden;
    
}



.lip{
    border:4px solid  black;
    width:100px;
    height:30px;

    position: absolute;
    margin-left: 50px;
    margin-top: -6px;

    border-radius: 0px 0px 0px 100px;

    background-color: #FFE600;

    z-index: 2;

}

.lip.left{
    transform: translateX(-48px) rotate(-15deg);
    border-radius: 0px 0px 0px 100px;
    border-color: transparent transparent transparent black;
}

.lip.right{
    transform: translateX(48px) rotate(15deg);
    border-radius: 0px 0px 100px 0px;
    border-color: transparent black transparent transparent;
}

.x1{
    width:8px;
    height:4px;
    background-color: black;

    border-radius: 50% 0 0  0;
    position: absolute;
    left:50%;
    margin-left: -5px;
    margin-top: 8px;

    z-index: 5;
}

.x2{
    width:180px;
    height:0px;
    margin-left: 8px;

    border:4px solid #FFE600;
    z-index: 8;
    position: absolute;

    background-color: #FFE600;

}

.mouth > .lipdown{
    border: 4px solid black;
    width: 150px;
    height: 400px;
    margin-left: 25px;

    position: absolute;
    bottom: 0;

    border-radius: 150px/400px;

    background-color: rgb(155,0,10);

    overflow: hidden;
    
}

.lipdown > .cop{
    border:1px solid red;
    width:150px;
    height:180px;

    position: absolute;
    bottom:0px;
    margin-left: -4px;

    border-radius: 150px/180px;
    background-color: rgb(255,72,95);
}
`

js中剩余的内容:

let n = 1

let step = ()=>{
    setInterval(()=>{ 
        if(n<string.length+1){
            console.log(string.substring(0,n))
            test1.innerText = string.substring(0,n)
            test2.innerHTML = string.substring(0,n)
            n=n+1
            step()
        }
    },10)
}

step()

image.png

image.png

这个时候已经做出粗略的效果了,但是还有很多的bug。

4.浅修一下bug

① 如果string读取的长度超过了length

它的循环是退出来了,但是对浏览器而言,它的行为还被记录在案。 因此,代码更改为:

let n = 1

let step = ()=>{
    setInterval(()=>{ 
        if(n<string.length+1){
            console.log(string.substring(0,n))
            test1.innerText = string.substring(0,n)
            test2.innerHTML = string.substring(0,n)
            n=n+1
            step()
        }else{
            window.clearInterval(step)
            return
        }
    },100)
}

step()

② 皮卡丘不固定

皮卡丘要拖滚动条拖到底才能看到,html中加入布局

    <style>
        #html{
            position: fixed;
            left:0;
            bottom:0;
            width:100%;
            height: 50vh;
        }
        #test1{
            position: fixed;
            top:0;
            left: 0;
            width:100%;
            height: 50vh;
            overflow-x: hidden;
            overflow-y: auto;
        }
        #test2{
            display: none;
        }
    </style>

image.png

③皮卡丘固定了,但是上方的代码像写死了

加滚动条。

     #test1{
            position: fixed;
            top:0;
            left: 0;
            width:100%;
            height: 50vh;
            overflow: scroll;  //滚动条
        }

image.png

加了滚条,,隐藏左右的,保留上下的,并让滚条动起来。

#test1{
            position: fixed;
            top:0;
            left: 0;
            width:100%;
            height: 50vh;
            overflow-x: hidden; //滚条样式
            overflow-y: auto; //滚条样式
        }
let step = ()=>{
    setInterval(()=>{ 
        if(n<string.length+1){
            test1.innerText = string.substring(0,n)
            test2.innerHTML = string.substring(0,n)
            test1.scrollTop = test1.scrollHeight || 9999999 
            //滚条保持最下端
            n=n+1

            step()
        }else{
            window.clearInterval(step)
            return
        }
    },100)
}

image.png

5.精进功能

①string模块化

string太长了,占位。把它弄到另外一个文件中,然后引入。

新建文件new.js:

const string = `……`
export default  string 

导入原来的文件

import string from './new.js'

这样做有个小问题,就是我使用parcel对我的代码进行预览,但是parcel2好像并不太支持这样做。

②添加暂停、播放、倍速按钮

先去html中加按键的组件

<body>
<div id="buttons">
        <button id="btnPause">暂停</button>
        <button id="btnPlay">播放</button>
        <button id="btnSlow">慢速</button>
        <button id="btnNormal">中速</button>
        <button id="btnFast">快速</button>
    </div>
</body>

然后在css中修改样式

<style>
    #buttons{
            position: fixed;
            top:0;
            right:0;
            display: flex;
            flex-direction: column;
            z-index: 20;
            margin-right: 24px;
        }
        #buttons > button{
            margin-top: 10px;
        }
</style>

复制粘贴过来,代码有些没有对齐…… image.png

好了,接下来在js中实现它的功能。

暂停:通过window.clear,清空step来实现

在写代码之前,发现了之前代码的错误,来自于对window.clearInterval的理解错误。 之前的使用:

let n = 1
let step = ()=>{
    setInterval(()=>{ 
        if(n<string.length+1){
            console.log(string.substring(0,n))
            test1.innerText = string.substring(0,n)
            test2.innerHTML = string.substring(0,n)
            n=n+1
            step()
        }else{
            window.clearInterval(step)
            return
        }
    },100)
}
step()

但是,写暂停写不出,查看了文档WindowTimers.clearInterval() - Web API 接口参考 | MDN (mozilla.org) 发现错误在,intervalID要取消的定时器的 ID。是由 setInterval() 返回的。写一个返回,再设置一个计时器ID。 更正为:

let step = ()=>{
    return setInterval(()=>{ //新增返回
        if(n>string.length){
            window.clearInterval(timeId)
            return
        }else{
            test1.innerText = string.substring(0,n)
            test2.innerHTML = string.substring(0,n)
            test1.scrollTop = test1.scrollHeight || 9999999
            n=n+1
            
        }
    },1)
}
let timeId = step() //新增计时器ID,将step的返回值给ID

所以暂停功能的代码为:

btnPause.onclick = ()=>{
    window.clearInterval(timeId)
}
播放:重新写入计时器
btnPlay.onclick = ()=>{
    timeId = step()
}
倍速:清空计时器,设置事件time,再写入计时器

重新写入计时器太麻烦了吧,要写一堆。 更改下:

let n = 1
let time = 100
let step = (time)=>{
    return setInterval(()=>{ 
        if(n>string.length){
            window.clearInterval(timeId)
            return
        }else{
            test1.innerText = string.substring(0,n)
            test2.innerHTML = string.substring(0,n)
            test1.scrollTop = test1.scrollHeight || 9999999
            n=n+1
            
        }
    },time)
}
let timeId = step(time)
// const pause = document.getElementById('btnPause')
btnPause.onclick = ()=>{
    window.clearInterval(timeId)
}
btnPlay.onclick = ()=>{
    timeId = step(time)
}

btnSlow.onclick = ()=>{
    window.clearInterval(timeId)
    time = 100
    timeId = step(time)
}

btnNormal.onclick = ()=>{
    window.clearInterval(timeId)
    time = 10
    timeId = step(time)
}

btnFast.onclick = ()=>{
    window.clearInterval(timeId)
    time = 0
    timeId = step(time)
}

6.高级写法

封装代码+面向对象编程
封装代码:

  • 重复代码封装成箭头函数
  • 重复代码部分的调用,看源代码是调用还是占位,占位就只写函数名,不要写()
  • 重复代码写入函数时,要注意return,重复部分有返回值,函数也要return 因为代码重构太容易出错,而且vscode提示功能不完善,我就不演示了。

面向对象:

  • 定义一个对象Object
  • 将方法写入对象,const去掉, = 改成 :
  • 在对方法进行调用时候,要加对象Object的名字
  • 初始化,写一个init对象,后续的引用放入init函数体,最后的时候调用init
  • 变量申明,const n = 0=> n : 0
  • 重复的部分,写哈希表和函数

代码重构后如下:

const player = {

    n : 0,
    time : 100,
    id : undefined ,

    init:()=>{
        player.bindEvents()
        player.play()
    },

    events :{
        'btnPause' :'pause',
        'btnPlay' :'play',
        'btnSlow' :'slow',
        'btnNormal' :'normal',
        'btnFast' :'fast'
    },
    bindEvents:()=>{
        for(let key in player.events){
            if(player.events.hasOwnProperty(key)){
                const value = player.events[key]
                document.querySelector('#'+key).onclick = player[value]
            }
        }
    },

    Pikachu : ()=>{
        if(player.n>string.length){
            window.clearInterval(player.id)
            return
        }else{
            player.n += 1;
            test1.innerText = string.substring(0,player.n)
            test2.innerHTML = string.substring(0,player.n)
            test1.scrollTop = test1.scrollHeight
        }
    },
    play : ()=>{
        player.id = setInterval(player.Pikachu,player.time)
    },

    pause : ()=>{
        window.clearInterval(player.id)
    },

    slow : ()=>{
        player.pause()
        player.time = 300
        player.play()
    },

    normal : ()=>{
        player.pause()
        player.time = 100
        player.play()
    },

    fast : ()=>{
        player.pause()
        player.time = 0
        player.play()
    }
}

player.init()

我的错误和过程缺陷

  • 首先是对css盒模型的理解不够,明白后,后面修改代码的过程顺利了很多
  • 然后是因为嘴巴画不出泄气了很久
  • 另外,我第一次做定位,是盲干,根本没考虑啥子相对定位绝对定位,没考虑布局,居中还是干啥,全是靠开发者工具盲调。正确的过程应该是发现问题,确认自己的目标,回头看看自己之前是怎么实现,它应该怎么实现,而不是盲干。这个地方浪费了很多很多时间,
  • 再者,在使用之中巩固了css的知识,加深了对盒模型,定位,布局的认识。

刚开始完全不会画舌头嘴唇,定位布局也是乱做

458C34EF06A16E80DF4E83DB17C6D8BE.png

画出了下嘴唇但是舌头还是不会画

image.png

终于画出来了

CCB8F53E9497256C4373FCE2A2DD9AC2.png

A125C1D594F75145C379B21A141CF6DE.png