Search code examples
vue.jskonva

vue konva editable text on video


Hey guys so I built this vuejs canvas app and it works quite nicely however I would like to add more functionality to it. For example, I would like to scale the text that is on top of the video with some sort of box around the text. that would be directly editable in the canvas. I've seen people do this with multiple libraries including Konva however I see that there is a Knova-vue and the documentation is horrible has anyone out there made a simple app with the library where you can write on top of a video?

https://jsfiddle.net/bshyvpo0/

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
   <canvas id="canvas" width='500' height='500' ref='canvas' @mousedown='handleMouseDown' @mousemove='handleMouseMove' @mouseup='handleMouseUp' @mouseout='handleMouseOut'>></canvas>
   <input v-model="text" placeholder='type your text'>
   <button @click='addText'>
   add text
   </button>
   <div v-for="(text, index) in texts" @dblclick='selectText(index)'>
   {{index}}:{{text.text}} <div @click='removeText(index)'>X</div>
   </div>
   <img src ='https://shop-resources.prod.cms.tractorsupply.com/resource/image/18248/portrait_ratio3x4/595/793/4c37b7f6d6f9d8a5b223334f1390191b/JJ/ten-reasons-not-to-buy-an-easter-bunny-main.jpg' @click="changeBackground('http://upload.wikimedia.org/wikipedia/commons/7/79/Big_Buck_Bunny_small.ogv')">
   <img src ='https://ce.prismview.com/api/files/templates/43s327k3oqfsf7/poster/poster.jpg' @click="changeBackground('http://ce.prismview.com/api/files/templates/43s327k3oqfsf7/main/360/43s327k3oqfsf7_360.mp4')">

    <video id="video" ref='video' :src="source" controls="false" autoplay loop></video>

    </div>
<script>
        new Vue({
              el: '#app',
              data: {
                source: "http://upload.wikimedia.org/wikipedia/commons/7/79/Big_Buck_Bunny_small.ogv",
                canvas: null,
                canvasOffset: null,
                ctx: null,
                offsetX: null, 
                offsetY:null,
                startX: null,
                startY:null,
                selectedText:null,
                video: null,
                text:'',
                texts: [],
                timer: null,
                index: null
              },
                methods: {
                addText(){
                if(this.text.length){
                let textObj = {
                text: this.text,
                x: 20,
                y: this.texts.length * 20 + 20
                };
                this.texts.push(textObj);
                this.text = '';
                }
                },
                removeText(i){
                this.texts.splice(i, 1);
                },
                textHittest(x, y, textIndex) {
    var text = this.texts[textIndex];
    return (x >= text.x && x <= text.x + text.width && y >= text.y - text.height && y <= text.y);
},
 handleMouseDown(e) {
    e.preventDefault();
    this.startX = parseInt(e.clientX - this.offsetX);
    this.startY = parseInt(e.clientY - this.offsetY);

    // Put your mousedown stuff here
    let vm = this;
/*     for (var i = 0; i < this.texts.length; i++) {
        if (vm.textHittest(vm.startX, vm.startY, i) || 1===1) {
        console.log('selected', vm.selected);
            vm.selectedText = i; 
        }

    }*/
    if(this.index!=null){
    this.selectedText = this.index;
    }
},
selectText(i){
this.index = i;
console.log(this.selectedText);
},
 handleMouseUp(e) {
    e.preventDefault();
    this.selectedText = -1;
},

// also done dragging
handleMouseOut(e) {
    e.preventDefault();
    this.selectedText = -1;
},
 handleMouseMove(e) {
    if (this.selectedText < 0) {
        return;
    }
    e.preventDefault();
    let mouseX = parseInt(e.clientX - this.offsetX);
    let mouseY = parseInt(e.clientY - this.offsetY);

    // Put your mousemove stuff here
    var dx = mouseX - this.startX;
    var dy = mouseY - this.startY;
    this.startX = mouseX;
    this.startY = mouseY;

    var text = this.texts[this.selectedText] || 1;
    text.x += dx;
    text.y += dy;
    this.drawFrame();
},



               drawFrame (){
                        console.log("drawing");
                            this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
                          this.ctx.drawImage(this.video, 0, 0,);
                          this.ctx.fillStyle = 'red';
                          this.ctx.font = "30px Arial";
                          for(let i =0; i<this.texts.length; i++){
                                                    this.ctx.fillText(this.texts[i].text, this.texts[i].x, this.texts[i].y);
                          }
                       this.timer = setTimeout(() => {
                       this.drawFrame()
                       }, 1000/30);

                      },

              initCanvas(){
                this.canvas = this.$refs['canvas'];
                this.video = this.$refs['video'];
                this.ctx = this.canvas.getContext('2d');
                this.canvasOffset = {left:this.canvas.offsetLeft, top: this.canvas.offsetTop};
                             this.offsetX = this.canvasOffset.left;
                            this.offsetY = this.canvasOffset.top;
               const vm = this;
               this.video.addEventListener('play', function(){
               vm.video.style.display = 'none';

               vm.drawFrame();
               })
              },
              changeBackground(source){
              if(source!=this.video.src){
              clearTimeout(this.timer);
              this.source = source;
              this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
              this.ctx.restore();
              }
              }
        },
        mounted: function(){
            this.initCanvas();
        }
      });

