Build a Viewfinder Web Component using Stencil.js

In tasks like barcode scanning and text recognition, we often need to limit the scan region so that only the desired info is extracted. In this article, we are going to build a viewfinder web component as the following which shows which area will be processed. Stencil.js is used to build such a component.

Viewfinder

Build a Viewfinder Web Component using Stencil.js

Let’s do this in steps.

Start a New Project

Run the following command:

npm init stencil

You will be prompted to create a component project or an app project. Here, we choose component.

? Select a starter project.

Starters marked as [community] are developed by the Stencil
Community, rather than Ionic. For more information on the 
Stencil Community, please see github.com/stencil-community
› - Use arrow-keys. Return to submit.

❯   component          Collection of web components that can be
                       used anywhere
    app [community]    Minimal starter for building a Stencil 
                       app or website
                       

Generate a New Component named Viewfinder

In the project, create a new component named viewfinder.

npx stencil g view-finder

To test the component, we can modify the src\index.html:

-   <my-component first="Stencil" last="'Don't call me a framework' JS"></my-component>
+   <view-finder>
+     <p>Inner elements</p>
+   </view-finder>

Then run the following command to test it:

npm run start

Draw the Viewfinder with SVG

We are going to draw the viewfinder with SVG.

  1. Define props of the viewfinder component.

    @Prop() width: number;
    @Prop() height: number;
    @Prop() left: number;
    @Prop() top: number;
    @Prop() right: number;
    @Prop() bottom: number;
    @Prop() preserveAspectRatio?: string;
    
    • width: width for the viewBox of the SVG.
    • height: height for the viewBox of the SVG.
    • left: left of the scan region.
    • top: top of the scan region.
    • right: right of the scan region.
    • bottom: bottom of the scan region.
    • preserveAspectRatio: the preserveAspectRatio attribute of the SVG.
  2. Draw a mask rect which covers the whole SVG.

    <svg xmlns="http://www.w3.org/2000/svg" 
      viewBox={"0 0 "+this.width+" "+this.height}>
      <rect width={this.width} height={this.height} class="mask-rect">
      </rect>
    </svg>
    

    The style:

    :host {
      --mask-color: rgba(0,0,0,0.7);
     }
    
    .mask-rect {
      fill:var(--mask-color);
      stroke-width:0;
    }
    
  3. Reveal the scan region using masking. Masking is a feature of SVG that has the ability to fully or partially hide portions of an object through the use of simple or complex shapes. 1 Shapes with white color show the object while shapes with black color hide the object.

    Add the following in the SVG element:

    <defs>
      <mask id="myMask">
        <rect 
          x="0" 
          y="0" 
          width={this.width} 
          height={this.height} fill="white" />
        <rect 
          x={this.left} 
          y={this.top} 
          width={this.right - this.left} 
          height={this.bottom - this.top} fill="black" 
        />
      </mask>
    </defs>
    

    Then use it in the previous rectangle.

     <rect width={this.width} height={this.height} 
       class="mask-rect" 
    +  mask="url(#myMask)">
     </rect>
    
  4. Draw a rect around the scan region as the border.

    <rect x={this.left} y={this.top} width={this.right - this.left} height={this.bottom - this.top} class="scan-rect"/>
    

    The style:

    :host {
      --scan-rect-color:green;
      --scan-rect-stroke-width:2;
    }
       
    .scan-rect {
      stroke:var(--scan-rect-color);
      stroke-width:var(--scan-rect-stroke-width);
      fill-opacity:0.0;
    }
    
  5. Draw a scan line which travels the scan region to indicate the status. The animation is done using the animate feature of SVG.

    <line 
      x1={this.left}
      y1={this.top}
      x2={this.right}
      y2={this.top} 
      class="scan-line">
      <animate attributeName="y1" to={this.bottom}  begin="0s" dur="2s"  repeatCount="indefinite" />
      <animate attributeName="y2" to={this.bottom}  begin="0s" dur="2s"  repeatCount="indefinite" />
    </line>
    

    The style:

    :host {
      --scan-line-color: red;
      --scan-line-stroke-width:2;
    }
       
    .scan-line {
      stroke:var(--scan-line-color);
      stroke-width:var(--scan-line-stroke-width);
    }
    

All right, we’ve now finished writing the component.

Use the Component in a React Barcode Scanner

The component can be used with vanilla JavaScript or with frameworks. We are going to use it in a previous React barcode scanner project using Dynamsoft Barcode Reader.

  1. Install the component.

    Open public/index.html and add the following so that we can use the component:

    <script type="module">
      import { defineCustomElements } from 'https://cdn.jsdelivr.net/npm/viewfinder-component@0.2.0/dist/esm/loader.js';
      defineCustomElements();
    </script>
    
  2. Create a React wrapper for the component.

    import React from 'react';
    
    export const ViewFinder = (props) => {
      return (
        <view-finder 
          left={props.left}
          top={props.top}
          right={props.right}
          bottom={props.bottom}
          height={props.height}
          width={props.width}
          preserve-aspect-ratio={props.preserveAspectRatio}
          style={props.style}
        >
        </view-finder>
      );
    }
    
  3. Then, we can add the viewfinder component above the barcode scanner’s camera.

     <BarcodeScanner 
       isActive={isActive}
       drawOverlay={true}
       desiredCamera={desiredCamera}
       desiredResolution={desiredResolution}
       onScanned={onScanned}
       onOpened={onOpened}
       onClosed={onClosed}
       onClicked={onClicked}
       onDeviceListLoaded={onDeviceListLoaded}
       onInitialized={onInitialized}
     >
    +  {((initialized && opened) && isActive) &&
    +    <ViewFinder 
    +      ref={viewFinder}
    +      width={currentVideoWidth}
    +      height={currentVideoHeight}
    +      left={left}
    +      top={top}
    +      right={right}
    +      bottom={bottom}
    +      preserveAspectRatio="xMidYMid slice"
    +      style={{"--scan-line-color":"red","--scan-rect-color":"green"}}
    +    >
    +    </ViewFinder>
    +  }
       {((!initialized || !opened) && isActive) &&
         <img src={loading} className="loading" alt="loading" />
       }
     </BarcodeScanner>
    
  4. We can take a step further to update the Dynamsoft Barcode Reader’s runtime settings so that only the scan region will be processed.

    const settings = await reader.current.getRuntimeSettings();
    settings.region.regionLeft = left;
    settings.region.regionTop = top;
    settings.region.regionBottom = bottom;
    settings.region.regionRight = right;
    settings.region.regionMeasuredByPercentage = 0;
    await reader.current.updateRuntimeSettings(settings);
    

You can check out the online demo to have a try.

Source Code

References