diff --git a/src/components/mz-rings.js b/src/components/mz-rings.js new file mode 100644 index 00000000..c9632d4f --- /dev/null +++ b/src/components/mz-rings.js @@ -0,0 +1,308 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { html, LitElement, createRef, ref } from "../vendor/lit-all.min.js"; +//#region shaders +// Vec Shader +// INPUTS: +// a rectangle = vec4 in pixelspace +// uniform resolution of the pixelspace +// Draws: +// A retangle in clipspace. +const vsSource = `#version 300 es +in vec4 a_rectangle; +uniform vec2 u_resolution; + +void main() { + vec2 cornerOffset; + if (gl_VertexID == 0) { + cornerOffset = vec2(0.0, 0.0); // Bottom-left + } else if (gl_VertexID == 1) { + cornerOffset = vec2(1.0, 0.0); // Bottom-right + } else if (gl_VertexID == 2) { + cornerOffset = vec2(0.0, 1.0); // Top-left + } else { + cornerOffset = vec2(1.0, 1.0); // Top-right + } + + vec2 pixelPosition = a_rectangle.xy + cornerOffset * a_rectangle.zw; + vec2 normalizedPosition = pixelPosition / u_resolution; + vec2 clipSpacePosition = normalizedPosition * 2.0 - 1.0; + clipSpacePosition.y = -clipSpacePosition.y; + gl_Position = vec4(clipSpacePosition, 0.0, 1.0); +}`; + +// A fragment shader drawing the VPN circle animation +const fsSource = `#version 300 es +precision mediump float; + +out vec4 outColor; + +// Uniforms +uniform vec2 u_center; // Center of the rectangle (in normalized screen space) +uniform float u_time; // Time for animation +uniform vec2 u_frag_resolution; // Canvas resolution + +// Constants +const float ringCount = 5.0; +const float baseStrokeWidth = 0.010; +const float radiusMinimumFadeIn = 0.01; // Start fading in +const float radiusStartFadeOut = 0.2; // Start fading out +const float maxRadius = 0.5; // Fully faded out +const float radiusStartThinning = 0.15; // Start thinning stroke width + +float drawCircle(float distance, float radius) { + float antialias = 0.005; + return smoothstep(radius, radius - antialias, distance); +} + +float composeRing(float distance, float strokeWidth, float radius) { + float circle1 = drawCircle(distance, radius); + float circle2 = drawCircle(distance, radius - strokeWidth); + return circle1 - circle2; +} + +float calcRingRadius(float minRadius, float maxRadius, float currentRadius, float offset) { + return mod(maxRadius * offset + currentRadius, maxRadius) + minRadius; +} + +float calculateFade(float distance) { + if (distance == 0.0) { + return 1.0; + } + if (distance < radiusMinimumFadeIn) { + return 0.0; + } + if (distance >= radiusMinimumFadeIn && distance < radiusStartFadeOut) { + return 0.2*((distance - radiusMinimumFadeIn) / (maxRadius - radiusStartFadeOut-0.1)); + } + if (distance > maxRadius) { + return 0.0; // Fully transparent + } + // Linear fade between radiusStartFadeOut and maxRadius + return 0.3 - (distance - radiusStartFadeOut) / (maxRadius - radiusStartFadeOut); +} + +float calculateThinning(float distance, float originalStrokeWidth) { + if (distance < radiusStartThinning) { + return originalStrokeWidth; // No thinning before the threshold + } + if (distance > maxRadius) { + return 0.0; // Stroke completely disappears at maxRadius + } + // Linearly reduce stroke width between radiusStartThinning and maxRadius + return originalStrokeWidth * (1.0 - (distance - radiusStartThinning) / (maxRadius - radiusStartThinning)); +} + +void main() { + // Calculate normalized fragment coordinates + vec2 fragCoord = gl_FragCoord.xy / u_frag_resolution; + + // Calculate the distance from the center + float centerDistance = length(fragCoord - u_center); + + // Ring properties + float strokeWidth = calculateThinning(centerDistance, baseStrokeWidth); + float minRadius = 0.0; + float maxRadiusInner = maxRadius; // Inner logic for rings + + // Animated progress + float animationProgress = mod(u_time * 0.5, maxRadiusInner); + + float rings = 0.0; + for(float rc =0.0; rc < ringCount; rc+=1.0 ){ + float radius = calcRingRadius(minRadius, maxRadiusInner, animationProgress, rc/ringCount); + float ring = composeRing(centerDistance, strokeWidth, radius); + rings += ring; + } + // Apply fade based on distance + float fade = calculateFade(centerDistance); + + // Use ring intensity for color and transparency + outColor = vec4(vec3(rings), rings * fade); +} +`; + +/** + * + * @param {HTMLCanvasElement} canvas + * @param {HTMLElement} other + */ +const getCenterCoords = (aCanvas, aOther) => { + const canvas = aCanvas.getBoundingClientRect(); + const other = aOther.getBoundingClientRect(); + + // Calculate the center of the rectangle + // 1: We need to normalize the coordinates, so lets + // calulate the shield bounding box in relation to the canvas. + const normOther = { + width: other.width, + height: other.height, + x: other.x - canvas.x, + y: other.y - canvas.y, + }; + // Now we can calc the center of the box in pixelspace + const center = { + x: normOther.x + normOther.width / 2.0, + y: normOther.y + normOther.height + 20, + }; + // Now let's make those relative to the dimentions of the canvas (clipspace) + const clipSpaceCenter = { + x: center.x / canvas.width, + y: center.y / canvas.width, + }; + return clipSpaceCenter; +}; + +//#endregion + +/** + * `Rings` + * + * Creates a canvas and will spawn multiple rings, when the animation is turned on + * Usage: + * + * + */ +export class Rings extends LitElement { + static properties = { + enabled: { attribute: false }, + targetElementRef: { attribute: false }, + }; + canvasElement = createRef(); + #running = false; + + constructor() { + super(); + this.enabled = true; + this.targetElementRef = createRef(); + } + connectedCallback() { + const rect = this.getBoundingClientRect(); + console.log(rect); + this.width = rect.width; + this.height = rect.height; + super.connectedCallback(); + } + render() { + return html` + + `; + } + + updated(changedProperties) { + super.updated(changedProperties); + // Put this into an idle callback. + // It's fine to delay the animation, as we ned to make sure the css layout is up + // to date before we can properly start this. + if (!this.enabled) { + return; + } + requestIdleCallback(() => { + this.maybeStartRender(); + }); + } + + maybeStartRender() { + if (this.#running) { + return; + } + const this_rect = this.getBoundingClientRect(); + + /** @type {HTMLElement} */ + const shield = this.targetElementRef.value; + const shieldBox = shield.getBoundingClientRect(); + + /** @type {HTMLCanvasElement?} */ + const canvas = this.canvasElement.value; + if (!canvas) { + return; + } + canvas.height = this_rect.height; + canvas.width = this_rect.width; + + const gl = canvas.getContext("webgl2"); + gl.viewport(0, 0, canvas.width, canvas.height); + // Enable blending for transparency + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + + // Set clear color to transparent + gl.clearColor(0.0, 0.0, 0.0, 0.0); + + // Rectangle data: x, y, width, height + const rectangle = [0, 0, canvas.width, canvas.height]; + + // Compile both shaders, and link them into a programm + function compileShader(gl, source, type) { + const shader = gl.createShader(type); + gl.shaderSource(shader, source); + gl.compileShader(shader); + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + console.error("Shader compile error:", gl.getShaderInfoLog(shader)); + return null; + } + return shader; + } + + const vertexShader = compileShader(gl, vsSource, gl.VERTEX_SHADER); + const fragmentShader = compileShader(gl, fsSource, gl.FRAGMENT_SHADER); + const program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + console.error("Program link error:", gl.getProgramInfoLog(program)); + } + // Now lets draw with the programm! + + gl.useProgram(program); + // Load the Arguments for the Vert Shader. + const aRectangleLocation = gl.getAttribLocation(program, "a_rectangle"); + gl.vertexAttrib4fv(aRectangleLocation, rectangle); + const uResolutionLocation = gl.getUniformLocation(program, "u_resolution"); + gl.uniform2f(uResolutionLocation, canvas.height, canvas.height); + + const uTimeLocation = gl.getUniformLocation(program, "u_time"); + const uCenterLocation = gl.getUniformLocation(program, "u_center"); + const uFragResolutionLocation = gl.getUniformLocation( + program, + "u_frag_resolution" + ); + gl.uniform2f(uFragResolutionLocation, canvas.width, canvas.width); + + let clipSpaceCenter = getCenterCoords(canvas, shield); + console.log(clipSpaceCenter); + gl.uniform2f(uCenterLocation, clipSpaceCenter.x, clipSpaceCenter.y); + + // The layout may be slower then we setup the renderer + // So let's check after 500ms that the ref-points are still up + // to date in case the layout changed + setTimeout(() => { + const clipSpaceCenter = getCenterCoords(canvas, shield); + gl.uniform2f(uCenterLocation, clipSpaceCenter.x, clipSpaceCenter.y); + }, 500); + + const render = (time) => { + gl.clear(gl.COLOR_BUFFER_BIT); + if (!this.enabled) { + this.#running = false; + canvas.height = 0; + this.requestUpdate(); + return; + } + const timeInSeconds = time * 0.0001; + gl.uniform1f(uTimeLocation, timeInSeconds); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + requestAnimationFrame(render); + }; + this.#running = true; + requestAnimationFrame(render); + } +} +customElements.define("mz-rings", Rings); diff --git a/src/components/vpncard.js b/src/components/vpncard.js index 8f3ce9aa..1b0fa5bd 100644 --- a/src/components/vpncard.js +++ b/src/components/vpncard.js @@ -8,12 +8,14 @@ import { LitElement, classMap, styleMap, + createRef, + ref, } from "../vendor/lit-all.min.js"; import { tr } from "../shared/i18n.js"; import { resetSizing, fontStyling, positioner } from "./styles.js"; import { VPNState } from "../background/vpncontroller/states.js"; - +import "./mz-rings.js"; /** * @typedef {import("../background/vpncontroller/states.js").VPNState} VPNState */ @@ -41,6 +43,7 @@ export class VPNCard extends LitElement { this.connecting = false; } #intervalHandle = null; + #shieldElement = createRef(); updated(changedProperties) { if (!changedProperties.has("enabled")) { @@ -107,23 +110,33 @@ export class VPNCard extends LitElement { return tr("vpnIsOff"); }; return html` -
-
- ${VPNCard.shield(this.enabled, this.connecting)} -
-

