普通视图

发现新文章,点击刷新页面。
今天 — 2025年10月15日首页

🎨 SCSS 高级用法完全指南:从入门到精通

2025年10月15日 11:19

🚀 想让 CSS 写得更爽?本文手把手教你 SCSS 的各种实用技巧,让你的样式代码又好写又好管理!

📚 目录


为了实时查看,我这边使用工程化来练习:

企业微信截图_17604966056743.png

1. 变量与作用域

1.1 局部变量与全局变量

// 全局变量
$primary-color: #3498db;

.container {
  // 局部变量
  $padding: 20px;
  padding: $padding;

  .item {
    // 可以访问父级局部变量
    margin: $padding / 2;
    color: $primary-color;
  }
}

// $padding 在这里不可用

1.2 !global 标志

.element {
  $local-var: 10px;

  @if true {
    // 使用 !global 将局部变量提升为全局
    $local-var: 20px !global;
  }
}

// 现在可以在外部访问
.another {
  padding: $local-var; // 20px
}

1.3 !default 标志

// 设置默认值,如果变量已存在则不覆盖
$base-font-size: 16px !default;
$primary-color: #333 !default;

// 这在创建主题或库时非常有用

1.4 Map 变量

// 定义颜色系统
$colors: (
  primary: #3498db,
  secondary: #2ecc71,
  danger: #e74c3c,
  warning: #f39c12,
  info: #9b59b6,
);

// 使用 map-get 获取值
.button {
  background: map-get($colors, primary);

  &.danger {
    background: map-get($colors, danger);
  }
}

// 深层嵌套的 Map
$theme: (
  colors: (
    light: (
      bg: #ffffff,
      text: #333333,
    ),
    dark: (
      bg: #1a1a1a,
      text: #ffffff,
    ),
  ),
  spacing: (
    small: 8px,
    medium: 16px,
    large: 24px,
  ),
);

// 获取深层值
.dark-mode {
  background: map-get(map-get(map-get($theme, colors), dark), bg);
}

2. 嵌套与父选择器

2.1 父选择器 & 的高级用法

// BEM 命名法
.card {
  padding: 20px;

  &__header {
    font-size: 18px;
  }

  &__body {
    margin: 10px 0;
  }

  &--featured {
    border: 2px solid gold;
  }

  // 伪类
  &:hover {
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  }

  // 父选择器在后面
  .dark-theme & {
    background: #333;
  }
}

2.2 嵌套属性

.button {
  // 嵌套属性值
  font: {
    family: 'Helvetica', sans-serif;
    size: 14px;
    weight: bold;
  }

  border: {
    top: 1px solid #ccc;
    bottom: 2px solid #999;
    radius: 4px;
  }

  transition: {
    property: all;
    duration: 0.3s;
    timing-function: ease-in-out;
  }
}

2.3 @at-root 跳出嵌套

.parent {
  color: blue;

  @at-root .child {
    // 这会在根级别生成 .child 而不是 .parent .child
    color: red;
  }

  @at-root {
    .sibling-1 {
      color: green;
    }
    .sibling-2 {
      color: yellow;
    }
  }
}

3. Mixins 高级技巧

3.1 带参数的 Mixin

// 基础 Mixin
@mixin flex-center($direction: row) {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: $direction;
}

// 使用
.container {
  @include flex-center(column);
}

3.2 可变参数 (...)

// 接收任意数量的参数
@mixin box-shadow($shadows...) {
  -webkit-box-shadow: $shadows;
  -moz-box-shadow: $shadows;
  box-shadow: $shadows;
}

// 使用
.card {
  @include box-shadow(0 2px 4px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.05));
}

// 传递多个值
@mixin transition($properties...) {
  transition: $properties;
}

.button {
  @include transition(background 0.3s ease, transform 0.2s ease-out);
}

3.3 @content 指令

// 响应式 Mixin
@mixin respond-to($breakpoint) {
  @if $breakpoint == 'mobile' {
    @media (max-width: 767px) {
      @content;
    }
  } @else if $breakpoint == 'tablet' {
    @media (min-width: 768px) and (max-width: 1023px) {
      @content;
    }
  } @else if $breakpoint == 'desktop' {
    @media (min-width: 1024px) {
      @content;
    }
  }
}

// 使用
.sidebar {
  width: 300px;

  @include respond-to('mobile') {
    width: 100%;
    display: none;
  }

  @include respond-to('tablet') {
    width: 200px;
  }
}

3.4 高级响应式 Mixin

$breakpoints: (
  xs: 0,
  sm: 576px,
  md: 768px,
  lg: 992px,
  xl: 1200px,
  xxl: 1400px,
);

@mixin media-breakpoint-up($name) {
  $min: map-get($breakpoints, $name);
  @if $min {
    @media (min-width: $min) {
      @content;
    }
  } @else {
    @content;
  }
}

@mixin media-breakpoint-down($name) {
  $max: map-get($breakpoints, $name) - 1px;
  @if $max {
    @media (max-width: $max) {
      @content;
    }
  }
}

// 使用
.container {
  padding: 15px;

  @include media-breakpoint-up(md) {
    padding: 30px;
  }

  @include media-breakpoint-up(lg) {
    padding: 45px;
  }
}

3.5 主题切换 Mixin

@mixin theme($theme-name) {
  @if $theme-name == 'light' {
    background: #ffffff;
    color: #333333;
  } @else if $theme-name == 'dark' {
    background: #1a1a1a;
    color: #ffffff;
  }
}


// 更灵活的主题系统
$themes: (
  light: (
    bg: #ffffff,
    text: #333333,
    primary: #3498db,
  ),
  dark: (
    bg: #1a1a1a,
    text: #ffffff,
    primary: #5dade2,
  ),
);

@mixin themed() {
  @each $theme, $map in $themes {
    .theme-#{$theme} & {
      $theme-map: $map !global;
      @content;
      $theme-map: null !global;
    }
  }
}

@function t($key) {
  @return map-get($theme-map, $key);
}

// 使用
.card {
  @include themed() {
    background: t(bg);
    color: t(text);
    border-color: t(primary);
  }
}

4. 函数的妙用

4.1 自定义函数

// 计算 rem
@function rem($pixels, $base: 16px) {
  @return ($pixels / $base) * 1rem;
}

.title {
  font-size: rem(24px); // 1.5rem
  margin-bottom: rem(16px); // 1rem
}

4.2 颜色操作函数

// 创建颜色变体
@function tint($color, $percentage) {
  @return mix(white, $color, $percentage);
}

@function shade($color, $percentage) {
  @return mix(black, $color, $percentage);
}

$primary: #3498db;

.button {
  background: $primary;

  &:hover {
    background: shade($primary, 20%);
  }

  &.light {
    background: tint($primary, 30%);
  }
}

4.3 字符串操作

@function str-replace($string, $search, $replace: '') {
  $index: str-index($string, $search);

  @if $index {
    @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index +
            str-length($search)), $search, $replace);
  }

  @return $string;
}

// 使用
$font-family: str-replace('Arial, sans-serif', 'Arial', 'Helvetica');

4.4 深度获取 Map 值

@function deep-map-get($map, $keys...) {
  @each $key in $keys {
    $map: map-get($map, $key);
  }
  @return $map;
}

$config: (
  theme: (
    colors: (
      primary: (
        base: #3498db,
        light: #5dade2,
      ),
    ),
  ),
);
    
.element {
  color: deep-map-get($config, theme, colors, primary, base);
}

5. 继承与占位符

5.1 基础继承

.message {
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.success-message {
  @extend .message;
  border-color: #2ecc71;
  background: #d5f4e6;
}

.error-message {
  @extend .message;
  border-color: #e74c3c;
  background: #fadbd8;
}

5.2 占位符选择器 %

// 占位符不会单独生成 CSS
%flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

%text-truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.card-title {
  @extend %text-truncate;
  font-size: 18px;
}

.modal {
  @extend %flex-center;
  min-height: 100vh;
}

5.3 多重继承

%bordered {
  border: 1px solid #ddd;
}

%rounded {
  border-radius: 8px;
}

%shadowed {
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.card {
  @extend %bordered;
  @extend %rounded;
  @extend %shadowed;
  padding: 20px;
}

6. 控制指令

6.1 @if / @else

@mixin theme-color($theme) {
  @if $theme == 'light' {
    background: white;
    color: black;
  } @else if $theme == 'dark' {
    background: black;
    color: white;
  } @else {
    background: gray;
    color: white;
  }
}

.app {
  @include theme-color('dark');
}

6.2 @for 循环

// 生成网格系统
@for $i from 1 through 12 {
  .col-#{$i} {
    width: percentage($i / 12);
  }
}

// 生成间距工具类
$spacing: (5, 10, 15, 20, 25, 30);

@for $i from 1 through length($spacing) {
  $space: nth($spacing, $i);

  .m-#{$space} {
    margin: #{$space}px;
  }
  .p-#{$space} {
    padding: #{$space}px;
  }
  .mt-#{$space} {
    margin-top: #{$space}px;
  }
  .pt-#{$space} {
    padding-top: #{$space}px;
  }
  .mb-#{$space} {
    margin-bottom: #{$space}px;
  }
  .pb-#{$space} {
    padding-bottom: #{$space}px;
  }
}

