Javascript 了解屏幕外画布以提高性能

Javascript 了解屏幕外画布以提高性能,javascript,canvas,konvajs,offscreen-canvas,Javascript,Canvas,Konvajs,Offscreen Canvas,大家好,我有一个非常复杂的画布编辑器,允许用户使用Konvajs和Gifler库选择视频背景、添加文本、GIF和乐蒂动画。这已经走过了很长的一段路,但是我正在努力提高画布应用程序的性能。我读了很多关于屏幕外画布的书,但我不太明白。假设我有一个常规HTML画布对象,我如何创建一个屏幕外画布并将其吐回浏览器?理想情况下,我希望能够以30 fps的速度从画布阵列中获取图像,而无延迟。我还有另一个担忧,即目前为止,屏幕外画布似乎还没有得到广泛的支持。每当我尝试从画布创建屏幕外画布时,我总是得到: Fai

大家好,我有一个非常复杂的画布编辑器,允许用户使用Konvajs和Gifler库选择视频背景、添加文本、GIF和乐蒂动画。这已经走过了很长的一段路,但是我正在努力提高画布应用程序的性能。我读了很多关于屏幕外画布的书,但我不太明白。假设我有一个常规HTML画布对象,我如何创建一个屏幕外画布并将其吐回浏览器?理想情况下,我希望能够以30 fps的速度从画布阵列中获取图像,而无延迟。我还有另一个担忧,即目前为止,屏幕外画布似乎还没有得到广泛的支持。每当我尝试从画布创建屏幕外画布时,我总是得到:

Failed to execute 'transferControlToOffscreen' on 
'HTMLCanvasElement': Cannot transfer control from a canvas that has a rendering context.
正如我所说,我只是想弄清楚如何平滑地渲染我的动画,但不确定如何进行。这里的任何帮助都会很好。这是代码

<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)"
        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>
    <template v-if="selectedNode && selectedNode.lottie">
    <input type="text" v-model="text">
    <button @click="updateAnim(selectedNode.image)">
      Update Animation
    </button>
    </template>
    <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>
import lottie from "lottie-web";
import * as anim from "../AEAnim/anim.json";
import * as anim2 from "../AEAnim/anim2.json";
import * as anim3 from "../AEAnim/anim3.json";
import * as anim4 from "../AEAnim/anim4.json";
import * as anim5 from "../AEAnim/anim5.json";

