freeCodeCamp 课程实战:用 box-shadow 与 border-radius 纯 CSS 绘制新月图形
本文围绕 freeCodeCamp「Applied Visual Design」课程中的挑战 Create a Graphic Using CSS 展开,讲解如何用 box-shadow、border-radius 与 transparent 背景三个属性,仅靠一个空 <div> 就"借"出一个月牙形图形。读完你能掌握 CSS 阴影五段式语法的精确用法、border-radius: 50% 的成圆原理,以及理解该挑战在课程中如何被正则测试断言验证。
挑战在课程中的位置
这个挑战位于 Applied Visual Design 模块的 Transform 小节后、复杂图形小节前,挑战 ID 为 587d78a6367417b2b2512add。从 模块结构文件 中的 challengeOrder 数组可以看到,它紧跟在两个 skew 变换挑战之后,紧接着就是 "Create a More Complex Shape Using CSS and HTML"(用 ::before/::after 伪元素画爱心):
{
"id": "587d78a6367417b2b2512adc",
"title": "Use the CSS Transform Property skewY to Skew an Element Along the Y-Axis"
},
{
"id": "587d78a6367417b2b2512add",
"title": "Create a Graphic Using CSS"
},
{
"id": "587d78a6367417b2b2512ade",
"title": "Create a More Complex Shape Using CSS and HTML"
}
这个排布并非随意:前置挑战 Add a box-shadow to a Card-like Element 刚刚讲过 box-shadow 的完整语法,本挑战要求你复用这套语法做一个"阴影即图形"的反转练习;后续挑战 Create a More Complex Shape Using CSS and HTML 则在此基础上引入伪元素拼装多部件图形。
核心原理:阴影才是你看到的月亮
挑战文档(description 部分)给出的思路是:
你其实要做的,是创建一个圆形、透明的物体,并给它一个略微偏移的清晰阴影——你最终看到的月亮形状,其实是那个阴影本身。
拆解这句话,就是一次"视觉欺骗"的三步构造:
- 本体外壳:
border-radius: 50%把一个 100×100 的方形元素变成正圆; - 本体隐藏:
background-color: transparent让本体完全不占视觉; - 阴影登场:
box-shadow的blur-radius和spread-radius都设为 0,得到边缘锐利(crisp)的阴影;offset-x/offset-y把它推开 25px/10px,于是阴影与"隐形本体"的圆形轮廓错开,重叠部分被本体"遮住",露出的那一段弧形就是月牙。
这里的关键直觉是:阴影是画在元素盒子外围/旁边的,它的形状跟随元素的边框圆角。当本体圆是透明的、阴影圆是不透明的,且两者圆心错开时,阴影被本体区域遮挡后剩下的部分,恰好就是弦切圆弧——即月牙。
box-shadow 语法回顾与参数逐项解析
文档提示读者回忆早前挑战中的 box-shadow 语法。结合前置挑战 587d781b367417b2b2512abe.md 的描述,box-shadow 按顺序接受以下值:
| 参数 | 含义 | 必填 | 本挑战取值 |
|---|---|---|---|
offset-x |
阴影相对元素水平方向的偏移量 | 是 | 25px |
offset-y |
阴影相对元素垂直方向的偏移量 | 是 | 10px |
blur-radius |
阴影模糊半径,越大越柔和 | 否 | 0(要锐利边缘,需显式写 0) |
spread-radius |
阴影扩展半径,正值放大负值缩小 | 否 | 0(需显式写 0,否则默认 0 但会改变后面 color 的位置) |
color |
阴影颜色 | 否 | blue |
两个"可选"参数是本挑战的易错点:blur-radius 与 spread-radius 虽然可选,但由于 color 排在它们之后,若想让颜色生效在第四位之后,就必须把两个半径显式写成 0,否则写法 25px 10px blue 会被解析为"偏移 25px/10px + 颜色 blue、默认模糊"——效果上仍是柔和阴影而非清晰月牙。文档中给的 box-shadow 正则断言(见后文测试部分)中 (px)? 是可选的,说明 0 或 0px 两种写法都通过,但顺序和空格不可乱。
前置挑战中还提到:多组阴影可以用逗号分隔叠加,例如:
box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);
本挑战只用单组阴影,但理解多阴影叠加后,你可以进一步用第二组阴影模拟"月晕"。
操作步骤:从种子代码到新月
种子代码
挑战的初始(seed)代码如下,一个绝对定位居中、100×100 的蓝色方块,带着一组"错误"的阴影参数:
<style>
.center {
position: absolute;
margin: auto;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 100px;
height: 100px;
background-color: blue;
border-radius: 0px;
box-shadow: 25px 10px 10px 10px green;
}
</style>
<div class="center"></div>
注意种子代码里有三处"故意的错误"待修正:background-color: blue(本体不透明)、border-radius: 0px(还是方块)、box-shadow: 25px 10px 10px 10px green(模糊半径和扩展半径都是 10,且颜色是 green——阴影又软又绿,看不出月牙)。
其中 position: absolute + 四个方向 0 + margin: auto 是经典绝对居中写法,保持不变即可。
三步修改
按文档 instructions 的顺序执行:
- 隐藏本体:将
background-color从blue改为transparent; - 变成正圆:将
border-radius从0px改为50%。对一个宽高相等的方形盒子,50%的圆角等于各角内切半圆,四角拼成一个正圆; - 重写阴影:将
box-shadow改为25px 10px 0 0 blue——offset-x25px、offset-y10px、blur-radius0、spread-radius0、颜色blue。
最终解法
<style>
.center {
position: absolute;
margin: auto;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 100px;
height: 100px;
background-color: transparent;
border-radius: 50%;
box-shadow: 25px 10px 0 0 blue;
}
</style>
<div class="center"></div>
渲染结果:屏幕上没有蓝色方块,只有右下方移出一个蓝色月牙。
参数微调实验
理解了"阴影即图形"后,可以安全地做以下变体来巩固手感:
- 把
offset-x从25px减小到10px:月牙变宽(本体与阴影更多重叠); - 把
blur-radius从0改为5px:月牙边缘变柔和,更像真实月光; - 把
spread-radius改为-10px:阴影整体缩小,月牙变细; - 在
box-shadow里追加第二组:box-shadow: 25px 10px 0 0 blue, 28px 13px 15px 0 rgba(0,0,0,0.3);,可叠出带柔光晕的效果(多阴影语法见 前置挑战)。
测试如何验证你的答案
该挑战的 --hints-- 部分内置了三条正则断言,这也是课程"边学边测"机制的落点。三条断言分别对应三步修改:
// 断言 1:背景必须透明
assert.match(code,/background-color:\s*?transparent;/gi);
// 断言 2:圆角必须是 50%
assert.match(code,/border-radius:\s*?50%;/gi);
// 断言 3:box-shadow 五段式,0 可以省略 px 单位
assert.match(code,/box-shadow:\s*?25px\s+?10px\s+?0(px)?\s+?0(px)?\s+?blue\s*?;/gi);
从正则本身能读出几个容错细节:\s*? 允许属性名与冒号间任意空白,/gi 忽略大小写(Transparent、BLUE 均可),0(px)? 表示 blur-radius/spread-radius 写 0 或 0px 都通过——与上一节"顺序不可乱、单位可省"的结论一致。
这些断言是怎么跑起来的?从客户端源码结构看,经典挑战页面由 classic/show.tsx 挂载,它在预览 iframe 之外另建一个隐藏的测试 iframe:utils/frame.ts 中 createFrame 通过 frame.srcdoc = createContent(id, frameContext) 用 srcdoc 属性把挑战代码直接写进 iframe,而 runTestsInTestFrame 则调用 window.FCCTestRunner.getRunner(type).runAllTests(tests, timeout) 执行上述断言。也就是说,你的 CSS 会先在 fcc-main-frame 里真实渲染给你看效果,再由测试 runner 对源码字符串做正则匹配来判断是否通过——这也是为什么断言检查的是 code 文本而非计算样式:正则只能验证"你写了什么",无法验证浏览器最终算出的像素。
与后续挑战的衔接
本挑战证明了"一个盒子 + 一组阴影"就能独立成图。下一个挑战 Create a More Complex Shape Using CSS and HTML 把同一思想推广到多个部件:用 .heart 本体加 ::before/::after 两个伪圆,再配合 transform: rotate(-45deg) 拼出爱心。两者合起来构成该模块的图形构造套路——先用 border-radius/transform 摆出部件,再用颜色与透明度决定"哪些部件是可见的",月牙是"本体不可见、阴影可见",爱心则是"本体与伪元素同色、旋转错位"。
小结
- 月牙图形的本质:
transparent圆形本体 +blur-radius: 0的偏移阴影,让阴影在错位处露出弧形; box-shadow五段式offset-x offset-y blur-radius spread-radius color中,blur-radius/spread-radius想为 0 时建议显式写出,既保证color位置正确,也符合课程断言的要求;- 该挑战源码位于 587d78a6367417b2b2512add.md,模块顺序定义在 applied-visual-design.json,测试执行链可从 frame.ts 的
runTestsInTestFrame追起。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00