- 作者:老汪软件技巧
- 发表时间:2024-08-28 10:02
- 浏览量:
前言
在这个课程里,我会跟大家说一下我2023年去滴滴应聘时,遇到的几个比较经典的面试题。
我当时应聘的岗位就是前端可视化,而滴滴也正好需要一个懂图形学的前端,因此他给我出的面试题也是跟图形学相关的。
滴滴我在1面过了后,就发给我3道图形学相关的笔试题,让我在3天内写完。
写完后,我需要把代码发给滴滴的HR,然后在2面的时候讲给面试官听。
这3道面试题我觉得挺不错的,所以事后就将其做成了课程,希望大家也可以更加顺利的找到自己喜欢的工作。
课程重点
这个课程的重点其实并不是canvas,而是图形学。
当你的图形学扎实了,无论你学习canvas,还是WebGL、WebGPU等,都会事半功倍。
我在滴滴的工作中,就用到了很多技术栈,比如canvas、js端OpenCV、Mapbox、WebGL、three.js 等,与此同时,还需要看懂后端的c++、c++端OpenCV、OpenGL等。
前后端的语言各不相同,唯有图形学算法和原理是不变的。
滴滴在出面试题的时候,其实并不要求你用什么语言实现,他在乎的是其中的图形学算法。
课前准备学习目标知识点1-题目内容1-1-绘制多角星
多角星就是有多个角的星形,比如五角星。
1-2-绘制球体与多边形的碰撞反弹路径
球体会朝某一方向运动,碰到墙体时会反弹,碰到障碍物时会停止运动。请画出球体的运动路径。
AABB和BVH优化策略:
1-3-制作b站弹幕效果
1-4-寻路
寻路算法是我最后要附赠给大家的一道题,它是我1面时的面试官问我的,他让我口述一下寻路算法。
我当时就把自己比较熟悉的dikjstra寻路和heuristic寻路跟他滔滔不绝的讲了一遍。
他说挺好的,这道题主要是考察一下面试者的知识积累。
在我实际的面试经历中,寻路是我遇到的比较多的,尤其是做智能驾驶的公司,考的概率会很大,所以我有所准备。
2-热身
在做题之前,我们先做个热身,说一下我自己搭建的canvas渲染引擎,之后做题时会用到。
2-1-canvas渲染引擎简介
这个canvas渲染引擎是在《canvas进阶-矩阵变换》课里的canvas渲染引擎的基础上改进的,算是其2.0版本了
其结构比较简单:
Scene 是场景对象,它是所有图形展示自我的舞台。
Scene对象有children和camera 两个属性。
children 是Scene里所有要展示自我的图形的集合。
camera 是给图形拍照,可以位移和缩放视口,从而给某个图形一个特写,或者给所有图形来个合照。
所有要渲染的图形都继承自Graph2D 对象,它有控点位的Geometry 属性和控样式的Style 属性。
玩过three.js 同学应该就会想到,他们就是Mesh、Geometry和Material的关系。
对,我这引擎就是照着three.js写的,我有点想叫它three2d.js。
我们举几个例子说以其基本用法。
2-2-绘图
我当前的课程项目是用vue3.0+vite+ts 搭建的,大家可以下载我的源码,然后yarn 安装依赖,vite 运行即可。
我不同的示例,都有不同的.vue 文件,这些文件都有相应的路由和导航,以便点击查看。
路由和导航的配置我就不说了,大家可以看源码。
1.建立一个Graph2D.vue文件,用来写canvas渲染引擎的测试案例。
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Scene } from '../lmm/core/Scene'
import { StandStyle } from '../lmm/style/StandStyle'
import { Graph2D } from '../lmm/objects/Graph2D'
import { CircleGeometry } from '../lmm/geometry/CircleGeometry'
import { RectGeometry } from '../lmm/geometry/RectGeometry'
import { PolyGeometry } from '../lmm/geometry/PolyGeometry'
import { TextGraph2D } from '../lmm/objects/TextGraph2D'
import { TextStyle } from '../lmm/style/TextStyle'
import { Vector2 } from '../lmm/math/Vector2'
import { ImageGraph2D } from '../lmm/objects/ImageGraph2D'
// 获取父级属性
defineProps({
size: { type: Object, default: { width: 0, height: 0 } },
})
// 对应canvas 画布的Ref对象
const canvasRef = ref<HTMLCanvasElement>()
onMounted(() => {
const canvas = canvasRef.value
})
script>
<template>
<canvas
ref="canvasRef"
:width="size.width"
:height="size.height"
>canvas>
template>
<style scoped>style>
在上面的代码中,我们建立了一个canvas画布,并在onMounted 生命周期中通过Ref 对象拿到了这张画布。
画布的尺寸是从其父组件App.vue 中拿到的,大家可以自己结合源码来看。
2.实例化一个场景。
/* 场景 */
const scene = new Scene()
onMounted(() => {
const canvas = canvasRef.value
if (canvas) {
scene.setOption({ canvas })
scene.render()
}
})
scene.setOption({ canvas }) 确定了要在哪个canvas上搭建舞台。
scene.render() 是渲染方法。
3.用Graph2D对象绘制一个三角形。
/* 多边形 */
const polyGeometry = new PolyGeometry([0, -300, 100, -250, -100, -250]).close()
const polyStyle = new StandStyle({
strokeStyle: '#fff',
fillStyle: '#00acec',
lineWidth: 6,
lineDash: [6],
})
const polyObj = new Graph2D(polyGeometry, polyStyle)
scene.add(polyObj)
效果如下:
PolyGeometry 是多边形Geometry,在其构造参数中可以写入多个点位,其点位是平展开的x,y位置。
PolyGeometry对象的.close() 方法可以闭合多边形路径。
StandStyle 是标准样式,其样式都是按照canvas API 定义的。
Graph2D 可以将图形和样式合二为一,形成一个有形有色的图形对象。
我们可以把不同的Geometry 图形交给Graph2D,比如圆、矩形、多角星等。
4.圆形
CircleGeometry对象继承自polyGeometry,所以它是可以分段的。
const circleGeometry = new CircleGeometry(50, 8)
const circleStyle = new StandStyle({
fillStyle: '#00acec',
})
const circleObj = new Graph2D(circleGeometry, circleStyle)
circleObj.position.set(0, -200)
scene.add(circleObj)
效果如下:
上例是一个半径50,分段为8的圆。
CircleGeometry 对象的构造函数有2种参数结构:
Graph2D对象可以通过position、rotate、scale 设置其本地模型矩阵。
5.矩形
RectGeometry对象继承自polyGeometry。
const rectObj = new Graph2D(
new RectGeometry(0,0,200, 50),
new StandStyle({ fillStyle: '#00acec' })
)
rectObj.position.set(-100, -150)
scene.add(rectObj)
效果如下:
除了路径图形,我还封装了文字和图像对象。
RectGeometry 对象的构造函数有多种参数结构:
constructor(offset?:Vector2,size?:Vector2,counterclockwise?:boolean)
6.文字
TextGraph2D 是继承自Graph2D的文本对象,其文字是用canvas 内置的文本方法画的。
const textStyle=new TextStyle({
fontSize:50,
textAlign:'center',
textBaseline:'middle',
fillStyle:'#00acec'
})
const textObj=new TextGraph2D('Sphinx',new Vector2(0,-30),textStyle )
textObj.position.set(0,-20)
textObj.rotate=0.1
scene.add(textObj)
效果如下:
TextGraph2D 对象的构造函数有多种参数结构:
7.图像
ImageGraph2D 是继承自Graph2D的图像对象,其图像是用canvas 内置的drawImage()画的。
/* 图像 */
const image = new Image()
image.src =
'https://yxyy-pandora.oss-cn-beijing.aliyuncs.com/stamp-images/1.png'
let imageObj:ImageGraph2D=new ImageGraph2D(image)
scene.add(imageObj)
image.onload=function(){
imageObj.style.setOption({
fillStyle:'rgba(0,172,236,0.1)',
strokeStyle:'#00acec',
order:['image','fill','stroke'],
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 5,
shadowOffsetY: 20,
})
imageObj.size.set(image.width/2,image.height/2)
imageObj.offset.copy(imageObj.size.clone().multiplyScalar(-0.5))
imageObj.scale.set(0.4)
imageObj.position.set(0,90)
imageObj.update()
scene.render()
}
效果如下:
StandStyle 中的order 属性是图像、描边和填充的渲染顺序。
当前案例的渲染顺序是先渲染图案,然后渲染填充色,最后渲染描边。
若你没有给填充色或描边色,或者没有在order 中给出相应的顺序,那相应的样式就不会被渲染。
ImageGraph2D 的构造函数有多种参数结构:
ImageGraph2D 的构造参数除了样式,其余的都来自于canvas 原生的绘制图像方法drawImage()
2-3-图形选择
我在选择图形的时候,用的是canvas 的内置方法isPointIn()。
不过,isPointIn()方法只适用于路径图形,无法选择图像和文字。
所以我提前在ImageGraph2D 和TextGraph2D 中做了封装,给了它们一个包围盒。
这种包围盒对于ImageGraph2D 还好,因为图像就是矩形的,包围盒即使图像的边界。
但对于文字而言,它就无法做出精确选择。因为文字只能通过文字的包围盒做选择,而无法精确到文字实体。
这种选择方式对于一般的需求是够用的,若一定要选择文字实体,可参考three.js将文字转顶点的方法,不过这样的话消耗就会非常大。
接下来我们对之前画的图形做一下选择。
1.给canvas 画布添加一个鼠标移动事件。
2.在脚本中建立pointermove 方法,并在其中选择图形。
function pointermove(event: PointerEvent) {
// 鼠标的世界坐标位
const worldPosition=scene.clientToWorld(event.clientX, event.clientY);
[polyObj, circleObj, rectObj,textObj,imageObj].forEach((obj) => {
// 鼠标在图形对象中的本地坐标位
const localPosition=worldPosition.clone().applyMatrix3(obj.worldMatrix.invert())
// 判断点位是否在图形中
const bool=obj.isPointIn(localPosition)
// 用于选择切换的颜色
const colors='isImageGraph2D' in obj?['rgba(0,172,236,0.1)','rgba(255,0,0,0.1)']:['#00acec','orange']
// 设置图形的填充样式
obj.style.fillStyle = colors[+bool]
})
// 渲染
scene.render()
}
图形的选择方法都是isPointIn(point:Vector2) 方法。
因为图形是在本地坐标系中绘制的,所以在判断一个点位是否在图形中的时候,也要将此点位放在图形本地。
3.文字的包围盒。
之前我们说文字的选择时,说到其选择的是文字的包围盒。
我们可以通过下面的方法将其文字的包围盒示出来:
const textBoundingBox=new Graph2D(
textObj.geometry.clone().applyMatrix3(textObj.worldMatrix),
new StandStyle({ strokeStyle: '#00acec' })
)
scene.add(textBoundingBox)
效果如下:
textObj 的geometry 是处于其本地坐标系的,我们要将其画到世界坐标系中,就需要通过applyMatrix3(textObj.worldMatrix) 方法将其变换到世界坐标系。
总结
这节课我们对课程做了简单介绍,说了我要跟大家讲的3个滴滴笔试题,外加1个寻路算法。
接下来,我还说了我自己搭建的一个canvas渲染引擎的基本用法,后面我会用它来做面试题。
如果大家对canvas渲染引擎感兴趣的话,可以跟我说,我可以根据大家的需求专门出一个讲canvas渲染引擎的课。