6.3 @each 循环

// 遍历列表
$colors: primary, secondary, success, danger, warning, info;

@each $color in $colors {
  .btn-#{$color} {
    background: var(--#{$color}-color);
  }
}

// 遍历 Map
$social-colors: (
  facebook: #3b5998,
  twitter: #1da1f2,
  instagram: #e4405f,
  linkedin: #0077b5,
  youtube: #ff0000,
);

@each $name, $color in $social-colors {
  .btn-#{$name} {
    background-color: $color;

    &:hover {
      background-color: darken($color, 10%);
    }
  }
}

// 多重值遍历
$sizes: (small, 12px, 500, medium, 14px, 600, large, 16px, 700);

@each $size, $font-size, $font-weight in $sizes {
  .text-#{$size} {
    font-size: $font-size;
    font-weight: $font-weight;
  }
}

6.4 @while 循环

// 生成渐进式字体大小
$i: 6;
@while $i > 0 {
  h#{$i} {
    font-size: 2em - ($i * 0.2);
  }
  $i: $i - 1;
}

7. 模块化系统

7.1 @use 和 @forward

// _variables.scss
$primary-color: #3498db;
$secondary-color: #2ecc71;

// _mixins.scss
@mixin flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

// _functions.scss
@function rem($px) {
  @return ($px / 16px) * 1rem;
}

// main.scss - 新的模块系统
@use 'variables' as vars;
@use 'mixins' as mix;
@use 'functions' as fn;

.container {
  @include mix.flex-center;
  color: vars.$primary-color;
  padding: fn.rem(20px);
}

7.2 命名空间

// _config.scss
$primary: #3498db;

@mixin button {
  padding: 10px 20px;
  border-radius: 4px;
}

// styles.scss
@use 'config' as cfg;

.btn {
  @include cfg.button;
  background: cfg.$primary;
}

// 或者移除命名空间前缀
@use 'config' as *;

.btn {
  @include button;
  background: $primary;
}

7.3 @forward 创建索引文件

// styles/_index.scss
@forward 'variables';
@forward 'mixins';
@forward 'functions';

// main.scss
@use 'styles';

.element {
  color: styles.$primary-color;
  @include styles.flex-center;
}

8. 内置函数库

8.1 颜色函数

$base-color: #3498db;

