theme |
---|
vue-pro |
有这么一个需求, 摇晃手机移动小球给小猫描个边.
拉了一堆人评估, 看看具体怎么去实现.
看到这需求确实愣了下, 然后仔细想了想, 好像也不是全没有办法.
一个很久没有用到的功能出现在我脑海里, 「陀螺仪」.
什么是陀螺仪
它是一种方向传感器, 在接收到方向改变的时候, 获得一些数据.
方向改变事件指的是手机等移动设备反转的时候触发的事件.
移动端具备的陀螺仪功能能够做的事情很多. 他在我们web中存在一个API.
window对象中可以监听deviceorientation事件, 来调用移动端的陀螺仪传感器.
但是我们本文主要使用它来进行方向的判断.
就是给这只猫描个边. 规则 --- 只能摇晃手机
首先, 摇晃手机, 移动画笔描边. 字越少, 事越大.
我们通过陀螺仪固然能拿到方向, 但是如何去描边呢?
试想一下, 我们可不可以事先绘制好路径, 然后移动画笔绘制路径呢?
貌似可以, 但是问题又来了,
如何绘制这一条路径呢?
这条路径我们需要怎么存储下来呢?
通过摇晃手机如何判断路径移动方向呢?
实践是检验代码的唯一标准
我们首先来制作一个画板工具, 可以手动给猫猫描边.
首先, 我们需要一块画板, 来绘制描边路径.
这里我选用react + pixijs来搭建画板.
项目结构我就不多赘述了, 直接开始. 我们先来看下完成后的画板.
功能介绍:
- 设置画笔宽度✅
- 设置画笔颜色✅
- 绘制底图支持网络图片和本地图片拖拽✅
- 展示并复制路径数组✅
- 撤销✅
- 清除✅
- 演示✅
使用react搭建页面, 同时搭建画布舞台.我们需要在舞台上绘制路径.
有兴趣的同学可以查看源码
这里我为了适配移动端, 使用750*1624
的舞台尺寸.
/** 初始化舞台 */
onInitStage = () => {
const _c = this.stageCancas.current;
//设计稿宽高
const width = 750;
const height = 1624;
const app = new PIXI.Application(width, height, {
view: _c,
backgroundColor: 0x1099bb
});
app.ticker.add((deltaTime) => {
});
this.stage = app.stage;
//底图
const barrel = this.stage.addChild(PIXI.Sprite.from("./barrel.png"));
barrel.interactive = true;
this.barrel = barrel;
//球
this.ball = this.stage.addChild(PIXI.Sprite.from("./ball.png"));
this.ball.anchor.set(this.ball.width / 2, this.ball.height / 2)
this.onAddEvents(barrel);
//绘制线
this.line = new PIXI.Graphics();
this.stage.addChild(this.line);
}
通过监听鼠标的按下、移动和抬起事件, 在画布上绘制不同的路径.
鼠标按下的时候记录第一步的第一个笔触.
this.line.lineStyle(this.state.lineWidth, this.state.color.replace("#", "0x"), 1);
this.line.moveTo(_x, _y);
//第一个点
this.currentPoints = [];
this.currentPoints.push({ x: _x, y: _y });
鼠标移动的时候,存储每一个点位, 这里我们可以设置稀疏或者密集采集点位.
//是否绘制点
let disX = Math.abs(point.x - this.lastPoint.x);
let disY = Math.abs(point.y - this.lastPoint.y);
let dis = Math.sqrt(disX * disX + disY * disY);
if (dis >= 2) {
this.line.lineStyle(this.state.lineWidth, this.state.color.replace("#", "0x"), 1);
this.line.lineTo(point.x, point.y);
this.lastPoint = point;
this.currentPoints.push({
x: Math.floor(point.x),
y: Math.floor(point.y)
});
}
在鼠标抬起的时候, 我们就完成了一步命令的绘制,存储这一步绘制命令的所有点位.
//每一步
this.step++;
this.stepPoints[this.step] = this.currentPoints;
然后输出所有绘制步骤的所有点位, 放在code标签中展示.
/**输出数组 */
onGetOutPutArr = () => {
let outArr = [];
for (let item in this.stepPoints) {
outArr = [...outArr, ...this.stepPoints[item]]
}
this.setState({
listTextArea: outArr.length > 0 ? JSON.stringify(outArr) : "路径数组..."
});
}
这样子, 我们就可以一通过鼠标在画板上随意绘画了, 并且每一条路径都有记录.
可以方便我们后面在正式项目中绘制.
- 添加笔触宽度
- 添加笔触颜色
state = {
listTextArea: "路径数组...",
color: "#ffffff", //画笔颜色
lineWidth: 8 //画笔宽度
}
这里我使用react-color
来搭建颜色拾取器.
import { ChromePicker } from 'react-color';
效果真不错.
- 添加复制路径功能
通过execCommand
功能来实现复制.
<input type="text" readOnly="readonly" ref={this._select} value={listTextArea} />
this._select.current.select();
document.execCommand("Copy");
- 添加撤销功能
通过控制步骤step
来控制命令撤销.
删除对象中对应的步骤, 然后再重新使用画笔绘制路径.
/** 撤销 */
onUndoLastStep = () => {
delete this.stepPoints[this.step];
this.step--;
//重新绘制
this.line.clear();
for (let item in this.stepPoints) {
let lineList = this.stepPoints[item];
if (lineList && lineList.length > 0) {
this.line.lineStyle(this.state.lineWidth, this.state.color.replace("#", "0x"), 1);
this.line.moveTo(lineList[0].x, lineList[0].y);
lineList.forEach(it => {
this.line.lineTo(it.x, it.y);
})
}
}
this.onGetOutPutArr();
}
- 添加清除功能
这里直接使用clear()
方法.
同时记得清除步骤和坐标点.
/** 清除 */
onClickClear = () => {
this.line.clear();
this.stepPoints = {};
this.currentPoints = [];
this.onGetOutPutArr();
}
- 添加演示功能
这里我用一个小球沿着路径运动.
/** 演示 */
onShowDemo = () => {
let count = 0;
this.stage.addChild(this.ball);
let outArr = [];
for (let item in this.stepPoints) {
outArr = [...outArr, ...this.stepPoints[item]]
}
let timer = setInterval(() => {
move();
}, 30);
//移动
const move = () => {
count++;
if (count >= outArr.length) {
clearInterval(timer);
return;
}
this.ball.position.set(outArr[count].x, outArr[count].y)
}
}
- 添加拖拽本地图片功能
这里我们使用onDragEnter onDragOver onDrop
这三个触发事件进行拖拽.
将本地图片拖拽入画板之后, 我们将图片渲染到舞台中.
/** 处理渲染图片文件 */
processFiles = (files) => {
var file = files[0];
var reader = new FileReader();
let self = this;
reader.onload = function (e) {
let bg = new Image();
bg.src = e.target.result;
//绘制
self.barrel.texture = PIXI.Texture.from(bg);
};
//读取图片
reader.readAsDataURL(file);
}
这样子我们的建议画板就搭建好了.
我们来看下效果.我在画板中写了个「猫」
前戏太长, 接下来才是主要需求.
首先绘制一张背景图, 背景图上带有一只猫.
需求需要我们摇晃手机, 移动小球给小猫描边.
为了方便演示, 我给路径先绘制一条虚线.
this.realLine = new PIXI.Graphics();
this.realLine.lineStyle(8, 0xf4813e, 1);
this.realLine.moveTo(firstList[0].x, firstList[0].y);
app.stage.addChild(this.realLine);
firstList.forEach((item, index) => {
if (index % 3 == 0) {
if (index > 1)
this.line.moveTo(firstList[index - 1].x, firstList[index - 1].y)
this.line.lineTo(item.x, item.y);
}
});
接着绘制一个小球.
后面会移动这个小球进行运动.
在移动端H5页面中使用陀螺仪, 获取小球移动方向.
注意
陀螺仪在ios中调用需要授权.
同时我们需要点击去触发授权弹窗.
下面是陀螺仪授权的一种兼容写法:
// iOS 13+
if (window.DeviceOrientationEvent !== undefined && typeof window.DeviceOrientationEvent.requestPermission === 'function') {
window.DeviceOrientationEvent.requestPermission()
.then(function (response) {
if (response == 'granted') {
this.onTestGyro();
}else{
}
}).catch(function (error) {
console.log("error", error);
});
} else {
console.log("启动")
this.onTestGyro();
}
授权成功之后, 我们就就可以获得方向数据了.
注意
陀螺仪会返回三个参数.
alpha 绕z轴旋转 beta 绕x轴旋转 gamma 绕y轴旋转.
我这里只处理四个方向.
/** 方向 右上 左上 右下 左下 */
const DIR = {
RT: "RT",
LT: "LT",
LB: "LB",
RB: "RB"
}
通过beta和gamma来确认方向, 因为只需要平面移动, 并不需要3d运动. 故只选择beta和gamma.
当e.beta
在[-180,0]范围的时候, 方向为top, 否则bottom;
当e.gamma
在[-180,0]范围的时候, 方向为left, 否则right;
使用代码判断如下:
window.addEventListener("deviceorientation", (e) => {
let _dir = "";
if (e.beta >= -180 && e.beta <= 0) {
_dir = _dir + "T";
} else {
_dir = _dir + "B";
}
if (e.gamma >= -180 && e.gamma <= 0) {
_dir = "L" + _dir;
} else {
_dir = "R" + _dir;
}
// console.info("陀螺仪", `x: ${e.alpha}, y: ${e.beta}, z: ${e.gamma}`, _dir);
this.curerentDir = DIR[_dir];
}, false);
这样就可以得到实时方向的变化了.
我们要怎么去移动小球呢, 这里需要将我们的路径数组做成一个双向循环列表.
//链表
var Node = function (element) {
this.element = element;
this.next = null;
this.prev = null;
};
为链表添加一个可以添加元素的方法:
this.append = function (element) {
var node = new Node(element),
current,
previous;
if (!head) {
head = node;
tail = node;
head.prev = tail;
tail.next = head;
} else {
current = head;
while (current.next !== head) {
previous = current;
current = current.next;
}
current.next = node;
node.next = head;
node.prev = current;
};
head.prev = node;
length++;
return true;
};
实现一个双向循环链表之后, 我们将描边路径转化为链表.
/**将坐标数组转化成双向链表 */
dealArr2List = () => {
const arr = firstList;
const list = new get2LoopList();
arr.forEach((item, index) => {
list.append(item);
});
this.currentPoint = list.getHead();
}
具有当前点位、上一个点位以及下一个点位. 我们每一帧去改变小球的位置.
同时通过点位之间的比较, 得出上一个点位和下一个点位相对于当前点位的方向.
如果方向与陀螺仪获取到的方向一致, 则移动.
/** 判断方向 */
onCheckDir = (element = {}, nextElement = {}) => {
let _dir = "", disX = element.x - nextElement.x, disY = element.y - nextElement.y;
//判断大方向
if (Math.abs(disX) > Math.abs(disY)) {
//判断左右
if (disX > 0) {
_dir = "L";
} else {
_dir = "R";
}
} else {
//判断上下
if (disY > 0) {
_dir = "T";
} else {
_dir = "B";
}
}
return this.curerentDir.indexOf(_dir) > -1 ? true : false;
}
确定了如何移动小球之后, 我们就需要去移动小球, 同时绘制出路径了.
/** 移动 */
onMoveDir = () => {
const { element, next = {}, prev = {} } = this.currentPoint;
const nextElement = next.element;
const prevElement = prev.element;
if (this.onCheckDir(element, nextElement)) {
this.ball.x = nextElement?.x;
this.ball.y = nextElement?.y;
this.realLine.lineStyle(8, 0xf4813e, 1);
this.realLine.lineTo(nextElement?.x, nextElement?.y);
this.currentPoint = next;
} else if (this.onCheckDir(element, prevElement)) {
this.ball.x = prevElement?.x;
this.ball.y = prevElement?.y;
this.realLine.lineStyle(8, 0xf4813e, 1);
this.realLine.lineTo(prevElement?.x, prevElement?.y);
this.currentPoint = prev;
}
}
这样子就大功告成了.
是不是很简单.
接到这种需求的那一瞬间, 人是蒙的, 慢慢理一下, 其实错怪cp了, 他们也不容易.
我们来看下演示效果:
这里视屏转gif太大了, 我就截取了中间一部分掩饰一下, 手机摇晃, 控制小球给猫猫描了一个边.
一句话的需求, 我搞了个画板, 还做了个demo.🐶🐶🐶
同时调研了陀螺仪在移动端的应用, 所以不能带着有色眼镜去看cp, 他们毕竟能给你想出很多奇怪的点子.
即使这些点子让人爱恨交加.
大家都遇到过什么奇怪的需求呢?
欢迎大家拍砖指正, 笔者功力尚浅, 如有不当之处请斧正.
参考
文章粗浅, 望诸位不吝您的评论和点赞~
注: 本文系作者呕心沥血之作, 转载须声明