一、 组件库
使用了react-konva,因为正方是规则图形所以可以使用Rect组件,但是三角形(尤其等腰直角三角形)并不是规则图形。所以使用Shape组件来通过点来围成图形。
Stage 和 Layer
-
Stage: 这是
react-konva
中的根容器,相当于一个 Canvas 元素。
-
Layer: 这是
Stage
内的容器,用于组织和管理图形元素。一个 Stage
可以包含多个 Layer
。
Stage 获取当前待拼区域的宽高。
<Stage width={widthAndHeight?.width} height={widthAndHeight?.height} ref={stageRef}>
1.正方形
正方形涉及到了
- 是否吸附状态 isAdsorb
- 是否是半拼图形 isHalf
- 未吸附的图形提交后需要高亮 isHighlight
- 没有材质(目标弹窗中是没材质,其他拖拽图形和半拼是有材质的) noMaterial
- 提交错误摇晃动画 isShake
- 旋转 旋转动画
- 是否可以拖拽 draggable
属性 |
解释 |
x |
正方形的左上角 x坐标 |
y |
正方形的左上角 y坐标 |
width |
正方形的宽 |
height |
正方形的高 |
dragBoundFunc |
正方形拖拽的边界 |
draggable |
是否可以拖拽 |
stroke |
线框颜色 |
fillPatternImage |
填充的图片 |
fillPatternScale |
填充的材质图片适配大小 |
<Rect
ref={rectRef}
x={position.x}
y={position.y}
width={width}
height={width}
dragBoundFunc={dragBounds}
draggable={draggablecopy}
// 无材质的-也就是目标图形需要边框
stroke={noMaterial ? 'rgba(216, 124, 60, 1)' : isHighlight ? '#FFFA7B' : ''}
// strokeWidth={noMaterial ? 4 : 0}
// onClick={() => rotate90Degrees()} // 点击时旋转
onTouchStart={handleTouchStart}
onTouchMove={(e) => {
// checkCollision()
if (!isHalf) {
clearTimeout(timer.current);
gestureRef.current.isDragging = true
}
}
}
onDragStart={(e) => {
// 拖拽开始时,将目标图形移到最上层
e.target.moveToTop();
}}
onDragMove={(e) => {
// if (e.target.attrs.x < 100 || e.target.attrs.y < 40) {
// onDragMoveEnd(e, uniqKey)
// // setDraggablecopy(false)
// } else {
// 吸附的拖拽后就分开
// test()
handleSeparate(e, uniqKey)
// }
}}
onDragEnd={(e) => {
if (gestureRef.current.isDragging == true && !isHalf && !isAdsorb) {
// test()
moveEnd(e, uniqKey)
}
gestureRef.current.isDragging = false
}}
onTouchEnd={(e) => {
// 拖拽结束时触发 避免点击也执行
if (!isAdsorb) {
rotate90Degrees(e)
}
// clearTimeout(timer.current);
}}
fillPatternImage={noMaterial ? null : imageObj}
fillPatternRepeat="no-repeat"
// 让填充的材质图片适配大小
fillPatternScale={{
x: width / (imageObj?.naturalWidth || 1),
y: width / (imageObj?.naturalHeight || 1)
}}
rotation={rotation}
key={uniqKey}
// 偏移量是中心点的位置
offset={offset}
// dragBoundFunc={dragBoundFunc}
/>
1.1 坐标
Rect 组件的坐标是左上角的坐标。涉及到:
- 为了跟三角形的坐标格式[[],[],[]]统一,也多包了一层[[]],是个二维数组。
- 计算拼后的图形的长宽时要多加一个shapeSize
1.2 添加材质
正方形添加材质简单,只需要区分一下半拼还是拖住图形。
useEffect(() => {
if (!imageObj) {
const img = new window.Image();
img.onload = () => {
setImageObj(img);
};
img.src = isHalf ? SqHalfIcon : SqIcon
}
}, [imageObj, isHalf]);
1.3 拖拽
-
onTouchStart 用户触碰屏幕,将gestureRef中的isDragging变为true。为了拖拽和旋转不同时发生。
-
onDragStart 当用户开始拖拽元素时触发,拖拽开始将图形移动到最上层。
-
onDragMove 当用户拖拽元素并移动时触发,这里触发了 handleSeparate 用于拖拽将吸附后的图像拆开。
-
onDragEnd 当用户完成拖拽操作时触发,判断当前是否是拖拽状态gestureRef.current.isDragging,并且拖拽结束重置isDragging状态。
拖拽结束
获取到当前位置的坐标
let newVertices = [e.target.attrs.x, e.target.attrs.y]
检测是否拖拽到弹窗下面,返回原位置
sqUnderModal()
检测是否拖拽到计数下面,返回原位置
sqUnderCounter()
验证是否存在吸附
let { resPoints, tarRtKey } = adsorb({
submitShapePoints: otherSubmitShapePoints,
basicShape,
currentPoint: newVertices,
shapeSize
})
如果是正方形 执行 adsorbSq()
- 找到原有图形(已经在画布上)的中心点、和当前拖拽图形的中心点。
- 求当前图形和画布上其他任意图形的距离。
- 为了区分边吸和点吸,我们要计算两个正方形边吸的中心点距离和点吸的中心点距离。
- 遍历原有图形的中心点坐标,如果两图形间距小于两正方形的边吸距离,这里还需要判断是往原有图形的上下左右吸附,根据方向再去计算吸附后的位置坐标。
- 同样如果间距大于边吸距离小于点吸距离 就算为点吸,要判断是往原有图形的左上右上左下右下去吸附。同样计算坐标。
- 过滤掉当前要吸附的位置上已经存在图形的位置
- 在过滤完的点中找到离得最近的点。
做吸附后的位置是否超出界限的检测(包括是否吸入到目标弹窗、和计数弹窗)。
如果存在可吸附点,并且满足吸附位置不存在其它图形、没有超出界限。将当前key对应的图形的坐标变为可吸附的坐标,并将吸附状态改为true。最后修改submitShapePoints。
如果不满足吸附条件,需要将isAdsorb变为false.
吸附图形的拆分
onDragMove事件触发时执行,将当前拖拽图形变为此时的位置并且状态变为未吸附。
1.4 旋转
onTouchEnd 事件触发,并且不能存在吸附状态。执行rotate90Degrees方法。
- timeElapsed 计算下触摸开始到触摸结束的时长。
- moveDistance 计算下移动距离
- 满足移动距离<10,触摸时间<200毫秒,执行旋转。
- new Konva.Tween 旋转动画。
- 旋转结束后将gestureRef的isDragging状态置为true
2.三角形
2.1坐标
三个坐标点组成的二维数组。按照顺时针或者逆时针连线。变量pointsArrayWithRotate用于记录点的位置并且是旋转之后的。通过drawShape来进行绘制。
2.2添加材质
坐标点、角度改变就要替换新的图片材质。
- fillImage 图片地址,首先判断是否有材质、是等边三角形(rt)还是等腰直角三角形(et)。然后判断角度替换不同的图片材质,在初始角度中还要判断是否是半拼图形。
- resizeImage 将图添加到画布上,然后重新给画布尺寸,再转为图片
- 通过shapeRef
shapeNode.fillPatternImage(imageObj)
填充图案。居中填充图案
shapeNode.fillPatternOffset({ x: -left, y: -top, });
。否则会出现填充图repeat。
currentRtDir() 判断三角形冲上还是冲下
// 两个y相等 并且y单独一样的点在另两个y相同的点的下面 三角形冲下
// 两个y相等 并且y单独一样的点在另两个y相同的点的上面 三角形冲上
// 等边三角形:每旋转60度 就换个图
// 直角三角形:没旋转9度 换图 还需要判断左右朝向
currentLeftDir()
两个x值相等的点 的x值大于单个点的x值 向右
反之 向左
2.3 图形旋转
旋转 围绕图形的中心点旋转 重新计算坐标点。onTouchEnd事件结束后
rotateTriangle来计算三角形旋转后的坐标。将坐标点替换。
getRtCenter() 获取直角三角形的中心点
getReCenter() 获取等边三角形中心点
2.4 添加错误摇晃动画
变量 isShake 2秒结束后停止动画 并且 重置位置
new Konva.Animation()
//2秒结束后停止动画 并且 重置位置
animRef.current.stop();
setPosition({ x: 0, y: 0 });
2.5 拖拽
moveEnd
- 计算新的坐标点
- 通用进行 underModal underCounter 检测。
- 看是否存在与当前坐标点吸附的点(要过滤掉自身)
- 这里三角形执行adsorbEt(之前区分了等边和直角三角形,但好像代码一样)
- 遍历 submitShapePoints 得到pointWidthDis 计算的是当前三角形的三个点和目标点的距离
计算了
* @targetPoint 目标要吸附的点
* @curPoint 当前拖拽的三角形离目标最近的点
* @curRt 当前拖拽的三角形的三个点
* @distance 两点间距离
* @adsorbRtPoint 要吸附的三角形的点
- pointWithAdsorbResult 遍历出吸附后的三角形位置
二、目标图形
- 根据给的属性去生成目标图形的点。
- 有内部边线 和 无内部边线(简称无边线)两种。 无边线的题目答错后给出边线
- 展开收起后 目标图形的适配
1.数据
属性 |
描述 |
basic_shape |
基本图形 sq(正方形) 、rt(等边三角形)、 et(直角三角形) |
target_shape |
目标图形 sq(正方形)、re(长方形) 、rt(等边三角形)、 et(直角三角形)、tr(梯形)、he(六边形)、pa(平行四边形) |
half_shape_position |
半拼图形所在位置 如 "1,2,3,4" |
question_hard_type |
题型 (1是有辅助线,2是无辅助线,3是有网格) |
operate_count |
可操作数量 |
target_shape_count |
目标图形数量 |
target_specific_value |
目标图形的特殊值(长方形的长宽、梯形的宽高、) |
shapseSize |
基本图形的边长 默认 pxToVhGeo |
2.根据数据生成图形
根据 size, targetShape, basicShape, targetShapeCount, shapeSize, showLine, targetSpecificValue 以上属性每次变化重新生成点。
const shapeSize = useMemo(() => {
return expanded ? pxToVhGeo(160) : samllModalShapeSize
}, [expanded, samllModalShapeSize])
- shapeSize进行了取整处理(解决0.几的误差造成的图片缝隙)