</script>

Solution

  • Under went a major change but finally figured it out.

    <template>
      <div>
        <button @click="render">Render</button>
        <h2>Backgrounds</h2>
        <template v-for="background in backgrounds">
          <img
            :src="background.poster"
            class="backgrounds"
            @click="changeBackground(background.video)"
          />
        </template>
        <h2>Images</h2>
        <template v-for="image in images">
          <img :src="image.source" @click="addImage(image.source)" class="images" />
        </template>
        <br />
        <button @click="addText">Add Text</button>
        <button v-if="selectedNode" @click="removeNode">
          Remove selected {{ selectedNode.type }}
        </button>
        <label>Font:</label>
        <select v-model="selectedFont">
          <option value="Arial">Arial</option>
          <option value="Courier New">Courier New</option>
          <option value="Times New Roman">Times New Roman</option>
          <option value="Desoto">Desoto</option>
          <option value="Kalam">Kalam</option>
        </select>
        <label>Font Size</label>
        <input type="number" v-model="selectedFontSize" />
        <label>Font Style:</label>
        <select v-model="selectedFontStyle">
          <option value="normal">Normal</option>
          <option value="bold">Bold</option>
          <option value="italic">Italic</option>
        </select>
        <label>Color:</label>
        <input type="color" v-model="selectedColor" />
        <button
          v-if="selectedNode && selectedNode.type === 'text'"
          @click="updateText"
        >
          Update Text
        </button>
        <br />
        <video
          id="preview"
          v-show="preview"
          :src="preview"
          :width="width"
          :height="height"
          preload="auto"
          controls
        />
        <a v-if="file" :href="file" download="dopeness.mp4">download</a>
        <div id="container"></div>
      </div>
    </template>
    <script>
    export default {
      data() {
        return {
          source: null,
          stage: null,
          layer: null,
          video: null,
          captures: [],
          backgrounds: [
            {
              poster: "/api/files/stock/3oref310k1uud86w/poster/poster.jpg",
              video:
                "/api/files/stock/3oref310k1uud86w/main/1080/3oref310k1uud86w_1080.mp4"
            },
            {
              poster: "/api/files/stock/3yj2e30tk5x6x0ww/poster/poster.jpg",
              video:
                "/api/files/stock/3yj2e30tk5x6x0ww/main/1080/3yj2e30tk5x6x0ww_1080.mp4"
            },
            {
              poster: "/api/files/stock/2ez931ik1mggd6j/poster/poster.jpg",
              video:
                "/api/files/stock/2ez931ik1mggd6j/main/1080/2ez931ik1mggd6j_1080.mp4"
            },
            {
              poster: "/api/files/stock/yxrt4ej4jvimyk15/poster/poster.jpg",
              video:
                "/api/files/stock/yxrt4ej4jvimyk15/main/1080/yxrt4ej4jvimyk15_1080.mp4"
            },
            {
              poster:
                "https://images.costco-static.com/ImageDelivery/imageService?profileId=12026540&itemId=100424771-847&recipeName=680",
              video: "/api/files/jedi/surfing.mp4"
            },
            {
              poster:
                "https://thedefensepost.com/wp-content/uploads/2018/04/us-soldiers-afghanistan-4308413-1170x610.jpg",
              video: "/api/files/jedi/soldiers.mp4"
            }
          ],
          images: [
            { source: "/api/files/jedi/solo.jpg" },
            { source: "api/files/jedi/yoda.jpg" },
            { source: "api/files/jedi/yodaChristmas.jpg" },
            { source: "api/files/jedi/darthMaul.jpg" },
            { source: "api/files/jedi/darthMaul1.jpg" },
            { source: "api/files/jedi/trump.jpg" },
            { source: "api/files/jedi/hat.png" },
            { source: "api/files/jedi/trump.png" },
            { source: "api/files/jedi/bernie.png" },
            { source: "api/files/jedi/skywalker.png" },
            { source: "api/files/jedi/vader.gif" },
            { source: "api/files/jedi/vader2.gif" },
            { source: "api/files/jedi/yoda.gif" },
            { source: "api/files/jedi/kylo.gif" }
          ],
          backgroundVideo: null,
          imageGroups: [],
          anim: null,
          selectedNode: null,
          selectedFont: "Arial",
          selectedColor: "black",
          selectedFontSize: 20,
          selectedFontStyle: "normal",
          width: 1920,
          height: 1080,
          texts: [],
          preview: null,
          file: null,
          canvas: null
        };
      },
      mounted: function() {
        this.initCanvas();
      },
      methods: {
        changeBackground(source) {
          this.source = source;
          this.video.src = this.source;
          this.anim.stop();
          this.anim.start();
          this.video.play();
        },
        removeNode() {
          if (this.selectedNode && this.selectedNode.type === "text") {
            this.selectedNode.transformer.destroy(
              this.selectedNode.text.transformer
            );
            this.selectedNode.text.destroy(this.selectedNode.text);
            this.texts.splice(this.selectedNode.text.index - 1, 1);
            this.selectedNode = null;
            this.layer.draw();
          } else if (this.selectedNode && this.selectedNode.type == "image") {
            this.selectedNode.group.destroy(this.selectedNode);
            this.imageGroups.splice(this.selectedNode.group.index - 1, 1);
            this.selectedNode = null;
            this.layer.draw();
          }
        },
        async addImage(src) {
          let imageObj = null;
          const type = src.slice(src.lastIndexOf("."));
          const vm = this;
          function process(img) {
            return new Promise((resolve, reject) => {
              img.onload = () => resolve({ width: img.width, height: img.height });
            });
          }
          imageObj = new Image();
          imageObj.src = src;
          imageObj.width = 200;
          imageObj.height = 200;
          await process(imageObj);
    
          if (type === ".gif") {
            const canvas = document.createElement("canvas");
            canvas.setAttribute("id", "gif");
            async function onDrawFrame(ctx, frame) {
              ctx.drawImage(frame.buffer, frame.x, frame.y);
              // redraw the layer
              vm.layer.draw();
            }
            gifler(src).frames(canvas, onDrawFrame);
    
            canvas.onload = async () => {
              canvas.parentNode.removeChild(canvas);
            };
            imageObj = canvas;
            const gif = new Image();
            gif.src = src;
            const gifImage = await process(gif);
            imageObj.width = gifImage.width;
            imageObj.height = gifImage.height;
          }
          const image = new Konva.Image({
            x: 50,
            y: 50,
            image: imageObj,
            width: imageObj.width,
            height: imageObj.height,
            position: (0, 0),
            strokeWidth: 10,
            stroke: "blue",
            strokeEnabled: false
          });
    
          this.frame = null;
          const group = new Konva.Group({
            draggable: true
          });
          // add the shape to the layer
          addAnchor(group, 0, 0, "topLeft");
          addAnchor(group, imageObj.width, 0, "topRight");
          addAnchor(group, imageObj.width, imageObj.height, "bottomRight");
          addAnchor(group, 0, imageObj.height, "bottomLeft");
          imageObj = null;
          image.on("click", function() {
            vm.hideAllHelpers();
            vm.selectedNode = { type: "image", group };
            group.find("Circle").show();
    
            vm.layer.draw();
          });
          image.on("mouseover", function(evt) {
            if (vm.selectedNode && vm.selectedNode.type === "image") {
              const index = image.getParent().index;
              const groupId = vm.selectedNode.group.index;
              if (index != groupId) {
                evt.target.strokeEnabled(true);
                vm.layer.draw();
              }
            } else {
              evt.target.strokeEnabled(true);
              vm.layer.draw();
            }
          });
          image.on("mouseout", function(evt) {
            evt.target.strokeEnabled(false);
            vm.layer.draw();
          });
          vm.hideAllHelpers();
          group.find("Circle").show();
          group.add(image);
          vm.layer.add(group);
          vm.imageGroups.push(group);
    
          vm.selectedNode = { type: "image", group };
          vm.layer.draw();
    
          function update(activeAnchor) {
            const group = activeAnchor.getParent();
    
            let topLeft = group.get(".topLeft")[0];
            let topRight = group.get(".topRight")[0];
            let bottomRight = group.get(".bottomRight")[0];
            let bottomLeft = group.get(".bottomLeft")[0];
            let image = group.get("Image")[0];
    
            let anchorX = activeAnchor.getX();
            let anchorY = activeAnchor.getY();
    
            // update anchor positions
            switch (activeAnchor.getName()) {
              case "topLeft":
                topRight.y(anchorY);
                bottomLeft.x(anchorX);
                break;
              case "topRight":
                topLeft.y(anchorY);
                bottomRight.x(anchorX);
                break;
              case "bottomRight":
                bottomLeft.y(anchorY);
                topRight.x(anchorX);
                break;
              case "bottomLeft":
                bottomRight.y(anchorY);
                topLeft.x(anchorX);
                break;
            }
    
            image.position(topLeft.position());
    
            let width = topRight.getX() - topLeft.getX();
            let height = bottomLeft.getY() - topLeft.getY();
            if (width && height) {
              image.width(width);
              image.height(height);
            }
          }
          function addAnchor(group, x, y, name) {
            let stage = vm.stage;
            let layer = vm.layer;
    
            let anchor = new Konva.Circle({
              x: x,
              y: y,
              stroke: "#666",
              fill: "#ddd",
              strokeWidth: 2,
              radius: 8,
              name: name,
              draggable: true,
              dragOnTop: false
            });
    
            anchor.on("dragmove", function() {
              update(this);
              layer.draw();
            });
            anchor.on("mousedown touchstart", function() {
              group.draggable(false);
              this.moveToTop();
            });
            anchor.on("dragend", function() {
              group.draggable(true);
              layer.draw();
            });
            // add hover styling
            anchor.on("mouseover", function() {
              let layer = this.getLayer();
              document.body.style.cursor = "pointer";
              this.strokeWidth(4);
              layer.draw();
            });
            anchor.on("mouseout", function() {
              let layer = this.getLayer();
              document.body.style.cursor = "default";
              this.strokeWidth(2);
              layer.draw();
            });
    
            group.add(anchor);
          }
        },
        hideAllHelpers() {
          for (let i = 0; i < this.texts.length; i++) {
            this.texts[i].transformer.hide();
          }
          for (let b = 0; b < this.imageGroups.length; b++) {
            this.imageGroups[b].find("Circle").hide();
          }
        },
        async startRecording(duration) {
          const chunks = []; // here we will store our recorded media chunks (Blobs)
          const stream = this.canvas.captureStream(30); // grab our canvas MediaStream
          const rec = new MediaRecorder(stream, {
            videoBitsPerSecond: 20000 * 1000
          });
          // every time the recorder has new data, we will store it in our array
          rec.ondataavailable = e => chunks.push(e.data);
          // // only when the recorder stops, we construct a complete Blob from all the chunks
          rec.onstop = async e => {
            this.anim.stop();
    
            const blob = new Blob(chunks, {
              type: "video/webm"
            });
    
            this.preview = await URL.createObjectURL(blob);
            const video = window.document.getElementById("preview");
            const previewVideo = new Konva.Image({
              image: video,
              draggable: false,
              width: this.width,
              height: this.height
            });
            this.layer.add(previewVideo);
    
            console.log("video", video);
            video.addEventListener("ended", () => {
              console.log("preview ended");
              if (!this.file) {
                const vid = new Whammy.fromImageArray(this.captures, 30);
                this.file = URL.createObjectURL(vid);
              }
              previewVideo.destroy();
              this.anim.stop();
              this.anim.start();
              this.video.play();
            });
            let seekResolve;
    
            video.addEventListener("seeked", async () => {
              if (seekResolve) seekResolve();
            });
            video.addEventListener("loadeddata", async () => {
              let interval = 1 / 30;
              let currentTime = 0;
              while (currentTime <= duration && !this.file) {
                video.currentTime = currentTime;
                await new Promise(r => (seekResolve = r));
    
                this.layer.draw();
                let base64ImageData = this.canvas.toDataURL("image/webp");
                this.captures.push(base64ImageData);
                currentTime += interval;
                video.currentTime = currentTime;
              }
    
              this.layer.draw();
            });
          };
          rec.start();
          setTimeout(() => rec.stop(), duration);
        },
        async render() {
          this.captures = [];
          this.preview = null;
          this.file = null;
          console.log(this.captures.length);
          this.hideAllHelpers();
          this.selectedNode = null;
          this.video.currentTime = 0;
          this.video.loop = false;
          const duration = this.video.duration * 1000;
          this.startRecording(duration);
          this.layer.draw();
        },
        updateText() {
          if (this.selectedNode && this.selectedNode.type === "text") {
            const text = this.selectedNode.text;
            const transformer = this.selectedNode.transformer;
            text.fontSize(this.selectedFontSize);
            text.fontFamily(this.selectedFont);
            text.fontStyle(this.selectedFontStyle);
            text.fill(this.selectedColor);
            this.layer.draw();
          }
        },
        addText() {
          const vm = this;
          const text = new Konva.Text({
            text: "new text " + (vm.texts.length + 1),
            x: 50,
            y: 80,
            fontSize: this.selectedFontSize,
            fontFamily: this.selectedFont,
            fontStyle: this.selectedFontStyle,
            fill: this.selectedColor,
            align: "center",
            width: this.width * 0.5,
            draggable: true
          });
          const transformer = new Konva.Transformer({
            node: text,
            keepRatio: true,
            enabledAnchors: ["top-left", "top-right", "bottom-left", "bottom-right"]
          });
          text.on("click", async () => {
            for (let i = 0; i < this.texts.length; i++) {
              let item = this.texts[i];
              if (item.index === text.index) {
                let transformer = item.transformer;
                this.selectedNode = { type: "text", text, transformer };
                this.selectedFontSize = text.fontSize();
                this.selectedFont = text.fontFamily();
                this.selectedFontStyle = text.fontStyle();
                this.selectedColor = text.fill();
                vm.hideAllHelpers();
                transformer.show();
                transformer.moveToTop();
                text.moveToTop();
                vm.layer.draw();
                break;
              }
            }
          });
          text.on("mouseover", () => {
            transformer.show();
            this.layer.draw();
          });
          text.on("mouseout", () => {
            if (
              (this.selectedNode &&
                this.selectedNode.text &&
                this.selectedNode.text.index != text.index) ||
              (this.selectedNode && this.selectedNode.type === "image") ||
              !this.selectedNode
            ) {
              transformer.hide();
              this.layer.draw();
            }
          });
          text.on("dblclick", () => {
            text.hide();
            transformer.hide();
            vm.layer.draw();
            let textPosition = text.absolutePosition();
    
            let stageBox = vm.stage.container().getBoundingClientRect();
    
            let areaPosition = {
              x: stageBox.left + textPosition.x,
              y: stageBox.top + textPosition.y
            };
    
            let textarea = document.createElement("textarea");
            window.document.body.appendChild(textarea);
            textarea.value = text.text();
            textarea.style.position = "absolute";
            textarea.style.top = areaPosition.y + "px";
            textarea.style.left = areaPosition.x + "px";
            textarea.style.width = text.width() - text.padding() * 2 + "px";
            textarea.style.height = text.height() - text.padding() * 2 + 5 + "px";
            textarea.style.fontSize = text.fontSize() + "px";
            textarea.style.border = "none";
            textarea.style.padding = "0px";
            textarea.style.margin = "0px";
            textarea.style.overflow = "hidden";
            textarea.style.background = "none";
            textarea.style.outline = "none";
            textarea.style.resize = "none";
            textarea.style.lineHeight = text.lineHeight();
            textarea.style.fontFamily = text.fontFamily();
            textarea.style.transformOrigin = "left top";
            textarea.style.textAlign = text.align();
            textarea.style.color = text.fill();
            let rotation = text.rotation();
            let transform = "";
            if (rotation) {
              transform += "rotateZ(" + rotation + "deg)";
            }
            let px = 0;
            let isFirefox =
              navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
            if (isFirefox) {
              px += 2 + Math.round(text.fontSize() / 20);
            }
            transform += "translateY(-" + px + "px)";
            textarea.style.transform = transform;
            textarea.style.height = "auto";
            textarea.focus();
    
            // start
            function removeTextarea() {
              textarea.parentNode.removeChild(textarea);
              window.removeEventListener("click", handleOutsideClick);
              text.show();
              transformer.show();
              transformer.forceUpdate();
              vm.layer.draw();
            }
    
            function setTextareaWidth(newWidth) {
              if (!newWidth) {
                // set width for placeholder
                newWidth = text.placeholder.length * text.fontSize();
              }
              // some extra fixes on different browsers
              let isSafari = /^((?!chrome|android).)*safari/i.test(
                navigator.userAgent
              );
              let isFirefox =
                navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
              if (isSafari || isFirefox) {
                newWidth = Math.ceil(newWidth);
              }
    
              let isEdge =
                document.documentMode || /Edge/.test(navigator.userAgent);
              if (isEdge) {
                newWidth += 1;
              }
              textarea.style.width = newWidth + "px";
            }
    
            textarea.addEventListener("keydown", function(e) {
              // hide on enter
              // but don't hide on shift + enter
              if (e.keyCode === 13 && !e.shiftKey) {
                text.text(textarea.value);
                removeTextarea();
              }
              // on esc do not set value back to node
              if (e.keyCode === 27) {
                removeTextarea();
              }
            });
    
            textarea.addEventListener("keydown", function(e) {
              let scale = text.getAbsoluteScale().x;
              setTextareaWidth(text.width() * scale);
              textarea.style.height = "auto";
              textarea.style.height =
                textarea.scrollHeight + text.fontSize() + "px";
            });
    
            function handleOutsideClick(e) {
              if (e.target !== textarea) {
                text.text(textarea.value);
                removeTextarea();
              }
            }
            setTimeout(() => {
              window.addEventListener("click", handleOutsideClick);
            });
            // end
          });
          text.transformer = transformer;
          this.texts.push(text);
          this.layer.add(text);
          this.layer.add(transformer);
          this.hideAllHelpers();
          this.selectedNode = { type: "text", text, transformer };
          transformer.show();
          this.layer.draw();
        },
        initCanvas() {
          const vm = this;
          this.stage = new Konva.Stage({
            container: "container",
            width: vm.width,
            height: vm.height
          });
          this.layer = new Konva.Layer();
    
          this.stage.add(this.layer);
    
          let video = document.createElement("video");
          video.setAttribute("id", "video");
          video.setAttribute("ref", "video");
          if (this.source) {
            video.src = this.source;
          }
          video.preload = "auto";
          video.loop = "loop";
          video.style.display = "none";
          this.video = video;
          this.backgroundVideo = new Konva.Image({
            image: vm.video,
            draggable: false
          });
          this.video.addEventListener("loadedmetadata", function(e) {
            vm.backgroundVideo.width(vm.width);
            vm.backgroundVideo.height(vm.height);
          });
          this.video.addEventListener("ended", () => {
            console.log("the video ended");
            this.anim.stop();
            this.anim.start();
            this.video.loop = "loop";
            this.video.play();
          });
    
          this.anim = new Konva.Animation(function() {
            console.log("animation called");
            // do nothing, animation just need to update the layer
          }, vm.layer);
    
          this.layer.add(this.backgroundVideo);
          this.layer.draw();
          const canvas = document.getElementsByTagName("canvas")[0];
          canvas.style.border = "3px solid red";
          this.canvas = canvas;
        }
      }
    };
    </script>
    <style scoped>
    body {
      margin: 0;
      padding: 0;
      background-color: #f0f0f0;
    }
    .backgrounds,
    .images {
      width: 100px;
      height: 100px;
      padding-left: 2px;
      padding-right: 2px;
    }
    </style>