${vpnHeader()}

- ${VPNCard.subline( +
+ +
+
+ ${VPNCard.shield( this.enabled, - this.stability, - this.clientConnected + this.connecting, + this.#shieldElement )} - ${timeString} -
- -
- ${this.enabled || this.connecting - ? VPNCard.footer(this.cityName, this.countryFlag) - : null} +
+

${vpnHeader()}

+ ${VPNCard.subline( + this.enabled, + this.stability, + this.clientConnected + )} + ${timeString} +
+ + + ${this.enabled || this.connecting + ? VPNCard.footer(this.cityName, this.countryFlag) + : null} +
`; } @@ -165,16 +178,16 @@ export class VPNCard extends LitElement { } } - static shield(enabled, connecting) { + static shield(enabled, connecting, shieldRef) { if (!enabled && !connecting) { return html` - + `; } return html` - + `; @@ -191,10 +204,6 @@ export class VPNCard extends LitElement { .box { border-radius: 8px; background: lch(from var(--panel-bg-color) calc(l + 5) c h); - display: flex; - align-items: flex-start; - justify-content: space-between; - flex-direction: column; box-shadow: var(--box-shadow-off); } .box.on, @@ -287,6 +296,15 @@ export class VPNCard extends LitElement { .noSignal .subline { color: var(--color-fatal-error); } + .stack { + display: grid; + grid-template-rows: 1fr; + grid-template-columns: 1fr; + } + .stack > * { + grid-row: 1 / 2; + grid-column: 1 / 2; + } svg { height: 48px; diff --git a/src/ui/browserAction/popup.html b/src/ui/browserAction/popup.html index 11d5f471..04a14ed7 100644 --- a/src/ui/browserAction/popup.html +++ b/src/ui/browserAction/popup.html @@ -14,6 +14,7 @@ + Mozilla VPN diff --git a/src/ui/browserAction/popupPage.js b/src/ui/browserAction/popupPage.js index 6f8c77a3..aab3b51c 100644 --- a/src/ui/browserAction/popupPage.js +++ b/src/ui/browserAction/popupPage.js @@ -30,6 +30,7 @@ import "./../../components/serverlist.js"; import "./../../components/vpncard.js"; import "./../../components/titlebar.js"; import "./../../components/iconbutton.js"; +import "./../../components/mz-rings.js"; import { SiteContext } from "../../background/proxyHandler/siteContext.js"; import { ServerCity,