export default {
  data() {
    return {
      source: null,
      stage: null,
      layer: null,
      video: null,
      animations: [],
      text: "",
      animationData: 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" },
        {
          source: "https://media3.giphy.com/media/R3IxJW14a3QNa/source.gif",
          animation: anim
        },
        {
        source: "https://bestanimations.com/Text/Cool/cool-story-3.gif",
        animation: anim2
        },
        {
          source: "https://freefrontend.com/assets/img/css-text-animations/HTML-CSS-Animated-Text-Fill.gif",
          animation: anim3
        },
        {
          source: "api/files/jedi/zoomer.gif",
          animation: anim4
        },
        {
          source: "api/files/jedi/slider.gif",
          animation: anim5
        }
      ],
      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);
        if (this.selectedNode.lottie) {
          clearTimeout(this.animations.interval);
          this.selectedNode.lottie.destroy();
          this.animations.splice(this.selectedNode.lottie.index - 1, 1);
        }
        this.selectedNode = null;
        this.layer.draw();
      }
    },
    async addImage(imageToAdd, isUpdate) {
      let lottieAnimation = null;
      let imageObj = null;
      const type = imageToAdd.source.slice(imageToAdd.source.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 = imageToAdd.source;
      imageObj.width = 200;
      imageObj.height = 200;
      await process(imageObj);

      if (type === ".gif" && !imageToAdd.animation) {
        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(imageToAdd.source).frames(canvas, onDrawFrame);

        canvas.onload = async () => {
          canvas.parentNode.removeChild(canvas);
        };
        imageObj = canvas;
        const gif = new Image();
        gif.src = imageToAdd.source;
        const gifImage = await process(gif);
        imageObj.width = gifImage.width;
        imageObj.height = gifImage.height;
      } else if (imageToAdd.animation) {
        if(!isUpdate){this.text = "new text";}
        const canvas = document.createElement("canvas");
        canvas.style.width = 1920;
        canvas.style.height= 1080;
        canvas.setAttribute("id", "animationCanvas");
        const ctx = canvas.getContext("2d");
        const div = document.createElement("div");
        div.setAttribute("id", "animationContainer");
        div.style.display = "none";
        canvas.style.display = "none";
        this.animationData = imageToAdd.animation.default;
        for(let i =0; i <this.animationData.layers.length; i++){
          for(let b =0; b<this.animationData.layers[i].t.d.k.length; b++){
            this.animationData.layers[i].t.d.k[b].s.t = this.text;
          }
        }
         lottieAnimation = lottie.loadAnimation({
          container: div, // the dom element that will contain the animation
          renderer: "svg",
          loop: true,
          autoplay: true,
          animationData: this.animationData
        });
        lottieAnimation.imgSrc = imageToAdd.source;
        lottieAnimation.text = this.text;
        const svg = await div.getElementsByTagName("svg")[0];
        const timer = setInterval(async () => {

          const xml = new XMLSerializer().serializeToString(svg);
          const svg64 = window.btoa(xml);
          const b64Start = "data:image/svg+xml;base64,";
          const image64 = b64Start + svg64;
          imageObj = new Image({ width: canvas.width, height: canvas.height });
          imageObj.src = image64;
          await process(imageObj);
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          ctx.drawImage(imageObj, 0, 0, canvas.width, canvas.height);
           this.layer.batchDraw();
        }, 1000 / 30);
        this.animations.push({ lottie: lottieAnimation, interval: timer });
        imageObj = canvas;
        canvas.onload = async () => {
          canvas.parentNode.removeChild(canvas);
        };
      }
      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
      });

      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,
          lottie: lottieAnimation,
          image: imageToAdd
        };
        if(lottieAnimation && lottieAnimation.text){vm.text = lottieAnimation.text}
        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,
        lottie: lottieAnimation,
        image: imageToAdd
      };
      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);
      }
    },
    async updateAnim(image){
     this.addImage(image, true);
      this.removeNode();

    },
    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;
      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.batchDraw();
      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>
关于错误消息 正如在获得上下文“a”后不能请求上下文“B”,在从该画布请求上下文后,也不能将DOM画布的控件转移到OffscreenCanvas

在这里,您正在使用Konva.js库来初始化DOM画布,我并不特别了解该库。该库将需要访问其中一个可用上下文,即该画布上的2D上下文。这意味着,当您访问该画布时,库已经请求了一个上下文,您将无法将其控制转移到OffscreenCanvas

在图书馆的回购协议中,有一项条款指出,不迟于12天前,他们增加了一些对离岸外包的初始支持。因此,我邀请你们看看如何继续使用该库

关于越洋表演 与常规画布相比,OffscreenCanvas本身不会提供任何性能提升。它不会神奇地让你的代码以10FPS的速度运行到60FPS。 它允许的是不阻塞主线程,也不被主线程阻塞。对于这个,你需要把它转移到一个

这意味着你可以使用它

如果您担心画布代码会阻塞UI,但实际上并不总是需要平滑动画。 如果你担心你的主线程可能会减慢你的画布动画-例如,如果你有很多其他的东西在页面上进行。 但在您的情况下,似乎只有您的代码在运行。所以你走这条路可能不会赢得任何东西

关于Offscreencavas限制 我们看到,要真正利用OffscreenCanvas,我们需要从Web Worker以并行线程运行它。但是Web工作者没有访问DOM的权限。 这是一个巨大的限制,将使很多事情更加难以处理

例如,要绘制视频,当前除了使用元素首先播放视频外,没有其他方法。辅助脚本无法访问该元素,并且无法在其自己的线程上创建一个。因此,唯一的解决方案是从主线程创建一个脚本,并将其传递回工作脚本。 所有的视频解码+位图生成都是在主线程上完成的。值得注意的是,尽管createImageBitmap方法返回一个承诺,但当我们使用视频作为源时,浏览器除了从视频同步创建位图之外别无选择。 因此,当在worker中使用ImageBitmap时,实际上是在重载主线程,如果主线程在执行其他操作时被锁定,则显然必须等待该操作完成,然后才能从视频中获取帧