.color-demo {
  // 颜色调整
  color: adjust-hue($base-color, 45deg);

  // 亮度
  background: lighten($base-color, 20%);
  border-color: darken($base-color, 15%);

  // 饱和度
  &.vibrant {
    background: saturate($base-color, 30%);
  }

  &.muted {
    background: desaturate($base-color, 20%);
  }

  // 透明度
  box-shadow: 0 2px 8px rgba($base-color, 0.3);
  border: 1px solid transparentize($base-color, 0.5);

  // 混合颜色
  &.mixed {
    background: mix(#3498db, #e74c3c, 50%);
  }

  // 补色
  &.complement {
    background: complement($base-color);
  }
}

8.2 数学函数

.math-demo {
  // 基础运算
  width: percentage(5 / 12); // 41.66667%
  padding: round(13.6px); // 14px
  margin: ceil(10.1px); // 11px
  height: floor(19.9px); // 19px

  // 最大最小值
  font-size: max(14px, 1rem);
  width: min(100%, 1200px);

  // 绝对值
  top: abs(-20px); // 20px

  // 随机数
  opacity: random(100) / 100;
}

8.3 列表函数

$list: 10px 20px 30px 40px;

.list-demo {
  // 获取长度
  $length: length($list); // 4

  // 获取元素
  padding-top: nth($list, 1); // 10px
  padding-right: nth($list, 2); // 20px

  // 索引
  $index: index($list, 20px); // 2

  // 追加
  $new-list: append($list, 50px);

  // 合并
  $merged: join($list, (60px 70px));
}

8.4 Map 函数

$theme: (
  primary: #3498db,
  secondary: #2ecc71,
  danger: #e74c3c,
);

.map-demo {
  // 获取值
  color: map-get($theme, primary);

  // 合并 Map
  $extended: map-merge(
    $theme,
    (
      success: #27ae60,
    )
  );

  // 检查键是否存在
  @if map-has-key($theme, primary) {
    background: map-get($theme, primary);
  }

  // 获取所有键
  $keys: map-keys($theme); // primary, secondary, danger

  // 获取所有值
  $values: map-values($theme);
}

8.5 字符串函数

$text: 'Hello World';

.string-demo {
  // 转大写
  content: to-upper-case($text); // "HELLO WORLD"

  // 转小写
  content: to-lower-case($text); // "hello world"

  // 字符串长度
  $length: str-length($text); // 11

  // 查找索引
  $index: str-index($text, 'World'); // 7

  // 切片
  content: str-slice($text, 1, 5); // "Hello"

  // 插入
  content: str-insert($text, ' Beautiful', 6); // "Hello Beautiful World"

  // 去引号
  font-family: unquote('"Arial"'); // Arial
}

9. 实战技巧

9.1 响应式字体大小

@function strip-unit($value) {
  @return $value / ($value * 0 + 1);
}

@mixin fluid-type($min-vw, $max-vw, $min-font-size, $max-font-size) {
  $u1: unit($min-vw);
  $u2: unit($max-vw);
  $u3: unit($min-font-size);
  $u4: unit($max-font-size);

  @if $u1 == $u2 and $u1 == $u3 and $u1 == $u4 {
    & {
      font-size: $min-font-size;

      @media screen and (min-width: $min-vw) {
        font-size: calc(
          #{$min-font-size} + #{strip-unit($max-font-size - $min-font-size)} *
            ((100vw - #{$min-vw}) / #{strip-unit($max-vw - $min-vw)})
        );
      }

      @media screen and (min-width: $max-vw) {
        font-size: $max-font-size;
      }
    }
  }
}

h1 {
  @include fluid-type(320px, 1200px, 24px, 48px);
}

9.2 深色模式切换

$themes: (
  light: (
    bg: #ffffff,
    text: #333333,
    border: #e0e0e0,
    primary: #3498db,
  ),
  dark: (
    bg: #1a1a1a,
    text: #f0f0f0,
    border: #404040,
    primary: #5dade2,
  ),
);

@mixin themed-component {
  @each $theme-name, $theme-colors in $themes {
    [data-theme='#{$theme-name}'] & {
      $theme-map: $theme-colors !global;
      @content;
      $theme-map: null !global;
    }
  }
}

@function theme-color($key) {
  @return map-get($theme-map, $key);
}

.card {
  @include themed-component {
    background: theme-color(bg);
    color: theme-color(text);
    border: 1px solid theme-color(border);
  }

  &__button {
    @include themed-component {
      background: themed-component {
      background: theme-color(primary);
      color: theme-color(bg);
    }
  }
}

9.3 原子化 CSS 生成器

$spacing-map: (
  0: 0,
  1: 0.25rem,
  2: 0.5rem,
  3: 0.75rem,
  4: 1rem,
  5: 1.25rem,
  6: 1.5rem,
  8: 2rem,
  10: 2.5rem,
  12: 3rem,
  16: 4rem,
  20: 5rem,
);

$directions: (
  '': '',
  't': '-top',
  'r': '-right',
  'b': '-bottom',
  'l': '-left',
  'x': (
    '-left',
    '-right',
  ),
  'y': (
    '-top',
    '-bottom',
  ),
);

@each $size-key, $size-value in $spacing-map {
  @each $dir-key, $dir-value in $directions {
    // Margin
    .m#{$dir-key}-#{$size-key} {
      @if type-of($dir-value) == 'list' {
        @each $d in $dir-value {
          margin#{$d}: $size-value;
        }
      } @else {
        margin#{$dir-value}: $size-value;
      }
    }

    // Padding
    .p#{$dir-key}-#{$size-key} {
      @if type-of($dir-value) == 'list' {
        @each $d in $dir-value {
          padding#{$d}: $size-value;
        }
      } @else {
        padding#{$dir-value}: $size-value;
      }
    }
  }
}

9.4 三角形生成器

@mixin triangle($direction, $size, $color) {
  width: 0;
  height: 0;
  border: $size solid transparent;

  @if $direction == 'up' {
    border-bottom-color: $color;
  } @else if $direction == 'down' {
    border-top-color: $color;
  } @else if $direction == 'left' {
    border-right-color: $color;
  } @else if $direction == 'right' {
    border-left-color: $color;
  }
}

.tooltip {
  position: relative;

  &::after {
    content: '';
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    @include triangle(down, 8px, #333);
  }
}

9.5 网格系统生成器

$grid-columns: 12;
$grid-gutter-width: 30px;
$container-max-widths: (
  sm: 540px,
  md: 720px,
  lg: 960px,
  xl: 1140px,
  xxl: 1320px,
);

@mixin make-container($padding-x: $grid-gutter-width / 2) {
  width: 100%;
  padding-right: $padding-x;
  padding-left: $padding-x;
  margin-right: auto;
  margin-left: auto;
}

@mixin make-row($gutter: $grid-gutter-width) {
  display: flex;
  flex-wrap: wrap;
  margin-right: -$gutter / 2;
  margin-left: -$gutter / 2;
}

@mixin make-col($size, $columns: $grid-columns) {
  flex: 0 0 auto;
  width: percentage($size / $columns);
  padding-right: $grid-gutter-width / 2;
  padding-left: $grid-gutter-width / 2;
}

.container {
  @include make-container;

  @each $breakpoint, $width in $container-max-widths {
    @include media-breakpoint-up($breakpoint) {
      max-width: $width;
    }
  }
}
.row {
  @include make-row;
}

@for $i from 1 through $grid-columns {
  .col-#{$i} {
    @include make-col($i);
  }
}

9.6 长阴影效果

@function long-shadow($length, $color, $opacity) {
  $shadow: '';

  @for $i from 0 through $length {
    $shadow: $shadow +
      '#{$i}px #{$i}px rgba(#{red($color)}, #{green($color)}, #{blue($color)}, #{$opacity})';

    @if $i < $length {
      $shadow: $shadow + ', ';
    }
  }

  @return unquote($shadow);
}

.text-shadow {
  text-shadow: long-shadow(50, #000, 0.05);
}

9.7 动画关键帧生成器

@mixin keyframes($name) {
  @keyframes #{$name} {
    @content;
  }
}

@include keyframes(fadeIn) {
  from {
    opacity: 0;
    transform: translateY(-20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.animate-fade {
  animation: fadeIn 0.5s ease-out;
}

9.8 清除浮动

@mixin clearfix {
  &::after {
    content: '';
    display: table;
    clear: both;
  }
}

.container {
  @include clearfix;
}

🎯 总结

SCSS 的高级特性让我们能够:

  1. 提高代码复用性 - 通过 mixin、函数和继承
  2. 增强可维护性 - 使用变量、模块化和命名空间
  3. 提升开发效率 - 利用循环、条件判断自动生成样式
  4. 保持代码整洁 - 嵌套、占位符和模块系统
  5. 创建强大的工具库 - 自定义函数和 mixin 集合

最佳实践建议

  1. 变量命名要语义化

    // Good
    $primary-color: #3498db;
    $spacing-unit: 8px;
    
    // Bad
    $blue: #3498db;
    $var1: 8px;
    
  2. 避免嵌套层级过深(建议不超过 3-4 层)

    // Good
    .card {
      &__header {
      }
      &__body {
      }
    }
    
    // Bad - 嵌套太深
    .card {
      .wrapper {
        .inner {
          .content {
            .text {
            }
          }
        }
      }
    }
    
  3. 优先使用 @use 而不是 @import

// Modern
@use 'variables';
@use 'mixins';

// Legacy
@import 'variables';
@import 'mixins';
  1. 使用占位符代替类继承

    // Good
    %btn-base {
    }
    .btn {
      @extend %btn-base;
    }
    
    // Less optimal
    .btn-base {
    }
    .btn {
      @extend .btn-base;
    }
    
  2. 合理组织文件结构 styles/ ├── abstracts/ │ ├── _variables.scss │ ├── _functions.scss │ └── _mixins.scss ├── base/ │ ├── _reset.scss │ └── _typography.scss ├── components/ │ ├── _buttons.scss │ └── _cards.scss ├── layout/ │ ├── _header.scss │ └── _footer.scss └── main.scss


📚 参考资源


如果这篇文章对你有帮助,欢迎点赞收藏! 👍

有任何问题或补充,欢迎在评论区讨论~ 💬

昨天 — 2025年10月14日首页

Canvas 入门及常见功能实现

作者 Mh
2025年10月14日 17:29

Canvas 绘制基础图形详解

Canvas 是 HTML5 核心绘图 API,支持在网页中动态绘制矢量图形。本文将系统讲解 Canvas 基础图形(线条、三角形、矩形、圆形)及组合图形(笑脸)的绘制方法,并附带完整代码与关键说明。

一、基础环境搭建(HTML + CSS + 初始化)

首先创建 Canvas 容器与绘图上下文,设置基础样式确保绘图区域清晰可见。

<style>
  /* 容器样式:优化布局与视觉效果 */
  .canvas-container {
    background-color: #f8fafc; /* 浅灰背景,区分页面其他区域 */
    padding: 20px;
    max-width: 600px;
    margin: 20px auto; /* 水平居中 */
    border-radius: 8px; /* 圆角优化 */
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 轻微阴影增强层次感 */
  }
  /* Canvas 样式:明确绘图边界 */
  #basic-canvas {
    border: 4px dashed #cbd5e1; /* 虚线边框,区分画布区域 */
    background-color: #ffffff; /* 白色画布,便于观察图形 */
    border-radius: 4px;
  }
</style>

<!-- 画布容器 -->
<div class="canvas-container">
  <!-- Canvas 核心元素:width/height 需直接设置(非CSS),确保图形不失真 -->
  <canvas id="basic-canvas" width="500" height="200"></canvas>
</div>

<script>
  // 1. 获取 Canvas 元素与 2D 绘图上下文(核心对象)
  const canvas = document.getElementById('basic-canvas')
  const ctx = canvas.getContext('2d') // 所有绘图操作都通过 ctx 实现

  // 2. 设置公共样式(避免重复代码)
  ctx.lineWidth = 2 // 线条宽度(所有图形通用)
  ctx.strokeStyle = '#2d3748' // 线条颜色(深灰,比黑色更柔和)

  // 3. 页面加载完成后执行绘图(确保 Canvas 已渲染)
  window.addEventListener('load', () => {
    drawLine() // 绘制线条
    drawTriangle() // 绘制三角形
    drawRectangle() // 绘制矩形(原 Square 更准确的命名)
    drawCircle() // 绘制圆形
    drawSmilingFace() // 绘制笑脸(组合图形)
  })
</script>

二、Canvas 路径绘制核心 API

在绘制路径之前先介绍几个常用的canvas的api。

  1. beginPath() 新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径。
  2. closePath() 闭合路径之后图形绘制命令又重新指向到上下文中。
  3. stroke() 通过线条来绘制图形轮廓。
  4. fill() 通过填充路径的内容区域生成实心的图形。
  5. moveTo(x, y) 将笔触移动到指定的坐标 x 以及 y 上。
  6. lineTo(x, y) 绘制一条从当前位置到指定 x 以及 y 位置的直线。

三、具体图形绘制实现

1. 绘制直线(基础入门)

通过 moveTo() 定位起点,lineTo() 绘制线段,最后用 stroke() 渲染轮廓。

function drawLine() {
  ctx.beginPath() // 开启新路径(避免与其他图形混淆)
  ctx.moveTo(25, 25) // 起点:(25,25)(Canvas 左上角为原点 (0,0))
  ctx.lineTo(105, 25) // 终点:(105,25)(水平向右绘制)
  ctx.stroke() // 渲染直线轮廓
}

2. 绘制三角形(空心 + 实心)

三角形由三条线段组成,空心需手动闭合路径,实心可直接填充(自动闭合)。

function drawTriangle() {
  // 1. 绘制空心三角形
  ctx.beginPath()
  ctx.moveTo(150, 25) // 顶点1
  ctx.lineTo(200, 25) // 顶点2(水平向右)
  ctx.lineTo(150, 75) // 顶点3(向左下方)
  ctx.closePath() // 闭合路径(连接顶点3与顶点1)
  ctx.stroke() // 渲染空心轮廓

  // 2. 绘制实心三角形(位置偏移,避免与空心重叠)
  ctx.beginPath()
  ctx.moveTo(155, 30) // 顶点1(右移5px,下移5px)
  ctx.lineTo(185, 30) // 顶点2(缩短宽度,更美观)
  ctx.lineTo(155, 60) // 顶点3(上移15px,避免超出范围)
  ctx.fillStyle = '#4299e1' // 单独设置填充色(蓝色)
  ctx.fill() // 填充实心(无需 closePath(),自动闭合)
}

3. 绘制矩形(专用 API,更高效)

Canvas 为矩形提供了专用方法,无需手动写路径,直接指定位置与尺寸即可。

function drawRectangle() {
  // 1. 空心矩形:strokeRect(x, y, 宽度, 高度)
  ctx.strokeRect(10, 100, 50, 50) // 位置(10,100),尺寸50x50

  // 2. 实心矩形:fillRect(x, y, 宽度, 高度)(偏移避免重叠)
  ctx.fillStyle = '#48bb78' // 填充色(绿色)
  ctx.fillRect(15, 105, 40, 40) // 位置(15,105),尺寸40x40

  // 3. 清除矩形区域:clearRect(x, y, 宽度, 高度)(生成“镂空”效果)
  ctx.clearRect(25, 115, 20, 20) // 清除中间20x20区域,变为透明
}

4. 绘制圆形(arc () 方法详解)

圆形通过 arc() 方法绘制,核心是理解「弧度制」与「绘制方向」。

arc () 方法语法: arc(x, y, radius, startAngle, endAngle, anticlockwise)

  • x, y:圆心坐标
  • radius:圆的半径
  • startAngle/endAngle:起始 / 结束角度(必须用弧度制,公式:弧度 = (Math.PI / 180) * 角度)
  • anticlockwise:是否逆时针绘制(布尔值,默认 false 顺时针)
function drawCircle() {
  // 1. 绘制完整圆形(360° = 2π 弧度)
  ctx.beginPath()
  ctx.arc(100, 125, 25, 0, Math.PI * 2, false) // 圆心(100,125),半径25
  ctx.stroke()

  // 2. 绘制上半圆(逆时针,180° = π 弧度)
  ctx.beginPath()
  ctx.arc(100, 125, 15, 0, Math.PI, true) // 半径15,逆时针绘制上半圆
  ctx.stroke()

  // 3. 绘制实心下半圆(顺时针)
  ctx.beginPath()
  ctx.arc(100, 130, 10, 0, Math.PI, false) // 圆心下移5px,半径10
  ctx.fillStyle = '#f6ad55' // 填充色(橙色)
  ctx.fill()
}

注意事项:为了保证新的圆弧不会追加到上一次的路径中,在每一次绘制圆弧的过程中都需要使用beginPath()方法。

5. 绘制组合图形(笑脸)

通过组合「圆形(脸)+ 小圆(眼睛)+ 半圆(嘴巴)」,实现复杂图形。

function drawSmilingFace() {
  // 1. 绘制脸部轮廓(圆形)
  ctx.beginPath()
  ctx.arc(170, 125, 25, 0, Math.PI * 2, false) // 圆心(170,125),半径25
  ctx.stroke()

  // 2. 绘制左眼(小圆)
  ctx.beginPath()
  ctx.arc(163, 120, 3, 0, Math.PI * 2, false) // 左眼位置:左移7px,上移5px
  ctx.fillStyle = '#2d3748' // 眼睛颜色(深灰)
  ctx.fill() // 实心眼睛,无需 stroke()

  // 3. 绘制右眼(小圆,与左眼对称)
  ctx.beginPath()
  ctx.arc(178, 120, 3, 0, Math.PI * 2, false) // 右眼位置:右移8px,上移5px
  ctx.fill()

  // 4. 绘制微笑嘴巴(下半圆,顺时针)
  ctx.beginPath()
  ctx.arc(170, 123, 18, 0, Math.PI, false) // 圆心(170,123),半径18,180°
  ctx.stroke()
}

完整效果展示:

四、常见问题与注意事项

  1. Canvas 尺寸设置: width 和 height 必须直接在 Canvas 标签上设置,若用 CSS 设置会导致图形拉伸失真。
  2. 路径隔离: 每次绘制新图形前,务必调用 beginPath(),否则新图形会与上一次路径叠加。
  3. 弧度与角度转换: arc() 方法仅支持弧度制,需用 (Math.PI / 180) * 角度 转换(如 90° = Math.PI/ 2)。
  4. 样式优先级: 若单个图形需要特殊样式(如不同颜色),需在 stroke()/fill() 前单独设置(如 ctx.fillStyle),否则会继承公共样式。

Canvas 实现电子签名功能

电子签名功能在现代 Web 应用中非常常见,从在线合同签署到表单确认都有广泛应用。本文将带你从零开始,使用 Canvas API 实现一个功能完备的电子签名组件。

一、实现思路与核心技术点

实现电子签名的核心思路是追踪用户的鼠标或触摸轨迹,并在 Canvas 上将这些轨迹绘制出来。

核心技术点:

  • Canvas API:用于在网页上动态绘制图形
  • 事件监听:监听鼠标 / 触摸的按下、移动和松开事件
  • 坐标转换:将鼠标 / 触摸事件的坐标转换为 Canvas 元素内的相对坐标
  • 线条优化:通过设置线条属性实现平滑的签名效果

二、HTML 结构设计

这是一份简单到爆的html结构,没错,就是这样简单...

<div class="container">
  <p>电子签名</p>
  <canvas id="signatureCanvas" class="signature-border"></canvas>
</div>

三、CSS 样式设置

为 Canvas 添加一些基础样式,使其看起来像一个签名板。

.container {
  background-color: #fff;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.signature-border {
  width: 98%;
  height: 300px;
  border: 4px dashed #cbd5e1;
  border-radius: 10px;
  cursor: crosshair;
}

四、JavaScript 核心实现

这是实现签名功能的关键部分,主要包含以下几个步骤:

  1. 获取 Canvas 元素和上下文
  2. 设置 Canvas 的实际绘制尺寸
  3. 定义变量存储签名状态和坐标
  4. 实现坐标转换函数
  5. 编写事件处理函数
  6. 绑定事件监听器
// 获取Canvas元素和上下文
const canvas = document.getElementById('signatureCanvas')
const ctx = canvas.getContext('2d', { willReadFrequently: true })

// 签名状态变量
let isDrawing = false
let lastX = 0
let lastY = 0
let lineColor = '#000000'
let lineWidth = 2

// 初始化Canvas
function initCanvas() {
  // 设置Canvas样式
  ctx.strokeStyle = lineColor
  ctx.lineWidth = lineWidth
  ctx.lineJoin = 'round'
  ctx.lineCap = 'round'

  resizeCanvas()
  window.addEventListener('resize', resizeCanvas)
}

// 响应窗口大小变化
function resizeCanvas() {
  const rect = canvas.getBoundingClientRect()
  const { width, height } = rect
  // 保存当前画布内容
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
  // 调整Canvas尺寸
  canvas.width = width
  canvas.height = height
  // 恢复画布内容
  ctx.putImageData(imageData, 0, 0)
  // 重新设置绘图样式
  ctx.strokeStyle = lineColor
  ctx.lineWidth = lineWidth
  ctx.lineJoin = 'round'
  ctx.lineCap = 'round'
}

// 获取坐标(适配鼠标和触摸事件)
function getCoordinates(e) {
  const rect = canvas.getBoundingClientRect()
  if (e.type.includes('mouse')) {
    return [e.clientX - rect.left, e.clientY - rect.top]
  } else if (e.type.includes('touch')) {
    return [e.touches[0].clientX - rect.left, e.touches[0].clientY - rect.top]
  }
}

// 开始绘制
function startDrawing(e) {
  isDrawing = true
  lastX = getCoordinates(e)[0]
  lastY = getCoordinates(e)[1]
}

// 绘制中
function draw(e) {
  if (!isDrawing) return
  const [currentX, currentY] = getCoordinates(e)
  ctx.beginPath()
  ctx.moveTo(lastX, lastY)
  ctx.lineTo(currentX, currentY)
  ctx.stroke()
  // 解释: 这里是将当前移动的坐标赋值给下一次绘制的起点,实现线条的流畅。
  ;[lastX, lastY] = [currentX, currentY]
}

// 结束绘制
function stopDrawing() {
  isDrawing = false
}

// 绑定事件监听
function bindEvents() {
  canvas.addEventListener('mousedown', startDrawing)
  canvas.addEventListener('mousemove', draw)
  canvas.addEventListener('mouseup', stopDrawing)
  canvas.addEventListener('mouseout', stopDrawing)
  // 触摸事件(移动设备)
  canvas.addEventListener('touchstart', e => {
    e.preventDefault() // 防止触摸事件被浏览器默认处理
    startDrawing(e)
  })
  canvas.addEventListener('touchmove', e => {
    e.preventDefault()
    draw(e)
  })
  canvas.addEventListener('touchend', e => {
    e.preventDefault()
    stopDrawing()
  })
}

// 初始化
window.addEventListener('load', () => {
  initCanvas()
  bindEvents()
})

五、功能亮点与设计思路

  1. 流畅的绘制体验:通过设置lineCap: 'round'lineJoin: 'round'让线条更加平滑自然。
  2. 响应式设计:监听窗口resize事件,动态调整 Canvas 尺寸,确保在不同设备和屏幕尺寸下都能正常工作。
  3. 跨设备支持:同时支持鼠标和触摸事件,兼容桌面和移动设备。

六、完整的代码

七、下一步可以探索的方向

  1. 颜色和粗细选择:增加 UI 控件让用户自定义签名的颜色和笔触粗细。
  2. 清空签名和保存签名:增加 UI 控件让用户清空当前的签名,同时支持保存和下载签名。

canvas 实现滚动序列帧动画

前言

在现代网页设计中,滚动触发的动画能极大增强用户体验,其中 Apple 官网的 AirPods Pro 产品页动画堪称经典 —— 通过滚动进度控制序列帧播放,营造出流畅的产品展示效果。本文将简单的实现一下这个动画效果。

一、动画核心逻辑

  1. 页面分为 3 个楼层:楼层 1(灰色背景)、楼层 2(黑色背景,核心动画区)、楼层 3(灰色背景)
  2. 楼层 2 高度为200vh(2 倍视口高度),内部有一个sticky定位的容器,包含文字和 Canvas
  3. 当用户滚动页面时,仅在楼层 2 进入并完全离开视口的过程中,Canvas 会根据滚动进度播放 147 帧 AirPods 序列图
  4. 窗口尺寸变化时,Canvas 会自动适配,保证动画显示比例正确

二、核心技术栈及原理拆解

要实现滚动序列帧动画,需要解决 3 个核心问题:序列帧加载与管理、滚动进度计算、Canvas 渲染与适配。

  1. HTML 部分的核心是三层 section 结构和Canvas 动画容器,结构清晰且语义化:
<!-- 楼层1:引导区 -->
<section class="floor1-container floor-container">
  <p>楼层一</p>
</section>
<!-- 楼层2:核心动画区(目标楼层) -->
<section class="floor2-container floor-container" id="targetFloor">
  <!-- sticky容器:滚动时"粘住"视口 -->
  <div class="sticky">
    <p>楼层二</p>
    <!-- Canvas:用于渲染序列帧 -->
    <canvas class="canvas" id="hero-lightpass"></canvas>
  </div>
</section>
<!-- 楼层3:结束区 -->
<section class="floor3-container floor-container">
  <p>楼层三</p>
</section>
  1. CSS 的核心作用是控制三层布局、实现 sticky 定位、保证 Canvas 适配,代码注释已标注关键逻辑:
/* 重置默认margin,避免布局偏移 */
body,
p {
  margin: 0;
}

/* 楼层1和楼层3样式:灰色背景+居中文字 */
.floor1-container,
.floor3-container {
  background-color: #474646; /* 深灰色背景 */
  height: 500px; /* 固定高度,模拟常规内容区 */
  display: flex; /* Flex布局:实现文字水平+垂直居中 */
  justify-content: center; /* 水平居中 */
  align-items: center; /* 垂直居中 */
}

/* 楼层1/3文字样式:响应式字体 */
.floor3-container p,
.floor1-container p {
  font-size: 5vw; /* 5vw:相对于视口宽度的5%,实现响应式字体 */
  color: #fff; /* 白色文字,与深色背景对比 */
}

/* 楼层2样式:黑色背景+高高度(动画触发区) */
.floor2-container {
  height: 200vh; /* 200vh:2倍视口高度,保证有足够滚动空间触发动画 */
  background-color: black; /* 黑色背景,突出产品图片 */
  color: #fff; /* 白色文字 */
}

/* 楼层2文字:水平居中 */
.floor2-container p {
  text-align: center;
}

/* 核心:sticky定位容器 */
.sticky {
  position: sticky; /* 粘性定位:滚动到top:0时固定 */
  top: 0; /* 固定在视口顶部 */
  height: 500px; /* 与楼层1/3高度一致,保证视觉连贯 */
  width: 100%; /* 占满视口宽度 */
}

/* Canvas样式:宽度自适应 */
.canvas {
  width: 100%; /* 宽度占满容器 */
  height: auto; /* 高度自动,保持图片比例 */
}
  1. JS 部分是整个动画的核心,负责预加载序列帧、计算滚动进度、控制 Canvas 渲染和窗口适配,我们分模块解析:

模块 1:初始化变量与 DOM 元素

首先定义动画所需的核心变量,包括序列帧数量、图片数组、Canvas 上下文等:

// 1. 动画核心配置
const frameCount = 147 // 序列帧总数(根据实际图片数量调整)
const images = [] // 存储所有预加载的序列帧图片
const canvas = document.getElementById('hero-lightpass') // 获取Canvas元素
const context = canvas.getContext('2d') // 获取Canvas 2D渲染上下文
const airpods = { frame: 0 } // 存储当前播放的帧序号(用对象便于修改)

// 2. 获取目标楼层(楼层2)的DOM元素,用于后续计算滚动位置
const targetFloor = document.getElementById('targetFloor')

// 3. 序列帧图片地址模板(Apple官网的AirPods序列帧地址)
// 作用:通过索引生成每帧图片的URL(如0001.jpg、0002.jpg...)
const currentFrame = index =>
  `https://www.apple.com/105/media/us/airpods-pro/2019/1299e2f5_9206_4470_b28e_08307a42f19b/anim/sequence/large/01-hero-lightpass/${(index + 1).toString().padStart(4, '0')}.jpg`

模块 2:预加载所有序列帧图片

序列帧动画需要所有图片加载完成后才能流畅播放,因此必须先预加载图片:

// 循环生成147帧图片,存入images数组
for (let i = 0; i < frameCount; i++) {
  const img = new Image() // 创建Image对象
  img.src = currentFrame(i) // 给图片设置URL(通过模板生成)
  images.push(img) // 将图片存入数组
}

// 当第一张图片加载完成后,执行首次渲染(避免页面空白)
images[0].onload = render

为什么要预加载:

  1. 如果不预加载,用户滚动时图片可能还在加载,导致动画卡顿或跳帧
  2. 监听第一张图片的onload事件:保证页面初始化时至少有一张图显示,提升首屏体验

模块 3:Canvas 渲染函数

定义render()函数,负责将当前帧图片绘制到 Canvas 上:

function render() {
  // 1. 清除Canvas画布(避免上一帧残留)
  context.clearRect(0, 0, canvas.width, canvas.height)

  // 2. 绘制当前帧图片
  // 参数:图片对象、绘制起点X、Y、绘制宽度、绘制高度
  context.drawImage(images[airpods.frame], 0, 0, canvas.width, canvas.height)
}

模块 4:Canvas 窗口适配函数

当窗口尺寸变化时,需要重新调整 Canvas 的宽高,避免图片拉伸或变形:

function resizeCanvas() {
  // 1. 获取Canvas元素的实际位置和尺寸(包含CSS样式的影响)
  const rect = canvas.getBoundingClientRect()

  // 2. 设置Canvas的实际宽高(Canvas的width/height是像素尺寸,而非CSS样式)
  canvas.width = rect.width
  canvas.height = rect.height

  // 3. 重新渲染当前帧(避免尺寸变化后画布空白)
  render()
}

易错点提醒:

  1. Canvas 有两个 "尺寸":一个是 HTML 属性width/height(实际像素尺寸),另一个是 CSS 样式width/height(显示尺寸)
  2. 如果只改 CSS 样式而不改canvas.width/height,图片会拉伸变形;因此必须通过getBoundingClientRect()获取实际显示尺寸,同步设置 Canvas >的像素尺寸

模块 5:滚动进度计算与帧控制(核心中的核心)

这是整个动画的逻辑核心 —— 根据用户的滚动位置,计算当前应播放的帧序号,实现 "滚动控制动画":

function handleScroll() {
  // 1. 获取关键尺寸数据
  const viewportHeight = window.innerHeight // 视口高度(浏览器可见区域高度)
  const floorTop = targetFloor.offsetTop // 目标楼层(楼层2)距离页面顶部的距离
  const floorHeight = targetFloor.offsetHeight // 目标楼层自身的高度(200vh)
  const currentScrollY = window.scrollY // 当前滚动位置(页面顶部到视口顶部的距离)

  // 2. 计算"滚动结束点":当目标楼层底部进入视口时,动画应播放到最后一帧
  const scrollEnd = floorTop + floorHeight - viewportHeight

  // 3. 计算滚动进度(0~1):0=未进入楼层2,1=完全离开楼层2
  let scrollProgress = 0
  if (currentScrollY < floorTop) {
    // 情况1:滚动位置在楼层2上方→进度0(显示第一帧)
    scrollProgress = 0
  } else if (currentScrollY > scrollEnd) {
    // 情况2:滚动位置在楼层2下方→进度1(显示最后一帧)
    scrollProgress = 1
  } else {
    // 情况3:滚动位置在楼层2内部→计算相对进度
    const scrollDistanceInFloor = currentScrollY - floorTop // 进入楼层2后滚动的距离
    const totalScrollNeeded = scrollEnd - floorTop // 楼层2内需要滚动的总距离(触发完整动画的距离)
    scrollProgress = scrollDistanceInFloor / totalScrollNeeded // 进度=已滚动距离/总距离
  }

  // 4. 根据进度计算当前应显示的帧序号
  // 公式:目标帧 = 进度 × (总帧数-1) → 保证进度1时显示最后一帧(避免数组越界)
  const targetFrame = Math.floor(scrollProgress * (frameCount - 1))

  // 5. 优化性能:仅当帧序号变化时才重新渲染
  if (targetFrame !== airpods.frame) {
    airpods.frame = targetFrame
    render() // 重新绘制当前帧
  }
}

模块 6:事件监听与初始化

最后,通过事件监听触发上述逻辑,完成动画初始化:

window.addEventListener('load', () => {
  // 1. 监听滚动事件:用户滚动时触发进度计算
  window.addEventListener('scroll', handleScroll)

  // 2. 监听窗口 resize 事件:窗口尺寸变化时适配Canvas
  window.addEventListener('resize', resizeCanvas)

  // 3. 初始化Canvas尺寸(页面加载完成后首次适配)
  resizeCanvas()
})

三、完成代码展示

更多canvas功能敬请期待...

昨天以前首页

现代CSS开发环境搭建

2025年10月11日 10:56

第2章: 现代CSS开发环境搭建

🎯 本章重点

  • 现代前端工具链配置
  • PostCSS和预处理器的使用
  • 开发工作流优化

📖 内容概述

2.1 基础开发环境

代码编辑器配置 (VS Code)
// .vscode/settings.json
{
  "css.validate": false,
  "less.validate": false,
  "scss.validate": false,
  "editor.formatOnSave": true,
  "files.associations": {
    "*.css": "css"
  }
}
推荐扩展
  • PostCSS Language Support
  • Auto Rename Tag
  • CSS Peek
  • Live Server

2.2 构建工具配置

package.json 依赖
{
  "devDependencies": {
    "postcss": "^8.4.0",
    "postcss-preset-env": "^7.0.0",
    "autoprefixer": "^10.4.0",
    "cssnano": "^5.0.0",
    "vite": "^3.0.0"
  }
}

2.3 PostCSS 配置

postcss.config.js
module.exports = {
  plugins: [
    require('postcss-preset-env')({
      stage: 3,
      features: {
        'nesting-rules': true,
        'custom-media-queries': true,
        'media-query-ranges': true
      }
    }),
    require('autoprefixer'),
    require('cssnano')({
      preset: 'default'
    })
  ]
}

2.4 现代CSS工作流

开发脚本
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "css:watch": "postcss src/**/*.css --dir dist --watch"
  }
}

2.5 浏览器兼容性处理

.browserslistrc
last 2 versions
> 1%
not dead
not ie 11
自动前缀示例
/* 输入 */
.container {
  display: grid;
  gap: 20px;
}

/* 输出 */
.container {
  display: -ms-grid;
  display: grid;
  -ms-grid-gap: 20px;
  gap: 20px;
}

2.6 开发服务器配置

vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  server: {
    port: 3000,
    open: true
  },
  css: {
    postcss: './postcss.config.js'
  }
})

