SVG from Zero to Senior · Part 3 — Painting: Fill, Stroke, Gradients & Patterns
Make SVG beautiful: fill and the two fill rules, the whole stroke family (width, linecap, linejoin, dasharray), then reusable linear/radial gradients and tiling patterns defined once in defs. With a live paint playground.
You can now describe any shape (Part 1) and any outline (Part 2). {Giờ con tả được mọi hình (Phần 1) và mọi đường viền (Phần 2).} But a shape with no paint is invisible — time to open the paint box. {Nhưng hình không sơn thì vô hình — đến lúc mở hộp màu.} In SVG, painting splits into two jobs: fill (the inside) and stroke (the outline), and each is far richer than CSS background/border. {Trong SVG, việc sơn chia làm hai: fill (bên trong) và stroke (đường viền), và mỗi cái phong phú hơn background/border của CSS nhiều.}
Tweak the playground — switch the fill source, drag the stroke sliders — then read how each piece works. {Chỉnh playground — đổi nguồn fill, kéo slider stroke — rồi mới đọc cách từng phần hoạt động.}
Open the full demo {Mở demo đầy đủ}: /tools/svg-paint-demo/.
Presentation attributes vs CSS {Thuộc tính trình bày vs CSS}
First, a senior-level fact that saves hours of confusion. {Đầu tiên, một sự thật tầm senior giúp tiết kiệm hàng giờ bối rối.} Paint can be set two ways: {Sơn có thể đặt theo hai cách:}
<!-- presentation attribute -->
<circle fill="red" stroke="black" />
<!-- ...or CSS, which wins over the attribute -->
<circle style="fill: red; stroke: black" />
Presentation attributes act like very weak CSS — any real CSS rule (even a class) overrides them. {Thuộc tính trình bày hoạt động như CSS rất yếu — bất kỳ luật CSS thật nào (kể cả class) cũng ghi đè chúng.} That is why you can theme an icon with .icon { fill: currentColor } even when the SVG file has fill="#000" baked in. {Đó là lý do con có thể đổi màu icon bằng .icon { fill: currentColor } dù file SVG đã nhúng sẵn fill="#000".} We exploit this hard in Part 10. {Ta khai thác mạnh điều này ở Phần 10.}
fill and the two fill rules {fill và hai fill rule}
fill takes any CSS color, none, currentColor, or a url(#id) reference. {fill nhận mọi màu CSS, none, currentColor, hoặc tham chiếu url(#id).} The subtle part is how the inside is decided for self-intersecting shapes, via fill-rule: {Phần tinh tế là bên trong được quyết định thế nào cho các hình tự cắt, qua fill-rule:}
nonzero(default) — counts winding direction; almost always what you want. {đếm hướng cuốn; gần như luôn là cái con muốn.}evenodd— fills based on how many edges a ray crosses; gives “donut hole” stars and overlaps. {tô dựa trên số cạnh một tia cắt qua; cho ra sao kiểu “lỗ bánh donut” và phần chồng.}
<path d="…star…" fill-rule="evenodd" />
Most of the time you ignore this — until a filled star looks solid when you wanted a hole. Now you know the switch. {Phần lớn thời gian con bỏ qua nó — cho tới khi một ngôi sao tô trông đặc dù con muốn có lỗ. Giờ con biết cái công tắc.}
The stroke family {Họ stroke}
stroke is where SVG outshines CSS borders, because the outline follows any path, not just a box. {stroke là nơi SVG vượt border của CSS, vì đường viền đi theo mọi path, không chỉ cái hộp.}
<path d="…"
stroke="#60a5fa"
stroke-width="6"
stroke-linecap="round" /* butt | round | square — the END of an open line */
stroke-linejoin="round" /* miter | round | bevel — where segments meet */
stroke-dasharray="12 6" /* 12 on, 6 off, repeating */
stroke-dashoffset="0" /* shift the dash pattern — the key to line-drawing */
/>
A few things beginners miss: {Vài điều người mới bỏ lỡ:}
- The stroke is centred on the path — half spills inside, half outside. {Stroke nằm giữa đường — nửa tràn vào trong, nửa ra ngoài.}
stroke-linecaponly affects the ends of open subpaths; closed shapes uselinejoineverywhere. {stroke-linecapchỉ ảnh hưởng đầu của subpath hở; hình kín dùnglinejoinkhắp nơi.}stroke-dasharray+stroke-dashoffsetis the trick behind the “drawing itself” animation we build in Part 6. {stroke-dasharray+stroke-dashoffsetlà mánh đằng sau animation “tự vẽ” ta dựng ở Phần 6.} Remember the pair — it pays off later. {Nhớ cặp đôi này — nó có ích về sau.}
defs + url(#id): define once, paint many {defs + url(#id): định nghĩa một lần, sơn nhiều lần}
Gradients and patterns are not colors — they are paint servers you define inside <defs> and reference by id. {Gradient và pattern không phải màu — chúng là paint server con định nghĩa trong <defs> và tham chiếu bằng id.} <defs> holds things that are not drawn directly, only referenced. {<defs> chứa những thứ không vẽ trực tiếp, chỉ để tham chiếu.}
Linear gradient {Gradient tuyến tính}
<defs>
<linearGradient id="brand" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#c8ff00" />
<stop offset="100%" stop-color="#60a5fa" />
</linearGradient>
</defs>
<rect width="100" height="100" fill="url(#brand)" />
x1,y1 → x2,y2 is the gradient’s direction. {x1,y1 → x2,y2 là hướng của gradient.} By default coordinates are objectBoundingBox (0→1 across the shape); switch to gradientUnits="userSpaceOnUse" to use real user coordinates. {Mặc định toạ độ là objectBoundingBox (0→1 trên hình); chuyển gradientUnits="userSpaceOnUse" để dùng toạ độ user thật.}
Radial gradient {Gradient toả tròn}
<radialGradient id="sun" cx="50%" cy="40%" r="60%">
<stop offset="0%" stop-color="#f59e0b" />
<stop offset="100%" stop-color="#ef4444" />
</radialGradient>
cx,cy,r set the circle; add fx,fy to move the highlight off-centre for a 3D sphere look. {cx,cy,r đặt vòng tròn; thêm fx,fy để dời điểm sáng lệch tâm cho hiệu ứng quả cầu 3D.}
Pattern {Pattern}
<pattern id="dots" width="20" height="20" patternUnits="userSpaceOnUse">
<circle cx="10" cy="10" r="4" fill="#34d399" />
</pattern>
<rect width="200" height="200" fill="url(#dots)" />
A pattern tiles a small drawing across the fill — dots, hatching, even a texture image. {Pattern lát một bản vẽ nhỏ khắp vùng fill — chấm, gạch chéo, thậm chí ảnh texture.} The same gradient or pattern id can paint hundreds of shapes; it is defined exactly once. {Cùng một id gradient/pattern sơn được hàng trăm hình; nó chỉ được định nghĩa đúng một lần.}
The master’s warnings {Lời cảnh báo của sư phụ}
filldefaults to black, not transparent. A shape with nofillattribute is solid black, not invisible. Setfill="none"for outline-only. {fillmặc định đen, không trong suốt. Hình không có thuộc tínhfilllà đen đặc, không phải vô hình. Đặtfill="none"nếu chỉ muốn viền.}- CSS beats presentation attributes. If
fill="red"“isn’t working,” a CSS rule is overriding it. {CSS thắng thuộc tính trình bày. Nếufill="red"“không ăn”, có một luật CSS đang ghi đè.} - Ids must be unique on the page. Two
<linearGradient id="g">and the second is ignored — a classic bug when inlining many SVGs. {Id phải duy nhất trên trang. Haiid="g"thì cái thứ hai bị bỏ qua — lỗi kinh điển khi inline nhiều SVG.}
Practice, or it didn’t happen {Luyện tập, không thì coi như chưa học}
- Brand badge {Huy hiệu thương hiệu}: a rounded rect filled with a 2-stop linear gradient and a 2px stroke. {rect bo góc tô gradient tuyến tính 2 stop và stroke 2px.}
- Dashed ring {Vòng nét đứt}: a
<circle>withfill="none", a thick stroke, andstroke-dasharrayto make a dotted loop. Then animatestroke-dashoffsetin DevTools and feel Part 6 coming. {<circle>vớifill="none", stroke dày, vàstroke-dasharrayđể thành vòng chấm. Rồi animatestroke-dashoffsettrong DevTools và cảm nhận Phần 6 sắp tới.} - One gradient, three shapes {Một gradient, ba hình}: define a single gradient in
<defs>and fill a rect, a circle, and a path with it. {định nghĩa một gradient trong<defs>và fill một rect, một circle, một path bằng nó.}
What’s next {Phần tiếp theo}
Shapes, paths, and paint — you can now draw and color anything. {Hình, path, và sơn — giờ con vẽ và tô được mọi thứ.} Next we add words. {Tiếp theo ta thêm chữ.} In Part 4 we master SVG text: precise positioning with x/y/dx/dy, styling spans with <tspan>, and the showpiece — flowing a label along any curve with <textPath>. {Ở Phần 4 ta làm chủ text SVG: định vị chính xác bằng x/y/dx/dy, style span bằng <tspan>, và món đinh — uốn nhãn chạy theo mọi đường cong bằng <textPath>.}