14333223

106 阅读20分钟

一、 组件库

使用了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 以上属性每次变化重新生成点。

  • getSmallModalSize 计算收起状态下的shapseSize是多少

    如下未展开之前取小图形的shapeSize 展开后以 160为基准

 const shapeSize = useMemo(() => {
        return expanded ? pxToVhGeo(160) : samllModalShapeSize
    }, [expanded, samllModalShapeSize])
  • shapeSize进行了取整处理(解决0.几的误差造成的图片缝隙)

image.png

  • 符合 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图image.png

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;
		},
	},

image.png

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
  • 计算三条边是否相等

直角三角形

验证规则:

符合勾股定理

  • 找到 上下的一堆点,然后从上的点中找到左上右上、从下的一堆点中找到左下和右下。最后找到最左和最右的点(特殊的直角三角形)
  • 依次左上 右上 左下 右下 左 右 去重
  • 去重后如果不是三个点 还需要再处理一下
  • 点点连线成边
  • 面积(海伦公式)取根号比较
  • 勾股定理

平行四边形

验证规则:

对边相等,对角相等、每个角都不为九十度。

  • 这里平行四边形因为朝向不同 所以找到的点有可能有问题。
  • 按正常的方式找到的点如果不满足
  • 就找上 右(多个) 下 左(多个)再去重

image.png

  • 计算面积

如果是特殊的形状 ,使用公式 边长和夹角法。 否则 底乘高

梯形

验证规则:

角两两相等、边长存在至少两条相等、并且不两两相等。面积

  • 三种不同角度的梯形 所以有三套找点的方式。
  • 面积还是使用海伦公式

六边形

6条边相等