💡 最佳实践

  1. 使用CSS原生特性 替代预处理器
  2. 配置PostCSS 处理兼容性和优化
  3. 设置浏览器列表 确保目标兼容性
  4. 使用开发服务器 获得实时预览

🛠️ 工具推荐

  • 构建工具: Vite, Parcel, Webpack
  • CSS处理: PostCSS, Lightning CSS
  • 开发服务器: Live Server, BrowserSync

🎯 下一章预览

下一章将深入探讨CSS变量和自定义属性的强大功能。


最后更新: 2024年12月

容器查询 - 组件级响应式设计

2025年10月11日 10:34

第7章: 容器查询 - 组件级响应式设计

🎯 本章重点

  • 容器查询基础概念
  • 与媒体查询的区别与优势
  • 实际应用场景和最佳实践
  • 浏览器兼容性处理

📖 内容概述

7.1 容器查询介绍

7.1.1 什么是容器查询

容器查询(Container Queries)允许组件根据其容器尺寸而非视口尺寸来调整样式,实现真正的组件级响应式设计。

7.1.2 解决的问题
  • 媒体查询的局限性: 只能基于视口尺寸
  • 组件独立性: 组件可以在不同容器中自适应
  • 布局灵活性: 组件无需知道外部布局结构

7.2 基础语法

