Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 17 additions & 18 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]
with:
github-actions-user: owulveryck
github-actions-token: ${{secrets.GITHUB_TOKEN}}
- name: GoGitOps Step
id: gogitops
uses: beaujr/[email protected]
with:
github-actions-user: owulveryck
github-actions-token: ${{secrets.GITHUB_TOKEN}}
2 changes: 1 addition & 1 deletion .github/workflows/goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
54 changes: 17 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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.
166 changes: 144 additions & 22 deletions client/glCanvas.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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;
}
}
`;
Expand Down Expand Up @@ -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'),
},
};

Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}
}
Loading