diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 7abab24..18ed763 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -2,31 +2,30 @@ name: Go on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] jobs: - build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2 - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.22 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.24.0 - - name: Build - run: go build -v ./... + - name: Build + run: go build -v ./... - - name: Test - run: go test -v ./... + - name: Test + run: go test -v ./... - - name: GoGitOps Step - id: gogitops - uses: beaujr/gogitops-action@v0.2 - with: - github-actions-user: owulveryck - github-actions-token: ${{secrets.GITHUB_TOKEN}} + - name: GoGitOps Step + id: gogitops + uses: beaujr/gogitops-action@v0.2 + with: + github-actions-user: owulveryck + github-actions-token: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 2c6a763..8db8094 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v2 with: - go-version: 1.23 + go-version: 1.24.0 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 with: diff --git a/README.md b/README.md index 49aed60..eb48b7e 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,9 @@ When running on the Remarkable Paper Pro, `RLE_COMPRESSION` environment variable - **Laser Pointer**: Features a laser pointer that activates on hovering. - **Gesture Support**: Full integration with Reveal.js, allowing slide switching directly from the reMarkable. - **Overlay Feature**: Allows overlaying over existing websites that support iframe embedding. -- **Built-in Ngrok**: Enables streaming over different networks easily. - **Live Parameter Tweaking**: Side menu for live adjustments, including screen rotation. +- **Dark Mode**: Toggle between light and dark themes for comfortable viewing in any environment. +- **Version API**: Check the current version via the `/version` endpoint. ## Quick Start @@ -173,23 +174,12 @@ Add query parameters to the URL (`?parameter=value&otherparameter=value`): - `rate`: (integer, 100-...) Set the frame rate. - `flip`: (true/false) Enable or disable flipping 180 degree. -## Tunneling -Tunneling with built-in Ngrok allows for streaming across different networks. -This feature is particularly useful for remote presentations or collaborative sessions. -To set up tunneling, simply enable Ngrok in the tool's settings and follow the instructions provided in the user interface. - -If your reMarkable is on a different network than the displaying device, you can use the `ngrok` builtin feature for automatic tunneling. -To utilize this tunneling, you need to sign up for an ngrok account and [obtain a token from the dashboard](https://dashboard.ngrok.com/get-started/your-authtoken). -Once you have the token, launch reMarkable using the following command: - -`NGROK_AUTHTOKEN=YOURTOKEN RK_SERVER_BIND_ADDR=ngrok ./goMarkableStream` - -The app will start, displaying a message similar to: - -`2023/09/29 16:49:20 listening on 72e5-22-159-32-48.ngrok-free.app` - -Then, connect to `https://72e5-22-159-32-48.ngrok-free.app` to view the result. - +### API Endpoints +- `/`: Main web interface +- `/stream`: The image data stream +- `/events`: WebSocket endpoint for pen input events +- `/gestures`: Endpoint for touch events +- `/version`: Returns the current version of goMarkableStream ## Presentation Mode `goMarkableStream` introduces an innovative experimental feature that allows users to set a presentation or video in the background, enabling live annotations using a reMarkable tablet. @@ -218,9 +208,9 @@ This includes a variety of presentation and video platforms. Switch slides or navigate through your presentation directly from your reMarkable tablet. This seamless integration enhances the experience of both presenting and viewing, making it ideal for educational and professional environments. -Howto: add the `?present=https://your-reveal-js-presentation` +How to: add the `?present=https://your-reveal-js-presentation` -_note_: due to browser restrictions, the URL mus +_note_: due to browser restrictions, the URL must be HTTPS. ### Limitations and Performance @@ -229,12 +219,12 @@ _note_: due to browser restrictions, the URL mus Users must use the side menu for navigation and control. - This feature operates seamlessly, with no additional load on the reMarkable tablet, as all rendering is done in the client's browser. -### Feedback and Contributions +### UI Features -- As this is an experimental feature, your feedback is crucial for its development. -Please share your experiences, suggestions, and any issues encountered using the GitHub issues section of this repository. - ---- +- **Dark Mode**: Toggle between light and dark themes using the sun/moon icon in the sidebar +- **Modern Interface**: Improved UI with better typography and layout +- **Tooltips**: Helpful tooltips on hover for all buttons +- **Feedback Messages**: Visual feedback for user actions ## Technical Details @@ -246,16 +236,7 @@ This tool suits my need and is an ongoing development. You can find various info This is a standalone application that runs directly on a Remarkable tablet. It does not have any dependencies on third-party libraries, making it a completely self-sufficient solution. -This application exposes an HTTP server with two endpoints: -### Endpoints - -- `/`: This endpoint serves an embedded HTML and JavaScript file containing the necessary logic to display an image from the Remarkable tablet on a client's web browser. - -- `/stream`: This endpoint streams the image data from the Remarkable tablet to the client continuously. -- `/events`: This endpoint streams the pen input events via websockets -- `gestures`: This endpoints streams the touch events in binary - -**Caution**: the API may change over time +This application exposes an HTTP server with several endpoints. ### Implementation @@ -296,5 +277,4 @@ Feel free to modify, distribute, and use the tool in accordance with the terms o ## Tipping If you plan to buy a reMarkable 2, you can use my [referal program link](https://remarkable.com/referral/PY5B-PH8U). -It will provide a discount for you and also for me. - +It will provide a discount for you and also for me. \ No newline at end of file diff --git a/client/glCanvas.js b/client/glCanvas.js index 5c028a6..6f46481 100644 --- a/client/glCanvas.js +++ b/client/glCanvas.js @@ -1,9 +1,12 @@ // WebGL initialization -//const gl = visibleCanvas.getContext('webgl'); -//const gl = canvas.getContext('webgl', { antialias: true, preserveDrawingBuffer: true }); -let laserX = 0; // Initialize with default values -let laserY = 0; -const gl = canvas.getContext('webgl', { antialias: true }); +// Use -10,-10 as the default laser coordinate (off-screen) to hide the pointer initially +let laserX = -10; +let laserY = -10; +const gl = canvas.getContext('webgl', { + antialias: true, + preserveDrawingBuffer: true, // Important for proper rendering + alpha: true // Enable transparency +}); if (!gl) { @@ -19,30 +22,64 @@ uniform float uScaleFactor; varying highp vec2 vTextureCoord; void main(void) { - gl_Position = uRotationMatrix * vec4(aVertexPosition.xy * uScaleFactor, aVertexPosition.zw); - vTextureCoord = aTextureCoord; + // Apply scaling and rotation transformations + gl_Position = uRotationMatrix * vec4(aVertexPosition.xy * uScaleFactor, aVertexPosition.zw); + + // Pass texture coordinates to fragment shader + vTextureCoord = aTextureCoord; } `; // Fragment shader program const fsSource = ` -precision mediump float; +precision highp float; varying highp vec2 vTextureCoord; uniform sampler2D uSampler; uniform float uLaserX; uniform float uLaserY; +uniform bool uDarkMode; +uniform float uContrastLevel; + +// Constants for laser pointer visualization +const float LASER_RADIUS = 6.0; +const float LASER_EDGE_SOFTNESS = 2.0; +const vec3 LASER_COLOR = vec3(1.0, 0.0, 0.0); + +// Constants for image processing +const float BRIGHTNESS = 0.05; // Slight brightness boost +const float SHARPNESS = 0.5; // Sharpness level + +// Get texture color without any sharpening - better for handwriting +vec4 getBaseTexture(sampler2D sampler, vec2 texCoord) { + return texture2D(sampler, texCoord); +} void main(void) { + // Get base texture color directly - no sharpening for clearer handwriting + vec4 texColor = getBaseTexture(uSampler, vTextureCoord); + + // Apply contrast adjustments based on the slider value + vec3 adjusted = (texColor.rgb - 0.5) * uContrastLevel + 0.5; + texColor.rgb = clamp(adjusted, 0.0, 1.0); + + // Calculate laser pointer effect float dx = gl_FragCoord.x - uLaserX; float dy = gl_FragCoord.y - uLaserY; float distance = sqrt(dx * dx + dy * dy); - float radius = 5.0; // Radius of the dot, adjust as needed - - if(distance < radius) { - gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // Red color for the laser dot + + if (uDarkMode) { + // Invert colors in dark mode, but preserve alpha + texColor.rgb = 1.0 - texColor.rgb; + } + + // Simple laser pointer - more reliable rendering + if (distance < 8.0 && uLaserX > 0.0 && uLaserY > 0.0) { + // Create solid circle with slight fade at edge + float fade = 1.0 - smoothstep(6.0, 8.0, distance); + gl_FragColor = vec4(1.0, 0.0, 0.0, fade); // Red with fade at edge } else { - gl_FragColor = texture2D(uSampler, vTextureCoord); + gl_FragColor = texColor; } } `; @@ -107,6 +144,8 @@ const programInfo = { uSampler: gl.getUniformLocation(shaderProgram, 'uSampler'), uLaserX: gl.getUniformLocation(shaderProgram, 'uLaserX'), uLaserY: gl.getUniformLocation(shaderProgram, 'uLaserY'), + uDarkMode: gl.getUniformLocation(shaderProgram, 'uDarkMode'), + uContrastLevel: gl.getUniformLocation(shaderProgram, 'uContrastLevel'), }, }; @@ -172,18 +211,34 @@ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); let imageData = new ImageData(screenWidth, screenHeight); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imageData); +// Variables to track display state +let isDarkMode = false; +let contrastValue = 1.15; // Default contrast value + // Draw the scene function drawScene(gl, programInfo, positionBuffer, textureCoordBuffer, texture) { + // Handle canvas resize for proper rendering if (resizeGLCanvas(gl.canvas)) { gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); } - gl.clearColor(0.5, 0.5, 0.5, 0.25); // Gray with 75% transparency - gl.clearDepth(1.0); // Clear everything - gl.enable(gl.DEPTH_TEST); // Enable depth testing - gl.depthFunc(gl.LEQUAL); // Near things obscure far things + + // Adjust background color based on dark mode + const bgColor = isDarkMode + ? [0.12, 0.12, 0.13, 0.25] // Darker, more neutral dark mode bg + : [0.98, 0.98, 0.98, 0.25]; // Nearly white light mode bg + gl.clearColor(bgColor[0], bgColor[1], bgColor[2], bgColor[3]); + + // Enable alpha blending for transparency + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + + // Setup depth buffer + gl.clearDepth(1.0); + gl.enable(gl.DEPTH_TEST); + gl.depthFunc(gl.LEQUAL); // Clear the canvas before we start drawing on it. - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Tell WebGL to use our program when drawing gl.useProgram(programInfo.program); @@ -206,9 +261,13 @@ function drawScene(gl, programInfo, positionBuffer, textureCoordBuffer, texture) // Tell the shader we bound the texture to texture unit 0 gl.uniform1i(programInfo.uniformLocations.uSampler, 0); - // Set the laser coordinates + // Set the laser coordinates gl.uniform1f(programInfo.uniformLocations.uLaserX, laserX); gl.uniform1f(programInfo.uniformLocations.uLaserY, laserY); + + // Set display flags + gl.uniform1i(programInfo.uniformLocations.uDarkMode, isDarkMode ? 1 : 0); + gl.uniform1f(programInfo.uniformLocations.uContrastLevel, contrastValue); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); } @@ -263,9 +322,72 @@ function resizeGLCanvas(canvas) { return false; // indicates no change in size } +// Direct laser pointer position - no animation for more reliability function updateLaserPosition(x, y) { - laserX = x / screenWidth * gl.canvas.width; - laserY = gl.canvas.height - (y / screenHeight * gl.canvas.height); - + // If x and y are valid positive values + if (x > 0 && y > 0) { + // Position is now directly proportional to canvas size + laserX = x * (gl.canvas.width / screenWidth); + laserY = gl.canvas.height - (y * (gl.canvas.height / screenHeight)); + } else { + // Hide the pointer by moving it off-screen + laserX = -10; + laserY = -10; + } + + // Redraw immediately drawScene(gl, programInfo, positionBuffer, textureCoordBuffer, texture); } + +// Function to update dark mode state with transition effect +let darkModeTransition = 0; // 0 = light mode, 1 = dark mode +let transitionActive = false; + +function setDarkMode(darkModeEnabled) { + isDarkMode = darkModeEnabled; + + // If not already transitioning, start a smooth transition + if (!transitionActive) { + transitionActive = true; + const startTime = performance.now(); + const duration = 300; // transition duration in ms + + function animateDarkModeTransition(timestamp) { + const elapsed = timestamp - startTime; + const progress = Math.min(elapsed / duration, 1); + + // Update transition value (0 to 1 for light to dark) + darkModeTransition = darkModeEnabled ? progress : 1 - progress; + + // Render with current transition value + drawScene(gl, programInfo, positionBuffer, textureCoordBuffer, texture); + + // Continue animation if not complete + if (progress < 1) { + requestAnimationFrame(animateDarkModeTransition); + } else { + transitionActive = false; + } + } + + requestAnimationFrame(animateDarkModeTransition); + } else { + // Just update the scene if already transitioning + drawScene(gl, programInfo, positionBuffer, textureCoordBuffer, texture); + } +} + +// Function to set contrast level +function setContrast(contrastLevel) { + // Store the contrast value (between 1.0 and 3.0) + contrastValue = parseFloat(contrastLevel); + + // If the value is valid, update rendering + if (!isNaN(contrastValue) && contrastValue >= 1.0 && contrastValue <= 3.0) { + // Update the scene with new contrast + drawScene(gl, programInfo, positionBuffer, textureCoordBuffer, texture); + + // Save user preference to localStorage + localStorage.setItem('contrastLevel', contrastValue); + } +} diff --git a/client/index.html b/client/index.html index 7f889be..26e5059 100644 --- a/client/index.html +++ b/client/index.html @@ -3,7 +3,7 @@