7.2.1 定义容器
/* 创建容器上下文 */
.component-container {
  container-type: inline-size;
  container-name: main-container;
}

/* 简写形式 */
.component-container {
  container: main-container / inline-size;
}

/* 多个容器属性 */
.component-container {
  container-type: size;        /* 支持尺寸查询 */
  container-name: card-layout; /* 容器名称 */
}
7.2.2 容器类型
  • inline-size: 只查询内联方向尺寸(水平方向)
  • size: 查询两个方向的尺寸(水平和垂直)
  • normal: 不创建容器上下文(默认)
7.2.3 容器查询语法
@container main-container (min-width: 400px) {
  .component {
    /* 当容器宽度 ≥ 400px 时的样式 */
    display: grid;
    grid-template-columns: 1fr 1fr;
  }
}

@container (max-width: 300px) {
  .component {
    /* 当容器宽度 ≤ 300px 时的样式 */
    flex-direction: column;
  }
}

7.3 实际应用案例

7.3.1 卡片组件
.card {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 16px;
  background: white;
}

.card-container {
  container-type: inline-size;
  container-name: card;
}

/* 小尺寸卡片 */
@container card (max-width: 300px) {
  .card {
    display: flex;
    flex-direction: column;
    text-align: center;
  }
  
  .card-image {
    width: 100%;
    height: 120px;
    object-fit: cover;
  }
  
  .card-title {
    font-size: 1rem;
    margin: 8px 0;
  }
  
  .card-description {
    display: none;
  }
}