另一个很大的限制是,目前*Web工作者无法对DOM事件做出反应。因此,您必须设置一个代理,将主线程中接收到的事件传递给工作线程。这再一次要求你的主线程是自由的,并且有很多新代码

关于你的代码 因为,是的,我相信如果你想提高性能,你应该在这里寻找

我只是快速浏览了一下,但我已经看到您在一些地方以高速率使用setInterval。不要。如果你需要动画的东西是可见的总是使用,如果你不需要全速,然后添加一个内部逻辑跳过帧,但继续使用rAF作为主要引擎

您要求浏览器在每帧执行繁重的操作。例如,您的svg部件在每一帧从DOM节点创建一个全新的svg标记,然后将该标记加载到一个文件中,这意味着浏览器必须为该图像启动一个全新的DOM,并在画布上进行光栅化。 在高帧速率下,这本身将很难应对,而OffscreenCanvas也无济于事

您正在将所有图像存储为静像,以生成最终视频。这将占用大量内存

在你的代码中可能还有很多其他类似的东西,所以请仔细检查并搜索是什么使你的代码无法到达屏幕 埃弗雷什率。可以改进的是,在支持MediaRecorder和好运的情况下,搜索替代品(例如MediaRecorder)可能比Whammy更好


*有一种方法可以解决这个问题

在使用屏幕外画布时,可以应用不同的策略。到目前为止,你读过哪些资料?您是否可以添加一个单独的代码段,显示您试图执行的操作,但会抛出该错误。另外,我从这篇关于屏幕外画布的文章中学到了很多。如果你还没有读过它,也许它可以帮助你入门。当然:@Wezelkrozum,下面是我的代码。canvas=canvas;在init canvas函数中,我执行const offscreen=canvas.transferControlToOffscreen;这就是失败的地方。谢谢你的链接,我一直在读它。我希望这能给你提供一些关于我所说内容的背景。如果你能想出一个简单的方法来加快速度,让我知道。谢谢你,我感谢你的建议。接下来还有两个问题:1。我试图使用requestAnimationFrame更新其中一个设置间隔,但它似乎不起作用。虽然我没有错误。我知道这是一个很大的帮助,但你能给我举个例子吗?2.使用SVG来创建图像位图,我的SVG是否已设置动画有关系?我的意思是当我创建位图时,它不是很静态吗?当svg正确移动时,它不会更新图像数据?如果是这种情况,您对svg还有其他建议吗?@codernoob8 1。requestAnimationFrame调度单个事件,因此您需要在回调中递归调用它,就像setTimeout cb,time_直到下一次屏幕刷新一样。2.对不起,我不知道你在画动画SVG。在这种情况下,您确实需要每帧重建它们。。。除非您有办法将这些动画转换为画布路径图形。但无论如何,OffscreenCanvas在这里也不会有任何帮助。根据本文,OffscreenCanvas确实提供了一些性能改进,即使在没有工人的情况下使用,尽管DOM中不存在的常规画布可能会从大多数相同的改进中受益?:.1/2@REJH说实话,这种说法有点奇怪。让我们注意到它是在2018年写的,从那时起很多事情都发生了变化。尽管如此,我不明白他们在哪里看到将渲染绑定到DOM会对渲染造成任何性能影响。我的最佳选择是,它们暗示正常画布渲染作为事件循环的更新渲染步骤的一部分进行。这确实可能会在输入和实际渲染之间造成一些延迟。但从那时起,普通画布的上下文赢得了一个去同步属性,这就解决了这个问题,而OffscreenCavases…2/2…确实丢失了提交方法,因此它们的渲染完全依赖于事件循环的此更新渲染步骤。所以在这方面,普通画布实际上可以更具性能。现在,我真的不确定他们在说什么,但如果不是,我不知道这是什么。。。