只用CSS和Sass,七巧板拼图游戏真的能不用JS吗?

2,787字
12–18 分钟
in

有没有想过,那些拖拖拽拽、转转形状的拼图小游戏,非得靠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-columngrid-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也能交互”的成就感,是真的上头。