Самый лучший и самый правильный выход из ситуации: забыть все плохое, сохранить в памяти хорошие моменты, просто сказать спасибо за все что мы пережили. И пойти дальше. Потому что все, что с нами происходит — к лучшему.
24-декабря-2023, 21:18 0 0
Предлагаю Вам сложный CSS-модуль, который позволяет создать рождественскую елку. Он поставляется с несколькими параметрами для настройки. Пользователь может менять цвета, количество лампочек, интенсивность света, повороты и т. д. Это потрясающий трехмерный элемент, который понравится вашим посетителям. Самое замечательное в этом то, что люди могут делиться своими деревьями. Вы можете сделать снимок экрана или видео (вы также можете установить продолжительность).
HTML
<canvas id="canvas"></canvas>
CSS(SCSS)
body {
background: #111;
overflow: hidden;
&.recording {
pointer-events: none;
&::before {
content: '📹 recording…';
position: fixed;
z-index: 999;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
display: inline-block;
width: 15ch;
height: 2em;
line-height: 2em;
text-align: center;
white-space: nowrap;
color: white;
background: rgba(0,0,0,.5);
padding: 1em;
border-radius: 50vh;
font-size: 2vw;
}
canvas {
opacity: .66;
}
}
}
canvas {
width: 100vw;
height: 100vh;
}
// dat.GUI customisation
.dg li.title {
font-weight: bold;
font-size: 14px;
line-height: 30px;
height: 30px;
margin-left: -7px;
}
// BUG
.dg .cr.function .property-name {
width: 100%;
}
JS
// # Default state
let settings = {
background: '#111',
rotationX: 30,
treeShape: 't => t',
'video length [s]': 10
}
let chains = [
{ bulbRadius: 2, bulbsCount: 100, endColor: "#FFC", glowOffset: 0, opacity: 1, startAngle: 0, startColor: "#FFC", turnsCount: 14},
{ bulbRadius: 50, bulbsCount: 20, endColor: "#0FF", glowOffset: 0, opacity: 0.3, startAngle: 120, startColor: "#FF0", turnsCount: 3},
{ bulbRadius: 12, bulbsCount: 50, endColor: "#FF0", glowOffset: 0, opacity: 0.68, startAngle: 240, startColor: "#0FF", turnsCount: -3}
];
// # Global vars
const pixelRatio = window.devicePixelRatio;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let gui = null;
let guiFirstFolder = null;
let guiLastFolder = null;
let rotationZ = 0;
// # Customisation via dat.GUI
function getRandomChain() {
return {
bulbsCount: Math.round(Math.random() * (100 - 10) + 10),
bulbRadius: Math.round(Math.random() * (20 - 1) + 1),
glowOffset: Math.random() < 0.5 ? 0 : Math.round(Math.random() * (20 - 10) + 10),
turnsCount: Math.round(Math.random() * (10 - 3) + 3) * (Math.random() < 0.5 ? -1 : 1),
startAngle: Math.round(Math.random() * 360),
startColor: '#FF0',
endColor: '#0FF',
opacity: Math.round(Math.random() * (100 - 60) + 60) / 100
};
}
const guiMethods = {
'ADD CHAIN': () => {
chains.push(getRandomChain());
updateDatGui();
guiLastFolder.open();
},
'REMOVE CHAIN': null,
removeChain: () => {
const index = guiMethods['REMOVE CHAIN'];
if (!Number.isNaN(parseInt(index))) {
chains.splice(index, 1);
guiMethods['REMOVE CHAIN'] = null;
updateDatGui();
}
},
'📷 Save as image': () => {
CGL.saveAs.PNG(canvas, 'my-christmas-tree');
},
'🎥 Save as video': () => {
const recorder = CGL.saveAs.WEBM(canvas, 'my-christmas-tree');
recorder.start();
document.body.classList.add('recording');
setTimeout(() => {
recorder.stop();
document.body.classList.remove('recording');
}, settings['video length [s]'] * 1000);
}
};
function updateDatGui() {
if (gui) {
gui.destroy();
}
gui = new dat.GUI();
chains.forEach((chain, i) => {
const guiChain = gui.addFolder('🎄 Chain ' + (i+1));
guiChain.add(chains[i], 'bulbsCount', 10, 500, 1);
guiChain.add(chains[i], 'bulbRadius', 1, 100, 1);
guiChain.add(chains[i], 'glowOffset', 0, 100, 1);
guiChain.add(chains[i], 'turnsCount', -50, 50, 1);
guiChain.add(chains[i], 'startAngle', 0, 360, 1);
guiChain.addColor(chains[i], 'startColor');
guiChain.addColor(chains[i], 'endColor');
guiChain.add(chains[i], 'opacity', 0, 1, .01);
if (i === 0) {
guiFirstFolder = guiChain;
} else if (i === chains.length - 1) {
guiLastFolder = guiChain;
}
});
let folders = {...guiMethods.removePlaceholder};
chains.forEach((chain, i) => folders[`Chain ${i+1}`] = i);
const shapes = {
linear: 't => t',
easeInQuad: 't => t*t',
easeOutQuad: 't => t*(2-t)',
easeInOutQuad: 't => (t<.5 ? 2*t*t : -1+(4-2*t)*t)',
easeInCubic: 't => t*t*t'
};
const guiOptions = gui.addFolder('⚙️ Options');
guiOptions.addColor(settings, 'background');
guiOptions.add(settings, 'rotationX', 0, 75, 1);
guiOptions.add(settings, 'treeShape', shapes);
guiOptions.add(guiMethods, 'ADD CHAIN');
guiOptions.add(guiMethods, 'REMOVE CHAIN', folders).onchange(guiMethods.removeChain);
guiOptions.open();
const guiExport = gui.addFolder('💾 Export');
guiExport.add(guiMethods, '📷 Save as image');
guiExport.add(guiMethods, '🎥 Save as video');
guiExport.add(settings, 'video length [s]', 1, 60, 1);
return gui;
}
updateDatGui();
guiFirstFolder.open();
// # Rendering of the tree
function updateScene() {
let {innerWidth: canvasWidth, innerHeight: canvasHeight} = window;
window.tiltAngle = settings.rotationX / 180 * Math.PI;
window.treeHeight = Math.min(canvasWidth, canvasHeight) * .8;
window.baseRadius = treeHeight * .3;
window.baseCenter = {x: canvasWidth/2, y: canvasHeight/2 + treeHeight/2 * Math.cos(tiltAngle) - baseRadius/2 * Math.sin(tiltAngle)};
ctx.canvas.width = canvasWidth * pixelRatio;
ctx.canvas.height = canvasHeight * pixelRatio;
ctx.scale(pixelRatio, pixelRatio);
ctx.fillStyle = settings.background;
ctx.rect(0, 0, canvasWidth, canvasHeight);
ctx.fill();
ctx.lineWidth = 1.1;
}
function renderChain(props) {
for (let i = 0; i < props.bulbsCount; i++) {
let progress = i / (props.bulbsCount - 1);
progress = Math.pow((progress), Math.sqrt(progress) + 1); // just an approximate amendment of the distances between lights
const turnProgress = (progress * props.turnsCount) % 1;
const easing = eval(settings.treeShape); // dat.GUI seems unable to handle functions as values
const sectionRadius = baseRadius * (1 - easing(progress));
const sectionAngle = ((turnProgress * 360 + props.startAngle + rotationZ) / 180 * Math.PI) % (Math.PI*2);
const opacity = Math.min(1, Math.max(0, Math.cos(sectionAngle)) + .2);
const X = baseCenter.x + (Math.sin(sectionAngle) * sectionRadius);
const Y = baseCenter.y - progress * treeHeight * Math.sin((90 - settings.rotationX) / 180 * Math.PI)
+ sectionRadius * Math.sin(tiltAngle) * Math.cos(sectionAngle);
const bulbRadius = props.bulbRadius * treeHeight/1000;
const glowRadius = (props.bulbRadius + props.glowOffset) * treeHeight/1000;
const currentColor = CGL.colorConvert.opacity(
CGL.colorConvert.mixBlendColors(props.startColor, props.endColor, progress), opacity
);
// opacity
ctx.globalAlpha = props.opacity;
// glow circles
if (props.glowOffset > 0) {
const gradient = ctx.createRadialGradient(X, Y, bulbRadius, X, Y, glowRadius);
gradient.addColorStop(0, CGL.colorConvert.opacity(CGL.colorConvert.mixBlendColors(currentColor, '#fff', .3), .5));
gradient.addColorStop(.25, CGL.colorConvert.opacity(currentColor, .6));
gradient.addColorStop(.5, CGL.colorConvert.opacity(currentColor, .3));
gradient.addColorStop(.75, CGL.colorConvert.opacity(currentColor, .125));
gradient.addColorStop(1, CGL.colorConvert.opacity(currentColor, 0));
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(X, Y, glowRadius, 0, 2 * Math.PI);
ctx.fill();
}
// bulbs
ctx.fillStyle = currentColor;
ctx.beginPath();
ctx.arc(X, Y, bulbRadius, 0, 2 * Math.PI);
ctx.fill();
}
}
function render() {
updateScene();
chains.forEach(chain => renderChain(chain));
}
function rotate() {
rotationZ = (rotationZ - 1) % 360;
render();
window.requestAnimationFrame(rotate);
}
rotate();
window.addEventListener('resize', render);
window.addEventListener('orientationchange', render);
Также не забудьте подключить дополнительные скрипты
https://codepen.io/ThiemelJiri/pen/RwNqZKZ.js
https://codepen.io/ThiemelJiri/pen/eYmQEpa.js
https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.6/dat.gui.min.js
https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.js