- 符合 specialEt条件和 tr rt he 几种图形的原点origin的x值为图形的中点,值为2 是为了不让图形贴到画布的边缘 ,否则原点为[1,1]也是为了不贴到画布边缘
-
2.1 计算小图形的shapeSize
根据画布的宽高来计算每一小块的shapeSize
const getSmallModalSize = ({ funcName, canvasWidth, targetShapeCount, targetSpecificValue }) => {
const data = configModalSize[funcName].handler({
canvasWidth,
targetShapeCount,
targetSpecificValue,
});
return data;
};
const configModalSize = {
// 正方形正方形 块数取根号就能知道每行多少块
'sqsq': {
handler: ({ canvasWidth, targetShapeCount }) => {
let row = Math.sqrt(targetShapeCount);
return canvasWidth / row;
},
},
// 正方形——长方形 targetSpecificValue 是长宽 画布除以行或列中最多的
'sqre': {
handler: ({ canvasWidth, targetShapeCount, targetSpecificValue }) => {
let row = targetSpecificValue.split(',')[0];
let height = targetSpecificValue.split(',')[1];
let max = row > height ? row : height;
return canvasWidth / max;
},
},
// 直角三角形平行四边形 画一个平行四边形 发现平行四边形外围的长方形的长事 2个直角边长
'etpa': {
handler: ({ canvasWidth, targetShapeCount, targetSpecificValue }) => {
let [side, height] = targetSpecificValue.split(',');
return canvasWidth / (Number(side) + Number(height));
},
},
// 等腰直角三角形-等腰直角三角形 有两种情况 如下2.1图一
'etet': {
handler: ({ canvasWidth, targetShapeCount, targetSpecificValue }) => { // 根据targetSpecificValue值来判断是哪种情况
let [w, h] = targetSpecificValue.split(',');
// 这里应该用不上这么求了 因为给了高
let height = reduceToZero(targetShapeCount);
// 底边和高相等的
return canvasWidth / height;
} else {
// 底边和高不等的
let max = w > h ? w : h;
return canvasWidth / max;
}
},
},
// 等腰直角三角形-长方形
'etre': {
handler: ({ canvasWidth, targetShapeCount, shapeSize, targetSpecificValue }) => {
let [column, row] = targetSpecificValue.split(',');
let max = column > row ? column : row;
return canvasWidth / max;
},
},
'etsq': {
handler: ({ canvasWidth, targetShapeCount, shapeSize, targetSpecificValue }) => {
// 两块拼出一个正方形
let side = Math.sqrt(targetShapeCount / 2);
return canvasWidth / side;
},
},
'ettr': {
handler: ({ canvasWidth, targetShapeCount, shapeSize, targetSpecificValue }) => {
// 8n - 4
let row = (targetShapeCount + 4) / 8;
// 2n+1
let countRow = 2 * row + 1;
// 4 8 12
return canvasWidth / countRow;
},
},
// 正三角形——六边形
'rthe': {
handler: ({ canvasWidth, targetShapeCount, shapeSize, targetSpecificValue }) => {
let height = Math.sqrt(shapeSize ** 2 - (0.5 * shapeSize) ** 2);
return canvasWidth / 3;
},
},
// 正三角形平行四边形
'rtpa': {
handler: ({ canvasWidth, targetShapeCount, targetSpecificValue }) => {
let [side, height] = targetSpecificValue.split(',');
// 依次错开1/2 1 3/2 2
return Number(canvasWidth) / (Number(side) + Number((1 / 2) * Number(height)));
},
},
'rtrt': {
handler: ({ canvasWidth, targetShapeCount, targetSpecificValue }) => {
// 共几行
let row = Math.sqrt(targetShapeCount);
return canvasWidth / row;
},
},
'rttr': {
handler: ({ canvasWidth, targetShapeCount, targetSpecificValue }) => {
let [endRow, column] = targetSpecificValue.split(',');
let endRowCount = 2 * endRow - 1;
// 共几行
// let row = reduceTr(targetShapeCount);
// let lastWidth = row + 1;
return canvasWidth / endRow;
},
},
'': {
handler: () => {
return 0;
},
},
};
2.1图一

