Как нарисовать и анимировать Деда Мороза?
Меня заинтересовала конкурсная работа участника @OPTIMUS PRIME, где он нарисовал и анимировал Деда Мороза с помощью элемента canvas.
Вот его конкурсный ответ в теме Новогодний конкурс 2021 года!:
"use strict";
let canvas;
// canvas.ctx, canvas.size (квадрат)
let _c, santa, ANIMATE;
/*****/
(function _namespace() {
// canvas mini-lib
_c /* GLOBAL */ = function (ctx) {
return new ContextWrap(ctx);
};
_c.rotateMatrix = function (matrix, centerPoints, angle) {
if (angle === 0) return matrix;
var cx = centerPoints[0];
var cy = centerPoints[1];
var sin = Math.sin(angle * Math.PI / 180);
var cos = Math.cos(angle * Math.PI / 180);
for (var i = 0, len = matrix.length; i < len; i++) {
var px = matrix[i][0];
var py = matrix[i][1];
matrix[i][0] = cos * (px - cx) - sin * (py - cy) + cx;
matrix[i][1] = sin * (px - cx) + cos * (py - cy) + cy;
}
return matrix;
};
_c.rotatePoint = function (point, center, angle) {
if (angle === 0) return point;
var cx = center[0];
var cy = center[1];
var px = point[0];
var py = point[1];
var sin = Math.sin(angle * Math.PI / 180);
var cos = Math.cos(angle * Math.PI / 180);
return [
cos * (px - cx) - sin * (py - cy) + cx,
sin * (px - cx) + cos * (py - cy) + cy
];
};
function ContextWrap(ctx) {
ctx.lineWidth = 1;
this.ctx = ctx;
}
CreatePrototype.call(ContextWrap.prototype);
/***/
function CreatePrototype() {
this.clear = function () {
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
return this;
};
this.fillStyle = function (color) {
this.ctx.fillStyle = color;
return this;
};
this.path = function (matrix) {
var ctx = this.ctx;
ctx.beginPath();
for (var i = 0; i < matrix.length; i++) {
ctx.lineTo(matrix[i][0], matrix[i][1]);
}
ctx.closePath();
ctx.fill();
return this;
};
this.arc = function (cx, cy, r, start, end) {
var ctx = this.ctx;
ctx.beginPath();
ctx.arc(cx, cy, r, start || 0, end || 2 * Math.PI);
ctx.fill();
return this;
};
this.ellipse = function (cx, cy, rx, ry, rotate, start, end) {
var ctx = this.ctx;
ctx.beginPath();
ctx.ellipse(cx, cy, rx, ry, rotate || 0, start || 0, end || 2 * Math.PI);
ctx.fill();
return this;
};
};
})();
/*****/
canvas /* GLOBAL */ = (function () {
let cnv = document.querySelector("#scene");
let ctx = cnv.getContext("2d");
let size = cnv.width = cnv.height = Math.min(innerWidth, innerHeight);
let _data_ = { ctx, size };
let timer = null;
addEventListener("resize", function () {
clearTimeout(timer);
timer = setTimeout(function () {
_data_.size = cnv.width = cnv.height = Math.min(innerWidth, innerHeight);
}, 500);
});
return _data_;
})();
/*****/
function DRAW() {
_c(canvas.ctx).clear();
let _tmp = [santa /* , snow_maiden */];
_tmp.forEach(char => {
char.init_skelet(canvas);
Object.values(char.render)
.sort((a, b) => a.z_index - b.z_index)
.forEach(function (obj) {
char[obj.call](canvas, obj);
});
});
};
/*****/
santa = {};
santa.position = {
// Относительные ключевые точки, домножаются на масштаб и рисуются.
head: {
dx: 0,
dy: 0,
r: 0.15,
},
body: {
bot_x: 0.5,
bot_y: 0.7,
rotate: 0,
},
leg1: {
dx: 1, // 0 → 1
},
leg2: {
dx: 1, // 0 → 1
},
};
santa.skelet = null;
santa.render = {
head: {
z_index: 100,
fill: "#ebba9a",
call: "head",
},
body: {
z_index: 40,
fill: "#c11",
call: "body",
},
hand: {
z_index: 50,
fill: "#c11",
call: "hand",
},
foot: {
z_index: 30,
fill: "#c11",
call: "foot",
},
};
santa.init_skelet = function (cnv) {
let size = cnv.size;
let pos = santa.position;
let skel = santa.skelet = {};
/*** BODY */
let b = pos.body;
let neck_x = b.bot_x * size;
let neck_y = b.bot_y * size - 0.2 * size;
skel.body = _c.rotateMatrix(
[
[b.bot_x * size - 0.1 * size, b.bot_y * size],
[neck_x - 0.1 * size, neck_y],
[neck_x + 0.1 * size, neck_y],
[b.bot_x * size + 0.1 * size, b.bot_y * size],
],
[b.bot_x * size, b.bot_y * size],
b.rotate
);
/*** HEAD */
let h = pos.head;
let head_center = _c.rotatePoint(
[neck_x, neck_y - h.r * size],
[b.bot_x * size, b.bot_y * size],
b.rotate
);
skel.head = {
cx: head_center[0],
cy: head_center[1],
r: h.r * size,
};
/*** EYES */
skel.eyes = {
"1": {
cx: skel.head.cx - 0.04 * size,
cy: skel.head.cy,
rx: size * h.r * 0.25,
ry: size * h.r * 0.35,
},
"2": {
cx: skel.head.cx + 0.04 * size,
cy: skel.head.cy,
rx: size * h.r * 0.25,
ry: size * h.r * 0.35,
}
};
/*** NOSE */
skel.nose = {
cx: skel.head.cx,
cy: skel.head.cy + 0.06 * size,
rx: size * h.r * 0.4,
ry: size * h.r * 0.25,
};
/*** BEARD */
let hx = skel.head.cx;
let hy = skel.head.cy;
skel.beard = [
[hx - size * h.r, hy - 0.06 * size],
[hx - size * h.r * 0.8, hy],
[hx, hy + 0.08 * size],
[hx + size * h.r * 0.8, hy],
[hx + size * h.r, hy - 0.06 * size],
[hx + size * h.r, hy + 0.05 * size],
[hx + size * h.r * 0.8, hy + 0.15 * size],
[hx, hy + 0.25 * size],
[hx - size * h.r * 0.8, hy + 0.15 * size],
[hx - size * h.r, hy + 0.05 * size],
];
/*** HAT_TOP */
skel.hat_top = {
cx: hx,
cy: hy - 0.2 * size,
r: size * h.r * 0.4
};
/*** HAT */
skel.hat = [
[hx - size * h.r, hy - 0.05 * size],
[hx - size * h.r, hy - 0.07 * size],
[hx - size * h.r * 0.6, hy - 0.15 * size],
[hx, hy - 0.2 * size],
[hx + size * h.r * 0.6, hy - 0.15 * size],
[hx + size * h.r, hy - 0.07 * size],
[hx + size * h.r, hy - 0.05 * size],
[hx + size * h.r * 0.6, hy - 0.06 * size],
[hx - size * h.r * 0.6, hy - 0.06 * size],
];
/***/
/*** HANDS */
// shoulder
let sh1_x = skel.body[1][0];
let sh1_y = skel.body[1][1];
skel.hand1 = [
[sh1_x - size * 0.042, sh1_y - size * 0.08],
[sh1_x, sh1_y],
[sh1_x - size * 0.03, sh1_y + size * 0.05],
[sh1_x, sh1_y + size * 0.12],
[sh1_x - size * 0.03, sh1_y + size * 0.18],
[sh1_x - size * 0.09, sh1_y + size * 0.05],
];
let sh2_x = skel.body[2][0];
let sh2_y = skel.body[2][1];
skel.hand2 = [
[sh2_x + size * 0.042, sh2_y - size * 0.08],
[sh2_x, sh2_y],
[sh2_x + size * 0.03, sh2_y + size * 0.05],
[sh2_x, sh2_y + size * 0.12],
[sh2_x + size * 0.03, sh2_y + size * 0.18],
[sh2_x + size * 0.09, sh2_y + size * 0.05],
];
/*** FEET */
let leg1_dx = pos.leg1.dx * 0.05;
let f1_x = skel.body[0][0];
let f1_y = skel.body[0][1];
skel.foot1 = [
[f1_x, f1_y],
[f1_x + size * 0.08, f1_y],
[f1_x + size * 0.09 - size * leg1_dx, f1_y + size * 0.1],
[f1_x + size * 0.1, f1_y + size * 0.24],
[f1_x - size * 0.05, f1_y + size * 0.24],
[f1_x - size * leg1_dx, f1_y + size * 0.1],
];
let leg2_dx = pos.leg2.dx * 0.05;
let f2_x = skel.body[3][0];
let f2_y = skel.body[3][1];
skel.foot2 = [
[f2_x, f2_y],
[f2_x - size * 0.08, f2_y],
[f2_x - size * 0.09 + size * leg2_dx, f2_y + size * 0.1],
[f2_x - size * 0.1, f2_y + size * 0.24],
[f2_x + size * 0.05, f2_y + size * 0.24],
[f2_x + size * leg2_dx, f2_y + size * 0.1],
];
};
/*****/
santa.body = function (cnv, obj) {
_c(cnv.ctx)
.fillStyle(obj.fill)
.path(santa.skelet.body);
};
santa.head = function (cnv, obj) {
let skel = santa.skelet;
let { cx, cy, r } = skel.head;
_c(cnv.ctx)
.fillStyle(obj.fill)
.arc(cx, cy, r);
santa.beard(cnv);
santa.eyes(cnv);
santa.nose(cnv);
santa.hat(cnv);
};
santa.beard = function (cnv) {
_c(cnv.ctx)
.fillStyle("#fff")
.path(santa.skelet.beard);
};
santa.eyes = function (cnv) {
let e = santa.skelet.eyes;
_c(cnv.ctx)
.fillStyle("#123")
.ellipse(e[1].cx, e[1].cy, e[1].rx, e[1].ry)
.ellipse(e[2].cx, e[2].cy, e[2].rx, e[2].ry);
};
santa.nose = function (cnv) {
let n = santa.skelet.nose;
_c(cnv.ctx)
.fillStyle("#a00")
.ellipse(n.cx, n.cy, n.rx, n.ry);
};
santa.hat = function (cnv) {
let t = santa.skelet.hat_top;
_c(cnv.ctx)
.fillStyle("#fff")
.arc(t.cx, t.cy, t.r)
.fillStyle("#c11")
.path(santa.skelet.hat);
};
//...
santa.hand = function (cnv) {
_c(cnv.ctx)
.fillStyle("#c11")
.path(santa.skelet.hand1)
.path(santa.skelet.hand2);
};
santa.foot = function (cnv) {
_c(cnv.ctx)
.fillStyle("#c11")
.path(santa.skelet.foot1)
.path(santa.skelet.foot2);
};
/*****/
ANIMATE /* GLOBAL */ = {
// 1. ANIMATE.add(obj) — заворачивает obj в функию,
// 2. Добавляет её в хранилище _fn_storage,
// 3. На каждом animationFrame оттуда циклом вызываются все функции
// 4. На основе данных из obj, каждая функция меняет анимируемые значения
// (сколько необходимо за один кадр)
_unique_key: 0,
_fn_storage: {},
add: function (obj) {
this._fn_storage[obj._self = this._unique_key++] = this._get_closure_fn(obj);
return this;
},
_get_closure_fn: function (obj) {
/* obj = {
root: santa.position.hand,
prop: "rotate",
chain: [
{to: -90, ms: 1000},
{sleep: 1000, callback: fn},
{to: 70, ms: 1000, callback: fn},
],
} */
/***/
let i = 0, curr = obj.chain[i];
let change_per_ms; // {Number} : изменение анимируемого значения за 1 мс.
let reached_end; // {Function → return Boolean}
_update_params();
return function _animate(dt) {
/*** dt {Number} : Время, прошедшее после предыдущего вызова animationFrame ***/
if (curr._sleeping) return;
/*** Будет прерываться, пока не пройдет curr.sleep миллисекунд ***/
if (curr.sleep) {
curr._sleeping = true;
setTimeout(
function () {
curr._sleeping = false;
next();
},
curr.sleep
);
return;
}
let newVal = obj.root[obj.prop] + change_per_ms * dt;
if (reached_end(newVal)) {
obj.root[obj.prop] = curr.to;
next();
return;
}
obj.root[obj.prop] = newVal;
/***/
function next() {
if (curr.callback) curr.callback(obj);
i = typeof curr.goto == "number" ? curr.goto : i + 1;
curr = obj.chain[i];
if (!curr) return ANIMATE.remove(obj);
_update_params();
}
}
/***/
function _update_params() {
if (curr.sleep) return;
let from = curr.from || obj.root[obj.prop];
change_per_ms = (curr.to - from) / curr.ms;
reached_end = (from > curr.to) ?
(newVal) => newVal <= curr.to :
(newVal) => newVal >= curr.to;
}
},
remove: function (obj) {
delete this._fn_storage[obj._self];
},
};
/*****/
ANIMATE.add({
root: santa.position.leg1,
prop: "dx",
chain: [
{ to: -0.2, ms: 138 },
{ to: 1, ms: 276, goto: 0 },
],
}).add({
root: santa.position.leg2,
prop: "dx",
chain: [
{ to: -0.2, ms: 138 },
{ to: 1, ms: 276, goto: 0 },
],
});
ANIMATE.start = function () {
let k = 0, last_call = null;
let loop = (time) => {
if (k++ % 2) return requestAnimationFrame(loop); // 30 fps
let dt = time - last_call;
for (let fn in this._fn_storage) {
this._fn_storage[fn](dt);
}
DRAW();
last_call = time;
requestAnimationFrame(loop);
};
requestAnimationFrame(function (init_time) {
last_call = init_time;
requestAnimationFrame(loop);
});
};
ANIMATE.start();
* { margin: 0; padding: 0 } canvas { background-color: #ddd }
<canvas id="scene"></canvas>
Автор: @OPTIMUS PRIME
Вопрос: возможно ли нарисовать и воспроизвести подобную анимацию, используя любые другие средства и технологии для рисования и создания эффекта анимации и трансформации, указанные в метках вопроса?
Ответы (2 шт):
Вариант CSS:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.container {
width: 100vh;
height: 105vh;
background-color: #ddd;
}
.ded {
margin-top: -5vh;
position: relative;
}
.ded_body {
width: 20vh;
height: 40vh;
position: absolute;
top: 30vh;
left: 40vh;
background-color: #c11;
}
.ded_head {
width: 30vh;
height: 30vh;
position: absolute;
top: 20vh;
left: 35vh;
border-radius: 100%;
background-color: #ebba9a;
}
.ded_head:before {
content: "";
display: block;
width: 12vh;
height: 12vh;
position: absolute;
top: -12vh;
left: 9vh;
border-radius: 100%;
background-color: #fff;
}
.ded_head:after {
content: "";
display: block;
width: 30vh;
height: 15vh;
position: absolute;
top: -5vh;
left: 0vh;
background-color: #c11;
-webkit-clip-path: polygon(50% 0%, 75% 25%, 100% 90%, 100% 100%, 75% 90%, 25% 90%, 0% 100%, 0% 90%, 25% 25%);
clip-path: polygon(50% 0%, 75% 25%, 100% 90%, 100% 100%, 75% 90%, 25% 90%, 0% 100%, 0% 90%, 25% 25%);
}
.ded_beard {
width: 30vh;
height: 30vh;
position: absolute;
top: 10vh;
left: 0vh;
background-color: #fff;
-webkit-clip-path: polygon(0% 0%, 12% 15%, 50% 40%, 88% 15%, 100% 0%, 100% 30%, 90% 70%, 50% 100%, 10% 70%, 0% 30%);
clip-path: polygon(0% 0%, 12% 15%, 50% 40%, 88% 15%, 100% 0%, 100% 30%, 90% 70%, 50% 100%, 10% 70%, 0% 30%);
}
.ded_eyes {
width: 8vh;
height: 11vh;
position: absolute;
top: 9vh;
left: 7vh;
border-radius: 100%;
background-color: #123;
}
.ded_eyes:before {
content: "";
display: block;
width: 8vh;
height: 11vh;
position: absolute;
top: 0vh;
left: 8.5vh;
border-radius: 100%;
background-color: #123;
}
.ded_nose {
width: 12vh;
height: 8vh;
position: absolute;
top: 17vh;
left: 9.5vh;
border-radius: 100%;
background-color: #a00;
}
.ded_hand_right {
width: 14vh;
height: 32vh;
position: absolute;
top: 38vh;
left: 26vh;
background-color: #c11;
-webkit-clip-path: polygon(100% 0%, 100% 40%, 60% 55%, 100% 80%, 75% 100%, 0% 50%);
clip-path: polygon(100% 0%, 100% 40%, 60% 55%, 100% 80%, 75% 100%, 0% 50%);
}
.ded_hand_left {
width: 14vh;
height: 32vh;
position: absolute;
top: 38vh;
left: 60vh;
background-color: #c11;
-webkit-clip-path: polygon(0% 0%, 100% 50%, 25% 100%, 0% 80%, 40% 55%, 0% 40%);
clip-path: polygon(0% 0%, 100% 50%, 25% 100%, 0% 80%, 40% 55%, 0% 40%);
}
.ded_leg_right {
width: 14vh;
height: 30vh;
position: absolute;
top: 70vh;
left: 36vh;
background-color: #c11;
animation: 0.5s infinite linear ded_leg_right;
}
.ded_leg_left {
width: 14vh;
height: 30vh;
position: absolute;
top: 70vh;
left: 50vh;
background-color: #c11;
animation: 0.5s infinite linear ded_leg_left;
}
@keyframes ded_leg_right {
0% {
-webkit-clip-path: polygon(4vh 0%, 85% 0%, 50% 50%, 100% 100%, 0% 100%, 0% 50%);
clip-path: polygon(4vh 0%, 85% 0%, 50% 50%, 100% 100%, 0% 100%, 0% 50%);
}
40% {
-webkit-clip-path: polygon(4vh 0%, 85% 0%, 100% 50%, 100% 100%, 0% 100%, 50% 50%);
clip-path: polygon(4vh 0%, 85% 0%, 100% 50%, 100% 100%, 0% 100%, 50% 50%);
}
100% {
-webkit-clip-path: polygon(4vh 0%, 85% 0%, 50% 50%, 100% 100%, 0% 100%, 0% 50%);
clip-path: polygon(4vh 0%, 85% 0%, 50% 50%, 100% 100%, 0% 100%, 0% 50%);
}
}
@keyframes ded_leg_left {
0% {
-webkit-clip-path: polygon(15% 0, calc(100% - 4vh) 0%, 100% 50%, 100% 100%, 0% 100%, 50% 50%);
clip-path: polygon(15% 0, calc(100% - 4vh) 0%, 100% 50%, 100% 100%, 0% 100%, 50% 50%);
}
40% {
-webkit-clip-path: polygon(15% 0, calc(100% - 4vh) 0%, 50% 50%, 100% 100%, 0% 100%, 0% 50%);
clip-path: polygon(15% 0, calc(100% - 4vh) 0%, 50% 50%, 100% 100%, 0% 100%, 0% 50%);
}
100% {
-webkit-clip-path: polygon(15% 0, calc(100% - 4vh) 0%, 100% 50%, 100% 100%, 0% 100%, 50% 50%);
clip-path: polygon(15% 0, calc(100% - 4vh) 0%, 100% 50%, 100% 100%, 0% 100%, 50% 50%);
}
}
<div class="container">
<div class="ded">
<div class="ded_body"></div>
<div class="ded_hand_right"></div>
<div class="ded_hand_left"></div>
<div class="ded_head">
<div class="ded_beard"></div>
<div class="ded_eyes"></div>
<div class="ded_nose"></div>
</div>
<div class="ded_leg_right"></div>
<div class="ded_leg_left"></div>
</div>
</div>
Упрощенная версия на том же canvas:
let santa_map = [ // Относительные координаты (размер стороны холста считается 1)
{ // Шарик на шапке
color: "#fff",
draw: "arc",
data: {
cx: 0.5,
cy: 0.15,
r: 0.062,
},
},
{ // Голова
color: "#ebba9a",
draw: "arc",
data: {
cx: 0.5,
cy: 0.35,
r: 0.15,
},
},
{ // Тело, руки, ноги
color: "#c11",
draw: "path",
data: [
[0.40, 0.50], // плечо (внутри)
[0.37, 0.55], // Внутрь локтя
[0.40, 0.62],
[0.37, 0.68],
[0.31, 0.55],
[0.36, 0.42],
[0.40, 0.50], // плечо
[0.40, 0.70],
[0.40, 0.80], // Колено (наружная сторона, аним: 0.40 → 0.35)
[0.35, 0.94],
[0.50, 0.94],
[0.50, 0.80], // Колено (внутренняя, аним: 0.50 → 0.45)
[0.48, 0.70],
[0.50, 0.70],
[0.50, 0.50], // шея
],
},
{ // Борода
color: "#fff",
draw: "path",
data: [
[0.35, 0.29],
[0.38, 0.35],
[0.50, 0.43],
[0.50, 0.60],
[0.38, 0.50],
[0.35, 0.40]
],
},
{ // Шапка
color: "#c11",
draw: "path",
data: [
[0.35, 0.30],
[0.35, 0.28],
[0.41, 0.20],
[0.50, 0.15],
[0.50, 0.29],
[0.41, 0.29],
],
},
{ // Глаза
color: "#123",
draw: "ellipse",
data: {
cx: 0.46,
cy: 0.35,
rx: 0.0375,
ry: 0.0525,
},
},
{ // Нос
color: "#a00",
draw: "ellipse",
data: {
cx: 0.50,
cy: 0.41,
rx: 0.06,
ry: 0.0375,
},
},
];
let draw = (function () {
// return : fn
// use : draw(santa_map)
let cnv1 = document.querySelector("#side-1");
let cnv2 = document.querySelector("#side-2");
let size;
let ctx_list = [
cnv1.getContext("2d"),
cnv2.getContext("2d"),
];
function update_size() {
size = Math.min(innerHeight, innerWidth);
cnv1.width = cnv2.width = size / 2;
cnv1.height = cnv2.height = size;
}
update_size();
addEventListener("resize", update_size);
/***/
function _draw(santa_map) {
ctx_list[0].clearRect(0, 0, cnv1.width, cnv1.height);
ctx_list[1].clearRect(0, 0, cnv2.width, cnv2.height);
santa_map.forEach(obj => {
ctx_list.forEach(ctx => {
ctx.fillStyle = obj.color;
_draw[obj.draw](ctx, obj.data);
ctx.fill();
});
});
}
_draw.arc = function (ctx, d /* data - obj */) {
ctx.beginPath();
ctx.arc(
d.cx * size, d.cy * size, d.r * size,
d.from || 0, d.to || 2 * Math.PI
);
};
_draw.ellipse = function (ctx, d) {
ctx.beginPath();
ctx.ellipse(
d.cx * size, d.cy * size, d.rx * size, d.ry * size,
d.rotate || 0, d.from || 0, d.to || 2 * Math.PI
);
};
_draw.path = function (ctx, matrix) {
ctx.beginPath();
for (let i = 0; i < matrix.length; i++) {
ctx.lineTo(matrix[i][0] * size, matrix[i][1] * size);
}
ctx.closePath();
};
return _draw;
})();
(function animate() {
let leg_x_outer = santa_map[2].data[8][0];
let leg_x_inner = santa_map[2].data[11][0];
let dx = 0; // принимает значения от 0 до -0.05
let sign = -1;
(function _loop() {
dx = dx + 0.004 * sign;
if (dx >= 0 || dx <= -0.05) sign *= -1;
santa_map[2].data[ 8][0] = leg_x_outer + dx;
santa_map[2].data[11][0] = leg_x_inner + dx;
draw(santa_map);
requestAnimationFrame(_loop);
})();
})();
body {
padding: 0;
display: flex;
}
canvas {
background: #ddd;
}
<canvas id="side-1"></canvas>
<canvas id="side-2" style="transform: scaleX(-1);"></canvas>
Т.к. анимация симметричная и изменений не предвидится, вместо одного квадрата, взял два canvas и поставил рядом. В коде прописана только левая часть деда, а правая часть зеркально дублируется во второй холст: transform: scaleX(-1)
Причина переусложненности начальной версии: Изначально планировался полноценный танец, в процессе задолбался прописывать координаты и знатно схалтурил!) Такой подход изначально придуман для более сложных анимаций: