有没有想过,那些拖拖拽拽、转转形状的拼图小游戏,非得靠JavaScript才能活起来?有人偏不信邪,拿Sass硬生生怼出了一套七巧板逻辑,从数据存储到旋转判断,再到胜利动画,全程没写一行JS。这波操作够不够“硬核”?下面就来拆解这套纯CSS+Sass的骚操作到底怎么玩。
核心神器:Sass映射
Sass里的映射(Maps)就像JS里的对象,键值对存数据。整个七巧板的颜色、剪裁路径、初始位置、旋转角度对应的坐标,全都塞进一堆映射里。比如下面这段存了蓝色三角形的家底:
$tansShapes: (
blueTriangle: (
color: #53a0e0,
clip-path: (0 0, 50 50, 0 100),
tan-position: (-6, -37),
transform-origin: (4.17, 12.5),
poss-positions: $bluePosiblePositions
)
);有了这些数据,后续的混入(mixins)就能像机器人一样,自动生成几百行CSS,不用手写重复代码。这感觉就像提前配好菜,大火一炒就出锅。
用混入生成网格定位
七巧板的七个块在初始区域得排好队。怎么排?靠一个混入读取映射里的网格坐标,给每个.tanagram-box的第n个子元素塞上grid-column和grid-row。看这个混入:
@mixin tanagram-grid-positioning($nth-child-grid) {
@for $i from 1 through 8 {
@if map.has-key($nth-child-grid, $i) {
$values: map.get($nth-child-grid, $i);
&:nth-child(#{$i}) {
grid-column: #{list.nth($values, 1)} / #{list.nth($values, 2)};
grid-row: #{list.nth($values, 3)} / #{list.nth($values, 4)};
}
}
}
}输出的CSS干净利落:
.tanagram-box:nth-child(1) {
grid-column: 2 / 3;
grid-row: 1 / 2;
}每个块的位置就这么安排得明明白白。如果映射里的坐标写错了,整个布局就会乱套,所以填数据的时候要反复核对——毕竟CSS不会报错说“你坐标写歪了”。
剪裁形状:clip-path的小把戏
七巧板的三角形、正方形、平行四边形全靠clip-path切出来。所有块的剪裁点都存在映射里,然后一个混入负责读数据、拼成polygon()。比如蓝色三角形的三点(0 0, 50 50, 0 100),加上%单位后变成:
.blueTriangle {
clip-path: polygon(0% 0%, 50% 50%, 0% 100%);
}这里有个坑:剪裁坐标用的是百分比,而位置移动用的是vmin。如果混在一起换算错位,形状就会飞出去。解决方法是写一个get-coordinates函数,动态给数值加上单位,保证每次调用的单位都一致。
旋转逻辑:八个单选按钮玩接力
旋转功能咋整?没有JS的事件监听,就用八个隐藏的radio按钮,每个对应45°的倍数。点击“旋转”按钮时,当前角度的radio被选中,同时隐藏自己、显示下一个角度的radio。这样循环下去,感觉就像同一个按钮能无限点。核心混入长这样:
@mixin set-tan-rotation-states($tanName, $values, $angles, $color) {
@each $angle in $angles {
& ~ #rot#{$angle}{ transform: translate(...); background: $color;}
& ~ #rotation-#{$angle}:checked{
@each $key in map.keys($tansShapes){
& ~ #tan#{$key}labRes{ visibility: visible; }
& ~ #tan#{$key}lab{ opacity:.3; }
}
}
}
}生成的CSS里会看到这样的选择器链:
#blueTriangle-tan:checked ~ #rotation-45:checked ~ #tanblueTrianglelab {
transform: translate(-6vmin,-37vmin) rotate(45deg);
}整个交互全靠相邻兄弟选择器(~)串联起来。命名必须极度规范,一个字符都不能错,否则选择器链就断了。实际项目中,每个阴影和按钮的ID都像tanblueTrianglelab-1-360这样带着形状名、序号和角度,查错的时候眼睛都快瞎了——但跑起来是真的丝滑。
阴影反馈与正确位置检测
当某个块被选中,任务板上会亮起它的阴影(半透明块)。阴影坐标存在poss-positions映射里,不同角度有不同的坐标列表。比如蓝色三角形在360°时有两个可选位置:
$bluePosiblePositions: ( 45: none, 90: ((6.7, 11.2),), 135: none, 180: none, 360: ((4.7,13.5), (18.8,13.3)) );混入会根据当前旋转角度,遍历这些坐标,生成对应的阴影样式。如果某个角度下没有坐标(比如45°标了none),就不显示阴影。点击阴影后,对应的实体会移动过去,然后系统检查这个新位置是否在correct-position列表里。若匹配,块会闪烁一下并变成不可点击(通过pointer-events: none或隐藏其交互按钮);若不匹配,块就停在阴影位置但依然可拖拽。
胜利条件存在$winningCombinations映射里,里面记录了每一块最终应该落在哪个坐标、旋转多少度。当所有块都落在正确位置,骆驼剪影变黄,游戏结束。整个过程没有任何if语句,全靠CSS选择器的“开关”组合——选中的radio会触发一套样式覆盖,未选中的则啥也不干。
开始按钮与重置表单
开始按钮也是一个radio,选中后所有块通过transform: translate()飞到初始摆放区。重置按钮用的是原生<button type="reset">包在<form>里,一点就把所有radio恢复未选中状态,游戏瞬间清空。注意这里不要用<input type="reset">,因为样式难调且可能触发表单提交。
整个HTML结构里塞了上百个radio,每个旋转按钮、每个阴影、每个块都是一个独立元素。用Pug模板生成会省很多头发,手写的话容易漏掉某个角度的按钮导致旋转卡死。调试的时候可以打开浏览器开发者工具,挨个勾选radio看对应的块有没有动——如果没动,八成是选择器写错了或者坐标单位没对上。
这波纯CSS七巧板,虽然代码量比JS方案还炸,但跑起来那种“不用JS也能交互”的成就感,是真的上头。
