<h1>Pure SCSS trigonometry animation</h1>
<p>The cyan ball is moving using a sine function path without using svg, css-path or JS"</p>
<p>The canvas element is only used for showing the calculated path using JS and confirm that the CSS is working properly.</p>
<div class="box">
<span class="x-axis"></span>
<span class="y-axis"></span>
<span class="marker"></span>
<canvas id="myCanvas"/>
</div>
<h2>Example</h2>
<div class="example">
<div class="animation-area">
<img src="https://upload.wikimedia.org/wikipedia/commons/e/e7/Featured_articles_star_modern.svg" class="star star-1"></img>
<img src="https://upload.wikimedia.org/wikipedia/commons/e/e7/Featured_articles_star_modern.svg" class="star star-2"></img>
</div>
</div>
// Wave keyframe generator based on trigonometry functions.
@mixin keyframes($animation-name, $func, $type: "", $segments: "", $length: "", $amplitude: "", $frequency: "", $mirrored: "") {
will-change: transform;
transform-style: preserve-3d;
backface-visibility: hidden;
// Exterior by default.
$element-width: 0%;
$element-height: 0%;
@if $type == "interior" {
$element-width: 100%;
$element-height: 50%;
}
@if $segments == "" {
$segments: 1;
}
@if $length == "" {
$length: 100cqw;
}
@if $amplitude == "" {
$amplitude: 50cqh;
}
@if $frequency == "" {
$frequency: 1;
}
--frequency: #{$frequency};
@if $mirrored == "" {
$mirrored: -1;
}
@else {
$mirrored: 1;
}
// Take in account that the length and amplitude is reduced by the current element size.
@keyframes #{$animation-name} {
@for $i from 0 through $segments {
$key-frame: ($i / $segments * 100) + '%';
#{$key-frame} {
transform: translate3d(calc((#{$length} - #{$element-width}) * #{$i} / #{$segments}), calc((#{$amplitude} - #{$element-height}) * #{$mirrored} * #{$func}(#{$i} * (360deg * #{$frequency} / #{$segments}))), 0);
}
}
}
}
// Default styles.
* {
box-sizing: border-box;
}
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0;
margin: 0;
min-height: 100vh;
min-height: 100svh;
color: white;
background-color: black;
}
.box {
position: relative;
// This is a "must have" requisite to make work the animation.
container-type: size;
width: 500px;
height: 300px;
margin-top: 1rem;
background-color: #D3D3D3;
margin-bottom: 100px;
}
.x-axis {
position: absolute;
top: 50%;
left: 0;
z-index: 2;
width: 100%;
height: 2px;
background-color: red;
transform: translate(0, -50%);
}
.y-axis {
position: absolute;
top: 0;
left: 0;
z-index: 2;
width: 2px;
height: 100%;
background-color: red;
}
.marker {
position: absolute;
top: calc(50% - 10px);
left: -10px;
z-index: 3;
display: block;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: cyan;
animation-name: sine;
animation-duration: 5s;
animation-timing-function: linear;
animation-iteration-count: infinite;
@include keyframes('sine', sin, 'exterior', 60, '', '', 3, '');
}
canvas {
position: absolute;
width: 100%;
height: 100%;
z-index: 1;
}
// Example.
.example {
width: 100%;
height: 300px;
padding: 50px 0;
margin-bottom: 100px;
background-image: url('https://images.pexels.com/photos/1341279/pexels-photo-1341279.jpeg');
background-size: cover;
background-position: center center;
background-repeat: no-repeat;
overflow: hidden;
}
.animation-area {
position: relative;
// This is a "must have" requisite to make work the animation.
container-type: size;
width: 100%;
height: 100%;
}
.star {
position: absolute;
top: calc(50% - 15px);
left: 0;
z-index: 3;
display: block;
width: 30px;
height: 30px;
animation-duration: 5s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.star-1 {
animation-name: star-1;
@include keyframes('star-1', sin, 'interior', 60, '', '', 3, '');
}
.star-2 {
animation-name: star-2;
@include keyframes('star-2', sin, 'interior', 60, '', '', 3, 'mirrored');
}
View Compiled
// Get the canvas element and its context
const canvas = document.getElementById('myCanvas');
const marker = document.getElementsByClassName('marker')[0];
// Check if canvas and marker exist.
if (canvas && marker) {
const ctx = canvas.getContext('2d');
// Get the CSS variable value of the previous sibling
const computedStyles = getComputedStyle(marker);
const cssVariableValue = parseFloat(computedStyles.getPropertyValue('--frequency'));
// Set the canvas width and height
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
// Calculate the frequency based on canvas size
const frequency = (2 * Math.PI) / canvas.width * cssVariableValue;
// Define the properties for drawing the sine function
const amplitude = canvas.height / 2; // Set the amplitude of the sine wave
const phaseShift = 0; // Set the phase shift of the sine wave
const yOffset = canvas.height / 2; // Set the y offset for centering the sine wave
// Clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Start drawing the sine wave
ctx.beginPath();
ctx.moveTo(0, yOffset);
// Calculate and draw the points of the sine wave
for (let x = 0; x < canvas.width; x++) {
const y = (-1) * amplitude * Math.sin(frequency * x + phaseShift) + yOffset;
ctx.lineTo(x, y);
}
// Set the line color and width
ctx.strokeStyle = 'blue';
ctx.lineWidth = 2;
// Set the line style to dashed
ctx.setLineDash([5, 3]); // The first number represents the length of the dash, and the second number represents the length of the gap
// Draw the sine wave with the dashed line style
ctx.stroke();
}
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.