From 3ddc7c79ac62775208e0554e42ffe62ebd85111e Mon Sep 17 00:00:00 2001 From: Luciano Ratamero Date: Sat, 17 Aug 2024 15:53:08 -0300 Subject: [PATCH] Reworks the CRT effect --- package-lock.json | 6 + package.json | 1 + src/routes/effects/+page.svelte | 54 +++- src/routes/effects/crt/index.js | 496 +++++++++++++++++++++++++++++++ src/routes/effects/crt/main.scss | 273 +++++++++++++++++ 5 files changed, 822 insertions(+), 8 deletions(-) create mode 100644 src/routes/effects/crt/index.js create mode 100644 src/routes/effects/crt/main.scss diff --git a/package-lock.json b/package-lock.json index ec2f14f..5f816c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "comfy.js": "^1.1.16", + "dat.gui": "^0.7.9", "lodash-es": "^4.17.21", "nice-color-palettes": "^3.0.0", "tmi.js": "^1.8.5" @@ -1371,6 +1372,11 @@ "node": ">=4" } }, + "node_modules/dat.gui": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/dat.gui/-/dat.gui-0.7.9.tgz", + "integrity": "sha512-sCNc1OHobc+Erc1HqiswYgHdVNpSJUlk/Hz8vzOCsER7rl+oF/4+v8GXFUyCgtXpoCX6+bnmg07DedLvBLwYKQ==" + }, "node_modules/debug": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", diff --git a/package.json b/package.json index 2df7ae5..005a1cb 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "type": "module", "dependencies": { "comfy.js": "^1.1.16", + "dat.gui": "^0.7.9", "lodash-es": "^4.17.21", "nice-color-palettes": "^3.0.0", "tmi.js": "^1.8.5" diff --git a/src/routes/effects/+page.svelte b/src/routes/effects/+page.svelte index 82640ca..90ff787 100644 --- a/src/routes/effects/+page.svelte +++ b/src/routes/effects/+page.svelte @@ -1,12 +1,18 @@ {#if crt_effect_enabled} - CRT background effect +
{/if} {#if confetti_effect_enabled} @@ -60,9 +104,3 @@ class="fixed h-full w-full bg-cover" > {/if} - - diff --git a/src/routes/effects/crt/index.js b/src/routes/effects/crt/index.js new file mode 100644 index 0000000..4cac89d --- /dev/null +++ b/src/routes/effects/crt/index.js @@ -0,0 +1,496 @@ +// @ts-nocheck + +// this is a heavily modified version of the original code +// thanks to Mobius1 (Karl Saunders) for the original version +// https://codepen.io/Mobius1/pen/ZNgwbr + +function getRandomInt(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +export class ScreenEffect { + constructor(parent, options) { + this.parent = parent; + if (typeof parent === 'string') { + this.parent = document.querySelector(parent); + } + + this.config = Object.assign( + {}, + { + // + }, + options + ); + + this.effects = {}; + + this.events = { + resize: this.onResize.bind(this) + }; + + window.addEventListener('resize', this.events.resize, false); + + this.render(); + } + + render() { + const container = document.createElement('div'); + container.classList.add('screen-container'); + + const wrapper1 = document.createElement('div'); + wrapper1.classList.add('screen-wrapper'); + + const wrapper2 = document.createElement('div'); + wrapper2.classList.add('screen-wrapper'); + + const wrapper3 = document.createElement('div'); + wrapper3.classList.add('screen-wrapper'); + + wrapper1.appendChild(wrapper2); + wrapper2.appendChild(wrapper3); + + container.appendChild(wrapper1); + + this.parent.parentNode.insertBefore(container, this.parent); + wrapper3.appendChild(this.parent); + + this.nodes = { container, wrapper1, wrapper2, wrapper3 }; + + this.onResize(); + } + + onResize(e) { + this.rect = this.parent.getBoundingClientRect(); + + if (this.effects.vcr && !!this.effects.vcr.enabled) { + this.generateVCRNoise(); + } + } + + add(type, options) { + const config = Object.assign( + {}, + { + fps: 30, + blur: 1 + }, + options + ); + + if (Array.isArray(type)) { + for (const t of type) { + this.add(t); + } + + return this; + } + + const that = this; + + if (type === 'snow') { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + canvas.classList.add(type); + canvas.width = this.rect.width / 2; + canvas.height = this.rect.height / 2; + canvas.style.opacity = config.opacity; + + this.nodes.wrapper2.appendChild(canvas); + + animate(); + // that.generateSnow(ctx); + + function animate() { + that.generateSnow(ctx); + that.snowframe = requestAnimationFrame(animate); + } + + this.effects[type] = { + wrapper: this.nodes.wrapper2, + node: canvas, + enabled: true, + config + }; + + return this; + } + + if (type === 'roll') { + return this.enableRoll(); + } + + if (type === 'vcr') { + const canvas = document.createElement('canvas'); + canvas.classList.add(type); + this.nodes.wrapper2.appendChild(canvas); + + canvas.width = this.rect.width; + canvas.height = this.rect.height; + + this.effects[type] = { + wrapper: this.nodes.wrapper2, + node: canvas, + ctx: canvas.getContext('2d'), + enabled: true, + config + }; + + this.generateVCRNoise(); + + return this; + } + + let node = false; + let wrapper = this.nodes.wrapper2; + + switch (type) { + case 'wobblex': + case 'wobbley': + wrapper.classList.add(type); + break; + case 'scanlines': + node = document.createElement('div'); + node.classList.add(type); + wrapper.appendChild(node); + break; + case 'vignette': + wrapper = this.nodes.container; + node = document.createElement('div'); + node.classList.add(type); + wrapper.appendChild(node); + break; + case 'image': + wrapper = this.parent; + node = document.createElement('img'); + node.classList.add(type); + + node.src = config.src; + + wrapper.appendChild(node); + break; + case 'video': + wrapper = this.parent; + node = document.createElement('video'); + node.classList.add(type); + + node.src = config.src; + node.crossOrigin = 'anonymous'; + node.autoplay = true; + node.muted = true; + node.loop = true; + wrapper.appendChild(node); + break; + } + + this.effects[type] = { + wrapper, + node, + enabled: true, + config + }; + + return this; + } + + remove(type) { + const obj = this.effects[type]; + if (type in this.effects && !!obj.enabled) { + obj.enabled = false; + + if (type === 'roll' && obj.original) { + this.parent.appendChild(obj.original); + } + + if (type === 'vcr') { + clearInterval(this.vcrInterval); + } + + if (type === 'snow') { + cancelAnimationFrame(this.snowframe); + } + + if (obj.node) { + obj.wrapper.removeChild(obj.node); + } else { + obj.wrapper.classList.remove(type); + } + } + + return this; + } + + enableRoll() { + const el = this.parent.firstElementChild; + + if (el) { + const div = document.createElement('div'); + div.classList.add('roller'); + + this.parent.appendChild(div); + div.appendChild(el); + div.appendChild(el.cloneNode(true)); + + // if ( this.effects.vcr.enabled ) { + // div.appendChild(this.effects.vcr.node); + // } + + this.effects.roll = { + enabled: true, + wrapper: this.parent, + node: div, + original: el + }; + } + } + + generateVCRNoise() { + const canvas = this.effects.vcr.node; + const config = this.effects.vcr.config; + const div = this.effects.vcr.node; + + if (config.fps >= 60) { + cancelAnimationFrame(this.vcrInterval); + const animate = () => { + this.renderTrackingNoise(); + this.vcrInterval = requestAnimationFrame(animate); + }; + + animate(); + } else { + clearInterval(this.vcrInterval); + this.vcrInterval = setInterval(() => { + this.renderTrackingNoise(); + }, 1000 / config.fps); + } + } + + // Generate CRT noise + generateSnow(ctx) { + var w = ctx.canvas.width, + h = ctx.canvas.height, + d = ctx.createImageData(w, h), + b = new Uint32Array(d.data.buffer), + len = b.length; + + for (var i = 0; i < len; i++) { + b[i] = ((255 * Math.random()) | 0) << 24; + } + + ctx.putImageData(d, 0, 0); + } + + renderTrackingNoise(radius = 2, xmax, ymax) { + const canvas = this.effects.vcr.node; + const ctx = this.effects.vcr.ctx; + const config = this.effects.vcr.config; + let posy1 = config.miny || 0; + let posy2 = config.maxy || canvas.height; + let posy3 = config.miny2 || 0; + const num = config.num || 20; + + if (xmax === undefined) { + xmax = canvas.width; + } + + if (ymax === undefined) { + ymax = canvas.height; + } + + canvas.style.filter = `blur(${config.blur}px)`; + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = `#fff`; + + ctx.beginPath(); + for (var i = 0; i <= num; i++) { + var x = Math.random(i) * xmax; + var y1 = getRandomInt((posy1 += 3), posy2); + var y2 = getRandomInt(0, (posy3 -= 3)); + ctx.fillRect(x, y1, radius, radius); + ctx.fillRect(x, y2, radius, radius); + ctx.fill(); + + this.renderTail(ctx, x, y1, radius); + this.renderTail(ctx, x, y2, radius); + } + ctx.closePath(); + } + + renderTail(ctx, x, y, radius) { + const n = getRandomInt(1, 50); + + const dirs = [1, -1]; + let rd = radius; + const dir = dirs[Math.floor(Math.random() * dirs.length)]; + for (let i = 0; i < n; i++) { + const step = 0.01; + let r = getRandomInt((rd -= step), radius); + let dx = getRandomInt(1, 4); + + radius -= 0.1; + + dx *= dir; + + ctx.fillRect((x += dx), y, r, r); + ctx.fill(); + } + } + + start() { + for (const prop in this.config.effects) { + if (!!this.config.effects[prop].enabled) { + this.add(prop, this.config.effects[prop].options); + } + } + this.render(); + } +} + +export async function initGUI(screen, config) { + const dat = await import('dat.gui'); + const gui = new dat.GUI(); + + config = config || { + effects: { + roll: { + enabled: false, + options: { + speed: 1000 + } + }, + image: { + enabled: false, + options: { + src: 'https://images.unsplash.com/photo-1505977404378-3a0e28ec6488?ixlib=rb-1.2.1&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjE0NTg5fQ', + blur: 1.2 + } + }, + vignette: { enabled: true }, + scanlines: { enabled: true }, + vcr: { + enabled: true, + options: { + opacity: 1, + miny: 220, + miny2: 220, + num: 70, + fps: 60 + } + }, + wobbley: { enabled: true }, + snow: { + enabled: true, + options: { + opacity: 0.1 + } + } + } + }; + + const f1 = gui.addFolder('Effects'); + const f2 = gui.addFolder('Snow'); + const f3 = gui.addFolder('VCR'); + const f4 = gui.addFolder('Roll'); + const f5 = gui.addFolder('Image'); + + for (const effect in config.effects) { + const type = config.effects[effect]; + f1.add(type, 'enabled') + .name(effect) + .onChange((bool) => { + if (bool) { + screen.add(effect, config.effects[effect].options); + } else { + screen.remove(effect); + } + }); + + if (type.options) { + let folder = effect === 'vcr' || effect === 'video' ? f3 : f2; + for (const p in type.options) { + if (p === 'speed') { + f4.add(type.options, p) + .min(100) + .step(1) + .max(10000) + .onChange((val) => { + screen.effects[effect].node.style.animationDuration = `${val}ms`; + }); + } + + if (p === 'opacity') { + folder + .add(type.options, p) + .name(`${effect} opacity`) + .min(0) + .step(0.1) + .max(1) + .onChange((val) => { + console.log(val); + + screen.effects[effect].node.style.opacity = val; + }); + } + + if (p === 'miny') { + folder + .add(type.options, p) + .name(`tracking`) + .min(0) + .step(0.1) + .max(400) + .onChange((val) => { + screen.effects[effect].config.miny = val; + screen.effects[effect].config.miny2 = 400 - val; + }); + } + + if (p === 'num') { + folder + .add(type.options, p) + .name(`tape age`) + .min(1) + .step(0.1) + .max(100) + .onChange((val) => { + screen.effects[effect].config.num = val; + }); + } + + if (p === 'blur') { + f5.add(type.options, p) + .name(`blur`) + .min(1) + .step(0.1) + .max(5) + .onChange((val) => { + if (effect === 'vcr') { + screen.effects[effect].config.blur = val; + } else { + screen.effects[effect].node.style.filter = `blur(${val}px)`; + } + }); + } + } + } + } + + f1.open(); + f2.open(); + f3.open(); + f4.open(); + f5.open(); + + setTimeout(() => { + for (const prop in screen.effects) { + screen.remove(prop); + } + for (const prop in config.effects) { + if (!!config.effects[prop].enabled) { + screen.add(prop, config.effects[prop].options); + } + } + }, 1000); +} diff --git a/src/routes/effects/crt/main.scss b/src/routes/effects/crt/main.scss new file mode 100644 index 0000000..405e6e0 --- /dev/null +++ b/src/routes/effects/crt/main.scss @@ -0,0 +1,273 @@ +canvas { + position: absolute; + left: 0; + top: 0; + z-index: 9998; + width: 100%; + height: 100%; + + &.snow { + background-color: #aaa; + opacity: 0.2; + } +} + +#screen { + width: 100%; + height: 100vh; + background: transparent linear-gradient(to bottom, #85908c21 0%, #32343121 100%) repeat scroll 0 0; + background-size: cover; +} + +$screen-background: #121010; + +@mixin pseudo { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + content: ' '; +} + +@mixin fill { + width: 100%; + height: 100%; + position: absolute; + left: 0; + top: 0; +} + +.screen-container { + width: 100%; + height: 100%; + overflow: hidden; + position: relative; +} + +.screen-wrapper { + position: relative; + width: 100%; + height: 100%; +} + +.vcr { + // filter: blur(1px); + opacity: 0.6; +} +.video { + filter: blur(1px); + width: 100%; + height: 100%; +} +.image { + width: 100%; + height: auto; + filter: blur(1.2px); +} +.vignette { + @include fill; + background-repeat: no-repeat; + background-size: 100% 100%; + z-index: 10000; +} +.scanlines { + @include fill; + z-index: 9999; + background: linear-gradient( + transparentize($screen-background, 1) 50%, + transparentize(darken($screen-background, 10), 0.75) 50% + ), + linear-gradient( + 90deg, + transparentize(#ff0000, 0.94), + transparentize(#00ff00, 0.98), + transparentize(#0000ff, 0.94) + ); + background-size: + 100% 2px, + 3px 100%; + pointer-events: none; +} + +.wobblex { + animation: wobblex 100ms infinite; +} + +.wobbley { + animation: wobbley 100ms infinite; +} + +.glitch { + animation: 5s ease 2000ms normal none infinite running glitch; +} + +@keyframes wobblex { + 50% { + transform: translateX(1px); + } + 51% { + transform: translateX(0); + } +} +@keyframes wobbley { + 0% { + transform: translateY(1px); + } + 100% { + transform: translateY(0); + } +} +@keyframes glitch { + 30% { + } + 40% { + opacity: 1; + transform: scale(1, 1); + transform: skew(0, 0); + } + 41% { + opacity: 0.8; + transform: scale(1, 1.2); + transform: skew(80deg, 0); + } + 42% { + opacity: 0.8; + transform: scale(1, 1.2); + transform: skew(-50deg, 0); + } + 43% { + opacity: 1; + transform: scale(1, 1); + transform: skew(0, 0); + } + 65% { + } +} + +@keyframes glitch1 { + 0% { + transform: translateX(0); + } + 30% { + transform: translateX(0); + } + 31% { + transform: translateX(10px); + } + 32% { + transform: translateX(0); + } + 98% { + transform: translateX(0); + } + 100% { + transform: translateX(10px); + } +} +.text span:nth-child(2) { + animation: glitch2 1s infinite; +} +@keyframes glitch2 { + 0% { + transform: translateX(0); + } + 30% { + transform: translateX(0); + } + 31% { + transform: translateX(-10px); + } + 32% { + transform: translateX(0); + } + 98% { + transform: translateX(0); + } + 100% { + transform: translateX(-10px); + } +} + +.overlay .text { + animation: 5s ease 2000ms normal none infinite running glitch; +} + +.on > .screen-wrapper { + animation: 3000ms linear 0ms normal forwards 1 running on; +} +.off > .screen-wrapper { + animation: 750ms cubic-bezier(0.23, 1, 0.32, 1) 0ms normal forwards 1 running off; +} + +@keyframes on { + 0% { + transform: scale(1, 0.8) translate3d(0, 0, 0); + filter: brightness(4); + opacity: 1; + } + 3.5% { + transform: scale(1, 0.8) translate3d(0, 100%, 0); + } + + 3.6% { + transform: scale(1, 0.8) translate3d(0, -100%, 0); + opacity: 1; + } + + 9% { + transform: scale(1.3, 0.6) translate3d(0, 100%, 0); + filter: brightness(4); + opacity: 0; + } + + 11% { + transform: scale(1, 1) translate3d(0, 0, 0); + filter: contrast(0) brightness(0); + opacity: 0; + } + + 100% { + transform: scale(1, 1) translate3d(0, 0, 0); + filter: contrast(1) brightness(1.2) saturate(1.3); + opacity: 1; + } +} + +@keyframes off { + 0% { + transform: scale(1, 1); + filter: brightness(1); + } + 40% { + transform: scale(1, 0.005); + filter: brightness(100); + } + 70% { + transform: scale(1, 0.005); + } + 90% { + transform: scale(0.005, 0.005); + } + 100% { + transform: scale(0, 0); + } +} + +.roller { + position: relative; + animation: 2000ms linear 0ms forwards infinite roll; +} + +@keyframes roll { + from { + transform: translate3d(0, 0, 0); + } + to { + transform: translate3d(0, -50%, 0); + } +} + +.dg.ac { + z-index: 10000 !important; +}