| <!doctype HTML> |
| |
| <!DOCTYPE html> |
| <title>Custom Image Upscaling</title> |
| <meta charset="utf-8" /> |
| <meta http-equiv="X-UA-Compatible" content="IE=edge"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <script type="text/javascript" src="https://unpkg.com/canvaskit-wasm@0.34.1/bin/full/canvaskit.js"></script> |
| |
| <style> |
| canvas { |
| border: 1px dashed grey; |
| } |
| </style> |
| |
| <body> |
| <h1>Custom Image Upscaling</h1> |
| |
| <div id=scale_text></div> |
| <div class="slidecontainer"> |
| <input type="range" min="100" max="500" value="100" class="slider" id="scale_slider"> |
| </div> |
| |
| <canvas id=draw width=1000 height=400></canvas> |
| </body> |
| |
| <script type="text/javascript" charset="utf-8"> |
| let CanvasKit; |
| onload = async () => { |
| CanvasKit = await CanvasKitInit({ locateFile: (file) => "https://unpkg.com/canvaskit-wasm@0.34.1/bin/full/" + file }); |
| init(); |
| }; |
| |
| function init() { |
| if (!CanvasKit.RuntimeEffect) { |
| console.log(CanvasKit.RuntimeEffect); |
| throw "Need RuntimeEffect"; |
| } |
| const surface = CanvasKit.MakeCanvasSurface('draw'); |
| if (!surface) { |
| throw 'Could not make surface'; |
| } |
| |
| const prog = ` |
| uniform shader image; |
| uniform float sharp; // slope of the lerp section of the kernel (steeper == sharper) |
| |
| float2 sharpen(float2 w) { |
| // we think of sharp as a slope on a shifted line |
| // y = sharp * (w - 0.5) + 0.5 |
| // Rewrite with mix needed for some GPUs to be correct |
| return saturate(mix(float2(0.5), w, sharp)); |
| } |
| |
| bool nearly_center(float2 p) { |
| float tolerance = 1/255.0; |
| p = abs(fract(p) - 0.5); |
| return p.x < tolerance && p.y < tolerance; |
| } |
| |
| half4 main(float2 p) { |
| // p+1/2, p-1/2 can be numerically unstable when near the center, so we |
| // detect that case, and just sample at our center. |
| float h = nearly_center(p) ? 0.0 : 0.5; |
| |
| // Manual bilerp logic |
| half4 pa = image.eval(float2(p.x-h, p.y-h)); |
| half4 pb = image.eval(float2(p.x+h, p.y-h)); |
| half4 pc = image.eval(float2(p.x-h, p.y+h)); |
| half4 pd = image.eval(float2(p.x+h, p.y+h)); |
| |
| // Now 'sharpen' the weighting. This is the magic sauce where we different |
| // from a normal bilerp |
| float2 w = sharpen(fract(p + 0.5)); |
| return mix(mix(pa, pb, w.x), |
| mix(pc, pd, w.x), w.y); |
| } |
| `; |
| const effect = CanvasKit.RuntimeEffect.Make(prog); |
| |
| const size = 100; |
| const shader_paint = new CanvasKit.Paint(); |
| const color_paint = new CanvasKit.Paint(); |
| |
| const image = function() { |
| let surf = CanvasKit.MakeSurface(size, size); |
| let c = surf.getCanvas(); |
| |
| color_paint.setColor([1, 1, 1, 1]); |
| c.drawRect([0, 0, size, size], color_paint); |
| |
| color_paint.setColor([0, 0, 0, 1]); |
| for (let x = 0; x < size; x += 2) { |
| c.drawRect([x, 0, x+1, size], color_paint); |
| } |
| return surf.makeImageSnapshot(); |
| }(); |
| |
| const imageShader = image.makeShaderOptions(CanvasKit.TileMode.Clamp, |
| CanvasKit.TileMode.Clamp, |
| CanvasKit.FilterMode.Nearest, |
| CanvasKit.MipmapMode.None); |
| |
| scale_slider.oninput = () => { surface.requestAnimationFrame(drawFrame); } |
| |
| const fract = function(value) { |
| return value - Math.floor(value); |
| } |
| |
| // Uses custom sampling (4 sample points per-pixel) |
| draw_one_pass = function(canvas, y, scale) { |
| canvas.save(); |
| canvas.scale(scale, 1.0); |
| shader_paint.setShader(effect.makeShaderWithChildren([Math.round(scale)], [imageShader], null)); |
| canvas.drawRect([0, 0, size, y], shader_paint); |
| canvas.restore(); |
| } |
| |
| // First creates an upscaled image, and then bilerps it |
| draw_two_pass = function(canvas, y, scale) { |
| let intScale = Math.max(1, Math.floor(scale + 0.5)); |
| let intImage = imageAtScale(intScale); |
| |
| canvas.save(); |
| canvas.scale(scale / intScale, 1); |
| canvas.drawImageOptions(intImage, 0, y, CanvasKit.FilterMode.Linear, CanvasKit.MipmapMode.None, null); |
| canvas.restore(); |
| } |
| |
| drawFrame = function(canvas) { |
| const scale = scale_slider.value / 100.0; |
| scale_text.innerText = scale |
| |
| canvas.clear(); |
| |
| draw_one_pass(canvas, 100, scale); |
| drawMagnified(canvas, 0, 100); |
| |
| draw_two_pass(canvas, 200, scale); |
| drawMagnified(canvas, 200, 300); |
| } |
| |
| function drawMagnified(canvas, sampleY, dstY) { |
| let pixels = canvas.readPixels( |
| 0, sampleY, |
| { width: 50, |
| height: 1, |
| colorType: CanvasKit.ColorType.RGBA_8888, |
| alphaType: CanvasKit.AlphaType.Premul, |
| colorSpace: CanvasKit.ColorSpace.DISPLAY_P3 |
| } |
| ); |
| |
| for (let i = 0; i < 50; i++) { |
| let color = |
| [ pixels[i*4 + 0] / 255.0, |
| pixels[i*4 + 1] / 255.0, |
| pixels[i*4 + 2] / 255.0, |
| pixels[i*4 + 3] / 255.0 ]; |
| color_paint.setColor(color); |
| canvas.drawRect([i*20, dstY, (i+1)*20, dstY + 100], color_paint); |
| } |
| } |
| |
| function imageAtScale(s) { |
| let surf = CanvasKit.MakeSurface(s * size, size); |
| let c = surf.getCanvas(); |
| |
| color_paint.setColor([1, 1, 1, 1]); |
| c.drawRect([0, 0, s * size, size], color_paint); |
| |
| color_paint.setColor([0, 0, 0, 1]); |
| for (let x = 0; x < size; x += 2) { |
| c.drawRect([x * s, 0, (x+1) * s, size], color_paint); |
| } |
| return surf.makeImageSnapshot(); |
| } |
| |
| surface.requestAnimationFrame(drawFrame); |
| } |
| |
| </script> |
| |