Expressive Cartoon Face: Animasi SVG Karakter Berambut dengan Ekspresi Interaktif
Daftar Isi
Expressive Cartoon Face adalah ilustrasi vektor animasi yang dikembangkan dari SVG wajah karakter kartun bergaris hitam. Referensi menampilkan kepala membulat, rambut tersisir ke samping, telinga simetris, mata bundar, alis ekspresif, dan mulut melengkung.
Rekonstruksi mempertahankan proporsi dasar tersebut melalui pendekatan Premium Vector Illustration, didukung Cartoon Character Rigging dan Soft 3D Illustration. Warna kulit hangat, rambut cokelat berlapis, pencahayaan lembut, serta detail mata memberikan dimensi visual tanpa menghilangkan karakter sederhana dari desain aslinya.
Keunggulan utama artwork terletak pada artikulasi wajah. Mata dapat berkedip, pupil mengikuti posisi pointer, alis bergerak, mulut berubah ekspresi, dan kepala memiliki gerakan mikro. Rambut serta telinga juga memiliki animasi sekunder yang mengikuti karakter gerakan kepala.
| Parameter | Implementasi | Fungsi |
|---|---|---|
| Objek Utama | Wajah karakter kartun berambut | Mempertahankan identitas SVG input |
| Gaya Artistik | Premium Vector Illustration, Cartoon Character Rigging, Soft 3D Illustration | Menghasilkan karakter ekspresif berlapis |
| Geometri SVG | Path Bézier, ellipse, circle, dan hierarchical groups | Membentuk wajah dan anatomi ekspresi |
| Material Visual | Linear gradient, radial gradient, local highlights | Menciptakan volume semu dan pencahayaan |
| Animasi Utama | Kedipan, perubahan senyum, dan gerakan kepala | Memberikan ekspresi karakter |
| Artikulasi Internal | Mata, pupil, alis, mulut, rambut, telinga | Mendukung gerakan independen |
| Matematika dan Fisika | Interpolasi eksponensial berbasis delta time | Menghaluskan respons pointer |
| JavaScript | Pointer Events dan requestAnimationFrame | Mengendalikan arah pandangan secara dinamis |
| Responsivitas | ViewBox dan CSS min() | Menyesuaikan ukuran artwork |
| Aksesibilitas | Accessible name, reduced motion, visibility handling | Mengurangi animasi sesuai preferensi pengguna |
Rekayasa SVG, Animasi, dan Source Code Lengkap
Artwork menggunakan koordinat SVG 260 × 270, dengan geometri wajah disusun mengikuti referensi 211 × 211. Lapisan telinga berada di belakang kepala, sementara rambut depan menutupi bagian dahi. Mata, alis, dan mulut menggunakan grup terpisah agar dapat dianimasikan tanpa mengubah siluet utama.
Pencahayaan dibangun menggunakan gradient SVG. Warna kulit memiliki area terang di bagian atas dan bayangan di sekitar rahang. Rambut menggunakan beberapa lapisan kurva untuk menghasilkan kesan helaian tersisir.
Animasi deklaratif menggunakan CSS keyframes. JavaScript hanya menangani respons arah pandangan terhadap pointer. Pergerakan pupil dihitung menggunakan interpolasi eksponensial dengan faktor 1 - Math.exp(-k * dt), sehingga kecepatan penyesuaian tidak bergantung langsung pada jumlah frame.
Ketika pointer meninggalkan artwork, pandangan kembali ke tengah. Animasi JavaScript dihentikan ketika dokumen tidak terlihat atau pengguna memilih reduced motion. Interaksi pointer bersifat tambahan; ilustrasi tetap dapat dinikmati tanpa interaksi.
Simpan source code berikut sebagai expressive-cartoon-face.html. Dokumen dapat dibuka langsung di browser tanpa dependency eksternal. Background transparan dan tidak terdapat elemen antarmuka dekoratif.
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Expressive Cartoon Face — SVG Motion Art</title>
<style>
:root {
color-scheme: light dark;
}
html,
body {
margin: 0;
width: 100%;
min-height: 100%;
}
body {
min-height: 100vh;
min-height: 100dvh;
display: grid;
place-items: center;
background: transparent;
overflow: hidden;
}
.ecf-artwork {
width: min(92vw, 90vh, 620px);
width: min(92vw, 90dvh, 620px);
height: auto;
display: block;
overflow: visible;
touch-action: pan-y;
}
.ecf-head {
transform-origin: 130px 135px;
animation: ecf-head-idle 5s ease-in-out infinite;
}
.ecf-hair-top {
transform-origin: 105px 55px;
animation: ecf-hair-motion 5s ease-in-out infinite;
}
.ecf-ear-left {
transform-origin: 51px 131px;
animation: ecf-ear-motion 5s ease-in-out infinite;
}
.ecf-ear-right {
transform-origin: 208px 131px;
animation: ecf-ear-motion 5s ease-in-out
infinite reverse;
}
.ecf-eyelid-left,
.ecf-eyelid-right {
transform-box: fill-box;
transform-origin: center;
animation: ecf-blink 5.7s ease-in-out infinite;
}
.ecf-eyelid-right {
animation-delay: .07s;
}
.ecf-brow-left {
transform-origin: 105px 98px;
animation: ecf-brow-motion 5s ease-in-out infinite;
}
.ecf-brow-right {
transform-origin: 158px 98px;
animation: ecf-brow-motion 5s ease-in-out
infinite reverse;
}
.ecf-mouth {
transform-origin: 132px 187px;
animation: ecf-smile-motion 5s ease-in-out infinite;
}
.ecf-cheek {
animation: ecf-cheek-glow 5s ease-in-out infinite;
}
.ecf-pupil-left,
.ecf-pupil-right {
will-change: transform;
}
@keyframes ecf-head-idle {
0%, 100% {
transform: translateY(0) rotate(0deg);
}
25% {
transform: translateY(-2px) rotate(-1deg);
}
55% {
transform: translateY(1px) rotate(1.3deg);
}
78% {
transform: translateY(-1px) rotate(-.5deg);
}
}
@keyframes ecf-hair-motion {
0%, 100% {
transform: rotate(0deg);
}
35% {
transform: rotate(-1.5deg);
}
70% {
transform: rotate(1deg);
}
}
@keyframes ecf-ear-motion {
0%, 100% {
transform: rotate(0deg);
}
50% {
transform: rotate(2deg);
}
}
@keyframes ecf-blink {
0%, 42%, 46%, 74%, 78%, 100% {
transform: scaleY(0);
}
44%, 76% {
transform: scaleY(1);
}
}
@keyframes ecf-brow-motion {
0%, 100% {
transform: translateY(0);
}
45% {
transform: translateY(-3px);
}
70% {
transform: translateY(1px);
}
}
@keyframes ecf-smile-motion {
0%, 100% {
transform: scaleY(1);
}
45% {
transform: scaleY(1.25);
}
70% {
transform: scaleY(.85);
}
}
@keyframes ecf-cheek-glow {
0%, 100% {
opacity: .32;
}
50% {
opacity: .57;
}
}
@media (prefers-reduced-motion: reduce) {
.ecf-artwork *,
.ecf-artwork {
animation: none !important;
transition: none !important;
}
.ecf-eyelid-left,
.ecf-eyelid-right {
transform: scaleY(0);
}
.ecf-pupil-left,
.ecf-pupil-right {
will-change: auto;
}
}
</style>
</head>
<body>
<svg
class="ecf-artwork"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 260 270"
role="img"
aria-labelledby="ecf-title ecf-desc"
>
<title id="ecf-title">
Expressive Cartoon Face
</title>
<desc id="ecf-desc">
Wajah karakter kartun dengan rambut cokelat
tersisir, mata besar, alis ekspresif, telinga,
dan senyum. Mata mengikuti gerakan pointer
ketika animasi diaktifkan.
</desc>
<defs>
<linearGradient
id="ecf-skin"
x1="0" y1="0"
x2=".8" y2="1"
objectBoundingBox="true"
>
<stop offset="0%" stop-color="#FFE3B8"/>
<stop offset="40%" stop-color="#F7BF91"/>
<stop offset="75%" stop-color="#ECA578"/>
<stop offset="100%" stop-color="#D98966"/>
</linearGradient>
<linearGradient
id="ecf-ear-skin"
x1="0" y1="0"
x2="1" y2="1"
objectBoundingBox="true"
>
<stop offset="0%" stop-color="#FAD1A1"/>
<stop offset="100%" stop-color="#D98C6F"/>
</linearGradient>
<linearGradient
id="ecf-hair"
x1="0" y1="0"
x2=".7" y2="1"
objectBoundingBox="true"
>
<stop offset="0%" stop-color="#A46A48"/>
<stop offset="28%" stop-color="#75472F"/>
<stop offset="70%" stop-color="#4F3028"/>
<stop offset="100%" stop-color="#38251F"/>
</linearGradient>
<linearGradient
id="ecf-hair-highlight"
x1="0" y1="0"
x2="1" y2="1"
objectBoundingBox="true"
>
<stop offset="0%" stop-color="#D59A67"/>
<stop offset="100%" stop-color="#815039"/>
</linearGradient>
<radialGradient id="ecf-cheek-gradient">
<stop offset="0%" stop-color="#E97875"
stop-opacity=".65"/>
<stop offset="100%" stop-color="#E97875"
stop-opacity="0"/>
</radialGradient>
<radialGradient id="ecf-eye-white">
<stop offset="0%" stop-color="#FFFFFF"/>
<stop offset="100%" stop-color="#E8F0F1"/>
</radialGradient>
<radialGradient id="ecf-iris">
<stop offset="0%" stop-color="#33291F"/>
<stop offset="45%" stop-color="#775036"/>
<stop offset="100%" stop-color="#A97848"/>
</radialGradient>
<linearGradient
id="ecf-lip"
x1="0" y1="0"
x2="0" y2="1"
objectBoundingBox="true"
>
<stop offset="0%" stop-color="#A6504C"/>
<stop offset="100%" stop-color="#763C3A"/>
</linearGradient>
<clipPath id="ecf-left-eye-clip">
<ellipse cx="105" cy="133" rx="17" ry="19"/>
</clipPath>
<clipPath id="ecf-right-eye-clip">
<ellipse cx="157" cy="133" rx="17" ry="19"/>
</clipPath>
</defs>
<g class="ecf-head">
<g class="ecf-ear-left">
<path
d="
M58 116
C43 107 29 116 28 132
C26 151 37 163 55 162
C65 161 69 150 68 137
Z"
fill="url(#ecf-ear-skin)"
stroke="#A96B55"
stroke-width="2.5"
/>
<path
d="
M52 126
C41 119 36 129 39 140
C42 149 49 153 56 150"
fill="none"
stroke="#BA786C"
stroke-width="4"
stroke-linecap="round"
/>
<path
d="M37 132 Q40 123 48 125"
fill="none"
stroke="#FFE5C3"
stroke-width="3"
stroke-linecap="round"
opacity=".65"
/>
</g>
<g class="ecf-ear-right">
<path
d="
M202 116
C217 107 231 116 232 132
C234 151 223 163 205 162
C195 161 191 150 192 137
Z"
fill="url(#ecf-ear-skin)"
stroke="#A96B55"
stroke-width="2.5"
/>
<path
d="
M208 126
C219 119 224 129 221 140
C218 149 211 153 204 150"
fill="none"
stroke="#BA786C"
stroke-width="4"
stroke-linecap="round"
/>
<path
d="M223 132 Q220 123 212 125"
fill="none"
stroke="#FFE5C3"
stroke-width="3"
stroke-linecap="round"
opacity=".65"
/>
</g>
<path
d="
M53 78
C53 42 82 22 127 21
C174 19 207 43 207 82
L207 161
C207 209 173 243 130 243
C87 243 53 209 53 161
Z"
fill="url(#ecf-skin)"
stroke="#B77B60"
stroke-width="2.8"
/>
<path
d="
M68 101
C62 143 66 190 88 216
C100 231 116 237 131 237"
fill="none"
stroke="#FFF0D3"
stroke-width="7"
stroke-linecap="round"
opacity=".36"
/>
<path
d="
M193 111
C202 151 192 204 165 225"
fill="none"
stroke="#B66E58"
stroke-width="9"
stroke-linecap="round"
opacity=".14"
/>
<ellipse
class="ecf-cheek"
cx="82"
cy="167"
rx="31"
ry="23"
fill="url(#ecf-cheek-gradient)"
/>
<ellipse
class="ecf-cheek"
cx="180"
cy="167"
rx="31"
ry="23"
fill="url(#ecf-cheek-gradient)"
/>
<path
d="
M122 145
C119 155 115 163 118 169
C122 174 135 175 142 169
C145 166 142 161 139 157"
fill="#E7A078"
stroke="#C8896B"
stroke-width="1.5"
/>
<path
d="
M121 165
Q129 169 138 165"
fill="none"
stroke="#FFE0B2"
stroke-width="2.5"
stroke-linecap="round"
opacity=".8"
/>
<g class="ecf-eye-left">
<ellipse
cx="105"
cy="133"
rx="18"
ry="20"
fill="#704B3C"
opacity=".2"
/>
<ellipse
cx="105"
cy="133"
rx="17"
ry="19"
fill="url(#ecf-eye-white)"
stroke="#774D42"
stroke-width="2"
/>
<g clip-path="url(#ecf-left-eye-clip)">
<g class="ecf-pupil-left">
<circle
cx="105"
cy="134"
r="10.5"
fill="url(#ecf-iris)"
/>
<circle
cx="105"
cy="134"
r="5.5"
fill="#231C1B"
/>
<circle
cx="101"
cy="129"
r="4"
fill="#FFFFFF"
/>
<circle
cx="109"
cy="138"
r="1.7"
fill="#FFFFFF"
opacity=".75"
/>
</g>
</g>
<ellipse
class="ecf-eyelid-left"
cx="105"
cy="133"
rx="17"
ry="19"
fill="#E9AA81"
stroke="#9D6654"
stroke-width="2"
/>
</g>
<g class="ecf-eye-right">
<ellipse
cx="157"
cy="133"
rx="18"
ry="20"
fill="#704B3C"
opacity=".2"
/>
<ellipse
cx="157"
cy="133"
rx="17"
ry="19"
fill="url(#ecf-eye-white)"
stroke="#774D42"
stroke-width="2"
/>
<g clip-path="url(#ecf-right-eye-clip)">
<g class="ecf-pupil-right">
<circle
cx="157"
cy="134"
r="10.5"
fill="url(#ecf-iris)"
/>
<circle
cx="157"
cy="134"
r="5.5"
fill="#231C1B"
/>
<circle
cx="153"
cy="129"
r="4"
fill="#FFFFFF"
/>
<circle
cx="161"
cy="138"
r="1.7"
fill="#FFFFFF"
opacity=".75"
/>
</g>
</g>
<ellipse
class="ecf-eyelid-right"
cx="157"
cy="133"
rx="17"
ry="19"
fill="#E9AA81"
stroke="#9D6654"
stroke-width="2"
/>
</g>
<g class="ecf-brow-left">
<path
d="
M86 103
Q102 96 119 105"
fill="none"
stroke="#513329"
stroke-width="7"
stroke-linecap="round"
/>
<path
d="
M89 101
Q102 97 115 103"
fill="none"
stroke="#A06B49"
stroke-width="2"
stroke-linecap="round"
/>
</g>
<g class="ecf-brow-right">
<path
d="
M143 105
Q160 96 177 103"
fill="none"
stroke="#513329"
stroke-width="7"
stroke-linecap="round"
/>
<path
d="
M147 103
Q161 98 174 101"
fill="none"
stroke="#A06B49"
stroke-width="2"
stroke-linecap="round"
/>
</g>
<g class="ecf-mouth">
<path
d="
M93 187
C109 199 150 201 169 185
C159 204 143 214 130 214
C115 214 101 204 93 187
Z"
fill="url(#ecf-lip)"
stroke="#934C45"
stroke-width="1.5"
/>
<path
d="
M99 190
Q130 203 162 189
Q155 199 130 200
Q110 200 99 190
Z"
fill="#FFF7E9"
/>
<path
d="
M113 207
Q130 201 148 208
Q130 216 113 207
Z"
fill="#DD827D"
opacity=".8"
/>
<path
d="
M91 185
Q95 190 100 190
M164 189
Q169 187 171 183"
fill="none"
stroke="#A75D55"
stroke-width="2"
stroke-linecap="round"
/>
</g>
<g class="ecf-hair-top">
<path
d="
M53 109
C51 88 49 65 57 47
C69 22 97 12 127 11
C162 10 195 23 205 48
C215 71 209 100 207 119
L199 115
C198 91 195 79 186 72
C170 58 146 58 127 61
C110 64 89 66 73 60
C66 69 62 88 61 110
Z"
fill="url(#ecf-hair)"
stroke="#392820"
stroke-width="3"
/>
<path
d="
M57 47
C74 25 105 20 135 23
C165 25 188 37 198 55
C180 47 161 44 140 48
C111 54 83 62 57 47
Z"
fill="url(#ecf-hair-highlight)"
opacity=".9"
/>
<path
d="
M61 40
C78 57 104 57 131 51
C158 45 181 50 199 66"
fill="none"
stroke="#DCA170"
stroke-width="4"
stroke-linecap="round"
opacity=".55"
/>
<path
d="
M66 31
C89 45 111 46 137 39
M77 24
C106 34 129 33 154 30
M105 18
C138 20 171 29 189 43"
fill="none"
stroke="#D29A68"
stroke-width="2.5"
stroke-linecap="round"
opacity=".48"
/>
<path
d="
M57 62
Q69 70 76 67
M58 75
Q67 81 71 79
M199 72
Q204 83 204 96"
fill="none"
stroke="#B47C55"
stroke-width="3"
stroke-linecap="round"
opacity=".5"
/>
<path
d="
M57 47
C68 63 83 66 102 64
C124 62 143 54 165 57
C182 58 194 67 199 83
C190 68 173 64 154 66
C126 69 105 75 82 70
C68 67 60 59 57 47
Z"
fill="#482D26"
opacity=".68"
/>
<path
d="
M62 49
C83 62 106 61 130 55"
fill="none"
stroke="#E1AD79"
stroke-width="2"
stroke-linecap="round"
opacity=".6"
/>
</g>
<path
d="
M64 111
Q64 94 70 80
M198 113
Q198 96 193 85"
fill="none"
stroke="#6A4030"
stroke-width="4"
stroke-linecap="round"
opacity=".45"
/>
<path
d="
M103 229
Q130 240 157 228"
fill="none"
stroke="#FFE5BE"
stroke-width="3"
stroke-linecap="round"
opacity=".52"
/>
</g>
</svg>
<script>
(() => {
'use strict';
const svg = document.querySelector('.ecf-artwork');
const left = svg.querySelector('.ecf-pupil-left');
const right = svg.querySelector('.ecf-pupil-right');
const motionQuery = window.matchMedia(
'(prefers-reduced-motion: reduce)'
);
const state = {
x: 0,
y: 0,
targetX: 0,
targetY: 0,
lastTime: 0,
frame: null
};
const maxOffset = 4.2;
const smoothing = 12;
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
function updateEyes() {
const transform =
`translate(${state.x.toFixed(3)} ` +
`${state.y.toFixed(3)})`;
left.setAttribute('transform', transform);
right.setAttribute('transform', transform);
}
function resetEyes() {
state.x = 0;
state.y = 0;
state.targetX = 0;
state.targetY = 0;
state.lastTime = 0;
updateEyes();
}
function stop() {
if (state.frame !== null) {
cancelAnimationFrame(state.frame);
state.frame = null;
}
state.lastTime = 0;
}
function animate(timestamp) {
state.frame = null;
if (document.hidden || motionQuery.matches) {
return;
}
const dt = state.lastTime
? Math.min(
(timestamp - state.lastTime) / 1000,
0.05
)
: 1 / 60;
state.lastTime = timestamp;
const alpha = 1 - Math.exp(-smoothing * dt);
state.x += (state.targetX - state.x) * alpha;
state.y += (state.targetY - state.y) * alpha;
updateEyes();
const moving =
Math.abs(state.targetX - state.x) > 0.01 ||
Math.abs(state.targetY - state.y) > 0.01;
if (moving) {
state.frame = requestAnimationFrame(animate);
} else {
state.x = state.targetX;
state.y = state.targetY;
updateEyes();
state.lastTime = 0;
}
}
function start() {
if (
state.frame !== null ||
document.hidden ||
motionQuery.matches
) {
return;
}
state.frame = requestAnimationFrame(animate);
}
function handlePointer(event) {
if (motionQuery.matches) {
return;
}
const rect = svg.getBoundingClientRect();
if (!rect.width || !rect.height) {
return;
}
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const dx = (event.clientX - centerX) /
(rect.width / 2);
const dy = (event.clientY - centerY) /
(rect.height / 2);
state.targetX = clamp(
dx * maxOffset,
-maxOffset,
maxOffset
);
state.targetY = clamp(
dy * maxOffset,
-maxOffset,
maxOffset
);
start();
}
function handlePointerLeave() {
state.targetX = 0;
state.targetY = 0;
start();
}
function handleVisibility() {
if (document.hidden) {
stop();
} else if (!motionQuery.matches) {
start();
}
}
function handleMotionChange() {
stop();
resetEyes();
}
svg.addEventListener('pointermove', handlePointer);
svg.addEventListener(
'pointerleave',
handlePointerLeave
);
document.addEventListener(
'visibilitychange',
handleVisibility
);
motionQuery.addEventListener(
'change',
handleMotionChange
);
window.addEventListener('pagehide', stop);
resetEyes();
})();
</script>
</body>
</html>
Dari ilustrasi SVG hitam putih menjadi karakter kartun yang bisa berekspresi!
Expressive Cartoon Face adalah eksperimen SVG Motion Art yang mengembangkan wajah karakter sederhana menjadi ilustrasi vektor dengan material lembut, rambut cokelat berlapis, mata bercahaya, dan ekspresi yang terasa hidup.
Detail menariknya bukan sekadar perubahan warna. Mata karakter dapat mengikuti gerakan pointer, kelopak mata berkedip, alis bergerak, senyum berubah secara halus, dan rambut memberikan gerakan sekunder yang memperkuat kepribadian visualnya.
Seluruh artwork dibuat menggunakan HTML, inline SVG, CSS animation, dan JavaScript native tanpa library eksternal. Desainnya responsif, memiliki background transparan, serta mendukung reduced motion.
Lihat animasinya, eksplorasi detail geometrinya, dan coba source code lengkap untuk mempelajari bagaimana karakter SVG dapat dibuat lebih ekspresif!
#SVGAnimation #CartoonCharacter #CreativeCoding #AnimatedSVG #CharacterAnimation #DigitalIllustration #SVGArt #CSSAnimation #JavaScriptAnimation #VectorIllustration #MotionDesign #InteractiveSVG #FrontendDevelopment #CharacterRigging #WebAnimation
Ditulis dan diedit oleh Ahmad Ismail Al Malik
