Start work on potential browser-based display like mcopy has, not a priority.
This commit is contained in:
parent
8547e905da
commit
ad97a94c0e
|
|
@ -0,0 +1,369 @@
|
|||
/**
|
||||
* FullscreenDisplay
|
||||
*
|
||||
* Takes over the screen of an iOS device running Safari.
|
||||
* - Requests fullscreen / uses iOS standalone-mode tricks
|
||||
* - Creates a canvas sized to the true physical pixel resolution
|
||||
* (CSS pixels × devicePixelRatio)
|
||||
* - Default state is solid black
|
||||
* - Acquires a Screen Wake Lock so the display never sleeps while active;
|
||||
* automatically re-acquires the lock if the page is briefly hidden then
|
||||
* shown again (iOS releases wake locks on page hide)
|
||||
* - display(image, ms) renders an HTMLImageElement for exactly `ms`
|
||||
* milliseconds, then reverts to black, using requestAnimationFrame
|
||||
* timestamps for sub-frame accuracy
|
||||
*/
|
||||
|
||||
export class FullscreenDisplay {
|
||||
// ─── Public readonly state ───────────────────────────────────────────────
|
||||
|
||||
/** The backing <canvas> element. */
|
||||
public readonly canvas: HTMLCanvasElement;
|
||||
|
||||
/** Physical pixel width (CSS width × devicePixelRatio). */
|
||||
public readonly physicalWidth: number;
|
||||
|
||||
/** Physical pixel height (CSS height × devicePixelRatio). */
|
||||
public readonly physicalHeight: number;
|
||||
|
||||
/** Device pixel ratio detected at construction time. */
|
||||
public readonly devicePixelRatio: number;
|
||||
|
||||
// ─── Private state ───────────────────────────────────────────────────────
|
||||
|
||||
private readonly ctx: CanvasRenderingContext2D;
|
||||
|
||||
/** Overlay <div> that acts as the fullscreen root. */
|
||||
private readonly overlay: HTMLDivElement;
|
||||
|
||||
/** rAF handle so we can cancel an in-flight display call. */
|
||||
private rafHandle: number | null = null;
|
||||
|
||||
/** Resolves the Promise returned by the active display() call. */
|
||||
private displayResolve: (() => void) | null = null;
|
||||
|
||||
/**
|
||||
* Active Screen Wake Lock sentinel.
|
||||
* Typed loosely because lib.dom.d.ts only gained WakeLockSentinel
|
||||
* in TypeScript 4.4+; the cast below keeps older configs happy.
|
||||
*/
|
||||
private wakeLock: WakeLockSentinel | null = null;
|
||||
|
||||
// ─── Construction ─────────────────────────────────────────────────────────
|
||||
|
||||
constructor() {
|
||||
// ── 1. Determine true screen density ──────────────────────────────────
|
||||
//
|
||||
// window.devicePixelRatio is the authoritative source on iOS Safari.
|
||||
// Common values: 2× (most iPhones / standard iPads), 3× (Pro iPhones).
|
||||
this.devicePixelRatio = window.devicePixelRatio ?? 1;
|
||||
|
||||
// ── 2. Build fullscreen overlay ────────────────────────────────────────
|
||||
this.overlay = document.createElement("div");
|
||||
|
||||
const overlayStyle = this.overlay.style;
|
||||
overlayStyle.position = "fixed";
|
||||
overlayStyle.inset = "0"; // top/right/bottom/left: 0
|
||||
overlayStyle.width = "100%";
|
||||
overlayStyle.height = "100%";
|
||||
overlayStyle.margin = "0";
|
||||
overlayStyle.padding = "0";
|
||||
overlayStyle.backgroundColor = "#000";
|
||||
overlayStyle.zIndex = "2147483647"; // max z-index
|
||||
// Prevent any rubber-band / scroll bleed-through on iOS
|
||||
overlayStyle.overflow = "hidden";
|
||||
overlayStyle.touchAction = "none";
|
||||
// GPU-composited layer – avoids repaint on canvas updates
|
||||
overlayStyle.transform = "translateZ(0)";
|
||||
overlayStyle.webkitTransform = "translateZ(0)";
|
||||
|
||||
document.body.appendChild(this.overlay);
|
||||
|
||||
// ── 3. Size the canvas to physical pixels ──────────────────────────────
|
||||
//
|
||||
// On iOS, window.innerWidth/Height gives the CSS pixel viewport after
|
||||
// the browser chrome has been accounted for. Multiply by DPR to get
|
||||
// the true raster resolution.
|
||||
const cssWidth = window.innerWidth;
|
||||
const cssHeight = window.innerHeight;
|
||||
|
||||
this.physicalWidth = Math.round(cssWidth * this.devicePixelRatio);
|
||||
this.physicalHeight = Math.round(cssHeight * this.devicePixelRatio);
|
||||
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.canvas.width = this.physicalWidth;
|
||||
this.canvas.height = this.physicalHeight;
|
||||
|
||||
const canvasStyle = this.canvas.style;
|
||||
canvasStyle.display = "block";
|
||||
canvasStyle.width = `${cssWidth}px`; // CSS size = viewport
|
||||
canvasStyle.height = `${cssHeight}px`;
|
||||
canvasStyle.backgroundColor = "#000";
|
||||
|
||||
this.overlay.appendChild(this.canvas);
|
||||
|
||||
// ── 4. Grab 2-D context with best quality hints ────────────────────────
|
||||
const ctx = this.canvas.getContext("2d", {
|
||||
alpha: false, // opaque → faster compositing
|
||||
desynchronized: false, // keep in sync with display
|
||||
});
|
||||
|
||||
if (ctx === null) {
|
||||
throw new Error("FullscreenDisplay: could not acquire 2D canvas context.");
|
||||
}
|
||||
|
||||
this.ctx = ctx;
|
||||
|
||||
// Scale once so every draw call works in physical pixels automatically.
|
||||
// (We do NOT use CSS transform scaling so image quality is always 1:1.)
|
||||
this.ctx.scale(this.devicePixelRatio, this.devicePixelRatio);
|
||||
|
||||
// ── 5. Paint initial black frame ───────────────────────────────────────
|
||||
this.paintBlack();
|
||||
|
||||
// ── 6. Attempt fullscreen (Web Fullscreen API) ─────────────────────────
|
||||
//
|
||||
// iOS 16.4+ supports Element.requestFullscreen() on Safari.
|
||||
// Older versions ignore it silently; the fixed overlay still covers
|
||||
// the screen, so we degrade gracefully.
|
||||
this.requestNativeFullscreen();
|
||||
|
||||
// ── 7. Listen for resize (orientation change, split-view, etc.) ────────
|
||||
window.addEventListener("resize", this.handleResize);
|
||||
|
||||
// ── 8. Acquire Screen Wake Lock ────────────────────────────────────────
|
||||
//
|
||||
// Prevents iOS from dimming or auto-locking the screen.
|
||||
// Supported in Safari 16.4+; silently no-ops on older versions.
|
||||
// The lock is automatically released by the browser whenever the page
|
||||
// is hidden (e.g. user switches apps), so we re-acquire it on
|
||||
// visibilitychange.
|
||||
this.acquireWakeLock();
|
||||
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
||||
}
|
||||
|
||||
// ─── Public API ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Display `image` for exactly `durationMs` milliseconds, then go back to
|
||||
* black. Returns a Promise that resolves when the black frame is shown.
|
||||
*
|
||||
* If called while a previous display() is still running, the previous one
|
||||
* is cancelled immediately (image removed, black shown) before the new
|
||||
* one starts.
|
||||
*
|
||||
* @param image A fully-loaded HTMLImageElement.
|
||||
* @param durationMs How long (in milliseconds) to show the image.
|
||||
*/
|
||||
public display(image: HTMLImageElement, durationMs: number): Promise<void> {
|
||||
// Cancel any in-flight display.
|
||||
this.cancelCurrentDisplay();
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
this.displayResolve = resolve;
|
||||
|
||||
// We capture the rAF timestamp on the very first paint frame to use
|
||||
// as the precise start time, avoiding any lag between Promise
|
||||
// construction and actual screen output.
|
||||
let startTime: number | null = null;
|
||||
|
||||
const frame = (timestamp: DOMHighResTimeStamp): void => {
|
||||
// ── First frame: record start, draw image ────────────────────────
|
||||
if (startTime === null) {
|
||||
startTime = timestamp;
|
||||
this.paintImage(image);
|
||||
}
|
||||
|
||||
const elapsed = timestamp - startTime;
|
||||
|
||||
if (elapsed < durationMs) {
|
||||
// Still within the window – schedule next check.
|
||||
this.rafHandle = requestAnimationFrame(frame);
|
||||
} else {
|
||||
// Duration expired – go back to black and resolve.
|
||||
this.paintBlack();
|
||||
this.rafHandle = null;
|
||||
this.displayResolve = null;
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
this.rafHandle = requestAnimationFrame(frame);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually cancel any active display() and return to black immediately.
|
||||
*/
|
||||
public cancelCurrentDisplay(): void {
|
||||
if (this.rafHandle !== null) {
|
||||
cancelAnimationFrame(this.rafHandle);
|
||||
this.rafHandle = null;
|
||||
}
|
||||
|
||||
if (this.displayResolve !== null) {
|
||||
this.paintBlack();
|
||||
const resolve = this.displayResolve;
|
||||
this.displayResolve = null;
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tear down the overlay and release all resources.
|
||||
* Cancels any running display() first.
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.cancelCurrentDisplay();
|
||||
window.removeEventListener("resize", this.handleResize);
|
||||
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
||||
|
||||
this.releaseWakeLock();
|
||||
|
||||
if (document.fullscreenElement === this.overlay) {
|
||||
document.exitFullscreen().catch(() => {/* ignore */});
|
||||
}
|
||||
|
||||
this.overlay.remove();
|
||||
}
|
||||
|
||||
// ─── Private helpers ──────────────────────────────────────────────────────
|
||||
|
||||
/** Fill the entire canvas with solid black. */
|
||||
private paintBlack(): void {
|
||||
// Reset transform before raw pixel fill to avoid double-scaling.
|
||||
this.ctx.save();
|
||||
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
this.ctx.fillStyle = "#000000";
|
||||
this.ctx.fillRect(0, 0, this.physicalWidth, this.physicalHeight);
|
||||
this.ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw `image` scaled to fill the canvas while preserving aspect ratio
|
||||
* (object-fit: contain semantics, centred, letterboxed with black).
|
||||
*/
|
||||
private paintImage(image: HTMLImageElement): void {
|
||||
const cssWidth = window.innerWidth;
|
||||
const cssHeight = window.innerHeight;
|
||||
|
||||
const imgW = image.naturalWidth || image.width;
|
||||
const imgH = image.naturalHeight || image.height;
|
||||
|
||||
if (imgW === 0 || imgH === 0) {
|
||||
// Malformed image – just show black.
|
||||
this.paintBlack();
|
||||
return;
|
||||
}
|
||||
|
||||
const scale = Math.min(cssWidth / imgW, cssHeight / imgH);
|
||||
const drawW = imgW * scale;
|
||||
const drawH = imgH * scale;
|
||||
const drawX = (cssWidth - drawW) / 2;
|
||||
const drawY = (cssHeight - drawH) / 2;
|
||||
|
||||
// Background black (letterbox / pillarbox areas).
|
||||
this.paintBlack();
|
||||
|
||||
// The canvas context was scaled by DPR at construction, so all
|
||||
// drawImage coordinates here are in CSS pixels – which is exactly
|
||||
// what we want.
|
||||
this.ctx.drawImage(image, drawX, drawY, drawW, drawH);
|
||||
}
|
||||
|
||||
/** Attempt the Web Fullscreen API; silently no-op if unavailable. */
|
||||
private requestNativeFullscreen(): void {
|
||||
const el = this.overlay as HTMLDivElement & {
|
||||
webkitRequestFullscreen?: () => Promise<void>;
|
||||
};
|
||||
|
||||
if (typeof el.requestFullscreen === "function") {
|
||||
el.requestFullscreen({ navigationUI: "hide" }).catch(() => {/* ok */});
|
||||
} else if (typeof el.webkitRequestFullscreen === "function") {
|
||||
// Older Safari / WebKit prefix
|
||||
el.webkitRequestFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Wake Lock helpers ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Request a 'screen' wake lock via the Screen Wake Lock API.
|
||||
* Fails silently if the API is unavailable or the request is rejected
|
||||
* (e.g. Low Power Mode on iOS).
|
||||
*/
|
||||
private acquireWakeLock(): void {
|
||||
if (!("wakeLock" in navigator)) return;
|
||||
|
||||
(navigator.wakeLock as WakeLock)
|
||||
.request("screen")
|
||||
.then((sentinel: WakeLockSentinel) => {
|
||||
this.wakeLock = sentinel;
|
||||
|
||||
// If the sentinel is released externally (browser-initiated),
|
||||
// clear our reference so we know to re-acquire on next
|
||||
// visibilitychange.
|
||||
sentinel.addEventListener("release", () => {
|
||||
if (this.wakeLock === sentinel) {
|
||||
this.wakeLock = null;
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// Permission denied, Low Power Mode, or API unavailable – no-op.
|
||||
});
|
||||
}
|
||||
|
||||
/** Release the active wake lock, if any. */
|
||||
private releaseWakeLock(): void {
|
||||
if (this.wakeLock !== null) {
|
||||
this.wakeLock.release().catch(() => {/* ignore */});
|
||||
this.wakeLock = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-acquire the wake lock when the page returns to the foreground.
|
||||
* iOS releases all wake locks the moment the page is hidden, so this
|
||||
* is required to keep the screen alive after the user briefly switches
|
||||
* away and comes back.
|
||||
*
|
||||
* Arrow function so `this` is correctly bound as an event listener.
|
||||
*/
|
||||
private readonly handleVisibilityChange = (): void => {
|
||||
if (document.visibilityState === "visible") {
|
||||
this.acquireWakeLock();
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Resize helper ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Resizes the canvas backing store and repaints black.
|
||||
*
|
||||
* Arrow function so `this` is always bound correctly as an event listener.
|
||||
*/
|
||||
private readonly handleResize = (): void => {
|
||||
const cssWidth = window.innerWidth;
|
||||
const cssHeight = window.innerHeight;
|
||||
const dpr = window.devicePixelRatio ?? 1;
|
||||
|
||||
const newPhysW = Math.round(cssWidth * dpr);
|
||||
const newPhysH = Math.round(cssHeight * dpr);
|
||||
|
||||
// Mutating readonly at runtime – use type assertion.
|
||||
(this as { physicalWidth: number }).physicalWidth = newPhysW;
|
||||
(this as { physicalHeight: number }).physicalHeight = newPhysH;
|
||||
|
||||
this.canvas.width = newPhysW;
|
||||
this.canvas.height = newPhysH;
|
||||
this.canvas.style.width = `${cssWidth}px`;
|
||||
this.canvas.style.height = `${cssHeight}px`;
|
||||
|
||||
// Re-apply the DPR scale after canvas resize (resizing resets transform).
|
||||
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
this.ctx.scale(dpr, dpr);
|
||||
|
||||
this.paintBlack();
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
|
||||
<html>
|
||||
<body>
|
||||
<button id="goFullscreen">Go Fullscreen</button>
|
||||
<script>
|
||||
document.getElementById('goFullscreen').addEventListener('click', () => {
|
||||
// 2. Request Fullscreen
|
||||
document.documentElement.requestFullscreen().then(() => {
|
||||
// 3. Get Resolution after entering fullscreen
|
||||
const width = window.screen.width;
|
||||
const height = window.screen.height;
|
||||
console.log(`Resolution: ${width}x${height}`); // [1, 10]
|
||||
}).catch((err) => {
|
||||
console.error(`Error: ${err.message}`);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
# Tests To Perform
|
||||
|
||||
This is a brain dump of all things that should be tested with this system and make it the highest quality it can be for the price.
|
||||
|
||||
### Stocks
|
||||
|
||||
Print and reversal stocks to project.
|
||||
|
||||
* 3302 - 2 - Orthochromatic
|
||||
* 3383 - 3 - Balance with RGB exposures
|
||||
* 3378E - 6-25
|
||||
* 7294 - 100 (?)
|
||||
* 7266 - 200
|
||||
|
||||
Negative stocks to be used as a kind of internegative.
|
||||
|
||||
* 7203 - 50
|
||||
* 7207 - 250
|
||||
* 7213 - 200 - Tungsten
|
||||
* 7219 - 500 - Tungsten
|
||||
* 7222 - 200
|
||||
|
||||
|
||||
### Factors
|
||||
|
||||
These are the different factors that can be controlled for in individual tests.
|
||||
Combining multiple tests that do not impact or influence one another
|
||||
|
||||
* Lens Distance x Image Size
|
||||
* Exposure
|
||||
* Density
|
||||
* Contrast
|
||||
* Color balance
|
||||
* Screens (finishes/resolution/technologies)
|
||||
* Saturation control
|
||||
* Resolution (screen)
|
||||
* Resolution (lens)
|
||||
* Focus
|
||||
* Glare
|
||||
* Speed
|
||||
|
||||
|
||||
#### Exposure
|
||||
|
||||
Track exposure data by printing full-frames of colors or values and then have a corresponding set of video analysis tests in place.
|
||||
|
||||
Factors to track in QR code/datamatrix as a single-line csv.
|
||||
Should be bookended with a single code containing the constants throughout.
|
||||
|
||||
* Identifier ("FO" for filmout)
|
||||
* Const Identifier ("CONST")
|
||||
* Test Name
|
||||
* Test ID
|
||||
* Frame number (this one)
|
||||
* Screen
|
||||
* Brightness of screen (nits)
|
||||
* Image Resolution
|
||||
* Filmstock
|
||||
* ISO (as rated for test)
|
||||
* F/stop
|
||||
* Lens
|
||||
* Distance
|
||||
* Process (D-19/D-96/C-41/ECN-2/ECP-2/D-19BWR/D-96BWR/E-6)
|
||||
* Lab
|
||||
|
||||
```
|
||||
FO,CONST,INITIALDENSITY,3378ED19@6,00003,MacBookPro,1000,1440x1080,3378E,12,1000,5.6,50,35000,D-19@6,MONO
|
||||
```
|
||||
|
||||
Factors to track per-frame.
|
||||
|
||||
* Identifier ("FO")
|
||||
* Frame number (of sample data frame)
|
||||
* Exposure
|
||||
* Filename
|
||||
* Notes
|
||||
|
||||
```
|
||||
FO,00005,1000,example.png,Measure_density
|
||||
```
|
||||
|
||||
Output to track:
|
||||
|
||||
* Brightness (Calculated)
|
||||
* Brightness (Perceptual)
|
||||
* Min/Max (Avg,R,G,B)
|
||||
* Density (Grayscale)
|
||||
* Histogram (R,G,B)
|
||||
* Histogram (Total)
|
||||
* Histogram (Perceptual)
|
||||
|
||||
### Processing Results
|
||||
|
||||
Scan to full frame ("Overscan") at 6.5K in .dpx.
|
||||
Once a test is processed, export frames to a ProRes 444 HQ proxy.
|
||||
Scans are flat, log and frame-to-frame accurate.
|
||||
Trim the fat.
|
||||
|
||||
Run the tests again on the proxy to determine if there would be any differences in the results.
|
||||
Move to only ProRes 444 if no difference.
|
||||
|
||||
Measure density with X-Rite portable unit.
|
||||
Log using a tool, preferably
|
||||
|
||||
Use scripts to determine frame dimensions within image.
|
||||
|
||||
|
||||
### ETC
|
||||
|
||||
Not sure where to log this, but each stock needs a "best guess" setting for producing a readable QR code or datamatrix between each test frame.
|
||||
Doing this properly will allow for valuable test data be printed between each test frame as a QR code or datamatrix and parsed to process the frame.
|
||||
Need to create a process for reading the data as part of the data analysis.
|
||||
Threshold the image in 5 different ways and calculate
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
|
||||
system_profiler SPDisplaysDataType
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
export declare class FullscreenDisplay {
|
||||
readonly canvas: HTMLCanvasElement;
|
||||
readonly physicalWidth: number;
|
||||
readonly physicalHeight: number;
|
||||
readonly devicePixelRatio: number;
|
||||
private readonly ctx;
|
||||
private readonly overlay;
|
||||
private rafHandle;
|
||||
private displayResolve;
|
||||
private wakeLock;
|
||||
constructor();
|
||||
display(image: HTMLImageElement, durationMs: number): Promise<void>;
|
||||
cancelCurrentDisplay(): void;
|
||||
destroy(): void;
|
||||
private paintBlack;
|
||||
private paintImage;
|
||||
private requestNativeFullscreen;
|
||||
private acquireWakeLock;
|
||||
private releaseWakeLock;
|
||||
private readonly handleVisibilityChange;
|
||||
private readonly handleResize;
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
define(["require", "exports"], function (require, exports) {
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.FullscreenDisplay = void 0;
|
||||
class FullscreenDisplay {
|
||||
constructor() {
|
||||
this.rafHandle = null;
|
||||
this.displayResolve = null;
|
||||
this.wakeLock = null;
|
||||
this.handleVisibilityChange = () => {
|
||||
if (document.visibilityState === "visible") {
|
||||
this.acquireWakeLock();
|
||||
}
|
||||
};
|
||||
this.handleResize = () => {
|
||||
const cssWidth = window.innerWidth;
|
||||
const cssHeight = window.innerHeight;
|
||||
const dpr = window.devicePixelRatio ?? 1;
|
||||
const newPhysW = Math.round(cssWidth * dpr);
|
||||
const newPhysH = Math.round(cssHeight * dpr);
|
||||
this.physicalWidth = newPhysW;
|
||||
this.physicalHeight = newPhysH;
|
||||
this.canvas.width = newPhysW;
|
||||
this.canvas.height = newPhysH;
|
||||
this.canvas.style.width = `${cssWidth}px`;
|
||||
this.canvas.style.height = `${cssHeight}px`;
|
||||
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
this.ctx.scale(dpr, dpr);
|
||||
this.paintBlack();
|
||||
};
|
||||
this.devicePixelRatio = window.devicePixelRatio ?? 1;
|
||||
this.overlay = document.createElement("div");
|
||||
const overlayStyle = this.overlay.style;
|
||||
overlayStyle.position = "fixed";
|
||||
overlayStyle.inset = "0";
|
||||
overlayStyle.width = "100%";
|
||||
overlayStyle.height = "100%";
|
||||
overlayStyle.margin = "0";
|
||||
overlayStyle.padding = "0";
|
||||
overlayStyle.backgroundColor = "#000";
|
||||
overlayStyle.zIndex = "2147483647";
|
||||
overlayStyle.overflow = "hidden";
|
||||
overlayStyle.touchAction = "none";
|
||||
overlayStyle.transform = "translateZ(0)";
|
||||
overlayStyle.webkitTransform = "translateZ(0)";
|
||||
document.body.appendChild(this.overlay);
|
||||
const cssWidth = window.innerWidth;
|
||||
const cssHeight = window.innerHeight;
|
||||
this.physicalWidth = Math.round(cssWidth * this.devicePixelRatio);
|
||||
this.physicalHeight = Math.round(cssHeight * this.devicePixelRatio);
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.canvas.width = this.physicalWidth;
|
||||
this.canvas.height = this.physicalHeight;
|
||||
const canvasStyle = this.canvas.style;
|
||||
canvasStyle.display = "block";
|
||||
canvasStyle.width = `${cssWidth}px`;
|
||||
canvasStyle.height = `${cssHeight}px`;
|
||||
canvasStyle.backgroundColor = "#000";
|
||||
this.overlay.appendChild(this.canvas);
|
||||
const ctx = this.canvas.getContext("2d", {
|
||||
alpha: false,
|
||||
desynchronized: false,
|
||||
});
|
||||
if (ctx === null) {
|
||||
throw new Error("FullscreenDisplay: could not acquire 2D canvas context.");
|
||||
}
|
||||
this.ctx = ctx;
|
||||
this.ctx.scale(this.devicePixelRatio, this.devicePixelRatio);
|
||||
this.paintBlack();
|
||||
this.requestNativeFullscreen();
|
||||
window.addEventListener("resize", this.handleResize);
|
||||
this.acquireWakeLock();
|
||||
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
||||
}
|
||||
display(image, durationMs) {
|
||||
this.cancelCurrentDisplay();
|
||||
return new Promise((resolve) => {
|
||||
this.displayResolve = resolve;
|
||||
let startTime = null;
|
||||
const frame = (timestamp) => {
|
||||
if (startTime === null) {
|
||||
startTime = timestamp;
|
||||
this.paintImage(image);
|
||||
}
|
||||
const elapsed = timestamp - startTime;
|
||||
if (elapsed < durationMs) {
|
||||
this.rafHandle = requestAnimationFrame(frame);
|
||||
}
|
||||
else {
|
||||
this.paintBlack();
|
||||
this.rafHandle = null;
|
||||
this.displayResolve = null;
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
this.rafHandle = requestAnimationFrame(frame);
|
||||
});
|
||||
}
|
||||
cancelCurrentDisplay() {
|
||||
if (this.rafHandle !== null) {
|
||||
cancelAnimationFrame(this.rafHandle);
|
||||
this.rafHandle = null;
|
||||
}
|
||||
if (this.displayResolve !== null) {
|
||||
this.paintBlack();
|
||||
const resolve = this.displayResolve;
|
||||
this.displayResolve = null;
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
destroy() {
|
||||
this.cancelCurrentDisplay();
|
||||
window.removeEventListener("resize", this.handleResize);
|
||||
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
||||
this.releaseWakeLock();
|
||||
if (document.fullscreenElement === this.overlay) {
|
||||
document.exitFullscreen().catch(() => { });
|
||||
}
|
||||
this.overlay.remove();
|
||||
}
|
||||
paintBlack() {
|
||||
this.ctx.save();
|
||||
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
this.ctx.fillStyle = "#000000";
|
||||
this.ctx.fillRect(0, 0, this.physicalWidth, this.physicalHeight);
|
||||
this.ctx.restore();
|
||||
}
|
||||
paintImage(image) {
|
||||
const cssWidth = window.innerWidth;
|
||||
const cssHeight = window.innerHeight;
|
||||
const imgW = image.naturalWidth || image.width;
|
||||
const imgH = image.naturalHeight || image.height;
|
||||
if (imgW === 0 || imgH === 0) {
|
||||
this.paintBlack();
|
||||
return;
|
||||
}
|
||||
const scale = Math.min(cssWidth / imgW, cssHeight / imgH);
|
||||
const drawW = imgW * scale;
|
||||
const drawH = imgH * scale;
|
||||
const drawX = (cssWidth - drawW) / 2;
|
||||
const drawY = (cssHeight - drawH) / 2;
|
||||
this.paintBlack();
|
||||
this.ctx.drawImage(image, drawX, drawY, drawW, drawH);
|
||||
}
|
||||
requestNativeFullscreen() {
|
||||
const el = this.overlay;
|
||||
if (typeof el.requestFullscreen === "function") {
|
||||
el.requestFullscreen({ navigationUI: "hide" }).catch(() => { });
|
||||
}
|
||||
else if (typeof el.webkitRequestFullscreen === "function") {
|
||||
el.webkitRequestFullscreen();
|
||||
}
|
||||
}
|
||||
acquireWakeLock() {
|
||||
if (!("wakeLock" in navigator))
|
||||
return;
|
||||
navigator.wakeLock
|
||||
.request("screen")
|
||||
.then((sentinel) => {
|
||||
this.wakeLock = sentinel;
|
||||
sentinel.addEventListener("release", () => {
|
||||
if (this.wakeLock === sentinel) {
|
||||
this.wakeLock = null;
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
});
|
||||
}
|
||||
releaseWakeLock() {
|
||||
if (this.wakeLock !== null) {
|
||||
this.wakeLock.release().catch(() => { });
|
||||
this.wakeLock = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.FullscreenDisplay = FullscreenDisplay;
|
||||
});
|
||||
//# sourceMappingURL=display.js.map
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,8 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Screen</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="/static/js/display.js"></script>
|
||||
</html>
|
||||
Loading…
Reference in New Issue