diff --git a/components/ghost.js b/components/ghost.js new file mode 100644 index 00000000..b09934be --- /dev/null +++ b/components/ghost.js @@ -0,0 +1,603 @@ +import React, { useRef, useEffect, useContext } from 'react' + +export const GhostContext = React.createContext(() => {}) + +export class GhostProvider extends React.Component { + state = { + ghosts: [] + } + + strike = () => { + const should = window.localStorage.getItem('lnAnimate') || 'yes' + if (should === 'yes') { + this.setState(state => { + return { + ghosts: [...state.ghosts, this.unstrike(state.ghosts.length)} />] + } + }) + return true + } + return false + } + + unstrike = (index) => { + this.setState(state => { + const ghosts = [...state.ghosts] + ghosts[index] = null + return { ghosts } + }) + } + + render () { + const { props: { children } } = this + return ( + + {this.state.ghosts} + {children} + + ) + } +} + +export const GhostConsumer = GhostContext.Consumer +export function useGhost () { + return useContext(GhostContext) +} + +function getRandom (min, max) { + return Math.random() * (max - min) + min +} + +export function Ghost ({ onDone }) { + const canvasRef = useRef(null) + const textureRef = useRef(null) + + useEffect(() => { + const canvas = canvasRef.current + const texture = textureRef.current + if (canvas.ghost) return + + canvas.ghost = new GhostCanvas(canvas, texture, { + size: getRandom(0.025, 0.075), + tail: { + dotsNumber: 25, // Math.floor(getRandom(10, 50)), + spring: 1.4, // getRandom(1, 1.8), + friction: 0.25, // getRandom(0.1, 0.25), + maxGravity: 250, + gravity: getRandom(5, 250) + }, + smile: 1, + mainColor: [getRandom(0.8, 1), getRandom(0.8, 1), getRandom(0.8, 1)], + borderColor: [getRandom(0, 0.2), getRandom(0, 0.2), getRandom(0, 0.2)], // [0.2, 0.5, 0.7], + isFlatColor: false, + onDone + }) + }, []) + + return ( + <> + + + + ) +} + +const GhostCanvas = (canvas, texture, params) => { + const mouseThreshold = 1 + const devicePixelRatio = Math.min(window.devicePixelRatio, 2) + + function getRandomOffscreen () { + return Math.random() > 0.5 + ? { + x: getRandom(0, window.innerWidth), + y: Math.random() > 0.5 ? getRandom(-200, -100) : getRandom(window.innerHeight + 100, window.innerHeight + 200) + } + : { + x: Math.random() > 0.5 ? getRandom(-200, -100) : getRandom(window.innerWidth + 100, window.innerWidth + 200), + y: getRandom(0, window.innerHeight) + } + } + + function getRandomOnScreen () { + return { + x: getRandom(0.1 * window.innerWidth, 0.9 * window.innerWidth), + y: getRandom(0.1 * window.innerHeight, 0.9 * window.innerHeight) + } + } + + const mouse = { + ...getRandomOffscreen(), + tX: 0, + tY: 0, + moving: false + } + + const textureCtx = texture.getContext('2d') + const pointerTrail = new Array(params.tail.dotsNumber) + const dotSize = (i) => + params.size * + window.innerHeight * + (1 - 0.2 * Math.pow((3 * i) / params.tail.dotsNumber - 1, 2)) + for (let i = 0; i < params.tail.dotsNumber; i++) { + pointerTrail[i] = { + x: mouse.x, + y: mouse.y, + vx: 0, + vy: 0, + opacity: 0.04 + getRandom(0.25, 0.35) * Math.pow(1 - i / params.tail.dotsNumber, 4), + bordered: 0.6 * Math.pow(1 - i / pointerTrail.length, 1), + r: dotSize(i) + } + } + + let uniforms + const gl = initShader() + + function generateRandomPath () { + const startPoint = { + x: mouse.x, + y: mouse.y + } + + const numSegments = 1 // Adjust for more or fewer segments + const path = [] + + let lastEndPoint = startPoint + for (let i = 0; i < numSegments; i++) { + const controlPoint1 = getRandomOnScreen() + // const controlPoint2 = getRandomOnScreen() + const endPoint = getRandomOnScreen() + + path.push({ bezier: [lastEndPoint, controlPoint1, endPoint, endPoint] }) + lastEndPoint = endPoint + } + + path.push({ pause: getRandom(500, 2000) }) + const finalPoint = getRandomOffscreen() + path.push(finalPoint) + + return path + } + + const path = generateRandomPath() + let currentPathIndex = 0 + let pathStartTime = null + const segmentDuration = getRandom(750, 3000) // How long each Bezier segment should take + + function evaluateBezier (bezier, t) { + const [p0, p1, p2, p3] = bezier + + const oneMinusT = 1 - t + const x = Math.pow(oneMinusT, 3) * p0.x + + 3 * Math.pow(oneMinusT, 2) * t * p1.x + + 3 * oneMinusT * t * t * p2.x + + t * t * t * p3.x + + const y = Math.pow(oneMinusT, 3) * p0.y + + 3 * Math.pow(oneMinusT, 2) * t * p1.y + + 3 * oneMinusT * t * t * p2.y + + t * t * t * p3.y + + return { x, y } + } + + function interpolate (start, end, factor) { + return start + (end - start) * factor + } + + function updateMousePositionBasedOnPath (currentTime) { + if (!pathStartTime) { + pathStartTime = currentTime + } + + const currentSegment = path[currentPathIndex] + + if (currentSegment.pause) { + mouse.moving = false + if (currentTime - pathStartTime > currentSegment.pause) { + mouse.moving = true + currentPathIndex++ + pathStartTime = null + } + return + } + + if (currentSegment.bezier) { + const t = (currentTime - pathStartTime) / segmentDuration + if (t <= 1) { + const position = evaluateBezier(currentSegment.bezier, t) + mouse.tX = position.x + mouse.tY = position.y + } else { + currentPathIndex++ + pathStartTime = null + } + return + } + + // Linear segment + const factor = (currentTime - pathStartTime) / segmentDuration + if (factor < 1) { + mouse.tX = interpolate(mouse.x, currentSegment.x, factor) + mouse.tY = interpolate(mouse.y, currentSegment.y, factor) + } else { + currentPathIndex++ + pathStartTime = null + } + } + + resizeCanvas() + render() + mouse.moving = true + + function initShader () { + const vsSource = vertShader + const fsSource = fragShader + + const gl = + canvas.getContext('webgl') || + canvas.getContext('experimental-webgl') + + if (!gl) { + console.log('WebGL is not supported by your browser.') + return + } + + function createShader (gl, sourceCode, type) { + const shader = gl.createShader(type) + gl.shaderSource(shader, sourceCode) + gl.compileShader(shader) + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + console.error( + 'An error occurred compiling the shaders: ' + + gl.getShaderInfoLog(shader) + ) + gl.deleteShader(shader) + return null + } + + return shader + } + + const vertexShader = createShader(gl, vsSource, gl.VERTEX_SHADER) + const fragmentShader = createShader(gl, fsSource, gl.FRAGMENT_SHADER) + + function createShaderProgram (gl, vertexShader, fragmentShader) { + const program = gl.createProgram() + gl.attachShader(program, vertexShader) + gl.attachShader(program, fragmentShader) + gl.linkProgram(program) + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + console.error( + 'Unable to initialize the shader program: ' + + gl.getProgramInfoLog(program) + ) + return null + } + + return program + } + + const shaderProgram = createShaderProgram( + gl, + vertexShader, + fragmentShader + ) + uniforms = getUniforms(shaderProgram) + + function getUniforms (program) { + const uniforms = [] + const uniformCount = gl.getProgramParameter( + program, + gl.ACTIVE_UNIFORMS + ) + for (let i = 0; i < uniformCount; i++) { + const uniformName = gl.getActiveUniform(program, i).name + uniforms[uniformName] = gl.getUniformLocation(program, uniformName) + } + return uniforms + } + + const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]) + + const vertexBuffer = gl.createBuffer() + gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer) + gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW) + + gl.useProgram(shaderProgram) + + const positionLocation = gl.getAttribLocation( + shaderProgram, + 'a_position' + ) + gl.enableVertexAttribArray(positionLocation) + + gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer) + gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0) + + const canvasTexture = gl.createTexture() + gl.bindTexture(gl.TEXTURE_2D, canvasTexture) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + gl.RGBA, + gl.UNSIGNED_BYTE, + texture + ) + gl.uniform1i(uniforms.u_texture, 0) + + gl.uniform1f(uniforms.u_size, params.size) + gl.uniform3f( + uniforms.u_main_color, + params.mainColor[0], + params.mainColor[1], + params.mainColor[2] + ) + gl.uniform3f( + uniforms.u_border_color, + params.borderColor[0], + params.borderColor[1], + params.borderColor[2] + ) + + return gl + } + + function updateTexture () { + textureCtx.fillStyle = 'black' + textureCtx.fillRect(0, 0, texture.width, texture.height) + + pointerTrail.forEach((p, pIdx) => { + if (Number.isNaN(mouse.x) || Number.isNaN(mouse.y)) { + return + } + + if (pIdx === 0) { + p.x = mouse.x + p.y = mouse.y + } else { + p.vx += (pointerTrail[pIdx - 1].x - p.x) * params.tail.spring + p.vx *= params.tail.friction + + p.vy += (pointerTrail[pIdx - 1].y - p.y) * params.tail.spring + p.vy *= params.tail.friction + p.vy += params.tail.gravity + p.x += p.vx + p.y += p.vy + } + + const grd = textureCtx.createRadialGradient( + p.x, + p.y, + p.r * p.bordered, + p.x, + p.y, + p.r + ) + grd.addColorStop( + 0, + 'rgba(255, 255, 255, ' + p.opacity + ')' + ) + grd.addColorStop(1, 'rgba(255, 255, 255, 0)') + + textureCtx.beginPath() + textureCtx.fillStyle = grd + textureCtx.arc(p.x, p.y, p.r, 0, Math.PI * 2) + textureCtx.fill() + }) + } + + function render () { + if (currentPathIndex > path.length - 1) { + params.onDone() + return + } + const currentTime = performance.now() + updateMousePositionBasedOnPath(currentTime) + gl.uniform1f(uniforms.u_time, currentTime) + + gl.clearColor(0.0, 0.0, 0.0, 1.0) + gl.clear(gl.COLOR_BUFFER_BIT) + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4) + + if (mouse.moving) { + params.smile -= 0.05 + params.smile = Math.max(params.smile, -0.1) + params.tail.gravity -= 10 * params.size + params.tail.gravity = Math.max(params.tail.gravity, 0) + } else { + params.smile += 0.01 + params.smile = Math.min(params.smile, 1) + if (params.tail.gravity > 30 * params.size) { + params.tail.gravity = + (30 + 9 * (1 + Math.sin(0.002 * currentTime))) * params.size + } else { + params.tail.gravity += params.size + } + } + + mouse.x += (mouse.tX - mouse.x) * mouseThreshold + mouse.y += (mouse.tY - mouse.y) * mouseThreshold + + gl.uniform1f(uniforms.u_smile, params.smile) + gl.uniform2f( + uniforms.u_pointer, + mouse.x / window.innerWidth, + 1 - mouse.y / window.innerHeight + ) + gl.uniform2f( + uniforms.u_target_pointer, + mouse.tX / window.innerWidth, + 1 - mouse.tY / window.innerHeight + ) + + updateTexture() + + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + gl.RGBA, + gl.UNSIGNED_BYTE, + texture + ) + window.requestAnimationFrame(render) + } + + function resizeCanvas () { + canvas.width = window.innerWidth * devicePixelRatio + canvas.height = window.innerHeight * devicePixelRatio + texture.width = window.innerWidth + texture.height = window.innerHeight + gl.viewport(0, 0, canvas.width, canvas.height) + gl.uniform1f(uniforms.u_ratio, canvas.width / canvas.height) + for (let i = 0; i < params.tail.dotsNumber; i++) { + pointerTrail[i].r = dotSize(i) + } + } +} + +const vertShader = ` + precision mediump float; + + varying vec2 vUv; + attribute vec2 a_position; + + void main() { + vUv = .5 * (a_position + 1.); + gl_Position = vec4(a_position, 0.0, 1.0); + }` + +const fragShader = ` +precision mediump float; + +varying vec2 vUv; +uniform float u_time; +uniform float u_ratio; +uniform float u_size; +uniform vec2 u_pointer; +uniform float u_smile; +uniform vec2 u_target_pointer; +uniform vec3 u_main_color; +uniform vec3 u_border_color; +uniform float u_flat_color; +uniform sampler2D u_texture; + +#define TWO_PI 6.28318530718 +#define PI 3.14159265358979323846 + +vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } +vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } +vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); } +float snoise(vec2 v) { + const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439); + vec2 i = floor(v + dot(v, C.yy)); + vec2 x0 = v - i + dot(i, C.xx); + vec2 i1; + i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); + vec4 x12 = x0.xyxy + C.xxzz; + x12.xy -= i1; + i = mod289(i); + vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0)); + vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0); + m = m*m; + m = m*m; + vec3 x = 2.0 * fract(p * C.www) - 1.0; + vec3 h = abs(x) - 0.5; + vec3 ox = floor(x + 0.5); + vec3 a0 = x - ox; + m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h); + vec3 g; + g.x = a0.x * x0.x + h.x * x0.y; + g.yz = a0.yz * x12.xz + h.yz * x12.yw; + return 130.0 * dot(m, g); +} +vec2 rotate(vec2 v, float angle) { + float r_sin = sin(angle); + float r_cos = cos(angle); + return vec2(v.x * r_cos - v.y * r_sin, v.x * r_sin + v.y * r_cos); +} + +float eyes(vec2 uv) { + uv.y -= .5; + uv.x *= 1.; + uv.y *= .8; + uv.x = abs(uv.x); + uv.y += u_smile * .3 * pow(uv.x, 1.3); + uv.x -= (.6 + .2 * u_smile); + + float d = clamp(length(uv), 0., 1.); + return 1. - pow(d, .08); +} + +float mouth(vec2 uv) { + uv.y += 1.5; + + uv.x *= (.5 + .5 * abs(1. - u_smile)); + uv.y *= (3. - 2. * abs(1. - u_smile)); + uv.y -= u_smile * 4. * pow(uv.x, 2.); + + float d = clamp(length(uv), 0., 1.); + return 1. - pow(d, .07); +} + +float face(vec2 uv, float rotation) { + uv = rotate(uv, rotation); + uv /= (.27 * u_size); + + float eyes_shape = 10. * eyes(uv); + float mouth_shape = 20. * mouth(uv); + + float col = 0.; + col = mix(col, 1., eyes_shape); + col = mix(col, 1., mouth_shape); + + return col; +} + +void main() { + + vec2 point = u_pointer; + point.x *= u_ratio; + + vec2 uv = vUv; + uv.x *= u_ratio; + uv -= point; + + float texture = texture2D(u_texture, vec2(vUv.x, 1. - vUv.y)).r; + float shape = texture; + + float noise = snoise(uv * vec2(.7 / u_size, .6 / u_size) + vec2(0., .0015 * u_time)); + noise += 1.2; + noise *= 2.1; + noise += smoothstep(-.8, -.2, (uv.y) / u_size); + + float face = face(uv, 5. * (u_target_pointer.x - u_pointer.x)); + shape -= face; + + shape *= noise; + + vec3 border = (1. - u_border_color); + border.g += .2 * sin(.005 * u_time); + border *= .5; + + vec3 color = u_main_color; + color -= (1. - u_flat_color) * border * smoothstep(.0, .01, shape); + + shape = u_flat_color * smoothstep(.8, 1., shape) + (1. - u_flat_color) * shape; + color *= shape; + + gl_FragColor = vec4(color, shape); +}` diff --git a/components/header.js b/components/header.js index f92bb617..f04943c8 100644 --- a/components/header.js +++ b/components/header.js @@ -20,7 +20,7 @@ import { Select } from './form' import SearchIcon from '../svgs/search-line.svg' import BackArrow from '../svgs/arrow-left-line.svg' import { SSR, SUBS } from '../lib/constants' -import { useLightning } from './lightning' +import { useGhost } from './ghost' import { HAS_NOTIFICATIONS } from '../fragments/notifications' import AnonIcon from '../svgs/spy-fill.svg' import Hat from './hat' @@ -147,7 +147,7 @@ function StackerCorner ({ dropNavKey }) { function LurkerCorner ({ path }) { const router = useRouter() - const strike = useLightning() + const strike = useGhost() useEffect(() => { if (!window.localStorage.getItem('striked')) { diff --git a/components/lightning.js b/components/lightning.js index 948abf56..44202d04 100644 --- a/components/lightning.js +++ b/components/lightning.js @@ -44,8 +44,8 @@ export class LightningProvider extends React.Component { } } -export const LightningConsumer = LightningContext.Consumer -export function useLightning () { +export const GhostConsumer = LightningContext.Consumer +export function useGhost () { return useContext(LightningContext) } diff --git a/components/upvote.js b/components/upvote.js index 34ec25e5..56e34bb1 100644 --- a/components/upvote.js +++ b/components/upvote.js @@ -10,7 +10,7 @@ import LongPressable from 'react-longpressable' import Overlay from 'react-bootstrap/Overlay' import Popover from 'react-bootstrap/Popover' import { useShowModal } from './modal' -import { LightningConsumer, useLightning } from './lightning' +import { GhostConsumer, useGhost } from './ghost' import { numWithUnits } from '../lib/format' import { payOrLoginError, useInvoiceModal } from './invoice' import useDebounceCallback from './use-debounce-callback' @@ -71,7 +71,7 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats } const [tipShow, _setTipShow] = useState(false) const ref = useRef() const me = useMe() - const strike = useLightning() + const strike = useGhost() const [setWalkthrough] = useMutation( gql` mutation setWalkthrough($upvotePopover: Boolean, $tipPopover: Boolean) { @@ -202,7 +202,7 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats } }, [item?.meSats, item?.meAnonSats, pendingSats, me?.tipDefault, me?.turboDefault]) return ( - + {(strike) =>
setTipShow(false)} /> setVoteShow(false)} />
} -
+ ) } diff --git a/pages/_app.js b/pages/_app.js index e41b9c18..2836d1ef 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -10,7 +10,7 @@ import { useRouter } from 'next/dist/client/router' import { useEffect } from 'react' import { ShowModalProvider } from '../components/modal' import ErrorBoundary from '../components/error-boundary' -import { LightningProvider } from '../components/lightning' +import { GhostProvider } from '../components/ghost' import { ToastProvider } from '../components/toast' import { ServiceWorkerProvider } from '../components/serviceworker' import { SSR } from '../lib/constants' @@ -92,7 +92,7 @@ export default function MyApp ({ Component, pageProps: { ...props } }) { - + @@ -100,7 +100,7 @@ export default function MyApp ({ Component, pageProps: { ...props } }) { - + diff --git a/pages/rewards/index.js b/pages/rewards/index.js index 6d833130..680420bd 100644 --- a/pages/rewards/index.js +++ b/pages/rewards/index.js @@ -15,7 +15,7 @@ import { useShowModal } from '../../components/modal' import dynamic from 'next/dynamic' import { SSR } from '../../lib/constants' import { useToast } from '../../components/toast' -import { useLightning } from '../../components/lightning' +import { useGhost } from '../../components/ghost' const GrowthPieChart = dynamic(() => import('../../components/charts').then(mod => mod.GrowthPieChart), { loading: () =>
Loading...
@@ -91,7 +91,7 @@ export default function Rewards ({ ssrData }) { export function DonateButton () { const showModal = useShowModal() const toaster = useToast() - const strike = useLightning() + const strike = useGhost() const [donateToRewards] = useMutation( gql` mutation donateToRewards($sats: Int!, $hash: String, $hmac: String) {