optica-core 0.2.5 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "optica-core",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "Shared frontend core of the Optica viewers",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -80,6 +80,11 @@ const canvas = useTemplateRef('canvas')
80
80
 
81
81
  // image drawn on the canvas, the thumbnail first then the full image
82
82
  let baseImage: HTMLImageElement = new Image()
83
+ // halved copies of baseImage (level i is 1/2^i of its size), drawn when zoomed out
84
+ // so the noise is averaged instead of aliased
85
+ let pyramid: CanvasImageSource[] = []
86
+ // no more halving below this size, in pixels
87
+ const PYRAMID_MIN_SIZE = 512
83
88
  // incremented on each image change so late loads of a previous image are ignored
84
89
  let loadToken = 0
85
90
 
@@ -97,7 +102,7 @@ function loadImage() {
97
102
  if (token != loadToken) {
98
103
  return
99
104
  }
100
- baseImage = first
105
+ setBaseImage(first)
101
106
  if (props.camera.zoom <= 0 || props.fitOnImageChange) {
102
107
  screenFit()
103
108
  }
@@ -112,7 +117,7 @@ function loadImage() {
112
117
  full.alt = image.name
113
118
  full.onload = () => {
114
119
  if (token == loadToken) {
115
- baseImage = full
120
+ setBaseImage(full)
116
121
  update()
117
122
  }
118
123
  }
@@ -123,6 +128,26 @@ function loadImage() {
123
128
  first.src = image.thumbnail || image.image
124
129
  }
125
130
 
131
+ function setBaseImage(image: HTMLImageElement) {
132
+ baseImage = image
133
+ pyramid = [image]
134
+ let prev: CanvasImageSource = image
135
+ let width = image.naturalWidth
136
+ let height = image.naturalHeight
137
+ while (width > PYRAMID_MIN_SIZE && height > PYRAMID_MIN_SIZE) {
138
+ width = Math.ceil(width / 2)
139
+ height = Math.ceil(height / 2)
140
+ const level = document.createElement('canvas')
141
+ level.width = width
142
+ level.height = height
143
+ const ctx = level.getContext('2d')!
144
+ ctx.imageSmoothingQuality = 'high'
145
+ ctx.drawImage(prev, 0, 0, width, height)
146
+ pyramid.push(level)
147
+ prev = level
148
+ }
149
+ }
150
+
126
151
  loadImage()
127
152
 
128
153
  watch(() => [props.image.name, props.image.image, props.image.thumbnail], loadImage)
@@ -253,7 +278,13 @@ function drawImage(c: HTMLCanvasElement) {
253
278
  x: Math.max(0, (c.width - baseImage.naturalWidth * zoomX) / 2) / zoomX,
254
279
  y: Math.max(0, (c.height - baseImage.naturalHeight * zoomY) / 2) / zoomY
255
280
  }
256
- ctx.drawImage(baseImage, 0, 0, baseImage.naturalWidth, baseImage.naturalHeight,
281
+ // the smallest level still at least as detailed as the screen, the last downscale stays under 2x
282
+ const level = Math.max(0, Math.min(pyramid.length - 1, Math.floor(Math.log2(1 / Math.max(zoomX, zoomY)))))
283
+ const source = pyramid[level]
284
+ const sourceWidth = level == 0 ? baseImage.naturalWidth : (source as HTMLCanvasElement).width
285
+ const sourceHeight = level == 0 ? baseImage.naturalHeight : (source as HTMLCanvasElement).height
286
+ ctx.imageSmoothingQuality = 'high'
287
+ ctx.drawImage(source, 0, 0, sourceWidth, sourceHeight,
257
288
  shiftCanvas.x, shiftCanvas.y, baseImage.naturalWidth, baseImage.naturalHeight)
258
289
 
259
290
  const toCanvas = (pos: Coordinates): Coordinates => ({