diff --git a/README.md b/README.md index 6dc4f61..be1045d 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,23 @@ Takes optional `options` configuration object: If no existing `canvasId` is provided then the effect will be applied to the entire browser screen. You can animate alpha changes with `fg.setAlpha(newValue, step)` where `step` is optional and will determine how quickly the fade in/out will occur. + +### CRT + +Based on original code from [Alec Lownes](http://aleclownes.com/2017/02/01/crt-display.html). + +``` +var crt = new Spooky.CRT({containerId: 'mydiv'}); +fg.execute(); +fg.stop(); +``` + +Takes required `options` configuration object: + - `containerId`: id of the element to apply the CRT effect to (required) + - `screenDoorColor`: color of the screen door effect (`#000000`) + - `screenDoorSize`: size in pixels of the screen door effect (`2`) + - `screenDoorOpacity`: opacity of the screen door effect from 0-1 (`0.25`) + - `separationColor`: color of the separation effect (`#000000`) + - `separationDistance`: horizontal movement distance in pixels of the separation effect (`5`) + - `separationBlur`: blur size in pixels of the separation effect (`1`) + - `separationOpacity`: opacity of the separation effect from 0-1 (`0.5`) diff --git a/package-lock.json b/package-lock.json index 4d1e73c..8e0fb01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "spooky.js", - "version": "0.0.1", + "version": "0.0.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -31,6 +31,12 @@ "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==", "dev": true }, + "@types/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-xSQfNcvOiE5f9dyd4Kzxbof1aTrLobL278pGLKOZI6esGfZ7ts9Ka16CzIN6Y8hFHE1C7jIBZokULhK1bOgjRw==", + "dev": true + }, "JSONStream": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", @@ -1178,6 +1184,24 @@ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "stream-browserify": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", @@ -1260,6 +1284,25 @@ "acorn-node": "^1.2.0" } }, + "terser": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.7.0.tgz", + "integrity": "sha512-Lfb0RiZcjRDXCC3OSHJpEkxJ9Qeqs6mp2v4jf2MHfy8vGERmVDuvjXdd/EnP5Deme5F2yBRBymKmKHCBg2echw==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -1303,15 +1346,6 @@ "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", "dev": true }, - "uglify-js": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.9.2.tgz", - "integrity": "sha512-zGVwKslUAD/EeqOrD1nQaBmXIHl1Vw371we8cvS8I6mYK9rmgX5tv8AAeJdfsQ3Kk5mGax2SVV/AizxdNGhl7Q==", - "dev": true, - "requires": { - "commander": "~2.20.3" - } - }, "umd": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", @@ -1372,6 +1406,11 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, + "uuid": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz", + "integrity": "sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==" + }, "vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", diff --git a/package.json b/package.json index b5340ad..926f6ae 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,25 @@ { "name": "spooky.js", - "version": "0.0.1", + "version": "0.0.2", "description": "Javascript/CSS effects to make a website spooky.", "scripts": { "build": "rm -rf ./build && tsc", "pack": "browserify build/unpacked/main.js --standalone Spooky > ./build/spooky.js", - "minify": "uglifyjs build/spooky.js > build/spooky.min.js", + "minify": "terser --compress -- build/spooky.js > build/spooky.min.js", "make": "npm run build && npm run pack && npm run minify" }, "dependencies": { "core-js": "^3.6.5", - "jquery": "^3.5.1" + "jquery": "^3.5.1", + "uuid": "^8.1.0" }, "devDependencies": { "@types/core-js": "^2.5.3", "@types/jquery": "^3.3.38", "@types/node": "^13.13.5", + "@types/uuid": "^8.0.0", "browserify": "^16.5.1", - "typescript": "^3.8.3", - "uglify-js": "^3.9.2" + "terser": "^4.7.0", + "typescript": "^3.8.3" } } diff --git a/src/CRT.ts b/src/CRT.ts new file mode 100644 index 0000000..6a1572e --- /dev/null +++ b/src/CRT.ts @@ -0,0 +1,120 @@ +// credit: http://aleclownes.com/2017/02/01/crt-display.html + +import * as $ from 'jquery'; +import * as uuid from 'uuid'; +import { IEffect } from './IEffect'; +import { rgbColor } from './Util'; + +export interface CRTOptions { + containerId: string; + screenDoorColor?: string; + screenDoorSize?: number; + screenDoorOpacity?: number; + separationColor?: string; + separationDistance?: number; + separationBlur?: number; + separationOpacity?: number; +} + +export class CRT implements IEffect { + private containerId: string; + private styleId: string; + private wrapperId: string; + + private separationColor: string; + private separationDistance: number; + private separationBlur: number; + private separationOpacity: number; + + constructor(options: CRTOptions) { + this.containerId = options.containerId; + this.styleId = `crt${uuid.v4().replace(/-/g, '')}`; + this.wrapperId = `crt${uuid.v4().replace(/-/g, '')}`; + + let screenDoorColor = options.screenDoorColor || '#000000'; + let screenDoorRgb = new rgbColor(screenDoorColor); + let screenDoorSize = options.screenDoorSize || 2; + if (screenDoorSize < 2) { + screenDoorSize = 2; + } + let screenDoorOpacity = options.screenDoorOpacity || 0.25; + + this.separationColor = options.separationColor || '#000000'; + this.separationDistance = options.separationDistance || 5; + this.separationBlur = options.separationBlur || 1; + this.separationOpacity = options.separationOpacity || 0.5; + + const style = $('').attr('id', this.styleId).text(` + #${this.wrapperId} { + position: relative; + } + #${this.containerId}::before { + content: " "; + display: block; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: linear-gradient( + rgba(${screenDoorRgb.toString()}, 0) 50%, + rgba(${screenDoorRgb.toString()}, ${screenDoorOpacity}) 50% + ), linear-gradient( + 90deg, + rgba(255, 0, 0, 0.06), + rgba(0, 255, 0, 0.02), + rgba(0, 0, 255, 0.06) + ); + z-index: 2; + background-size: 100% ${screenDoorSize}px, 3px 100%; + pointer-events: none; + } + ${this.makeFlickerFrames()} + #${this.containerId}::after { + content: " "; + display: block; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: rgba(18, 16, 16, 0.1); + opacity: 0; + z-index: 2; + pointer-events: none; + animation: flicker_${this.styleId} 0.15s infinite; + } + ${this.makeSeparationFrames()} + #${this.containerId} { + animation: separation_${this.styleId} 1.6s infinite; + } + `).appendTo($('body')); + } + + private makeFlickerFrames(): string { + let css = `@keyframes flicker_${this.styleId} {`; + for(let i = 0; i <= 20; i++) { + css += ` ${i * 5}% { opacity: ${Math.random()}; }`; + } + css += ' }'; + return css; + } + + private makeSeparationFrames(): string { + const rgb = new rgbColor(this.separationColor); + let css = `@keyframes separation_${this.styleId} {`; + for(let i = 0; i <= 20; i++) { + css += ` ${i * 5}% { text-shadow: ${Math.random() * this.separationDistance}px 0 ${this.separationBlur}px rgba(${rgb.toString()}, ${this.separationOpacity}), -${Math.random() * this.separationDistance}px 0 ${this.separationBlur}px rgba(${rgb.toString()}, ${this.separationOpacity * 0.5}), 0 0 3px; }`; + } + css += ' }'; + return css; + } + + public execute(): void { + $(`#${this.containerId}`).wrap($('
').attr('id', this.wrapperId)); + } + + public stop(): void { + $(`#${this.containerId}`).unwrap(`#${this.wrapperId}`); + } +} diff --git a/src/FilmGrain.ts b/src/FilmGrain.ts index a1030f4..9cacafd 100644 --- a/src/FilmGrain.ts +++ b/src/FilmGrain.ts @@ -1,6 +1,7 @@ // credit: https://codepen.io/zadvorsky/pen/PwyoMm import * as $ from 'jquery'; +import { IEffect } from './IEffect'; export interface FilmGrainOptions { patternSize?: number; @@ -10,7 +11,7 @@ export interface FilmGrainOptions { canvasId?: string; } -export class FilmGrain { +export class FilmGrain implements IEffect { private patternSize: number; private grainScaleX: number; private grainScaleY: number; diff --git a/src/IEffect.ts b/src/IEffect.ts new file mode 100644 index 0000000..aa81f92 --- /dev/null +++ b/src/IEffect.ts @@ -0,0 +1,4 @@ +export interface IEffect { + execute(): void; + stop(): void; +} diff --git a/src/Util.ts b/src/Util.ts new file mode 100644 index 0000000..185463d --- /dev/null +++ b/src/Util.ts @@ -0,0 +1,24 @@ +export class rgbColor { + public r: number = 0; + public g: number = 0; + public b: number = 0; + + constructor(hex: string) { + const regex: RegExp = hex.length > 4 + ? /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i + : /^#?([a-f\d])([a-f\d])([a-f\d])$/i; + const repeat = hex.length > 4 ? 1 : 2; + + const result = regex.exec(hex); + + if (result) { + this.r = parseInt(result[1].repeat(repeat), 16); + this.g = parseInt(result[2].repeat(repeat), 16); + this.b = parseInt(result[3].repeat(repeat), 16); + } + } + + public toString(): string { + return `${this.r}, ${this.g}, ${this.b}`; + } +} diff --git a/src/main.ts b/src/main.ts index 5a93d83..4be398d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,3 +1,4 @@ module.exports = { + CRT: require('./CRT.js').CRT, FilmGrain: require('./FilmGrain.js').FilmGrain, }