999 个小技巧

归档 枯白啃设计(@Kubai087)系列《999 个小技巧,让你 Vibe 的产品更有质感》

已收录 4 条北京时间倒序原文主页 @Kubai087

  1. 004

    004 使用递进式圆角数值 + 连续曲率圆角(超椭圆)

    演示视频 · 竖版
    新栏目《999 个小技巧,让你 Vibe 的产品更有质感》
    
    ✨ 004 使用递进式圆角数值 + 连续曲率圆角(超椭圆)
    
    001–004 都在我主页,关注我,每天一个让产品更有质感的小技巧
    
    👉要点一:当你想让表单、嵌套卡片的各种元素看起来更规整的时候,使用递进式圆角。保证每一层都遵守:
    
    内圆角 = 外圆角 − 间距,内外共用一个圆心
    
    👉要点二:普通圆角接直边的地方有一道折痕,连续圆角过渡更顺(类比 iOS): 
    
    border-radius: 32px
    corner-shape: superellipse(1.25)
    
    Chrome / Edge 139+ 支持;Safari、Firefox 暂不支持,会自动退回普通圆角。
    
    Prompt 如下,可以直接抄 ⬇️
    
    Make the corner radii in this UI consistent, then switch them to continuous corners. Work on the screen or components I name; ask me if I have not named them. If the project has radius tokens, change those instead of hard-coding values.
    
    Inner radius = outer radius - padding (r = R - p)
    Go from the outside in: dialog or card -> panels, inputs, buttons -> chips and tags. A box p inside a container with radius R gets R - p, so both corners share one center. Example: dialog 32, padding 16 -> panels and buttons 16; a chip 8 inside a 16 panel -> 8.
    Boxes on the same level share one radius. Never go below 4.
    
    Pills and circles stay fully round.
    Keep every padding, gap and radius on a 4px grid.
    Continuous corners
    Keep border-radius and add the curve as progressive enhancement: @ supports (corner-shape: superellipse(1.25)) { :where(*) { corner-shape: superellipse(1.25); } } Set corner-shape: round on pills and circles.
    
    Chrome and Edge 139+ render it; Safari and Firefox skip the block and show the plain border-radius.
    When done, list every radius you changed as "element: old -> new".
  2. 003

    003 Dither 与 ASCII 参数化效果

    演示视频 · 竖版
    新栏目《999 个小技巧,让你 Vibe 的产品更有质感》 
    
    ✨ 003 Dither 与 ASCII 参数化效果
    
    001–003 都在我主页,关注我,每天一个让产品更有质感的小技巧
    
    为你的登录页面/卡片添加可控的 Dither 与 ASCII 效果,并附带遮罩、色调、采样强度、大小、间距等可调参数。
    
    两种效果怎么拆: 
    
    ASCII:把画面切成网格,每格按亮度换成一个字符,从 “.” 到 “@” 越来越密。大小是字号,间距是格子疏密,采样强度是明暗对比,取样区域决定字画的是暗部还是亮部。 
    
    Dither:把画面切成像素块,用 4×4 的 Bayer 矩阵当阈值,把每个颜色压成几级。大小是像素块,间距是纹理的尺度,采样强度越高,颜色级数越少,颗粒感越重。 两者都可以加一层遮罩,让画面退后,效果更像纹理而不是噪点。
    
    推荐参数: 
    ASCII:遮罩 90、取样暗部、采样强度 100、大小 37、间距 0 
    Dither:遮罩 88、采样强度 100、大小 15、间距 0
    
    Prompt 如下,可以直接抄 ⬇️
    
    Add an ASCII / ordered-dither effect to one specific image or video in this project, with a live control panel next to it. Use the asset and the place I name (an , a , or a file path or URL, and the page or section it sits in); ask me if I have not named them. Apply the effect to that asset only, in place: keep its size, position, rounded corners and anything layered on top of it. If the project already has an image or canvas effect pipeline, or slider, segmented-control and panel components, build on them; otherwise create a reusable EffectLayer and EffectPanel.
    
    Settings, one object shared by the layer and the panel: { filter: 'ascii' | 'dither', mask, palette, inkTone, strength, size, spacing } Keep separate values per filter, so switching filters brings back that filter's own settings.
    What each control does in each filter, its range, and the recommended value (ASCII / Dither):
    
    Mask effect, 0-100 (90 / 88): a vertical veil over the picture so the effect reads as texture rather than noise. Same in both filters.
    
    Color, Original or one of five palettes (Original): gradient-maps the asset's luminance into one color family before filtering.
    Sample tones, Shadows | Highlights (Shadows; ASCII only, hidden for Dither): which end of the tonal range becomes dense characters.
    Sampling strength, 0-100 (100 / 100). ASCII: contrast of the tone curve and how bright and opaque the characters are. Dither: how few levels each color channel keeps (8 at 0, 2 at 100) and how fully the quantized color replaces the original.
    Size, 0-100 (37 / 15). ASCII: font size, 7-24px. Dither: pixel block, 2-9px.
    Spacing, 0-100 (0 / 0). ASCII: grid step, 8-24px across and 9-26px down, never tighter than the glyphs. Dither: scale of the Bayer pattern, 1-4 blocks per matrix cell.
    Helpers used throughout:
    range(v, a, b) = a + (b - a) * v / 100
    luminance L = (0.299 R + 0.587 G + 0.114 B) / 255
    
    s = strength / 100
    "The box" is the asset's rendered size on the page.
    Sampling:
    Cover-fit the asset into the box and draw it into a small sampling canvas (about 480px on the long side), then read it with getImageData. A sample is the pixel under a point in the box, composited over white.
    With a palette, recolor that canvas first: map each pixel's L through a 256-entry lookup built from the palette's four stops.
    
    Per image or video frame, take about 12,000 evenly spaced samples: low = 8th percentile of L, high = 92nd percentile.
    Shadows means dark ink, Highlights light ink.
    ASCII:
    Characters, from empty to full: " .,:;-=+xX08S#&@" (starts with two spaces).
    Font: round(range(size, 7, 24))px monospace (22 instead of 24 when the box is under 720px wide), centered in each cell.
    
    Grid: x step = max(range(spacing, 8, 24), widest glyph * 1.08); line height = max(range(spacing, 9, 26), height of "@" * 1.12, font * 1.04). The first cell sits half a step in from the edge.
    Tone of a cell: raw = dark ink ? 1 - L : L; normalized = dark ink ? (high - L) / (high - low) : (L - low) / (high - low), clamped 0-1 (use raw alone when high - low < 0.025); mix = 0.72 + 0.14 s; t = raw (1 - mix) + normalized mix. Then a tone curve: black = 0.22 s, white = 1 - 0.08 s, lev = clamp((t - black) / (white - black), 0, 1), tone = lev (1 - s) + smoothstep(lev) s.
    Skip cells with tone <= 0.02. Character = chars[round(tone * 16)]; skip spaces.
    Ink: the sampled color lifted in HSV: value += (1 - value) * (0.24 + 0.18 s + 0.24 tone); saturation *= 1 + 0.16 + 0.2 s (clamped). Alpha = clamp((0.12 + 0.7 tone) (0.72 + 0.28 s), 0.06, 0.94).
    Dither (ordered, Bayer 4x4):
    pixel size = max(2, round(range(size, 2, 9))) (7 instead of 9 under 720px); pattern scale = max(1, round(range(spacing, 1, 4))); levels = max(2, round(range(strength, 8, 2))) per channel; blend = range(strength, 0.32, 1).
    Matrix [[0,8,2,10],[12,4,14,6],[3,11,1,9],[15,7,13,5]]; threshold = (m + 0.5) / 16, indexed by floor(x / pattern scale), floor(y / pattern scale).
    
    For each block sample its center, push saturation away from luma by 1 + 0.06 + 0.18 s, then per channel: scaled = c / 255 * (levels - 1); q = floor(scaled) + (fraction > threshold ? 1 : 0); out = c + (q / (levels - 1) * 255 - c) * blend.
    Write the blocks into a canvas of box / pixel size and draw it scaled up with image smoothing off.
    Mask and layering:
    The mask is a vertical gradient with stops at the top, 42% and the bottom. Base opacities: on a light page white 54/20/38%, on a dark page black 44/20/50%. For a mask value v: stop = v <= 50 ? base * (v / 50)^1.08 : base + (100 - base) * ((v - 50) / 50)^1.08.
    ASCII: draw the (recolored) picture, the mask, then the characters. Dither: draw the dithered picture, then the mask on top.
    
    Changing filter or palette cross-fades from the last frame over 150ms.
    Palettes (four stops at 0, 0.4, 0.74, 1):
    meadow: #2B361F, #677D45, #AEBA78, #E5E1BD
    blue: #192A4D, #326AC5, #A2C2F1, #EDF3FC
    amber: #3C200F, #A74C15, #EF9A46, #F7E7CA
    lavender: #292144, #6552AB, #B3A5E4, #F0ECFB
    
    mono: #151515, #575757, #A6A6A6, #ECECEC
    Rendering:
    Draw into a canvas laid over the asset with the same size and corner radius, at devicePixelRatio capped at 1.6x and about 2048 x 2048.
    Image: redraw when a setting changes (debounced 72ms).
    Video: redraw on every new frame (requestVideoFrameCallback, falling back to requestAnimationFrame) while it plays and is on screen; keep the video element underneath as the source.
    Control panel:
    
    Place it next to the asset, below it when there is room, otherwise beside it, never covering it. About 420px wide, centered on the asset.
    Rows, in order: Mask effect, Filter (two tabs: ASCII | Dither), Color (round swatches), Sample tones (ASCII only), Sampling strength, Size, Spacing (shown with %).
    Each row 46px tall: a 136px label column, then the control. Slider: a 22px track with 8px radius and a filled part, a 9x30 rounded-rectangle thumb (3px radius), a number box with the value centered, and a snap mark at 50. Swatches: 22px circles, each filled with its palette's two middle stops at 135deg (Original: a tiny crop of the asset); the selected one gets a ring with a small gap.
    Every change applies live while dragging. Add a "Reset to recommended" action that restores the values above for the current filter.
    
    Follow the page's light or dark theme and the project's tokens.
  3. 002

    002 少数人才知道的级联菜单小技巧:添加安全区域延时。

    演示视频 · 竖版
    《999 个小技巧,让你 Vibe 的产品更有质感》
    
    ✨ 002 少数人才知道的级联菜单小技巧:添加安全区域延时。
    
    什么是三角区?
    
     鼠标从父菜单斜着移向子菜单时,难免擦过下面几行。普通菜单一碰就切换,子菜单就“跑丢”了。
    
    原理很简单:以鼠标上一刻的位置为顶点,连到子菜单靠近一侧的上下两个角,围成一个三角形。鼠标在三角形里,说明它正朝子菜单走,路过的行一律忽略;离开三角形,或者停留超过 1.2 秒,才切换到新的一行。
    
    另外还有两个延时:悬停 220ms 后才打开子菜单;鼠标离开菜单 360ms 后才关闭。
    
    Prompt 如下,可以直接抄 ⬇️
    
    Define a cascading (nested) menu component that keeps its submenus steady: a safe zone plus three delays, so submenus open on purpose, survive diagonal moves toward them, and do not vanish on a brief slip out of the menu. If the project already has a menu or dropdown component, build this into its existing submenu logic and keep its API, keyboard support and accessibility as they are. Otherwise create one reusable CascadeMenu:
    
    Props: an anchor (the trigger button that opens it on click) and a tree of items, each { label, icon?, href?, children? }. An item with children is a folder row; one without is a plain row that navigates on click.
    Each level shows its folder's name as a small title above its rows.
    Keyboard: arrow keys move through rows, Right/Enter opens a folder, Left goes back a level, Escape closes everything and returns focus to the anchor. Use menu / menuitem roles and aria-expanded on folder rows.
    Behavior, with the values to use:
    
    Open delay: hovering a row schedules it to take effect after 220ms: a folder row opens its submenu, a plain row closes any deeper submenu. Leaving the row before then cancels it. Touch input acts at once.
    Safe zone: while a submenu is open, build a triangle from the pointer's previous position to the two corners of the submenu's near edge (the left edge's top and bottom for a submenu on the right). Track the last two pointermove positions and use the older one as the apex, so the triangle never collapses onto the current point. If the pointer is inside that triangle and within the parent's last min(90px, max(56px, 43% of the parent width)) toward the submenu, it is on its way there: rows it crosses must not highlight or open their own submenus.
    Safe window: a row hovered inside the safe zone does not switch the submenu right away. Recheck every 80ms; switch once the pointer leaves the zone, or when 1200ms have passed since the hover began, so a pointer that simply stops there still gets its new submenu.
    Close delay: when the pointer leaves the anchor, every open level and the safe zone, close the whole menu after 360ms. Coming back in time cancels the close. Clicking outside closes at once.
    Dragging an item over a folder row opens it after 420ms instead of 220ms.
    The pointer inside an open submenu ends all protection. Drive row highlight from component state (hovered, active, skipped), not from CSS :hover, or rows crossed inside the safe zone will still light up.
    Geometry and motion:
    
    Submenus open to the right, overlap their parent by 8px and align their first row with the parent row. Flip to the left when there is no room; the safe zone then uses the submenu's right edge.
    Levels: 210-260px wide, sized to content, max height min(420px, 100vh - 24px) with their own scroll, 6px padding, 12px radius, 1px border, backdrop-filter blur(18px) saturate(135%).
    Rows: 32px tall, 8px radius, 4px apart, 0 8px padding, 8px gap; 18px icon, 14px weight-500 label, a 16px chevron on folder rows. Level title: 11px, weight 600, quiet ink, padding 4px 8px 2px.
    A submenu opens from its parent's edge: from translateX(-8px) scale(0.88, 0.98) and opacity 0, with opacity over 170ms and transform over 360ms on cubic-bezier(0.2, 1.45, 0.35, 1).
    The root level opens the same way from its anchor: from translateY(-6px) scale(0.96, 0.86), top-left origin. Keep the overshoot curve: the slight spring is part of the feel.
    A folder row's icon turns from a closed folder into an open one while its submenu is open, and back when it closes, over 460ms on cubic-bezier(0.22, 1, 0.36, 1). Morph the paths if the two glyphs share their structure; otherwise cross-fade them with a slight lift of the front panel.
    Light and dark themes come from tokens, not separate components:
    
    Light: surface rgba(255,255,255,.96), border rgba(0,0,0,.14), row ink #263140, row hover rgba(15,23,42,.07), quiet ink #7A8491, chevron #94A3B8, shadow 0 18px 34px rgba(15,23,42,.12), 0 2px 4px rgba(15,23,42,.05), inset 0 1px 0 rgba(255,255,255,.74).
    Dark, neutral grays with no blue cast: surface rgba(34,34,34,.96), border rgba(255,255,255,.12), row ink #E4E4E4, row hover rgba(255,255,255,.08), quiet ink #9A9A9A, chevron #8A8A8A, shadow 0 18px 34px rgba(0,0,0,.42), 0 2px 4px rgba(0,0,0,.2), inset 0 1px 0 rgba(255,255,255,.06).
    Monochrome brand icons invert in dark. Switching themes cross-fades colors, borders and shadows over about 500ms.
    Keep every timing value as a named constant in one place, and keep the safe-zone math in a pure, unit-tested helper (point-in-triangle by the sign of the three edge cross products). Add an optional debug overlay that draws the zone (blue fill at 16%, dashed blue outline) and the running timer, off by default.
  4. 001

    001 富有点击手感的拟物按钮

    演示视频 · 竖版
    新栏目《999 个小技巧,让你 Vibe 的产品更有质感》
    
    ✨ 001 富有点击手感的拟物按钮
    
    Prompt 如下,可以直接抄 ⬇️
    
    Define a tactile, skeuomorphic Button component with five variants, from
    strongest to weakest: primary, secondary, tertiary, ghost, text.
    If the project already has a component library or design system, do not
    add a new component: restyle its existing Button through its own variant
    and token system, and keep its API, props and accessibility as they are.
    Otherwise create one reusable Button with `variant` and `size` props.
    
    Style: calm and precise, like the Lumno browser extension and the Cloudflare
    dashboard. Neutral grays only, no brand hues. Lit from straight above.
    No glossy Web 2.0 shine, no glow, no large blur.
    
    Variants:
    1. primary   - near-black slate face (#404859 -> #273042), white label
    2. secondary - raised white key (#FFFFFF -> #F4F5F7), dark gray label
    3. tertiary  - soft gray key (#F3F4F6 -> #E7E9EC), dark gray label
    4. ghost     - no face at rest; faint gray fill on hover; a recessed well
                   when pressed
    5. text      - gray label with a thin underline and an arrow that nudges
                   right on hover
    
    Every filled variant is the same five material layers; only the values
    change, so define them once as tokens and let each variant set its own:
    1. Face: 180deg linear gradient, top 6-8% lighter than bottom.
    2. Edge: 1px solid border one step darker than the bottom of the face
       (primary #12161F, secondary rgba(15,23,42,.14), tertiary rgba(15,23,42,.1)).
    3. Top highlight: inset 0 1px 0 white (18% on slate, 85-100% on light
       faces). Put every inset layer in the button's own box-shadow, before the
       drop layers, never on a pseudo-element overlay: inset shadows on the
       element follow the inner radius (outer radius minus border), so the
       corners line up in every state.
    4. Bottom shade: inset 0 -1px 0 black (22% on slate, 6% on light faces).
    5. Drop shadow in two layers: a tight contact shadow 0 1px 2px plus a soft
       lift 0 4px 10px -3px (18% and 28% on slate, about a third of that on
       light faces).
    
    States (hover, pressed/active, focus-visible, disabled):
    - Hover: face one step lighter; the lift grows to 0 8px 16px -5px.
    - Pressed: translateY(1px); flip the gradient (darker on top); the outer
      shadow collapses to 0 1px 1px; the top highlight becomes an inner shadow,
      inset 0 1px 2px black (40% on slate, 12-16% on light faces). Light faces
      keep a 1px white line below, 0 1px 0 rgba(255,255,255,.9), like a key
      pressed into paper.
    - Give rest and pressed the same number of inset layers (pad with
      inset 0 0 0 transparent) so the shadow animates instead of jumping.
    - Focus-visible and disabled follow the library's existing conventions.
    - Timing: 120ms for transform, 140ms for shadows, 160ms for colors.
    
    Sizes: all values above are for the 14px label size: height 40px,
    padding 0 18px, radius 10px, label weight 600, optional 16px leading icon.
    On the slate face add a label text-shadow 0 -1px 0 rgba(0,0,0,.28).
    Scale the material with the type, never keep it at fixed pixels: express
    every length (height, padding, radius, border, icon, the 1px press offset,
    and every shadow offset, blur and spread) as a multiple of a size unit
    u = label size / 14. Provide two sizes: md (14px label, u = 1px) and
    lg (16px label, u = 16/14 px, so height 45.7px, radius 11.4px, lift shadow
    0 4.6px 11.4px -3.4px).