2.2 生成目标图形
generateTargetPoints() 内部调用了 getTargetPoints()
const getTargetPoints = ({
funcName,
targetShapeCount,
shapeSize,
origin,
targetSpecificValue,
}) => {
// 后端给的是 宽高 但是这里遍历是行列
// const [column, row] = targetSpecificValue.split(',');
const data = config[funcName].handler({
origin,
targetShapeCount,
shapeSize,
targetSpecificValue,
});
const groupedPoints: any = [];
data.map((item) => {
groupedPoints.push(groupByTwo(item));
});
let res = groupedPoints.map((item) => ({
...basicPointParam,
points: item,
key: uuid(),
}));
return res;
};
2.2.1 sqsq 正方形-正方形
正方形使用的是Rect组件,坐标点是左上角的点。两层循环 向右向下平移shapeSize。得出坐标点
handler: ({ origin, targetShapeCount, shapeSize }) => {
let row = Math.sqrt(targetShapeCount);
let triangles: any = [];
for (let i = 0; i < row; i++) {
for (let j = 0; j < row; j++) {
triangles.push([origin[0] + j * shapeSize, origin[1] + i * shapeSize]);
}
}
return triangles;
},
2.2.2 sqre 正方形长方形
与上一个类似
handler: ({ origin, targetShapeCount, shapeSize, targetSpecificValue }) => {
let triangles: any = [];
// 后端给的是长宽 这里 长对应列 宽对应行
let [column, row] = targetSpecificValue.split(',');
for (let i = 1; i <= row; i++) {
for (let j = 1; j <= column; j++) {
triangles.push([
origin[0] + (j - 1) * shapeSize,
origin[1] + (i - 1) * shapeSize,
]);
}
}
return triangles;
},
2.2.3 etpa 直角三角形平行四边形
这里给的targetSpecificValue是平行四边形的底边长和高。 剩下的其他图形大部分都是找规律
- 底边长*2 对应的是有几列
- 高 对应的是有几行
- 按规律进行平移
// 直角三角形平行四边形
'etpa': {
handler: ({ origin = [0, 0], targetShapeCount, shapeSize, targetSpecificValue }) => {
let [column, row] = targetSpecificValue.split(',');
// 这里给的是边长 和
column = column * 2;
let triangles: any = [];
// 遍历行
for (let i = 1; i <= row; i++) {
let moveY = (i - 1) * shapeSize;
// 遍历列
for (let j = 1; j <= column; j++) {
if (j % 2 === 1) {
// 1 3 5 移动了 0 1 2所以向下取整,每行依次错位 0 1 2个shapeSize
let moveX = Math.floor(j / 2) * shapeSize + (i - 1) * shapeSize;
// 点的顺序 上右下
triangles.push([
origin[0] + moveX,
origin[1] + moveY,
origin[0] + shapeSize + moveX,
origin[1] + moveY,
origin[0] + shapeSize + moveX,
origin[1] + shapeSize + moveY,
]);
} else {
// 2 4 6 移动了 0 1 2
let moveX = (j / 2) * shapeSize + (i - 2) * shapeSize;
triangles.push([
origin[0] + shapeSize + moveX,
origin[1] + moveY,
origin[0] + shapeSize + moveX,
origin[1] + shapeSize + moveY,
origin[0] + 2 * shapeSize + moveX,
origin[1] + shapeSize + moveY,
]);
}
}
}
return triangles;
},
},
2.2.4 etet 直角三角形直角三角形
- 分为两种底和高相等的 底和高不等的
- 底和高相等的 第一行1块,第二行3块,第三行5块。具体规律在注释中
- 底和高不相等 没找出太多规律 所以 维护了个朝向的数组leftPointIndex
'etet': {
handler: ({ origin = [0, 0], targetShapeCount, shapeSize, targetSpecificValue }) => {
let [column, row] = targetSpecificValue.split(',');
let side = reduceToZero(targetShapeCount);
let triangles: any = [];
const leftBeginMap = new Map([
[1, 1],
[2, 3],
[3, 9],
[4, 19],
]);
const leftEndMap = new Map([
[1, 1],
[2, 5],
[3, 13],
[4, 25],
]);
const rightEndMap = new Map([
[1, 2],
[2, 8],
[3, 18],
[4, 32],
]);
const rightBeginMap = new Map([
[1, 2],
[2, 6],
[3, 14],
[4, 26],
]);
if (row === column) {
for (let i = 1; i <= side; i++) {
// 每行有几块
let count = 2 * i - 1;
for (let j = 1; j <= count; j++) {
let arr: any = [];
// 向上取整 第二行第一块移动0 第二行第二块移动0 第二行第三块移动1
let x_move = (Math.ceil(j / 2) - 1) * shapeSize;
// 每行都向下移动 i-1
let y_move = (i - 1) * shapeSize;
// 奇数 偶数 的三角形朝向不一样 点的生成 有一点区别
if (j % 2 === 1) {
// x+len y+len
arr.push(
origin[0] + x_move,
origin[1] + y_move,
origin[0] + x_move,
origin[1] + y_move + shapeSize,
origin[0] + x_move + shapeSize,
origin[1] + y_move + shapeSize
);
} else if (j % 2 === 0) {
arr.push(
origin[0] + x_move,
origin[1] + y_move,
origin[0] + x_move + shapeSize,
origin[1] + y_move + shapeSize,
origin[0] + x_move + shapeSize,
origin[1] + y_move
);
}
triangles.push(arr);
}
}
} else {
for (let i = 1; i <= row; i++) {
let count = (2 * i - 1) * 2;
let beginVal = leftBeginMap.get(i);
for (let j = beginVal; j <= beginVal + count - 1; j++) {
let endVal = leftEndMap.get(i);
if (leftPointIndex.indexOf(j) > -1) {
let arr: any = [];
let y_move = (i - 1) * shapeSize;
if (j % 2 === 1) {
let x_move = ((j - endVal) / 2) * shapeSize;
// x+len y+len
arr.push(
origin[0] + x_move,
origin[1] + y_move,
origin[0] + x_move,
origin[1] + y_move + shapeSize,
origin[0] + x_move - shapeSize,
origin[1] + y_move + shapeSize
);
} else if (j % 2 === 0) {
let x_move = ((j - endVal + 1) / 2) * shapeSize;
arr.push(
origin[0] + x_move,
origin[1] + y_move,
origin[0] + x_move - shapeSize,
origin[1] + y_move + shapeSize,
origin[0] + x_move - shapeSize,
origin[1] + y_move
);
}
triangles.push(arr);
} else {
// let x_move = (Math.ceil(j / 2) - 1) * shapeSize;
let y_move = (i - 1) * shapeSize;
let arr = [];
if (j % 2 === 1) {
let x_move = ((j - endVal) / 2 - 1) * shapeSize;
// x+len y+len
arr.push(
origin[0] + x_move,
origin[1] + y_move,
origin[0] + x_move + shapeSize,
origin[1] + y_move + shapeSize,
origin[0] + x_move + shapeSize,
origin[1] + y_move
);
} else if (j % 2 === 0) {
let x_move = ((j - endVal + 1) / 2 - 1) * shapeSize;
arr.push(
origin[0] + x_move,
origin[1] + y_move,
origin[0] + x_move,
origin[1] + y_move + shapeSize,
origin[0] + x_move + shapeSize,
origin[1] + y_move + shapeSize
);
}
triangles.push(arr);
}
}
}
}
return triangles;
},
},