/* 中等尺寸卡片 */
@container card (min-width: 301px) and (max-width: 500px) {
  .card {
    display: grid;
    grid-template-areas:
      "image title"
      "image description"
      "button button";
    grid-template-columns: 100px 1fr;
    gap: 12px;
  }
  
  .card-image {
    grid-area: image;
    width: 100%;
    height: 80px;
    object-fit: cover;
    border-radius: 4px;
  }
  
  .card-title {
    grid-area: title;
    font-size: 1.1rem;
  }
  
  .card-description {
    grid-area: description;
    font-size: 0.9rem;
    color: #666;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
  
  .card-button {
    grid-area: button;
  }
}

/* 大尺寸卡片 */
@container card (min-width: 501px) {
  .card {
    display: grid;
    grid-template-areas:
      "image title button"
      "image description button";
    grid-template-columns: 150px 1fr auto;
    grid-template-rows: auto 1fr;
    gap: 16px;
  }
  
  .card-image {
    grid-area: image;
    width: 100%;
    height: 120px;
    object-fit: cover;
    border-radius: 6px;
  }
  
  .card-title {
    grid-area: title;
    font-size: 1.2rem;
    margin: 0;
  }
  
  .card-description {
    grid-area: description;
    font-size: 1rem;
    line-height: 1.4;
  }
  
  .card-button {
    grid-area: button;
    align-self: center;
  }
}
7.3.2 导航组件
.nav-container {
  container-type: inline-size;
  container-name: navigation;
}

.navigation {
  display: flex;
  background: #2c3e50;
  padding: 0 20px;
}

/* 小尺寸导航 - 汉堡菜单 */
@container navigation (max-width: 600px) {
  .navigation {
    justify-content: space-between;
    padding: 0 16px;
    height: 60px;
  }
  
  .nav-logo {
    font-size: 1.2rem;
    color: white;
  }
  
  .nav-menu {
    display: none;
    position: absolute;
    top: 60px;
    left: 0;
    right: 0;
    background: #34495e;
    flex-direction: column;
    padding: 16px;
  }
  
  .nav-menu.open {
    display: flex;
  }
  
  .nav-item {
    padding: 12px 0;
    border-bottom: 1px solid #4a6278;
  }
  
  .hamburger {
    display: block;
    color: white;
    background: none;
    border: none;
    font-size: 1.5rem;
    cursor: pointer;
  }
}

/* 大尺寸导航 - 水平菜单 */
@container navigation (min-width: 601px) {
  .navigation {
    justify-content: space-between;
    align-items: center;
    height: 70px;
  }
  
  .nav-logo {
    font-size: 1.5rem;
    color: white;
    font-weight: bold;
  }
  
  .nav-menu {
    display: flex;
    gap: 30px;
    list-style: none;
    margin: 0;
    padding: 0;
  }
  
  .nav-item {
    color: white;
    text-decoration: none;
    padding: 8px 16px;
    border-radius: 4px;
    transition: background-color 0.2s;
  }
  
  .nav-item:hover {
    background-color: #34495e;
  }
  
  .hamburger {
    display: none;
  }
}
7.3.3 表单组件
.form-container {
  container-type: inline-size;
  container-name: form;
}

.form-group {
  margin-bottom: 20px;
}

/* 小尺寸表单 - 垂直布局 */
@container form (max-width: 400px) {
  .form-group {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  
  .form-label {
    font-weight: bold;
    font-size: 0.9rem;
  }
  
  .form-input {
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 1rem;
  }
  
  .form-button {
    width: 100%;
    padding: 12px;
    background: #3498db;
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 1rem;
  }
}

/* 大尺寸表单 - 水平布局 */
@container form (min-width: 401px) {
  .form-group {
    display: grid;
    grid-template-columns: 120px 1fr;
    gap: 16px;
    align-items: center;
  }
  
  .form-label {
    text-align: right;
    font-weight: bold;
  }
  
  .form-input {
    padding: 12px;
    border: 1px solid #ddd;
    border-radius: 6px;
    font-size: 1rem;
  }
  
  .form-button {
    grid-column: 2;
    justify-self: start;
    padding: 12px 24px;
    background: #3498db;
    color: white;
    border: none;
    border-radius: 6px;
    font-size: 1rem;
  }
}

7.4 高级特性

7.4.1 容器单位
.component {
  /* 使用容器相对单位 */
  font-size: clamp(1rem, 5cqi, 2rem);
  padding: clamp(1rem, 10cqi, 2rem);
  gap: clamp(0.5rem, 2cqi, 1rem);
}

/* 可用单位 */
.example {
  width: 50cqi;    /* 容器内联尺寸的50% */
  height: 30cqb;   /* 容器块尺寸的30% */
  font-size: 5cqi;  /* 容器内联尺寸的5% */
  padding: 10cqmin; /* 容器最小尺寸的10% */
  margin: 5cqmax;   /* 容器最大尺寸的5% */
}
7.4.2 嵌套容器查询
.layout-container {
  container-type: inline-size;
  container-name: layout;
}

.card-container {
  container-type: inline-size;
  container-name: card;
}

/* 外层容器查询 */
@container layout (min-width: 800px) {
  .card-container {
    /* 在大布局中调整卡片容器 */
    container-type: size;
  }
}

/* 内层容器查询 */
@container card (min-width: 300px) and (min-height: 200px) {
  .card {
    /* 基于卡片容器尺寸的样式 */
    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  }
}
7.4.3 容器查询与CSS变量
:root {
  --card-padding: 16px;
  --card-gap: 12px;
}

.card-container {
  container-type: inline-size;
  container-name: card;
}

@container card (min-width: 400px) {
  .card {
    --card-padding: 24px;
    --card-gap: 20px;
    --image-size: 120px;
  }
}

@container card (min-width: 600px) {
  .card {
    --card-padding: 32px;
    --card-gap: 24px;
    --image-size: 160px;
  }
}

.card {
  padding: var(--card-padding);
  gap: var(--card-gap);
}

.card-image {
  width: var(--image-size, 80px);
  height: var(--image-size, 80px);
}

7.5 性能优化

7.5.1 避免过度使用
/* 好的做法:合理的断点 */
@container (min-width: 300px) { /* ... */ }
@container (min-width: 600px) { /* ... */ }
@container (min-width: 900px) { /* ... */ }

/* 避免的做法:过多断点 */
@container (min-width: 100px) { /* ... */ }
@container (min-width: 200px) { /* ... */ }
@container (min-width: 300px) { /* ... */ }
/* ... 太多类似的查询 */
7.5.2 使用容器单位
/* 使用容器单位替代多个查询 */
.component {
  /* 替代多个min-width查询 */
  font-size: clamp(1rem, 4cqi, 1.5rem);
  padding: clamp(1rem, 5cqi, 2rem);
}

/* 响应式间距 */
.spacing {
  gap: clamp(0.5rem, 2cqi, 1.5rem);
}

7.6 浏览器兼容性

7.6.1 特性检测
/* 现代浏览器支持 */
@supports (container-type: inline-size) {
  .component-container {
    container-type: inline-size;
  }
  
  @container (min-width: 400px) {
    .component {
      /* 容器查询样式 */
    }
  }
}

/* 传统浏览器回退 */
@supports not (container-type: inline-size) {
  .component {
    /* 基于视口的回退样式 */
    max-width: 400px;
  }
  
  @media (min-width: 768px) {
    .component {
      /* 媒体查询替代 */
    }
  }
}
7.6.2 JavaScript检测
// 检测容器查询支持
if (CSS.supports('container-type', 'inline-size')) {
  console.log('容器查询支持');
} else {
  console.log('容器查询不支持,使用回退方案');
  // 添加回退类
  document.documentElement.classList.add('no-container-queries');
}
/* 回退样式 */
.no-container-queries .component {
  /* 传统响应式设计 */
  max-width: 400px;
}

.no-container-queries .component--large {
  /* 手动控制的大尺寸样式 */
}

7.7 最佳实践

  1. 语义化命名: 使用有意义的容器名称
  2. 合理断点: 基于内容需求设置断点
  3. 性能意识: 避免不必要的容器查询
  4. 渐进增强: 提供适当的回退方案
  5. 测试覆盖: 在不同容器尺寸下测试组件

💡 实战技巧

  • 使用容器查询实现真正的组件独立性
  • 结合CSS变量创建灵活的组件系统
  • 利用容器单位实现流畅的尺寸变化
  • 为不支持的环境提供优雅降级

🎯 下一章预览

下一章将探索CSS网格布局的高级技巧,包括子网格、自动布局算法和复杂网格模式。


最后更新: 2024年12月

重新思考CSS Reset:normalize.css vs reset.css vs remedy.css,在2025年该如何选?

作者 ErpanOmer
2025年10月9日 11:44

what-is-css.png

我带团队Review一个新项目的启动代码时,有一个文件我一定会仔细看,那就是CSS Reset

它虽然不起眼,但却像我们整个CSS架构的地基。地基打不好,上面的楼盖得再漂亮,也容易出问题,后期维护成本会非常高。

从十多年前 reset.css 横空出世,到后来normalize.css 成为事实标准,再到近几年出现的一些新方案,CSS Reset的理念,其实也在不断演进。

但现在都2025年10月了,IE早已入土为安,主流浏览器对标准的支持也空前一致。我们还有必要像十年前那样做重置样式吗?

今天,我就想聊聊我对这几个主流方案的看法,以及在我们团队的当前项目中,我是如何选择的。


reset.css

  • 它的原理:非常暴力直接——抹平所有浏览器默认样式margin, padding, font-size, line-height...通通归零,h1pulli在外观上变得一模一样,所有元素都回到最原始、最裸的状态。

  • 代码片段感受一下

    /* http://meyerweb.com/eric/tools/css/reset/ 
       v2.0 | 20110126
       License: none (public domain)
    */
    
    html, body, div, span, applet, object, iframe,
    h1, h2, h3, h4, h5, h6, p, blockquote, pre,
    a, abbr, acronym, address, big, cite, code,
    del, dfn, em, img, ins, kbd, q, s, samp,
    small, strike, strong, sub, sup, tt, var,
    b, u, i, center,
    dl, dt, dd, ol, ul, li,
    fieldset, form, label, legend,
    table, caption, tbody, tfoot, thead, tr, th, td,
    article, aside, canvas, details, embed, 
    figure, figcaption, footer, header, hgroup, 
    menu, nav, output, ruby, section, summary,
    time, mark, audio, video {
            margin: 0;
            padding: 0;
            border: 0;
            font-size: 100%;
            font: inherit;
            vertical-align: baseline;
    }
    /* HTML5 display-role reset for older browsers */
    article, aside, details, figcaption, figure, 
    footer, header, hgroup, menu, nav, section {
            display: block;
    }
    body {
            line-height: 1;
    }
    ol, ul {
            list-style: none;
    }
    blockquote, q {
            quotes: none;
    }
    blockquote:before, blockquote:after,
    q:before, q:after {
            content: '';
            content: none;
    }
    table {
            border-collapse: collapse;
            border-spacing: 0;
    }
    
  • 优点:提供了一个绝对干净、可预测,非常适合那些需要从零开始、高度定制视觉风格的网站。

  • 2025年的缺点

    1. 太粗暴了:它移除了很多有用的默认样式。比如,你写了一个<ul>,却发现前面的项目符号没了,还得自己手动加回来。
    2. 破坏了语义化:一个<h1>在视觉上和<p>毫无区别,这在开发初期,会削弱HTML语义化的默认视觉反馈。
    3. 调试困难:当你在DevTools里审查一个元素时,你看到的样式,和它本该有的默认样式天差地别,这会增加调试的心智负担。

在2025年,对于绝大多数项目,我不推荐再使用这种粗暴的Reset样式。


normalize.css

screenshot-20251009-114007.png

  • 原理:与reset.css完全相反——保留有用的浏览器默认样式,只修复已知的浏览器不一致和Bug。它不在重置,而是修正。

  • 代码片段感受一下

    /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
    /* Document
       ========================================================================== */
    
    /**
     * 1. Correct the line height in all browsers.
     * 2. Prevent adjustments of font size after orientation changes in iOS.
     */
    
    html {
      line-height: 1.15; /* 1 */
      -webkit-text-size-adjust: 100%; /* 2 */
    }
    
    /* Sections
       ========================================================================== */
    
    /**
     * Remove the margin in all browsers.
     */
    
    body {
      margin: 0;
    }
    
    /**
     * Render the `main` element consistently in IE.
     */
    
    main {
      display: block;
    }
    
    /**
     * Correct the font size and margin on `h1` elements within `section` and
     * `article` contexts in Chrome, Firefox, and Safari.
     */
    
    h1 {
      font-size: 2em;
      margin: 0.67em 0;
    }
    
    /* Grouping content
       ========================================================================== */
    
    /**
     * 1. Add the correct box sizing in Firefox.
     * 2. Show the overflow in Edge and IE.
     */
    
    hr {
      box-sizing: content-box; /* 1 */
      height: 0; /* 1 */
      overflow: visible; /* 2 */
    }
    
    /**
     * 1. Correct the inheritance and scaling of font size in all browsers.
     * 2. Correct the odd `em` font sizing in all browsers.
     */
    
    pre {
      font-family: monospace, monospace; /* 1 */
      font-size: 1em; /* 2 */
    }
    
    /* Text-level semantics
       ========================================================================== */
    
    /**
     * Remove the gray background on active links in IE 10.
     */
    
    a {
      background-color: transparent;
    }
    
    /**
     * 1. Remove the bottom border in Chrome 57-
     * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
     */
     
     /*  大部分已省略,完整的版本可以查看👉 https://github.com/necolas/normalize.css/blob/8.0.1/normalize.css */
    
  • 优点

    1. 保留了元素的默认语义化样式,h1就是比h2大。
    2. 只修复问题,代码注释清晰,像一本浏览器修复手册。
    3. 它成为了过去十年里,包括Bootstrap、Ant Design在内,无数框架和组件库的基石。
  • 2025年的缺点

    1. 过于保守:它只修复不一致,但并没有提供一些我们现代开发中普遍认为更好的默认值。比如,它就没有设置box-sizing: border-box;
    2. 部分规则已过时:它里面的一些修复,是针对我们现在根本不需要支持的、非常古老的浏览器版本的(比如旧版IE)。

normalize.css在今天,依然是一个安全、稳妥的选择。它不会犯错,但我觉得,它有点不够看了😫。


最佳选择:remedy.css

  • 原理:在normalize.css的基础上,再往前走一步。它不仅修正了不一致,还提供了一套我们现代Web开发中,普遍认为 更好的默认样式

  • 核心特性

    1. 开箱即用的border-box

      *, ::before, ::after {
        box-sizing: border-box;
      }
      

      这几乎是所有现代CSS项目的第一行代码,它帮你写好了。

    2. 更好的响应式媒体元素

      img, picture, video, canvas, svg {
        display: block;
        max-width: 100%;
      }
      

      这能天然地防止图片、视频等媒体元素撑破布局,是响应式设计的基础。

    3. 更平滑的字体渲染和滚动

      html {
        -moz-text-size-adjust: none;
        -webkit-text-size-adjust: none;
        text-size-adjust: none;
        scroll-behavior: smooth;
      }
      
    4. 更友好的可用性/无障碍默认值

      [disabled] {
        cursor: not-allowed;
      }
      
  • 优点:它像一个经验丰富的老手,把你开新项目时,那些不得不写的、或者最好要写的样板代码,都提前帮你准备好了。

  • 缺点:它带有一定的主观性。比如,它默认移除了所有元素的margin,统一用padding来控制间距,这需要你适应它的理念。

对于我们团队的新项目,尤其是那些需要快速启动的中后台项目,remedy.css或者类似的现代Reset方案(比如modern-css-reset),已经成为了我的首选


选择与建议🤞

Reset 类型 哲学思想 适用场景 在2025年的建议
reset.css 简单粗暴的重置 高度定制视觉、几乎没有原生HTML元素的UI 不推荐❌
normalize.css 保留并修正 任何项目,尤其是需要保持浏览器原生感的 安全,但略显保守👍
remedy.css 现代最佳实践 所有新项目,尤其是中后台、需要快速启动的项目 强烈推荐首选👍👍👍
自己定义 量身定制 大型项目、有完整设计系统的团队 终极方案,成本高🤔

CSS Reset 只有权衡,没有什么可选,不可选。

但在2025年,我们权衡的基点,已经从如何抹平IE的差异,变成了如何以一个更现代、更高效、更符合最佳实践的基点,来开始我们的工作

所以,下次当你的新项目npm init之后,别再下意识地npm install normalize.css了。

或许,remedy.css会给你一个更好的开始。

祝大家国庆愉快🙌

大家都在找的手绘/素描风格图编辑器它它它来了

2025年10月9日 10:00

我正在做的开源项目:du-editor,一个基于X6的图编辑器

image.png

image.png

要在 X6 中实现手绘/素描风格,核心是 SVG 滤镜 (Filter)SVG 填充模式 (Pattern) 。X6 的节点和边都基于 SVG,因此我们可以充分利用 SVG 的强大功能来创建这种独特的视觉效果。

这种风格主要包含三个要素:

  1. 抖动、不规则的边框:这通过 SVG 的 feTurbulencefeDisplacementMap 滤镜实现。
  2. 阴影线填充效果:这通过 SVG 的 <pattern> 元素创建可平铺的填充图案来实现。
  3. 手写体字体:选择一个合适的手写风格字体

第 1 步:定义 SVG 滤镜和填充模式

首先,需要在你的 HTML 文件中,通常是在图表容器 <div> 的旁边,定义好我们需要的 SVG 资源。我们将它们放在一个 <svg> 标签的 <defs> 元素中,这样 X6 就可以通过 ID 引用它们。

HTML

<div id="container"></div>

<svg width="0" height="0">
  <defs>
    <filter id="filter-sketch">
      <feTurbulence
        type="fractalNoise"
        baseFrequency="0.01"
        numOctaves="5"
        result="noise"
      />
      <feDisplacementMap
        in="SourceGraphic"
        in2="noise"
        scale="5"
        xChannelSelector="R"
        yChannelSelector="G"
        result="displaced"
      />
    </filter>

    <pattern
      id="pattern-hatch"
      patternUnits="userSpaceOnUse"
      width="8"
      height="8"
      patternTransform="rotate(45)"
    >
      <path
        d="M -1,1 l 2,-2 M 0,8 l 8,-8 M 7,9 l 2,-2"
        stroke="#c58d6a"
        stroke-width="1"
      />
    </pattern>
  </defs>
</svg>
  • <filter id="filter-sketch"> :

    • feTurbulence: 用于生成一种叫做 "Perlin noise" 的伪随机噪点图。baseFrequency 控制噪点的“波纹”大小,值越小,线条抖动越平缓。numOctaves 决定了细节层次。
    • feDisplacementMap: 将上面生成的噪点图应用到原始图形上,使其像素发生位移,从而产生抖动的“手绘”效果。scale 属性控制抖动的剧烈程度,是调整效果最关键的参数。
  • <pattern id="pattern-hatch"> :

    • 我们创建了一个 8x8 像素大小的单元格,并将其旋转了 45 度。
    • 在单元格内部,我们用 <path> 画了几条斜线。
    • 这个 pattern 会像瓷砖一样自动平铺,以填充整个形状。你可以通过修改 width, height<path>stroke-width 来调整阴影线的密度和粗细。

第 2 步:在 X6 中注册自定义节点和样式

接下来,我们在 X6代码中注册新的节点,并在其 attrs 中引用上面定义的滤镜和填充。

// 准备好图表实例
const graph = new Graph({
  container: document.getElementById('container'),
  // ... 其他配置
});

// --- 注册手绘风格的节点 ---

// 1. 手绘风格 - 圆角矩形 (对应图片右侧的形状)
Graph.registerNode('sketch-rect', {
  inherit: 'rect', // 继承自矩形
  attrs: {
    body: {
      // 关键样式
      stroke: '#a26740',
      strokeWidth: 2,
      fill: 'url(#pattern-hatch)', // 使用阴影线填充
      filter: 'url(#filter-sketch)', // 应用手绘滤镜
    },
    label: {
      // 推荐使用手写体
      fontFamily: '"Comic Sans MS", "Caveat", cursive',
      fill: '#a26740',
      fontSize: 14,
    },
  },
});

// 2. 手绘风格 - 菱形 (对应图片左侧的形状)
// 菱形需要自定义 SVG 路径
const diamondPath = 'M 30 0 L 60 30 L 30 60 L 0 30 Z';
Graph.registerNode('sketch-diamond', {
  inherit: 'path', // 继承自路径
  width: 60,
  height: 60,
  attrs: {
    body: {
      // 关键样式
      refD: diamondPath,
      stroke: '#a26740',
      strokeWidth: 2,
      fill: 'url(#pattern-hatch)',
      filter: 'url(#filter-sketch)',
    },
    label: {
      fontFamily: '"Comic Sans MS", "Caveat", cursive',
      fill: '#a26740',
      fontSize: 14,
    },
  },
});

第 3 步:配置边的样式

边的样式也需要应用相同的滤镜,可以在图表初始化配置中统一设置。

const graph = new Graph({
  container: document.getElementById('container'),
  grid: true,
  // ...
  // --- 配置边的连接样式 ---
  connecting: {
    router: 'manhattan',
    createEdge() {
      return new Shape.Edge({
        attrs: {
          line: {
            stroke: '#a26740',
            strokeWidth: 2,
            filter: 'url(#filter-sketch)', // 为边线应用滤镜
            targetMarker: {
              name: 'block', // 箭头样式
              width: 12,
              height: 8,
              // 注意:需要给箭头也加上滤镜和填充/描边
              attrs: {
                  filter: 'url(#filter-sketch)',
              }
            },
          },
        },
      });
    },
  },
});

效果调整

可以实现一个完整的手绘风格主题。如果你想微调效果,可以重点关注:

  • 抖动程度:修改 <filter> 中的 scale 值。值越大,抖动越厉害。
  • 阴影密度:修改 <pattern>width, height 和内部 <path>stroke-width
  • 颜色:直接在 X6 节点的 attrs 中修改 strokefill 颜色,以及在 <pattern> 中修改 stroke 颜色。
  • 字体:为了更好的效果,你可以通过 CSS 引入一些免费的在线手写字体,例如 Google Fonts 上的 CaveatPatrick Hand
❌
❌