@bubo-squared/gyroview 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/dist/define.d.ts +3 -0
- package/dist/define.js +4 -0
- package/dist/index.d.ts +959 -0
- package/dist/index.js +13 -0
- package/dist/packages/adapters/blob/src/BlobRandomAccessSource.js +37 -0
- package/dist/packages/adapters/blob/src/index.js +2 -0
- package/dist/packages/adapters/fetch/src/HttpRangeSource.js +194 -0
- package/dist/packages/adapters/fetch/src/HttpResourceLocator.js +35 -0
- package/dist/packages/adapters/fetch/src/httpRequest.js +126 -0
- package/dist/packages/adapters/fetch/src/index.js +3 -0
- package/dist/packages/adapters/mediabunny/src/MediabunnyAudioSegments.js +133 -0
- package/dist/packages/adapters/mediabunny/src/MediabunnyDemuxer.js +63 -0
- package/dist/packages/adapters/mediabunny/src/MediabunnyVideoTrackReader.js +84 -0
- package/dist/packages/adapters/mediabunny/src/SegmentChannel.js +84 -0
- package/dist/packages/adapters/mediabunny/src/bufferSources.js +12 -0
- package/dist/packages/adapters/mediabunny/src/index.js +2 -0
- package/dist/packages/adapters/mse-audio/src/MediaSourceAudioClock.js +139 -0
- package/dist/packages/adapters/mse-audio/src/SourceBufferFeeder.js +174 -0
- package/dist/packages/adapters/mse-audio/src/events.js +26 -0
- package/dist/packages/adapters/mse-audio/src/index.js +2 -0
- package/dist/packages/adapters/mse-audio/src/mediaSourceSupport.js +82 -0
- package/dist/packages/adapters/three/src/ThreeFrameRenderer.js +274 -0
- package/dist/packages/adapters/three/src/compileAndProve.js +14 -0
- package/dist/packages/adapters/three/src/fullscreenPass.js +52 -0
- package/dist/packages/adapters/three/src/index.js +2 -0
- package/dist/packages/adapters/three/src/lensTextures.js +25 -0
- package/dist/packages/adapters/three/src/pictureMaterials.js +44 -0
- package/dist/packages/adapters/three/src/renderInto.js +15 -0
- package/dist/packages/adapters/three/src/rendererUniforms.js +157 -0
- package/dist/packages/adapters/three/src/samplingStrategies.js +26 -0
- package/dist/packages/adapters/three/src/seamMeter/SeamMeterPass.js +79 -0
- package/dist/packages/adapters/three/src/seamMeter/rowMeans.js +38 -0
- package/dist/packages/adapters/three/src/shaderPrograms.js +66 -0
- package/dist/packages/adapters/three/src/shaders/analysis.frag.js +4 -0
- package/dist/packages/adapters/three/src/shaders/equirectangularRays.js +4 -0
- package/dist/packages/adapters/three/src/shaders/fixedJoin.js +4 -0
- package/dist/packages/adapters/three/src/shaders/fullscreen.vert.js +4 -0
- package/dist/packages/adapters/three/src/shaders/header.js +4 -0
- package/dist/packages/adapters/three/src/shaders/lensModels.js +4 -0
- package/dist/packages/adapters/three/src/shaders/lensSampling.js +4 -0
- package/dist/packages/adapters/three/src/shaders/lensTextures.js +4 -0
- package/dist/packages/adapters/three/src/shaders/rawLenses.frag.js +4 -0
- package/dist/packages/adapters/three/src/shaders/rectilinearRays.js +4 -0
- package/dist/packages/adapters/three/src/shaders/screenAreas.js +4 -0
- package/dist/packages/adapters/three/src/shaders/stitch.frag.js +4 -0
- package/dist/packages/adapters/three/src/webglRenderer.js +37 -0
- package/dist/packages/adapters/webcodecs/src/WebCodecsVideoDecoderPort.js +135 -0
- package/dist/packages/adapters/webcodecs/src/index.js +2 -0
- package/dist/packages/core/src/application/gainMatching/GainMatching.js +66 -0
- package/dist/packages/core/src/application/gainMatching/GainMatchingFrameSink.js +62 -0
- package/dist/packages/core/src/application/playback/ClockWatch.js +76 -0
- package/dist/packages/core/src/application/playback/DecodePipeline.js +188 -0
- package/dist/packages/core/src/application/playback/DecodeRun.js +93 -0
- package/dist/packages/core/src/application/playback/FramePairQueue.js +93 -0
- package/dist/packages/core/src/application/playback/FramePairer.js +60 -0
- package/dist/packages/core/src/application/playback/PlaybackSession.js +417 -0
- package/dist/packages/core/src/application/playback/SeekOrder.js +36 -0
- package/dist/packages/core/src/application/playback/SessionLifecycle.js +42 -0
- package/dist/packages/core/src/application/playback/StartGate.js +55 -0
- package/dist/packages/core/src/application/playback/TimeUpdates.js +31 -0
- package/dist/packages/core/src/application/playback/WallClock.js +46 -0
- package/dist/packages/core/src/application/playback/keyframeTimeAt.js +15 -0
- package/dist/packages/core/src/application/playback/probeDecoding.js +168 -0
- package/dist/packages/core/src/application/recording/frameTimesOf.js +22 -0
- package/dist/packages/core/src/application/recording/inspectLayout.js +21 -0
- package/dist/packages/core/src/application/recording/inspectRecording.js +124 -0
- package/dist/packages/core/src/application/recording/locateOtherLensFile.js +18 -0
- package/dist/packages/core/src/application/recording/motionOf.js +39 -0
- package/dist/packages/core/src/application/recording/readRecording.js +31 -0
- package/dist/packages/core/src/application/recording/timeRecording.js +57 -0
- package/dist/packages/core/src/application/stabilization/StabilizingFrameSink.js +31 -0
- package/dist/packages/core/src/domain/format/boxes/BoxLayout.js +7 -0
- package/dist/packages/core/src/domain/format/boxes/boxConstants.js +16 -0
- package/dist/packages/core/src/domain/format/boxes/scanBoxes.js +59 -0
- package/dist/packages/core/src/domain/format/calibration/CalibrationVersion.js +13 -0
- package/dist/packages/core/src/domain/format/calibration/layouts/CalibrationStringLayout.js +34 -0
- package/dist/packages/core/src/domain/format/calibration/layouts/LegacyCalibrationLayout.js +44 -0
- package/dist/packages/core/src/domain/format/calibration/layouts/MeiCalibrationLayout.js +45 -0
- package/dist/packages/core/src/domain/format/calibration/layouts/PolynomialCalibrationLayout.js +44 -0
- package/dist/packages/core/src/domain/format/calibration/offsetTokens.js +65 -0
- package/dist/packages/core/src/domain/format/calibration/parseOffsetString.js +52 -0
- package/dist/packages/core/src/domain/format/calibration/selectCalibration.js +46 -0
- package/dist/packages/core/src/domain/format/captureOrigin.js +20 -0
- package/dist/packages/core/src/domain/format/constants.js +33 -0
- package/dist/packages/core/src/domain/format/info/infoFields.js +110 -0
- package/dist/packages/core/src/domain/format/info/parseInfoRecord.js +90 -0
- package/dist/packages/core/src/domain/format/info/readInfoRecord.js +14 -0
- package/dist/packages/core/src/domain/format/layout/detectLensLayout.js +122 -0
- package/dist/packages/core/src/domain/format/naming/RecordingFileName.js +67 -0
- package/dist/packages/core/src/domain/format/records/TrailerRecords.js +83 -0
- package/dist/packages/core/src/domain/format/records/exposure/parseExposureRecord.js +48 -0
- package/dist/packages/core/src/domain/format/records/gyro/FloatGyroSampleLayout.js +39 -0
- package/dist/packages/core/src/domain/format/records/gyro/RawGyroSampleLayout.js +55 -0
- package/dist/packages/core/src/domain/format/records/gyro/SampleColumns.js +82 -0
- package/dist/packages/core/src/domain/format/records/gyro/gyroLayouts.js +12 -0
- package/dist/packages/core/src/domain/format/records/gyro/parseGyroRecord.js +129 -0
- package/dist/packages/core/src/domain/format/trailer/RecordHeader.js +23 -0
- package/dist/packages/core/src/domain/format/trailer/Trailer.js +23 -0
- package/dist/packages/core/src/domain/format/trailer/TrailerFooter.js +27 -0
- package/dist/packages/core/src/domain/format/trailer/locateRecords.js +80 -0
- package/dist/packages/core/src/domain/format/trailer/parseRecordIndex.js +28 -0
- package/dist/packages/core/src/domain/format/trailer/readTrailer.js +32 -0
- package/dist/packages/core/src/domain/motion/exposure/ExposureRecord.js +53 -0
- package/dist/packages/core/src/domain/motion/gyro/GyroTrack.js +60 -0
- package/dist/packages/core/src/domain/motion/gyro/medianStep.js +11 -0
- package/dist/packages/core/src/domain/motion/gyro/repairedTimeline.js +101 -0
- package/dist/packages/core/src/domain/motion/imu/ImuFrame.js +112 -0
- package/dist/packages/core/src/domain/motion/orientation/OrientationTrack.js +146 -0
- package/dist/packages/core/src/domain/motion/orientation/estimateGyroBias.js +64 -0
- package/dist/packages/core/src/domain/motion/orientation/gravity.js +97 -0
- package/dist/packages/core/src/domain/motion/stabilization/Stabilizer.js +13 -0
- package/dist/packages/core/src/domain/motion/stabilization/stabilizers.js +86 -0
- package/dist/packages/core/src/domain/motion/timing/CaptureClock.js +41 -0
- package/dist/packages/core/src/domain/motion/timing/FrameTimes.js +92 -0
- package/dist/packages/core/src/domain/motion/timing/frameTimeSources.js +61 -0
- package/dist/packages/core/src/domain/motion/timing/resolveFrameTimes.js +68 -0
- package/dist/packages/core/src/domain/optics/EquidistantModel.js +39 -0
- package/dist/packages/core/src/domain/optics/MeiModel.js +45 -0
- package/dist/packages/core/src/domain/optics/PolynomialModel.js +44 -0
- package/dist/packages/core/src/domain/optics/gainMatch.js +62 -0
- package/dist/packages/core/src/domain/optics/lensGeometry.js +18 -0
- package/dist/packages/core/src/domain/optics/lensPose.js +35 -0
- package/dist/packages/core/src/domain/optics/opticsConstants.js +5 -0
- package/dist/packages/core/src/domain/playback/PlayerState.js +83 -0
- package/dist/packages/core/src/domain/stitching/LensLayout.js +21 -0
- package/dist/packages/core/src/domain/stitching/StitchingSetup.js +78 -0
- package/dist/packages/core/src/domain/stitching/seamDisparity.js +4 -0
- package/dist/packages/core/src/domain/stitching/seamDisparityField.js +5 -0
- package/dist/packages/core/src/domain/stitching/seamJoin.js +16 -0
- package/dist/packages/core/src/domain/stitching/seamMismatch.js +1 -0
- package/dist/packages/core/src/domain/stitching/seamRing.js +4 -0
- package/dist/packages/core/src/domain/stitching/seamStrip.js +26 -0
- package/dist/packages/core/src/domain/view/Framing.js +31 -0
- package/dist/packages/core/src/domain/view/PictureQuality.js +26 -0
- package/dist/packages/core/src/domain/view/ViewMode.js +9 -0
- package/dist/packages/core/src/domain/view/ViewState.js +48 -0
- package/dist/packages/core/src/domain/view/lensTilesView.js +63 -0
- package/dist/packages/core/src/domain/view/magnification.js +103 -0
- package/dist/packages/core/src/domain/view/normalView.js +29 -0
- package/dist/packages/core/src/domain/view/panoramaView.js +99 -0
- package/dist/packages/core/src/domain/view/rectilinear.js +40 -0
- package/dist/packages/core/src/domain/view/screenLayout.js +113 -0
- package/dist/packages/core/src/domain/view/viewGestures.js +98 -0
- package/dist/packages/core/src/domain/view/viewModes.js +14 -0
- package/dist/packages/core/src/index.js +59 -0
- package/dist/packages/core/src/ports/FramePair.js +6 -0
- package/dist/packages/core/src/shared/async/Deferred.js +42 -0
- package/dist/packages/core/src/shared/async/RunStop.js +40 -0
- package/dist/packages/core/src/shared/binary/ByteRange.js +38 -0
- package/dist/packages/core/src/shared/binary/ByteReader.js +77 -0
- package/dist/packages/core/src/shared/errors/GyroViewError.js +81 -0
- package/dist/packages/core/src/shared/errors/reportLater.js +11 -0
- package/dist/packages/core/src/shared/events/Outbox.js +68 -0
- package/dist/packages/core/src/shared/events/TypedEmitter.js +55 -0
- package/dist/packages/core/src/shared/keysOf.js +10 -0
- package/dist/packages/core/src/shared/lazy.js +11 -0
- package/dist/packages/core/src/shared/math/Matrix3.js +93 -0
- package/dist/packages/core/src/shared/math/Quaternion.js +133 -0
- package/dist/packages/core/src/shared/math/Vector3.js +54 -0
- package/dist/packages/core/src/shared/math/clamp.js +9 -0
- package/dist/packages/core/src/shared/protobuf/ProtobufMessage.js +157 -0
- package/dist/packages/core/src/shared/text/urlPath.js +31 -0
- package/dist/packages/core/src/shared/text/utf8.js +12 -0
- package/dist/packages/core/src/shared/units/angle.js +22 -0
- package/dist/packages/core/src/shared/units/time.js +13 -0
- package/dist/packages/player/src/PlayerSource.js +15 -0
- package/dist/packages/player/src/browserPlayer.js +18 -0
- package/dist/packages/player/src/choices.js +25 -0
- package/dist/packages/player/src/composition/Disposables.js +31 -0
- package/dist/packages/player/src/composition/browserPorts.js +46 -0
- package/dist/packages/player/src/composition/buildPipeline.js +153 -0
- package/dist/packages/player/src/composition/ensureDecodable.js +63 -0
- package/dist/packages/player/src/composition/openInputs.js +179 -0
- package/dist/packages/player/src/composition/openRecording.js +42 -0
- package/dist/packages/player/src/controls/ChoiceMenu.js +113 -0
- package/dist/packages/player/src/controls/PictureMenus.js +47 -0
- package/dist/packages/player/src/controls/SeekBar.js +125 -0
- package/dist/packages/player/src/controls/SoundControls.js +41 -0
- package/dist/packages/player/src/controls/TransportButtons.js +31 -0
- package/dist/packages/player/src/controls/ViewGestures.js +136 -0
- package/dist/packages/player/src/controls/Wording.js +78 -0
- package/dist/packages/player/src/controls/controlParts.js +36 -0
- package/dist/packages/player/src/controls/controls.js +4 -0
- package/dist/packages/player/src/controls/controlsBar.js +23 -0
- package/dist/packages/player/src/controls/controlsMarkup.js +85 -0
- package/dist/packages/player/src/controls/customControls.js +59 -0
- package/dist/packages/player/src/controls/formatTime.js +19 -0
- package/dist/packages/player/src/controls/icons.js +40 -0
- package/dist/packages/player/src/controls/keyboard.js +118 -0
- package/dist/packages/player/src/controls/messages.js +94 -0
- package/dist/packages/player/src/controls/parseMarkup.js +31 -0
- package/dist/packages/player/src/controls/viewButtons.js +14 -0
- package/dist/packages/player/src/element/ElementLoads.js +119 -0
- package/dist/packages/player/src/element/FullscreenToggle.js +39 -0
- package/dist/packages/player/src/element/GyroViewElement.js +313 -0
- package/dist/packages/player/src/element/IdleWatcher.js +71 -0
- package/dist/packages/player/src/element/TypedEventElement.js +22 -0
- package/dist/packages/player/src/element/accessibleRegion.js +19 -0
- package/dist/packages/player/src/element/attributeNames.js +51 -0
- package/dist/packages/player/src/element/attributes.js +101 -0
- package/dist/packages/player/src/element/defineGyroView.js +21 -0
- package/dist/packages/player/src/element/earlyProperties.js +36 -0
- package/dist/packages/player/src/element/elementSource.js +23 -0
- package/dist/packages/player/src/element/liveSettings.js +138 -0
- package/dist/packages/player/src/element/playbackAttributes.js +53 -0
- package/dist/packages/player/src/element/playerEventMirror.js +33 -0
- package/dist/packages/player/src/element/reflectedProperties.js +78 -0
- package/dist/packages/player/src/element/styles.js +4 -0
- package/dist/packages/player/src/element/template.js +47 -0
- package/dist/packages/player/src/index.js +10 -0
- package/dist/packages/player/src/inspectRecording.js +22 -0
- package/dist/packages/player/src/player/DrawingBufferFit.js +77 -0
- package/dist/packages/player/src/player/FrameLoop.js +40 -0
- package/dist/packages/player/src/player/PictureSettings.js +60 -0
- package/dist/packages/player/src/player/Player.js +377 -0
- package/dist/packages/player/src/player/PlayerEvents.js +28 -0
- package/dist/packages/player/src/player/PlayerPhase.js +21 -0
- package/dist/packages/player/src/player/PlayerSound.js +61 -0
- package/dist/packages/player/src/player/PlayerView.js +113 -0
- package/dist/packages/player/src/player/SessionRelay.js +66 -0
- package/dist/packages/player/src/player/ensureFinite.js +26 -0
- package/dist/packages/player/src/player/loadRecording.js +65 -0
- package/dist/packages/player/src/player/transportEvents.js +29 -0
- package/dist/standalone.js +4919 -0
- package/package.json +73 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { seconds } from "../../shared/units/time.js";
|
|
2
|
+
import { interpolateVectors } from "../../shared/math/Vector3.js";
|
|
3
|
+
import { clamp } from "../../shared/math/clamp.js";
|
|
4
|
+
var DEFAULT_TIME_CONSTANT_SECONDS = 1.5;
|
|
5
|
+
/**
|
|
6
|
+
* A channel darker than this (0..1) carries no exposure information; its gain is left at one.
|
|
7
|
+
*/
|
|
8
|
+
var DARK_CHANNEL = .01;
|
|
9
|
+
var UNIT_GAIN = [
|
|
10
|
+
1,
|
|
11
|
+
1,
|
|
12
|
+
1
|
|
13
|
+
];
|
|
14
|
+
var DEFAULT_GAIN_MATCH_OPTIONS = {
|
|
15
|
+
maxGain: 2,
|
|
16
|
+
timeConstant: seconds(DEFAULT_TIME_CONSTANT_SECONDS)
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Per-channel gains that bring every lens's overlap brightness to the first lens's. Lens 0 is
|
|
20
|
+
* the reference and keeps unit gain; the first is the lens the viewer starts facing, so its
|
|
21
|
+
* exposure is the one that must not shift under them.
|
|
22
|
+
*/
|
|
23
|
+
function gainsMatching(overlapMeans, maxGain) {
|
|
24
|
+
const [reference] = overlapMeans;
|
|
25
|
+
return reference ? overlapMeans.map((mean, lensIndex) => lensIndex === 0 ? UNIT_GAIN : channelGains(reference, mean, maxGain)) : [];
|
|
26
|
+
}
|
|
27
|
+
function channelGains(reference, mean, maxGain) {
|
|
28
|
+
return [
|
|
29
|
+
channelGain(reference[0], mean[0], maxGain),
|
|
30
|
+
channelGain(reference[1], mean[1], maxGain),
|
|
31
|
+
channelGain(reference[2], mean[2], maxGain)
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
function channelGain(reference, mean, maxGain) {
|
|
35
|
+
return reference < DARK_CHANNEL || mean < DARK_CHANNEL ? 1 : clamp(reference / mean, 1 / maxGain, maxGain);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Follows the matching gains over time with a first-order low-pass, so a cloud passing one
|
|
39
|
+
* lens does not flicker the other. The first measurement is taken as is.
|
|
40
|
+
*/
|
|
41
|
+
var GainMatcher = class {
|
|
42
|
+
options;
|
|
43
|
+
current;
|
|
44
|
+
lastTime;
|
|
45
|
+
constructor(options = DEFAULT_GAIN_MATCH_OPTIONS) {
|
|
46
|
+
this.options = options;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Feeds the overlap means measured at `time` (media time) and returns the gains to apply.
|
|
50
|
+
*/
|
|
51
|
+
update(overlapMeans, time) {
|
|
52
|
+
const target = gainsMatching(overlapMeans, this.options.maxGain);
|
|
53
|
+
const elapsed = this.lastTime === void 0 ? Infinity : Math.max(time - this.lastTime, 0);
|
|
54
|
+
this.lastTime = time;
|
|
55
|
+
const weight = this.current ? 1 - Math.exp(-elapsed / this.options.timeConstant) : 1;
|
|
56
|
+
const previous = this.current;
|
|
57
|
+
this.current = target.map((gain, lensIndex) => interpolateVectors(previous?.[lensIndex] ?? UNIT_GAIN, gain, weight));
|
|
58
|
+
return this.current;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
//#endregion
|
|
62
|
+
export { GainMatcher, gainsMatching };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { radians } from "../../shared/units/angle.js";
|
|
2
|
+
//#region ../../packages/core/src/domain/optics/lensGeometry.ts
|
|
3
|
+
function toPolar(direction) {
|
|
4
|
+
const [x, y, z] = direction;
|
|
5
|
+
const lateral = Math.hypot(x, y);
|
|
6
|
+
return {
|
|
7
|
+
theta: radians(Math.atan2(lateral, z)),
|
|
8
|
+
radialUnit: lateral === 0 ? [0, 0] : [x / lateral, y / lateral]
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function pixelAtRadius(principalPoint, radius, radialUnit) {
|
|
12
|
+
return {
|
|
13
|
+
x: principalPoint.x + radius * radialUnit[0],
|
|
14
|
+
y: principalPoint.y + radius * radialUnit[1]
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { pixelAtRadius, toPolar };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { HALF_TURN, QUARTER_TURN, degrees, degreesToRadians } from "../../shared/units/angle.js";
|
|
2
|
+
import { multiplyMatrices, rotationAboutX, rotationAboutY, rotationAboutZ } from "../../shared/math/Matrix3.js";
|
|
3
|
+
//#region ../../packages/core/src/domain/optics/lensPose.ts
|
|
4
|
+
/**
|
|
5
|
+
* The roll the rotation applies for a calibration roll: mirrored about the sensor's mounting,
|
|
6
|
+
* the quarter turn nearest to it, since the strings measure the in-plane roll in the other sense
|
|
7
|
+
* than the lens frame turns (measured on two X5 units, ADR 0025). The mounting itself is kept,
|
|
8
|
+
* so a sensor mounted sideways, as on the X5, or upright stays so.
|
|
9
|
+
*/
|
|
10
|
+
function mirroredRoll(roll) {
|
|
11
|
+
const mounting = Math.round(roll / QUARTER_TURN) * QUARTER_TURN;
|
|
12
|
+
return degrees(2 * mounting - roll);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Turns directions in the camera body frame (x right, y down, z forward along lens 0's optical
|
|
16
|
+
* axis) into the lens frame the {@link LensCalibration.model} projects from.
|
|
17
|
+
*
|
|
18
|
+
* The calibration's yaw (about the body's vertical) and pitch (about its lateral axis) turn each
|
|
19
|
+
* lens in the body frame, alike for both lenses; then lens `i` turns half a turn per lens index
|
|
20
|
+
* about the lateral axis to face backwards, its sensor upside down relative to the front one
|
|
21
|
+
* (ADR 0008); then the roll turns the image about the optical axis, read mirrored
|
|
22
|
+
* ({@link mirroredRoll}), the order measured in ADR 0025. The X5 strings carry a roll near 90
|
|
23
|
+
* degrees for both lenses and no half turn, which is why the facing is part of the convention.
|
|
24
|
+
*/
|
|
25
|
+
function lensRotation(lens) {
|
|
26
|
+
const { yaw, pitch, roll } = lens.orientation;
|
|
27
|
+
const yawed = rotationAboutY(degreesToRadians(yaw));
|
|
28
|
+
const pitched = multiplyMatrices(rotationAboutX(degreesToRadians(pitch)), yawed);
|
|
29
|
+
const facing = rotationAboutX(degreesToRadians(degrees(HALF_TURN * lens.lensIndex)));
|
|
30
|
+
const faced = multiplyMatrices(facing, pitched);
|
|
31
|
+
const rolled = rotationAboutZ(degreesToRadians(mirroredRoll(roll)));
|
|
32
|
+
return multiplyMatrices(rolled, faced);
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { lensRotation, mirroredRoll };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { GyroViewError } from "../../shared/errors/GyroViewError.js";
|
|
2
|
+
//#region ../../packages/core/src/domain/playback/PlayerState.ts
|
|
3
|
+
/**
|
|
4
|
+
* Every legal transition; anything else is a programming error. A session is `ready` from
|
|
5
|
+
* construction (its parts are already open). `buffering` is playing without a picture to show:
|
|
6
|
+
* the clock waits for the decoders, on starting, after a seek and when they fall behind. A
|
|
7
|
+
* seek always resumes through `buffering` or lands `paused`; the end is reached from `playing`
|
|
8
|
+
* only; `error` leads only to `disposed`, which is terminal.
|
|
9
|
+
*/
|
|
10
|
+
var TRANSITIONS = {
|
|
11
|
+
ready: [
|
|
12
|
+
"playing",
|
|
13
|
+
"buffering",
|
|
14
|
+
"paused",
|
|
15
|
+
"seeking",
|
|
16
|
+
"error",
|
|
17
|
+
"disposed"
|
|
18
|
+
],
|
|
19
|
+
playing: [
|
|
20
|
+
"buffering",
|
|
21
|
+
"paused",
|
|
22
|
+
"seeking",
|
|
23
|
+
"ended",
|
|
24
|
+
"error",
|
|
25
|
+
"disposed"
|
|
26
|
+
],
|
|
27
|
+
buffering: [
|
|
28
|
+
"playing",
|
|
29
|
+
"paused",
|
|
30
|
+
"seeking",
|
|
31
|
+
"error",
|
|
32
|
+
"disposed"
|
|
33
|
+
],
|
|
34
|
+
paused: [
|
|
35
|
+
"playing",
|
|
36
|
+
"buffering",
|
|
37
|
+
"seeking",
|
|
38
|
+
"error",
|
|
39
|
+
"disposed"
|
|
40
|
+
],
|
|
41
|
+
seeking: [
|
|
42
|
+
"buffering",
|
|
43
|
+
"paused",
|
|
44
|
+
"error",
|
|
45
|
+
"disposed"
|
|
46
|
+
],
|
|
47
|
+
ended: [
|
|
48
|
+
"seeking",
|
|
49
|
+
"error",
|
|
50
|
+
"disposed"
|
|
51
|
+
],
|
|
52
|
+
error: ["disposed"],
|
|
53
|
+
disposed: []
|
|
54
|
+
};
|
|
55
|
+
var FLOWING_STATES = /* @__PURE__ */ new Set(["playing", "buffering"]);
|
|
56
|
+
/**
|
|
57
|
+
* Whether playback is under way: frames flow, or the clock holds for them. A media element is not
|
|
58
|
+
* `paused` then.
|
|
59
|
+
*/
|
|
60
|
+
function isFlowing(state) {
|
|
61
|
+
return state !== void 0 && FLOWING_STATES.has(state);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Explicit player lifecycle instead of a set of booleans.
|
|
65
|
+
*/
|
|
66
|
+
var PlayerStateMachine = class {
|
|
67
|
+
current = "ready";
|
|
68
|
+
get state() {
|
|
69
|
+
return this.current;
|
|
70
|
+
}
|
|
71
|
+
canTransitionTo(next) {
|
|
72
|
+
return TRANSITIONS[this.current].includes(next);
|
|
73
|
+
}
|
|
74
|
+
transitionTo(next) {
|
|
75
|
+
if (!this.canTransitionTo(next)) throw new GyroViewError("invariant-violation", `player cannot go from ${this.current} to ${next}`);
|
|
76
|
+
this.current = next;
|
|
77
|
+
}
|
|
78
|
+
isOneOf(...states) {
|
|
79
|
+
return states.includes(this.current);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
//#endregion
|
|
83
|
+
export { PlayerStateMachine, isFlowing };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region ../../packages/core/src/domain/stitching/LensLayout.ts
|
|
2
|
+
var FULL_FRAME = {
|
|
3
|
+
x: 0,
|
|
4
|
+
y: 0,
|
|
5
|
+
width: 1,
|
|
6
|
+
height: 1
|
|
7
|
+
};
|
|
8
|
+
var LEFT_HALF = {
|
|
9
|
+
x: 0,
|
|
10
|
+
y: 0,
|
|
11
|
+
width: .5,
|
|
12
|
+
height: 1
|
|
13
|
+
};
|
|
14
|
+
var RIGHT_HALF = {
|
|
15
|
+
x: .5,
|
|
16
|
+
y: 0,
|
|
17
|
+
width: .5,
|
|
18
|
+
height: 1
|
|
19
|
+
};
|
|
20
|
+
//#endregion
|
|
21
|
+
export { FULL_FRAME, LEFT_HALF, RIGHT_HALF };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { degrees, degreesToRadians } from "../../shared/units/angle.js";
|
|
2
|
+
import { GyroViewError } from "../../shared/errors/GyroViewError.js";
|
|
3
|
+
import { lensRotation } from "../optics/lensPose.js";
|
|
4
|
+
import { SEAM_RING_ANGLE } from "./seamRing.js";
|
|
5
|
+
//#region ../../packages/core/src/domain/stitching/StitchingSetup.ts
|
|
6
|
+
/**
|
|
7
|
+
* Two 200-degree lenses overlap between 80 and 100 degrees from their axes; the blend spans 5
|
|
8
|
+
* degrees either side of the seam ring, which lies 90 degrees from both.
|
|
9
|
+
*/
|
|
10
|
+
var FEATHER_HALF_WIDTH_DEGREES = 5;
|
|
11
|
+
var DEFAULT_FEATHER = {
|
|
12
|
+
start: degreesToRadians(degrees(SEAM_RING_ANGLE - FEATHER_HALF_WIDTH_DEGREES)),
|
|
13
|
+
end: degreesToRadians(degrees(SEAM_RING_ANGLE + FEATHER_HALF_WIDTH_DEGREES))
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The distinct video tracks a layout draws from, in order of first use by lens index. The
|
|
17
|
+
* composition root hands the session its readers in this order, and {@link LensStitch.frameSlot}
|
|
18
|
+
* indexes the pairs the session presents.
|
|
19
|
+
*/
|
|
20
|
+
function lensFrameOrder(layout) {
|
|
21
|
+
const keys = [];
|
|
22
|
+
for (const source of layout.sources) if (keys.every((key) => !isSameSource(key, source))) keys.push({
|
|
23
|
+
inputIndex: source.inputIndex,
|
|
24
|
+
trackIndex: source.trackIndex
|
|
25
|
+
});
|
|
26
|
+
return keys;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Use case: joins the factory calibration with the detected lens layout into the per-lens
|
|
30
|
+
* parameters a renderer binds. Pure; the renderer only copies numbers into uniforms.
|
|
31
|
+
*/
|
|
32
|
+
function buildStitchingSetup(inputs) {
|
|
33
|
+
const { calibration, layout } = inputs;
|
|
34
|
+
if (calibration.lenses.length !== layout.sources.length) throw new GyroViewError("unsupported-layout", `the calibration describes ${calibration.lenses.length} lenses but the layout has ${layout.sources.length}`);
|
|
35
|
+
const frames = lensFrameOrder(layout);
|
|
36
|
+
return {
|
|
37
|
+
lenses: layout.sources.map((source) => {
|
|
38
|
+
const lens = lensOf(calibration, source.lensIndex);
|
|
39
|
+
return {
|
|
40
|
+
lensIndex: source.lensIndex,
|
|
41
|
+
frameSlot: frames.findIndex((key) => isSameSource(key, source)),
|
|
42
|
+
region: source.region,
|
|
43
|
+
window: canvasWindowOf(calibration, lens),
|
|
44
|
+
rotation: lensRotation(lens),
|
|
45
|
+
projection: lens.model.projection,
|
|
46
|
+
halfFieldOfView: lens.model.halfFieldOfView
|
|
47
|
+
};
|
|
48
|
+
}),
|
|
49
|
+
frameSlotCount: frames.length,
|
|
50
|
+
feather: DEFAULT_FEATHER
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function isSameSource(key, source) {
|
|
54
|
+
return key.inputIndex === source.inputIndex && key.trackIndex === source.trackIndex;
|
|
55
|
+
}
|
|
56
|
+
function lensOf(calibration, lensIndex) {
|
|
57
|
+
const lens = calibration.lenses.find((candidate) => candidate.lensIndex === lensIndex);
|
|
58
|
+
if (!lens) throw new GyroViewError("invalid-calibration", `the calibration has no lens ${lensIndex}`);
|
|
59
|
+
return lens;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The canvas holds the lens images side by side in squares of its height; each lens's square is
|
|
63
|
+
* the one holding its principal point, and the encoded frame shows that whole square. The info
|
|
64
|
+
* record's sensor window (5312 of 5376 at offset 0 on the X5) is deliberately not applied: the
|
|
65
|
+
* fisheye image circle sits at the centre of every X5 frame, where the whole square puts the
|
|
66
|
+
* principal point, not 32 canvas pixels off where the window would (ADR 0014).
|
|
67
|
+
*/
|
|
68
|
+
function canvasWindowOf(calibration, lens) {
|
|
69
|
+
const side = calibration.canvas.height;
|
|
70
|
+
return {
|
|
71
|
+
x: Math.floor(lens.model.principalPoint.x / side) * side,
|
|
72
|
+
y: 0,
|
|
73
|
+
width: side,
|
|
74
|
+
height: side
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
export { buildStitchingSetup, lensFrameOrder };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { degrees } from "../../shared/units/angle.js";
|
|
2
|
+
import { SEAM_BIN_COUNT } from "./seamStrip.js";
|
|
3
|
+
Array.from({ length: SEAM_BIN_COUNT }, () => degrees(0));
|
|
4
|
+
degrees(15);
|
|
5
|
+
degrees(4);
|
|
6
|
+
/**
|
|
7
|
+
* Beyond the most a bent join bends, the blend narrows from the feather band to this half width
|
|
8
|
+
* as the rest of the disparity grows to {@link SEAM_CUT_DISPARITY}: a degree still hides the step
|
|
9
|
+
* in exposure between the lenses, and a double image farther apart than three degrees reads as
|
|
10
|
+
* two objects.
|
|
11
|
+
*/
|
|
12
|
+
var CUT_HALF_WIDTH_DEGREES = 1;
|
|
13
|
+
var CUT_DISPARITY_DEGREES = 3;
|
|
14
|
+
degrees(CUT_HALF_WIDTH_DEGREES);
|
|
15
|
+
degrees(CUT_DISPARITY_DEGREES);
|
|
16
|
+
//#endregion
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//#endregion
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { degrees } from "../../shared/units/angle.js";
|
|
2
|
+
import { SEAM_RING_ANGLE } from "./seamRing.js";
|
|
3
|
+
//#region ../../packages/core/src/domain/stitching/seamStrip.ts
|
|
4
|
+
/**
|
|
5
|
+
* The seam strip: the band of body directions 83 to 97 degrees from body +z, where both lenses
|
|
6
|
+
* see the same scene and their agreement is measured. It is sampled every half degree in
|
|
7
|
+
* azimuth and in the angle from body +z, and read in 5-degree azimuth bins.
|
|
8
|
+
*
|
|
9
|
+
* Azimuth runs from body +x (the right seam) towards body +y (down): 0 right, 90 nadir, 180
|
|
10
|
+
* left, 270 zenith. The strip reaches 7 degrees either side of the ring; a slide of one lens's
|
|
11
|
+
* sampling moves the directions it reads, and the share of each bin both lenses still image
|
|
12
|
+
* (its validity) says how much of a slide the strip holds.
|
|
13
|
+
*/
|
|
14
|
+
var STRIP_HALF_WIDTH_DEGREES = 7;
|
|
15
|
+
var STRIP_STEP_DEGREES = .5;
|
|
16
|
+
var BIN_WIDTH_DEGREES = 5;
|
|
17
|
+
var FULL_CIRCLE_DEGREES = 360;
|
|
18
|
+
degrees(SEAM_RING_ANGLE - STRIP_HALF_WIDTH_DEGREES);
|
|
19
|
+
degrees(STRIP_STEP_DEGREES);
|
|
20
|
+
degrees(BIN_WIDTH_DEGREES);
|
|
21
|
+
Math.round(14 / STRIP_STEP_DEGREES);
|
|
22
|
+
Math.round(BIN_WIDTH_DEGREES / STRIP_STEP_DEGREES);
|
|
23
|
+
var SEAM_BIN_COUNT = Math.round(FULL_CIRCLE_DEGREES / BIN_WIDTH_DEGREES);
|
|
24
|
+
degrees(60), degrees(120);
|
|
25
|
+
//#endregion
|
|
26
|
+
export { SEAM_BIN_COUNT };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { DEFAULT_VIEW, isSameView } from "./ViewState.js";
|
|
2
|
+
import { FITTED, isSameMagnification } from "./magnification.js";
|
|
3
|
+
//#region ../../packages/core/src/domain/view/Framing.ts
|
|
4
|
+
var DEFAULT_FRAMING = {
|
|
5
|
+
view: DEFAULT_VIEW,
|
|
6
|
+
panorama: FITTED,
|
|
7
|
+
lenses: FITTED
|
|
8
|
+
};
|
|
9
|
+
function isSameFraming(a, b) {
|
|
10
|
+
return isSameView(a.view, b.view) && isSameMagnification(a.panorama, b.panorama) && isSameMagnification(a.lenses, b.lenses);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The framing with the normal view replaced, the flat pictures' parts kept.
|
|
14
|
+
*/
|
|
15
|
+
function withView(framing, view) {
|
|
16
|
+
return {
|
|
17
|
+
...framing,
|
|
18
|
+
view
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The framing with the lens tiles' magnification replaced, the other parts kept.
|
|
23
|
+
*/
|
|
24
|
+
function withLenses(framing, lenses) {
|
|
25
|
+
return {
|
|
26
|
+
...framing,
|
|
27
|
+
lenses
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { DEFAULT_FRAMING, isSameFraming, withLenses, withView };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region ../../packages/core/src/domain/view/PictureQuality.ts
|
|
2
|
+
var PICTURE_QUALITIES = [
|
|
3
|
+
"fast",
|
|
4
|
+
"balanced",
|
|
5
|
+
"high"
|
|
6
|
+
];
|
|
7
|
+
/**
|
|
8
|
+
* A mip chain per frame per lens is what every platform the player targets affords, and what
|
|
9
|
+
* the equirectangular view and the raw tiles need to hold still.
|
|
10
|
+
*/
|
|
11
|
+
var DEFAULT_PICTURE_QUALITY = "balanced";
|
|
12
|
+
/**
|
|
13
|
+
* The most device pixels per CSS pixel each quality draws: the screen's own ratio up to this.
|
|
14
|
+
* Above two, the stitch's cost grows faster than what the eye gains from it, so only `high`
|
|
15
|
+
* follows a phone's screen all the way.
|
|
16
|
+
*/
|
|
17
|
+
var PIXEL_RATIO_CAPS = {
|
|
18
|
+
fast: 1,
|
|
19
|
+
balanced: 2,
|
|
20
|
+
high: 3
|
|
21
|
+
};
|
|
22
|
+
function pixelRatioCapOf(quality) {
|
|
23
|
+
return PIXEL_RATIO_CAPS[quality];
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { DEFAULT_PICTURE_QUALITY, PICTURE_QUALITIES, pixelRatioCapOf };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { degrees, degreesToRadians, wrapHalfTurn } from "../../shared/units/angle.js";
|
|
2
|
+
import { ensureInvariant } from "../../shared/errors/GyroViewError.js";
|
|
3
|
+
import { clamp } from "../../shared/math/clamp.js";
|
|
4
|
+
import { multiplyMatrices, rotationAboutX, rotationAboutY } from "../../shared/math/Matrix3.js";
|
|
5
|
+
//#region ../../packages/core/src/domain/view/ViewState.ts
|
|
6
|
+
/**
|
|
7
|
+
* From a short telephoto to a wide angle: past 120 degrees a rectilinear view stretches its
|
|
8
|
+
* edges more than it shows.
|
|
9
|
+
*/
|
|
10
|
+
var NARROWEST_DEGREES = 30;
|
|
11
|
+
var WIDEST_DEGREES = 120;
|
|
12
|
+
var DEFAULT_FIELD_OF_VIEW_DEGREES = 90;
|
|
13
|
+
var MAX_PITCH_DEGREES = 90;
|
|
14
|
+
var DEFAULT_VIEW = {
|
|
15
|
+
yaw: degrees(0),
|
|
16
|
+
pitch: degrees(0),
|
|
17
|
+
fieldOfView: degrees(DEFAULT_FIELD_OF_VIEW_DEGREES)
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Keeps the view inside the sphere: yaw wrapped to (-180, 180], pitch within the poles, field
|
|
21
|
+
* of view within its bounds. Non-finite angles are a programming error.
|
|
22
|
+
*/
|
|
23
|
+
function clampView(view) {
|
|
24
|
+
ensureInvariant([
|
|
25
|
+
view.yaw,
|
|
26
|
+
view.pitch,
|
|
27
|
+
view.fieldOfView
|
|
28
|
+
].every((angle) => Number.isFinite(angle)), "view angles must be finite");
|
|
29
|
+
return {
|
|
30
|
+
...view,
|
|
31
|
+
yaw: wrapHalfTurn(view.yaw),
|
|
32
|
+
pitch: degrees(clamp(view.pitch, -90, MAX_PITCH_DEGREES)),
|
|
33
|
+
fieldOfView: degrees(clamp(view.fieldOfView, NARROWEST_DEGREES, WIDEST_DEGREES))
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function isSameView(a, b) {
|
|
37
|
+
return a.yaw === b.yaw && a.pitch === b.pitch && a.fieldOfView === b.fieldOfView;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Turns view-space directions (z forward) into camera body directions: pitch about the body's
|
|
41
|
+
* lateral axis first, then yaw about its vertical axis. Body y points down, so a positive
|
|
42
|
+
* rotation about x tilts the forward direction upwards.
|
|
43
|
+
*/
|
|
44
|
+
function viewRotation(view) {
|
|
45
|
+
return multiplyMatrices(rotationAboutY(degreesToRadians(view.yaw)), rotationAboutX(degreesToRadians(view.pitch)));
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { DEFAULT_VIEW, clampView, isSameView, viewRotation };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { aspectOf, boundsOf, lensTiles, shiftOf } from "./screenLayout.js";
|
|
2
|
+
import { FITTED, clampMagnification, magnifiedArea, magnifyAt, panMagnification } from "./magnification.js";
|
|
3
|
+
import { withLenses } from "./Framing.js";
|
|
4
|
+
//#region ../../packages/core/src/domain/view/lensTilesView.ts
|
|
5
|
+
/**
|
|
6
|
+
* An arrow's angle moves zoomed tiles as a drag across the viewport would move them, a 90-degree
|
|
7
|
+
* sweep crossing its width: as far as the arrow turns the normal view at its default field of
|
|
8
|
+
* view, so the arrows feel alike in both.
|
|
9
|
+
*/
|
|
10
|
+
var DEGREES_ACROSS_VIEWPORT = 90;
|
|
11
|
+
/**
|
|
12
|
+
* The lens tiles fitted into the viewport, and the rectangle they fill together.
|
|
13
|
+
*/
|
|
14
|
+
function fittedTiles({ viewport, lensCount }) {
|
|
15
|
+
const tiles = lensTiles(lensCount, aspectOf(viewport));
|
|
16
|
+
return {
|
|
17
|
+
tiles,
|
|
18
|
+
bounds: boundsOf(tiles)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function panLenses(framing, delta, context) {
|
|
22
|
+
const shift = shiftOf(delta, context.viewport);
|
|
23
|
+
return withLenses(framing, panMagnification(fittedTiles(context).bounds, framing.lenses, shift));
|
|
24
|
+
}
|
|
25
|
+
function turnLenses(framing, turn, context) {
|
|
26
|
+
const pixelsPerDegree = context.viewport.width / DEGREES_ACROSS_VIEWPORT;
|
|
27
|
+
return panLenses(framing, {
|
|
28
|
+
x: -(turn.yaw * pixelsPerDegree),
|
|
29
|
+
y: turn.pitch * pixelsPerDegree
|
|
30
|
+
}, context);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Every tile moved and enlarged by the same map that takes the fitted tiles to the magnified.
|
|
34
|
+
*/
|
|
35
|
+
function magnifiedTiles(framing, context) {
|
|
36
|
+
const { tiles, bounds } = fittedTiles(context);
|
|
37
|
+
const lenses = clampMagnification(bounds, framing.lenses);
|
|
38
|
+
const area = magnifiedArea(bounds, lenses);
|
|
39
|
+
return tiles.map((tile) => ({
|
|
40
|
+
x: area.x + (tile.x - bounds.x) * lenses.scale,
|
|
41
|
+
y: area.y + (tile.y - bounds.y) * lenses.scale,
|
|
42
|
+
width: tile.width * lenses.scale,
|
|
43
|
+
height: tile.height * lenses.scale
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The decoded images as the camera recorded them, magnified up to four times toward the pointer
|
|
48
|
+
* and moved within their edges once zoomed; never turned or stitched.
|
|
49
|
+
*/
|
|
50
|
+
var LENS_TILES_VIEW = {
|
|
51
|
+
isStabilized: false,
|
|
52
|
+
canPan: ({ lenses }) => lenses.scale > 1,
|
|
53
|
+
pan: panLenses,
|
|
54
|
+
turn: turnLenses,
|
|
55
|
+
zoom: (framing, zoom, context) => withLenses(framing, magnifyAt(fittedTiles(context).bounds, framing.lenses, zoom)),
|
|
56
|
+
reset: (framing) => withLenses(framing, FITTED),
|
|
57
|
+
picture: (framing, context) => ({
|
|
58
|
+
kind: "lens-tiles",
|
|
59
|
+
tiles: magnifiedTiles(framing, context)
|
|
60
|
+
})
|
|
61
|
+
};
|
|
62
|
+
//#endregion
|
|
63
|
+
export { LENS_TILES_VIEW };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { clamp } from "../../shared/math/clamp.js";
|
|
2
|
+
import { HALF, SCREEN_CENTRE, pictureAt } from "./screenLayout.js";
|
|
3
|
+
import { zoomFactor } from "./viewGestures.js";
|
|
4
|
+
/**
|
|
5
|
+
* The picture whole: every flat picture starts fitted into the viewport, its middle at the
|
|
6
|
+
* viewport's centre.
|
|
7
|
+
*/
|
|
8
|
+
var FITTED = {
|
|
9
|
+
scale: 1,
|
|
10
|
+
centre: {
|
|
11
|
+
x: HALF,
|
|
12
|
+
y: HALF
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A scale this close to 1 is the fitted picture: zooming out notch by notch as far as it zoomed
|
|
17
|
+
* in lands within rounding of 1, and must show the picture fitted again, not a hair enlarged.
|
|
18
|
+
*/
|
|
19
|
+
var FITTED_TOLERANCE = 1e-9;
|
|
20
|
+
function isSameMagnification(a, b) {
|
|
21
|
+
return a.scale === b.scale && a.centre.x === b.centre.x && a.centre.y === b.centre.y;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Where the magnified picture lies on the viewport, given where it lies fitted. The fitted
|
|
25
|
+
* rectangle is centred, as every fitted picture is.
|
|
26
|
+
*/
|
|
27
|
+
function magnifiedArea(fitted, magnification) {
|
|
28
|
+
const width = fitted.width * magnification.scale;
|
|
29
|
+
const height = fitted.height * magnification.scale;
|
|
30
|
+
return {
|
|
31
|
+
x: SCREEN_CENTRE.x - magnification.centre.x * width,
|
|
32
|
+
y: SCREEN_CENTRE.y - magnification.centre.y * height,
|
|
33
|
+
width,
|
|
34
|
+
height
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The magnification brought within its limits: a scale from fitted to `MAX_MAGNIFICATION`, and a
|
|
39
|
+
* centre that leaves no bar along an axis where the picture is larger than the viewport, and
|
|
40
|
+
* keeps the picture centred along an axis where it is smaller.
|
|
41
|
+
*/
|
|
42
|
+
function clampMagnification(fitted, magnification) {
|
|
43
|
+
const limited = clamp(magnification.scale, 1, 4);
|
|
44
|
+
const scale = limited - 1 < FITTED_TOLERANCE ? 1 : limited;
|
|
45
|
+
return {
|
|
46
|
+
scale,
|
|
47
|
+
centre: {
|
|
48
|
+
x: clampedCentre(magnification.centre.x, fitted.width * scale),
|
|
49
|
+
y: clampedCentre(magnification.centre.y, fitted.height * scale)
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Magnifies by the zoom's steps, keeping the point of the picture under its focus where it is,
|
|
55
|
+
* as far as the limits allow.
|
|
56
|
+
*/
|
|
57
|
+
function magnifyAt(fitted, magnification, zoom) {
|
|
58
|
+
const current = clampMagnification(fitted, magnification);
|
|
59
|
+
const underFocus = pictureAt(magnifiedArea(fitted, current), zoom.focus);
|
|
60
|
+
const scale = current.scale * zoomFactor(zoom.steps);
|
|
61
|
+
return clampMagnification(fitted, {
|
|
62
|
+
scale,
|
|
63
|
+
centre: {
|
|
64
|
+
x: underFocus.x + (SCREEN_CENTRE.x - zoom.focus.x) / (fitted.width * scale),
|
|
65
|
+
y: underFocus.y + (SCREEN_CENTRE.y - zoom.focus.y) / (fitted.height * scale)
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Moves the picture by a shift across the viewport, as far as its edges allow.
|
|
71
|
+
*/
|
|
72
|
+
function panMagnification(fitted, magnification, shift) {
|
|
73
|
+
const current = clampMagnification(fitted, magnification);
|
|
74
|
+
const moved = pictureShiftOf(fitted, current, shift);
|
|
75
|
+
return clampMagnification(fitted, {
|
|
76
|
+
scale: current.scale,
|
|
77
|
+
centre: {
|
|
78
|
+
x: current.centre.x - moved.across,
|
|
79
|
+
y: current.centre.y - moved.down
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A shift across the viewport as fractions of the picture as magnified: the part of the picture
|
|
85
|
+
* that passes under a fixed point.
|
|
86
|
+
*/
|
|
87
|
+
function pictureShiftOf(fitted, magnification, shift) {
|
|
88
|
+
return {
|
|
89
|
+
across: shift.across / (fitted.width * magnification.scale),
|
|
90
|
+
down: shift.down / (fitted.height * magnification.scale)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Along one axis: the picture's centre point within the range that keeps the viewport covered,
|
|
95
|
+
* `extent` being the picture's size in viewport lengths; the middle when it cannot cover it.
|
|
96
|
+
*/
|
|
97
|
+
function clampedCentre(centre, extent) {
|
|
98
|
+
if (extent <= 1) return HALF;
|
|
99
|
+
const halfViewport = HALF / extent;
|
|
100
|
+
return clamp(centre, halfViewport, 1 - halfViewport);
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
export { FITTED, clampMagnification, isSameMagnification, magnifiedArea, magnifyAt, panMagnification, pictureShiftOf };
|