<div data-app-container></div>
<div class='message'>
<p>Based on <a target="_blank" href='https://themadeshop.com/work/purple-door-coffee'>Purple Door Coffee Identity</a> by <a target="_blank" href='https://themadeshop.com'>The Made Shop</a></p>
</div>
xxxxxxxxxx
// ---------------------------------------------------------------
// Base
// ---------------------------------------------------------------
:root {
--outer-gap: 2vw;
--color-light-orange: #fcc8af;
--color-rose: #f58c83;
--color-green: #5bba47;
--color-lilac: #d6d0e8;
--color-periwinkle: #739dd2;
--color-purple: #ae68ab;
--color-orange: #f2844b;
--color-aqua: #67c9d3;
}
html,
body {
min-height: 100vh;
background-color: var(--color-bg);
}
html {
font-size: 62.5%;
box-sizing: border-box;
}
body {
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
*, *:before, *:after {
box-sizing: inherit;
}
// ---------------------------------------------------------------
// Objects
// ---------------------------------------------------------------
// General purposes canvas container - applies to all examples
[data-app-container] {
position: absolute;
top: var(--outer-gap);
left: var(--outer-gap);
right: var(--outer-gap);
bottom: var(--outer-gap);
> canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
&:focus {
outline: none;
}
}
}
// ---------------------------------------------------------------
// Message
// ---------------------------------------------------------------
.message {
position: absolute;
bottom: calc(var(--outer-gap) * 2);
left: calc(var(--outer-gap) * 2);
background-color: var(--color-bg);
padding: 2rem;
font-size: 14px;
font-weight: bold;
&, * {
color: var(--color-fg);
}
a {
text-underline-offset: 0.3em;
}
}
xxxxxxxxxx
import Gui from "https://cdn.skypack.dev/@malven/gui@1.6.0";
/**
* Boilerplate module using canvas
*/
type Line = { xStart: number, xEnd: number, yStart: number, yEnd: number };
type Rect = { x: number, y: number, width: number, height: number };
type Point = { x: number, y: number };
type Color = 'light-orange' | 'rose' | 'green' | 'lilac' | 'periwinkle' | 'purple' | 'orange' | 'aqua';
type Colors = {
[key: string]: Color[];
}
class Rays {
// Container
container: HTMLElement | null;
// Rect
rect = {
width: 0.2,
height: 0.5,
};
// Canvas
canvas?: HTMLCanvasElement;
ctx?: CanvasRenderingContext2D | null;
// Set the size
width = 2000;
// Mouse
mouseTarget: Point = { x: 0, y: 0 };
mouseCurrent: Point = { x: 0, y: 0 };
// Time
lastTime = performance.now() / 1000;
time = 0;
colors: Colors = {
orange: ['light-orange', 'rose'],
green: ['lilac', 'green'],
purpleOrange: ['orange', 'purple'],
purple: ['aqua', 'purple'],
lightOrange: ['periwinkle', 'orange'],
};
currentColor!: { fg: string, bg: string };
// Settings
settings = {
rayCount: 50,
timeSpeed: 1,
mouseEase: 0.06,
mouseStrength: 0.35,
strokeWidth: 7,
};
constructor(containerSelector = '[data-app-container]') {
this.container = document.querySelector(containerSelector);
this.init();
}
init = () => {
this.setColor('orange');
this.createGui();
this.createCanvas();
this.addEventListeners();
this.update();
};
createGui = () => {
const gui = new Gui({
midi: window.location.hostname === 'localhost',
});
gui.configureDevice('Midi Fighter Twister');
// Hide initially
gui.hide();
const folder = gui.setFolder('Rays');
folder.open();
gui.add(this.settings, 'rayCount', 5, 150, 1);
gui.add(this.settings, 'timeSpeed', 0.001, 5);
gui.add(this.settings, 'mouseEase', 0.01, 0.3);
gui.add(this.settings, 'mouseStrength', 0.1, 0.5);
gui.add(this.settings, 'strokeWidth', 1, 20);
window.setTimeout(() => {
gui.show();
}, 4000);
};
addEventListeners = () => {
this.container?.addEventListener('mousemove', this.onMouseMove);
// Every five seconds set a random color
setInterval(() => {
const keys = Object.keys(this.colors);
const randomKey = keys[Math.floor(Math.random() * keys.length)];
this.setColor(randomKey as Color);
}, 3000);
};
onMouseMove = (evt: MouseEvent) => {
if (!this.container) return;
this.mouseTarget = {
x: this.mapToRange(evt.clientX, 0, this.container.offsetWidth, -1, 1),
y: this.mapToRange(evt.clientY, 0, this.container.offsetHeight, -1, 1),
};
};
setColor = (color: string) => {
const colorSet = this.colors[color];
this.currentColor = {
bg: getComputedStyle(document.documentElement).getPropertyValue(`--color-${colorSet[0]}`),
fg: getComputedStyle(document.documentElement).getPropertyValue(`--color-${colorSet[1]}`),
};
// Set CSS vars
document.documentElement.style.setProperty('--color-bg', this.currentColor.bg);
document.documentElement.style.setProperty('--color-fg', this.currentColor.fg);
};
createCanvas = () => {
if (!this.container) return;
this.canvas = document.createElement('canvas');
this.container.appendChild(this.canvas);
this.canvas.setAttribute('width', String(this.width));
this.canvas.setAttribute('height', String(this.width));
this.ctx = this.canvas.getContext('2d');
// Resize
window.addEventListener('resize', this.resize);
this.resize();
};
resize = () => {
if (!this.canvas) return;
const winRatio = window.innerHeight / window.innerWidth;
const height = this.width * winRatio;
this.canvas.width = this.width;
this.canvas.height = height;
};
clear = () => {
if (!this.canvas || !this.ctx) return;
this.ctx.fillStyle = this.currentColor.bg;
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
};
updateItems = () => {
if (!this.ctx || !this.canvas || !this.container) return;
// Dimensions
const w = this.canvas.width * this.rect.width;
const h = this.canvas.height * this.rect.height;
const x = this.canvas.width / 2 - w/2;
const y = this.canvas.height / 2 - h/2;
// Draw the rect
const drawInner = false;
if (drawInner) {
this.ctx.fillStyle = `rgba(255, 255, 255, 0.05)`;
this.ctx.beginPath();
this.ctx.rect(x, y, w, h);
this.ctx.fill();
}
// Create the rays
const rays = [];
const rayCount = this.settings.rayCount;
const rayAngle = 360 / rayCount;
for (let i = 0; i < rayCount; i++) {
const angle = rayAngle * (i + this.time*this.settings.timeSpeed);
const radians = angle * (Math.PI / 180);
const xStart = x + w/2;
const yStart = y + h/2;
const xEnd = xStart + Math.cos(radians) * this.canvas.width;
const yEnd = yStart + Math.sin(radians) * this.canvas.height;
const line: Line = { xStart, yStart, xEnd, yEnd };
// Find the intersection point
const intersectionPoint = this.getIntersectionPoint(line, { x, y, width: w, height: h });
if (intersectionPoint) {
line.xStart = intersectionPoint[0];
line.yStart = intersectionPoint[1];
}
// Add the ray line
rays.push(line);
}
this.ctx.strokeStyle = this.currentColor.fg;
this.ctx.lineCap = 'round';
this.ctx.lineWidth = this.settings.strokeWidth;
this.ctx.beginPath();
rays.forEach(ray => {
if (!this.canvas) return;
const offsetX = this.mouseCurrent.x * this.settings.mouseStrength * this.canvas.width;
const offsetY = this.mouseCurrent.y * this.settings.mouseStrength * this.canvas.height;
this.ctx?.moveTo(ray.xStart + offsetX, ray.yStart + offsetY);
this.ctx?.lineTo(ray.xEnd, ray.yEnd);
});
this.ctx.stroke();
};
getIntersectionPoint = (line: Line, rect: Rect): [number, number] | null => {
// Define the four lines of the rectangle
const lines: Line[] = [
{ xStart: rect.x, xEnd: rect.x + rect.width, yStart: rect.y, yEnd: rect.y }, // Top line
{ xStart: rect.x, xEnd: rect.x + rect.width, yStart: rect.y + rect.height, yEnd: rect.y + rect.height }, // Bottom line
{ xStart: rect.x, xEnd: rect.x, yStart: rect.y, yEnd: rect.y + rect.height }, // Left line
{ xStart: rect.x + rect.width, xEnd: rect.x + rect.width, yStart: rect.y, yEnd: rect.y + rect.height }, // Right line
];
const lineSegmentsIntersect = (x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number): [number, number] | false => {
/* eslint-disable camelcase */
const a_dx = x2 - x1;
const a_dy = y2 - y1;
const b_dx = x4 - x3;
const b_dy = y4 - y3;
const s = (-a_dy * (x1 - x3) + a_dx * (y1 - y3)) / (-b_dx * a_dy + a_dx * b_dy);
const t = (+b_dx * (y1 - y3) - b_dy * (x1 - x3)) / (-b_dx * a_dy + a_dx * b_dy);
return (s >= 0 && s <= 1 && t >= 0 && t <= 1) ? [x1 + t * a_dx, y1 + t * a_dy] : false;
/* eslint-enable camelcase */
};
const intersections = null;
for (let i = 0; i < lines.length; i++) {
const intersection = lineSegmentsIntersect(line.xStart, line.yStart, line.xEnd, line.yEnd, lines[i].xStart, lines[i].yStart, lines[i].xEnd, lines[i].yEnd);
if (intersection) return intersection;
}
return intersections;
};
mapToRange = (value: number, inMin: number, inMax: number, outMin: number, outMax: number): number => {
return (value - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
};
update = () => {
// Update time
const now = performance.now() / 1000;
this.time += now - this.lastTime;
this.lastTime = now;
// Update mouse
this.mouseCurrent = {
x: this.mouseCurrent.x + (this.mouseTarget.x - this.mouseCurrent.x) * this.settings.mouseEase,
y: this.mouseCurrent.y + (this.mouseTarget.y - this.mouseCurrent.y) * this.settings.mouseEase,
};
// Update + draw
this.clear();
this.updateItems();
window.requestAnimationFrame(this.update);
};
}
new Rays();
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.