2.2.5 etre 直角三角形矩形
- targetSpecificValue 是长方形的长宽。长对应列、宽对应行。
- 同样因为是直角三角形所以 生成的点的朝向需要通过 奇偶来区分
// 直角三角形矩形 row, column 行列
'etre': {
handler: ({ origin, targetShapeCount, shapeSize, targetSpecificValue }) => {
let [column, row] = targetSpecificValue.split(',');
let triangles: any = [];
for (let i = 1; i <= row; i++) {
let count = column * 2;
for (let j = 1; j <= count; j++) {
let arr: any = [];
let x_move = (Math.ceil(j / 2) - 1) * shapeSize;
let y_move = (i - 1) * shapeSize;
if (j % 2 === 1) {
// x+len y+len
arr.push(
origin[0] + x_move,
origin[1] + y_move,
origin[0] + x_move,
origin[1] + y_move + shapeSize,
origin[0] + x_move + shapeSize,
origin[1] + y_move + shapeSize
);
} else if (j % 2 === 0) {
arr.push(
origin[0] + x_move,
origin[1] + y_move,
origin[0] + x_move + shapeSize,
origin[1] + y_move + shapeSize,
origin[0] + x_move + shapeSize,
origin[1] + y_move
);
}
triangles.push(arr);
}
}
return triangles;
},
},
2.2.6 etsq 直角三角形正方形
'etsq': {
handler: ({ origin, targetShapeCount, shapeSize, targetSpecificValue }) => {
let triangles: any = [];
let row = Math.sqrt(targetShapeCount / 2);
for (let i = 1; i <= row; i++) {
let count = row * 2;
for (let j = 1; j <= count; j++) {
let arr: any = [];
let x_move = (Math.ceil(j / 2) - 1) * shapeSize;
let y_move = (i - 1) * shapeSize;
if (j % 2 === 1) {
// x+len y+len
arr.push(
origin[0] + x_move,
origin[1] + y_move,
origin[0] + x_move,
origin[1] + y_move + shapeSize,
origin[0] + x_move + shapeSize,
origin[1] + y_move + shapeSize
);
} else if (j % 2 === 0) {
arr.push(
origin[0] + x_move,
origin[1] + y_move,
origin[0] + x_move + shapeSize,
origin[1] + y_move + shapeSize,
origin[0] + x_move + shapeSize,
origin[1] + y_move
);
}
triangles.push(arr);
}
}
return triangles;
},
},
2.2.7 ettr 直角三角形梯形
- 最开始没给targetSpecificValue,所以求了一下 rows 规律是: 8n - 4
- 求得梯形中点 从中点向两边画图形。
// 直角三角形梯形
'ettr': {
handler: ({ origin, targetShapeCount, shapeSize, targetSpecificValue }) => {
let triangles: any = [];
// 8n - 4
let rows = (targetShapeCount + 4) / 8;
// 2n+1
// 梯形求得中点
let halfPoint = ((2 * rows + 1) * shapeSize) / 2;
// 如果不传自动计算
origin = origin[1] > 0 ? origin : [halfPoint, 0];
for (let i = 1; i <= rows; i++) {
let count = 4 * i;
for (let j = 1; j <= count; j++) {
let arr: any = [];
let half = count / 2;
if (j === 1) {
let move_x = (i - 1) * shapeSize;
let move_y = (i - 1) * shapeSize;
arr.push(
origin[0] - 0.5 * shapeSize - move_x,
origin[1] + move_y,
origin[0] - 1.5 * shapeSize - move_x,
origin[1] + shapeSize + move_y,
origin[0] - 0.5 * shapeSize - move_x,
origin[1] + shapeSize + move_y
);
} else if (j % 2 === 0) {
// 左移 1.5 左移0.5 右移0.5 右移1.5
let move_x = 0.5 * shapeSize * (j - 1 - half);
let move_y = (i - 1) * shapeSize;
arr.push(
origin[0] + move_x,
origin[1] + move_y,
origin[0] + move_x,
origin[1] + shapeSize + move_y,
origin[0] + move_x + shapeSize,
origin[1] + shapeSize + move_y
);
} else {
let move_x = 0.5 * shapeSize * (j - 2 - half);
let move_y = (i - 1) * shapeSize;
arr.push(
origin[0] + move_x,
origin[1] + move_y,
origin[0] + move_x + shapeSize,
origin[1] + move_y,
origin[0] + move_x + shapeSize,
origin[1] + shapeSize + move_y
);
}
triangles.push(arr);
}
}
return triangles;
},
},
2.2.8 rthe 等边三角形-六边形
- 六边形只有六块拼的题型 所以没有太大众的生成规律,应该可以不用动
// 六边形
'rthe': {
handler: ({ origin, targetShapeCount, shapeSize, targetSpecificValue }) => {
let height = Math.sqrt(shapeSize ** 2 - (shapeSize / 2) ** 2);
// 因为目标弹窗中六边形有锯齿 所以 往右移动了一些 涉及到 errorX errorY(如果origin[1] >10 则为0 相乘也是0)
let errorX = 0;
let errorY = 0;
if (origin[1] > 0) {
if (origin[1] > 10) {
} else {
origin = [(shapeSize * 2) / 2, origin[1]];
errorX = 1;
errorY = 1;
}
} else {
origin = [(shapeSize * 2) / 2, 0];
}
let triangles: any = [];
let rows = 2;
for (let i = 1; i <= rows; i++) {
let count = targetShapeCount / 2;
for (let j = 1; j <= count; j++) {
let arr: number[] = [];
let x_move = (Math.ceil(j / 2) - 1) * shapeSize;
let y_move = (i - 1) * height;
if (i % 2 === 1) {
// 这也是为了解决目标弹窗锯齿
let valY = -1;
let valX = j === 3 ? 2 : -0.5;
if (j % 2 === 1) {
// x+len y+len
arr.push(
origin[0] + x_move + valX * errorX,
origin[1] + y_move + valY,
origin[0] + x_move - shapeSize / 2 + valX * errorX,
origin[1] + y_move + height + valY,
origin[0] + x_move + shapeSize / 2,
origin[1] + y_move + height + valY
);
} else if (j % 2 === 0) {
arr.push(
origin[0] + x_move + errorY,
origin[1] + y_move + valY,
origin[0] + x_move + shapeSize / 2 + errorY,
origin[1] + y_move + height + valY,
origin[0] + x_move + shapeSize + errorY,
origin[1] + y_move + valY
);
}
} else {
if (j % 2 === 1) {
// 下部分顶点变了 其余两个点去掉移动
let val = j === 3 ? 1 : -0.5;
arr.push(
origin[0] + x_move + val * errorX,
origin[1] + height + y_move + errorY,
origin[0] + x_move - shapeSize / 2 + val * errorX,
origin[1] + height + errorY,
origin[0] + x_move + shapeSize / 2 + val * errorX,
origin[1] + height + errorY
);
} else if (j % 2 === 0) {
// 跟 i % 2 === 1 & j % 2 === 1 的区别
arr.push(
origin[0] + x_move + shapeSize / 2 + errorX,
origin[1] + y_move + 1.5 * errorY,
origin[0] + x_move - shapeSize / 2 + shapeSize / 2 + errorX,
origin[1] + y_move + height + 1.5 * errorY,
origin[0] + x_move + shapeSize / 2 + shapeSize / 2 + errorX,
origin[1] + y_move + height + 1.5 * errorY
);
}
}
let roundArr = arr.map((num) => Math.round(num));
triangles.push(roundArr);
}
}
return triangles;
},
},
2.2.9 rtpa 正三角形平行四边形
- 平行四边形的 底边对应列、高对应行
- 这里等边三角形的高是勾股定理求完后的
// 正三角形平行四边形
'rtpa': {
handler: ({ origin, targetShapeCount, shapeSize, targetSpecificValue }) => {
let [column, row] = targetSpecificValue.split(',');
let triangles: any = [];
for (let i = 0; i < row; i++) {
// 一列的
let count = column * 2;
for (let j = 0; j < count; j++) {
let arr: any = [];
let x_move = (i / 2) * shapeSize + (j / 2) * shapeSize;
// let x_move = (Math.ceil(j / 2) - 1) * shapeSize - (shapeSize * i) / 2;
let height = Math.sqrt(shapeSize ** 2 - (shapeSize / 2) ** 2);
let y_move = i * height;
if (j % 2 === 0) {
// x+len y+len
arr.push(
origin[0] + x_move,
origin[1] + y_move,
origin[0] + x_move + shapeSize / 2,
origin[1] + height + y_move,
origin[0] + shapeSize + x_move,
origin[1] + y_move
);
} else if (j % 2 === 1) {
let x_move = (i / 2) * shapeSize + ((j - 1) / 2) * shapeSize;
arr.push(
origin[0] + x_move + shapeSize / 2,
origin[1] + height + y_move,
origin[0] + shapeSize + x_move,
origin[1] + y_move,
origin[0] + (3 / 2) * shapeSize + x_move,
origin[1] + height + y_move
);
}
triangles.push(arr);
}
}
return triangles;
},
},
2.2.10 rtrt正三角形正三角形
- 这里因为之前后端数据不给targetSpecificValue,所以通过 Math.sqrt(targetShapeCount)计算有几行
- 原点,如果不是[0,0]就取传进来的原点,否则[最后一行的宽/2,0]
// 正三角形正三角形
'rtrt': {
handler: ({ origin, targetShapeCount, shapeSize, targetSpecificValue }) => {
let row = Math.sqrt(targetShapeCount);
let lastRowCount = 2 * row - 1;
let height = Math.sqrt(shapeSize ** 2 - (0.5 * shapeSize) ** 2);
origin = origin[0] > 0 ? origin : [(lastRowCount * shapeSize) / 2, 0];
let triangles: any = [];
let rows = reduceToZero(targetShapeCount);
for (let i = 1; i <= rows; i++) {
let count = 2 * i - 1;
for (let j = 1; j <= count; j++) {
let arr: any = [];
let height = (Math.sqrt(3) / 2) * shapeSize;
let move_x = (shapeSize / 2) * (j - 1) - (shapeSize / 2) * (i - 1);
let move_y = (i - 1) * height;
if (j % 2 === 1) {
arr.push(
origin[0] + move_x,
origin[1] + move_y,
origin[0] - shapeSize / 2 + move_x,
origin[1] + height + move_y,
origin[0] + shapeSize / 2 + move_x,
origin[1] + height + move_y
);
} else {
let move_h = i * height;
arr.push(
origin[0] - shapeSize / 2 + move_x,
origin[1] + move_y,
origin[0] + shapeSize / 2 + move_x,
origin[1] + move_y,
origin[0] + move_x,
origin[1] + move_h
);
}
triangles.push(arr);
}
}
return triangles;
},
},
2.2.11 rttr正三角形梯形
// 正三角形梯形
'rttr': {
handler: ({ origin, targetShapeCount, shapeSize, targetSpecificValue }) => {
let [endRow, column] = targetSpecificValue.split(',');
let beginRow = endRow - column + 1;
let triangles: any = [];
// let rows = reduceTr(targetShapeCount);
origin = origin[1] > 0 ? origin : [(endRow * shapeSize) / 2, 0];
for (let i = beginRow; i <= endRow; i++) {
let count = 2 * i - 1;
for (let j = 1; j <= count; j++) {
let arr: any = [];
let height = (Math.sqrt(3) / 2) * shapeSize;
let move_x = (shapeSize / 2) * (j - 1) - (shapeSize / 2) * (i - 1);
// let move_y = (i - 1) * height;
let move_y = (i - beginRow) * height;
if (j % 2 === 1) {
arr.push(
origin[0] + move_x,
origin[1] + move_y,
origin[0] - shapeSize / 2 + move_x,
origin[1] + height + move_y,
origin[0] + shapeSize / 2 + move_x,
origin[1] + height + move_y
);
} else {
// let move_h = i * height;
arr.push(
origin[0] - shapeSize / 2 + move_x,
origin[1] + move_y,
origin[0] + shapeSize / 2 + move_x,
origin[1] + move_y,
origin[0] + move_x,
origin[1] + height + move_y
);
}
triangles.push(arr);
}
}
return triangles;
},
},
2. 边界处理
三、 生成半拼图形
generateHalfShape
- 首先要生成好答案 原点[0,0]
- 求生成的答案居中后的原点
- 根据中心点再去生成新的坐标点。
- 根据提供的半成品位置 过滤出半成品的坐标点。
四 、验证算法
上面说过正方形组件Rect、和三角形组件Shape的点不一样。正方形用的是左上角的坐标,三角形用到的是三个点的坐标。
正方形
验证规则:
四条边相等(但是因为或多或少存在误差 改为四条边的相差在10以内)。
为了校验存在空心的情况,还校验了面积是否跟答案相同。
1. sq-sq
要通过basicIsSq获取坐标点。
- 要按照顺时针找点
- 因为正方形记录的是左上角的点所以最右的点的x坐标要+shapeSize,最下边的点的y坐标要+shapeSize
2.et-sq
- 找到 左上、右上、左下、右下四个点 ,在进行个去重
- 同上
长方形
验证规则:
对边相等(只要两两相等),每个角都是90°(区分平行四边形)。并且还需要和正方形区分。
1. sq-re
- 通过basicIsSq获取坐标点
- 将边长从小到大排序,最小和最大的不相等(误差不小于10)才是长方形
- 最大的两条边、最短的两条边都相等(相减小于误差)
- 顺时针三点连线出两条边,计算出四个角,四个角都相等(误差不小于10)
- 与答案面积比较
2. et-re
- 找到 左上、右上、左下、右下四个点 ,在进行个去重
- 同上
等边三角形(正三角形)
验证规则:
三边相等
- 找到最上、最下、最左、最右的点,去重
- 计算出相邻点的距离
- 计算了面积-海伦公式 比较面积取根号的误差是否小于3
- 计算三条边是否相等
直角三角形
验证规则:
符合勾股定理
- 找到 上下的一堆点,然后从上的点中找到左上右上、从下的一堆点中找到左下和右下。最后找到最左和最右的点(特殊的直角三角形)
- 依次左上 右上 左下 右下 左 右 去重
- 去重后如果不是三个点 还需要再处理一下
- 点点连线成边
- 面积(海伦公式)取根号比较
- 勾股定理
平行四边形
验证规则:
对边相等,对角相等、每个角都不为九十度。
- 这里平行四边形因为朝向不同 所以找到的点有可能有问题。
- 按正常的方式找到的点如果不满足
- 就找上 右(多个) 下 左(多个)再去重

如果是特殊的形状 ,使用公式 边长和夹角法。
否则 底乘高
梯形
验证规则:
角两两相等、边长存在至少两条相等、并且不两两相等。面积
- 三种不同角度的梯形 所以有三套找点的方式。
- 面积还是使用海伦公式
六边形
6条边相等