653 lines
18 KiB
Vue
653 lines
18 KiB
Vue
<script setup>
|
|
import {
|
|
computed,
|
|
getCurrentInstance,
|
|
nextTick,
|
|
onBeforeUnmount,
|
|
onMounted,
|
|
ref,
|
|
watch,
|
|
} from "vue";
|
|
|
|
const defaultCanvasSize = 300;
|
|
const defaultRingCount = 10;
|
|
const highlightRevealProgressFrames = [0.08, 0.2, 0.38, 0.6, 0.82, 1];
|
|
const highlightRevealFrameInterval = 60;
|
|
|
|
const props = defineProps({
|
|
// canvas 唯一标识;不传时组件内部自动生成,避免多个靶面 canvas-id 冲突。
|
|
canvasId: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
// 业务坐标半径,例如 20 表示命中点坐标范围为 -20 到 20。
|
|
// 当前组件主要用它参与重绘判断,外层命中点定位也应使用同一半径。
|
|
coordinateRadius: {
|
|
type: Number,
|
|
default: 20,
|
|
},
|
|
// 是否显示靶心十字辅助线。
|
|
showCrosshair: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
// 是否显示环数文字。
|
|
showRingLabels: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
// 从正上方开始顺时针等分的区域数量。
|
|
sectorCount: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
// 当前高亮区域,范围为 1 到 sectorCount。
|
|
activeSector: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
// 指定环数,1 到 10;无效值表示高亮整个区域。
|
|
activeRing: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
// 每次变化时以固定低帧数重新展开当前高亮扇区;默认关闭。
|
|
highlightRefreshToken: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
showSectorLabels: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
// 只绘制透明高亮层,不绘制完整靶纸;用于叠加在靶纸图片上。
|
|
highlightOnly: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
// 外部指定 canvas 绘制尺寸;用于让高亮层跟随靶图真实显示区域。
|
|
canvasWidth: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
canvasHeight: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
// 靶纸样式覆盖配置,例如环数、环色、环线颜色、环数字体等。
|
|
targetStyleConfig: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
// 十字辅助线样式覆盖配置。
|
|
crosshairStyle: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
// 区域分割线样式覆盖配置。
|
|
sectorStyle: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
// 区域数字样式覆盖配置。
|
|
sectorLabelStyle: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
// 高亮样式覆盖配置。
|
|
highlightStyle: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
});
|
|
|
|
const instance = getCurrentInstance();
|
|
const localCanvasId = `target-canvas-${Math.random().toString(36).slice(2, 10)}`;
|
|
const currentCanvasId = computed(() => props.canvasId || localCanvasId);
|
|
const lastDrawKey = ref("");
|
|
const canvasSize = ref({
|
|
width: defaultCanvasSize,
|
|
height: defaultCanvasSize,
|
|
});
|
|
let drawRequestGeneration = 0;
|
|
let highlightAnimationGeneration = 0;
|
|
let highlightAnimationTimer = null;
|
|
let mountDrawTimer = null;
|
|
|
|
// 完整靶纸默认样式,调用方可以通过 targetStyleConfig 局部覆盖。
|
|
const defaultTargetStyleConfig = {
|
|
ringCount: defaultRingCount,
|
|
ringColors: {
|
|
1: "#f8f8f3",
|
|
2: "#f8f8f3",
|
|
3: "#595959",
|
|
4: "#595959",
|
|
5: "#24aee0",
|
|
6: "#24aee0",
|
|
7: "#ff1f35",
|
|
8: "#ff1f35",
|
|
9: "#f7d34a",
|
|
10: "#f7d34a",
|
|
},
|
|
ringLineColor: "rgba(150, 150, 150, 0.55)",
|
|
ringLineWidthRatio: 0.0022,
|
|
centerDotColor: "#ffffff",
|
|
centerDotRadiusRatio: 0.0048,
|
|
ringLabelFontRatio: 0.032,
|
|
ringLabelDarkColor: "#111111",
|
|
ringLabelLightColor: "#ffffff",
|
|
};
|
|
|
|
// 十字辅助线默认样式。
|
|
const defaultCrosshairStyle = {
|
|
color: "rgba(20, 20, 20, 0.38)",
|
|
lineWidthRatio: 0.0025,
|
|
};
|
|
|
|
// 顺时针等分线默认样式。
|
|
const defaultSectorStyle = {
|
|
color: "rgba(255, 255, 255, 0.82)",
|
|
lineWidthRatio: 0.004,
|
|
};
|
|
|
|
// 区域数字默认样式。
|
|
const defaultSectorLabelStyle = {
|
|
color: "#ffffff",
|
|
backgroundColor: "rgba(0, 0, 0, 0.62)",
|
|
fontSizeRatio: 0.075,
|
|
radiusRatio: 0.76,
|
|
badgeRadiusRatio: 0.07,
|
|
};
|
|
|
|
// 高亮区域默认样式。
|
|
const defaultHighlightStyle = {
|
|
color: "rgba(255, 228, 0, 0.6)",
|
|
strokeColor: "rgba(254, 216, 71, 0.82)",
|
|
lineWidthRatio: 0.003,
|
|
};
|
|
|
|
// 合并默认靶纸样式和外部传入样式,ringColors 单独深合并。
|
|
const mergeTargetStyleConfig = () => ({
|
|
...defaultTargetStyleConfig,
|
|
...props.targetStyleConfig,
|
|
ringColors: {
|
|
...defaultTargetStyleConfig.ringColors,
|
|
...(props.targetStyleConfig?.ringColors || {}),
|
|
},
|
|
});
|
|
|
|
// 统一把外部传入值转成有效数字,非法值使用 fallback。
|
|
const getNumber = (value, fallback = 0) => {
|
|
const numberValue = Number(value);
|
|
return Number.isFinite(numberValue) ? numberValue : fallback;
|
|
};
|
|
|
|
// 获取指定环数的填充色,兼容数字 key 和字符串 key。
|
|
const getRingColor = (ring, config) => {
|
|
return config.ringColors?.[ring] || config.ringColors?.[String(ring)] || "#ffffff";
|
|
};
|
|
|
|
const getPositiveInteger = (value) => {
|
|
const numberValue = Number(value);
|
|
return Number.isInteger(numberValue) && numberValue > 0 ? numberValue : 0;
|
|
};
|
|
|
|
// 正上方作为第一区起始边界,Canvas 角度递增方向即为顺时针。
|
|
const getSectorAngles = (sector, sectorCount) => {
|
|
const count = getPositiveInteger(sectorCount);
|
|
const index = getPositiveInteger(sector);
|
|
if (!count || !index || index > count) return null;
|
|
|
|
const step = (Math.PI * 2) / count;
|
|
const startAngle = -Math.PI / 2 + (index - 1) * step;
|
|
return {
|
|
startAngle,
|
|
endAngle: startAngle + step,
|
|
middleAngle: startAngle + step / 2,
|
|
};
|
|
};
|
|
|
|
// 绘制实心圆,靶纸环区和中心点都会用到。
|
|
const drawCircle = (ctx, centerX, centerY, radius, fillColor) => {
|
|
ctx.beginPath();
|
|
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
ctx.setFillStyle(fillColor);
|
|
ctx.fill();
|
|
};
|
|
|
|
// 绘制环形扇区,用于按象限高亮指定环数。
|
|
const drawAnnularSector = (
|
|
ctx,
|
|
centerX,
|
|
centerY,
|
|
innerRadius,
|
|
outerRadius,
|
|
startAngle,
|
|
endAngle,
|
|
fillColor,
|
|
strokeColor = "",
|
|
lineWidth = 0
|
|
) => {
|
|
ctx.beginPath();
|
|
ctx.arc(centerX, centerY, outerRadius, startAngle, endAngle);
|
|
|
|
if (innerRadius > 0) {
|
|
ctx.arc(centerX, centerY, innerRadius, endAngle, startAngle, true);
|
|
} else {
|
|
ctx.lineTo(centerX, centerY);
|
|
}
|
|
|
|
ctx.closePath();
|
|
ctx.setFillStyle(fillColor);
|
|
ctx.fill();
|
|
|
|
if (strokeColor && lineWidth > 0) {
|
|
ctx.setStrokeStyle(strokeColor);
|
|
ctx.setLineWidth(lineWidth);
|
|
ctx.stroke();
|
|
}
|
|
};
|
|
|
|
// 从外到内绘制完整靶纸色环。
|
|
const drawTargetRings = (ctx, centerX, centerY, targetRadius, config) => {
|
|
for (let ring = 1; ring <= config.ringCount; ring += 1) {
|
|
const radius = targetRadius * ((config.ringCount + 1 - ring) / config.ringCount);
|
|
drawCircle(ctx, centerX, centerY, radius, getRingColor(ring, config));
|
|
}
|
|
};
|
|
|
|
// 高亮后端指定区域;activeRing 有效时只高亮该区域内的单个环。
|
|
const drawSectorHighlight = (
|
|
ctx,
|
|
centerX,
|
|
centerY,
|
|
targetRadius,
|
|
config,
|
|
revealProgress = 1
|
|
) => {
|
|
const angles = getSectorAngles(props.activeSector, props.sectorCount);
|
|
if (!angles) return;
|
|
|
|
const safeRevealProgress = Math.min(
|
|
Math.max(getNumber(revealProgress, 1), 0),
|
|
1
|
|
);
|
|
if (safeRevealProgress <= 0) return;
|
|
|
|
const ring = getPositiveInteger(props.activeRing);
|
|
const hasActiveRing = ring >= 1 && ring <= config.ringCount;
|
|
const innerRadius = hasActiveRing
|
|
? targetRadius * ((config.ringCount - ring) / config.ringCount)
|
|
: 0;
|
|
const outerRadius = hasActiveRing
|
|
? targetRadius * ((config.ringCount + 1 - ring) / config.ringCount)
|
|
: targetRadius;
|
|
const style = {
|
|
...defaultHighlightStyle,
|
|
...props.highlightStyle,
|
|
};
|
|
|
|
drawAnnularSector(
|
|
ctx,
|
|
centerX,
|
|
centerY,
|
|
innerRadius,
|
|
outerRadius,
|
|
angles.startAngle,
|
|
angles.startAngle +
|
|
(angles.endAngle - angles.startAngle) * safeRevealProgress,
|
|
style.color,
|
|
style.strokeColor,
|
|
Math.max(1, targetRadius * style.lineWidthRatio)
|
|
);
|
|
};
|
|
|
|
// 从正上方开始顺时针绘制所有区域边界。
|
|
const drawSectorLines = (ctx, centerX, centerY, targetRadius) => {
|
|
const count = getPositiveInteger(props.sectorCount);
|
|
if (!count) return;
|
|
|
|
const style = {
|
|
...defaultSectorStyle,
|
|
...props.sectorStyle,
|
|
};
|
|
const step = (Math.PI * 2) / count;
|
|
|
|
ctx.beginPath();
|
|
for (let index = 0; index < count; index += 1) {
|
|
const angle = -Math.PI / 2 + index * step;
|
|
ctx.moveTo(centerX, centerY);
|
|
ctx.lineTo(
|
|
centerX + Math.cos(angle) * targetRadius,
|
|
centerY + Math.sin(angle) * targetRadius
|
|
);
|
|
}
|
|
ctx.setStrokeStyle(style.color);
|
|
ctx.setLineWidth(Math.max(1, targetRadius * style.lineWidthRatio));
|
|
ctx.stroke();
|
|
};
|
|
|
|
// 绘制各环之间的分割线。
|
|
const drawRingLines = (ctx, centerX, centerY, targetRadius, config) => {
|
|
const lineWidth = Math.max(1, targetRadius * config.ringLineWidthRatio);
|
|
|
|
ctx.setStrokeStyle(config.ringLineColor);
|
|
ctx.setLineWidth(lineWidth);
|
|
|
|
for (let index = 1; index <= config.ringCount; index += 1) {
|
|
const radius = targetRadius * (index / config.ringCount);
|
|
ctx.beginPath();
|
|
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
}
|
|
};
|
|
|
|
// 绘制靶心十字辅助线。
|
|
const drawCrosshair = (ctx, centerX, centerY, targetRadius) => {
|
|
if (!props.showCrosshair) {
|
|
return;
|
|
}
|
|
|
|
const style = {
|
|
...defaultCrosshairStyle,
|
|
...props.crosshairStyle,
|
|
};
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(centerX - targetRadius, centerY);
|
|
ctx.lineTo(centerX + targetRadius, centerY);
|
|
ctx.moveTo(centerX, centerY - targetRadius);
|
|
ctx.lineTo(centerX, centerY + targetRadius);
|
|
ctx.setStrokeStyle(style.color);
|
|
ctx.setLineWidth(Math.max(1, targetRadius * style.lineWidthRatio));
|
|
ctx.stroke();
|
|
};
|
|
|
|
// 绘制环数文字。
|
|
const drawRingLabels = (ctx, centerX, centerY, targetRadius, config) => {
|
|
if (!props.showRingLabels) {
|
|
return;
|
|
}
|
|
|
|
const ringWidth = targetRadius / config.ringCount;
|
|
const fontSize = Math.max(10, targetRadius * config.ringLabelFontRatio);
|
|
|
|
ctx.setFontSize(fontSize);
|
|
ctx.setTextAlign("center");
|
|
ctx.setTextBaseline("middle");
|
|
|
|
for (let ring = config.ringCount; ring >= 1; ring -= 1) {
|
|
const y = centerY + (config.ringCount - ring + 0.45) * ringWidth;
|
|
const color = ring <= 2 ? config.ringLabelDarkColor : config.ringLabelLightColor;
|
|
ctx.setFillStyle(color);
|
|
ctx.fillText(String(ring), centerX, y);
|
|
}
|
|
};
|
|
|
|
// 在每个区域中线位置绘制编号,编号层始终位于高亮和分割线之上。
|
|
const drawSectorLabels = (ctx, centerX, centerY, targetRadius) => {
|
|
const count = getPositiveInteger(props.sectorCount);
|
|
if (!props.showSectorLabels || !count) return;
|
|
|
|
const style = {
|
|
...defaultSectorLabelStyle,
|
|
...props.sectorLabelStyle,
|
|
};
|
|
const labelRadius = targetRadius * style.radiusRatio;
|
|
const badgeRadius = Math.max(10, targetRadius * style.badgeRadiusRatio);
|
|
|
|
ctx.setFontSize(Math.max(11, targetRadius * style.fontSizeRatio));
|
|
ctx.setTextAlign("center");
|
|
ctx.setTextBaseline("middle");
|
|
|
|
for (let sector = 1; sector <= count; sector += 1) {
|
|
const angles = getSectorAngles(sector, count);
|
|
const x = centerX + Math.cos(angles.middleAngle) * labelRadius;
|
|
const y = centerY + Math.sin(angles.middleAngle) * labelRadius;
|
|
drawCircle(ctx, x, y, badgeRadius, style.backgroundColor);
|
|
ctx.setFillStyle(style.color);
|
|
ctx.fillText(String(sector), x, y);
|
|
}
|
|
};
|
|
|
|
// 生成本次绘制状态的唯一 key,用于避免相同内容重复 draw。
|
|
const getDrawKey = (width, height) => {
|
|
return JSON.stringify({
|
|
width,
|
|
height,
|
|
coordinateRadius: props.coordinateRadius,
|
|
showCrosshair: props.showCrosshair,
|
|
showRingLabels: props.showRingLabels,
|
|
sectorCount: props.sectorCount,
|
|
activeSector: props.activeSector,
|
|
activeRing: props.activeRing,
|
|
showSectorLabels: props.showSectorLabels,
|
|
targetStyleConfig: props.targetStyleConfig,
|
|
crosshairStyle: props.crosshairStyle,
|
|
sectorStyle: props.sectorStyle,
|
|
sectorLabelStyle: props.sectorLabelStyle,
|
|
highlightStyle: props.highlightStyle,
|
|
highlightOnly: props.highlightOnly,
|
|
});
|
|
};
|
|
|
|
// 主绘制入口:根据 highlightOnly 决定画完整靶纸,还是只画透明高亮层。
|
|
const drawTarget = ({ force = false, highlightProgress = 1 } = {}) => {
|
|
const width = Math.max(getNumber(canvasSize.value.width, defaultCanvasSize), 1);
|
|
const height = Math.max(getNumber(canvasSize.value.height, defaultCanvasSize), 1);
|
|
const drawKey = getDrawKey(width, height);
|
|
|
|
if (!force && drawKey === lastDrawKey.value) {
|
|
return;
|
|
}
|
|
|
|
const size = Math.min(width, height);
|
|
const centerX = width / 2;
|
|
const centerY = height / 2;
|
|
const targetRadius = size / 2;
|
|
const config = mergeTargetStyleConfig();
|
|
const ctx = uni.createCanvasContext(currentCanvasId.value, instance?.proxy);
|
|
|
|
ctx.clearRect(0, 0, width, height);
|
|
|
|
if (!props.highlightOnly) {
|
|
drawTargetRings(ctx, centerX, centerY, targetRadius, config);
|
|
}
|
|
|
|
drawSectorHighlight(
|
|
ctx,
|
|
centerX,
|
|
centerY,
|
|
targetRadius,
|
|
config,
|
|
highlightProgress
|
|
);
|
|
|
|
if (!props.highlightOnly) {
|
|
drawRingLines(ctx, centerX, centerY, targetRadius, config);
|
|
drawCircle(
|
|
ctx,
|
|
centerX,
|
|
centerY,
|
|
Math.max(1, targetRadius * config.centerDotRadiusRatio),
|
|
config.centerDotColor
|
|
);
|
|
drawCrosshair(ctx, centerX, centerY, targetRadius);
|
|
drawRingLabels(ctx, centerX, centerY, targetRadius, config);
|
|
}
|
|
|
|
// 高亮先画,等分线和编号后画,避免高亮覆盖区域边界。
|
|
drawSectorLines(ctx, centerX, centerY, targetRadius);
|
|
drawSectorLabels(ctx, centerX, centerY, targetRadius);
|
|
|
|
ctx.draw();
|
|
lastDrawKey.value = highlightProgress >= 1 ? drawKey : "";
|
|
};
|
|
|
|
const cancelHighlightAnimation = () => {
|
|
highlightAnimationGeneration += 1;
|
|
if (highlightAnimationTimer) {
|
|
clearTimeout(highlightAnimationTimer);
|
|
highlightAnimationTimer = null;
|
|
}
|
|
};
|
|
|
|
// 固定 6 帧展开黄色扇区,避免对原生 canvas 节点做缩放导致低端机错位。
|
|
const runHighlightRevealAnimation = () => {
|
|
cancelHighlightAnimation();
|
|
const generation = highlightAnimationGeneration;
|
|
let frameIndex = 0;
|
|
|
|
const drawNextFrame = () => {
|
|
if (generation !== highlightAnimationGeneration) return;
|
|
|
|
drawTarget({
|
|
force: true,
|
|
highlightProgress: highlightRevealProgressFrames[frameIndex],
|
|
});
|
|
frameIndex += 1;
|
|
|
|
if (frameIndex < highlightRevealProgressFrames.length) {
|
|
highlightAnimationTimer = setTimeout(
|
|
drawNextFrame,
|
|
highlightRevealFrameInterval
|
|
);
|
|
} else {
|
|
highlightAnimationTimer = null;
|
|
}
|
|
};
|
|
|
|
drawNextFrame();
|
|
};
|
|
|
|
const setCanvasSizeAndDraw = async (
|
|
width,
|
|
height,
|
|
{ animateHighlight = false, requestGeneration } = {}
|
|
) => {
|
|
canvasSize.value = {
|
|
width: width > 0 ? width : defaultCanvasSize,
|
|
height: height > 0 ? height : width || defaultCanvasSize,
|
|
};
|
|
|
|
await nextTick();
|
|
if (requestGeneration !== drawRequestGeneration) return;
|
|
|
|
const canAnimateHighlight =
|
|
animateHighlight &&
|
|
props.highlightOnly &&
|
|
!!getSectorAngles(props.activeSector, props.sectorCount);
|
|
|
|
if (canAnimateHighlight) {
|
|
runHighlightRevealAnimation();
|
|
} else {
|
|
cancelHighlightAnimation();
|
|
drawTarget();
|
|
}
|
|
};
|
|
|
|
// 读取 canvas 实际渲染尺寸后再绘制,保证小程序真机尺寸和坐标一致。
|
|
const measureAndDraw = ({ animateHighlight = false } = {}) => {
|
|
const requestGeneration = ++drawRequestGeneration;
|
|
const propWidth = Math.round(getNumber(props.canvasWidth, 0));
|
|
const propHeight = Math.round(getNumber(props.canvasHeight, 0));
|
|
|
|
if (propWidth > 0 && propHeight > 0) {
|
|
setCanvasSizeAndDraw(propWidth, propHeight, {
|
|
animateHighlight,
|
|
requestGeneration,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const query = uni.createSelectorQuery().in(instance?.proxy);
|
|
|
|
query
|
|
.select(`#${currentCanvasId.value}`)
|
|
.boundingClientRect(async (rect) => {
|
|
if (requestGeneration !== drawRequestGeneration) return;
|
|
|
|
const width = Math.round(getNumber(rect?.width, defaultCanvasSize));
|
|
const height = Math.round(getNumber(rect?.height, width || defaultCanvasSize));
|
|
|
|
await setCanvasSizeAndDraw(width, height, {
|
|
animateHighlight,
|
|
requestGeneration,
|
|
});
|
|
})
|
|
.exec();
|
|
};
|
|
|
|
// 等待 Vue 完成 DOM 更新后重新测量和绘制。
|
|
const scheduleDraw = async ({ animateHighlight = false } = {}) => {
|
|
await nextTick();
|
|
measureAndDraw({ animateHighlight });
|
|
};
|
|
|
|
watch(
|
|
() => [
|
|
props.coordinateRadius,
|
|
props.showCrosshair,
|
|
props.showRingLabels,
|
|
props.sectorCount,
|
|
props.activeSector,
|
|
props.activeRing,
|
|
props.highlightRefreshToken,
|
|
props.showSectorLabels,
|
|
props.highlightOnly,
|
|
props.canvasWidth,
|
|
props.canvasHeight,
|
|
props.targetStyleConfig,
|
|
props.crosshairStyle,
|
|
props.sectorStyle,
|
|
props.sectorLabelStyle,
|
|
props.highlightStyle,
|
|
],
|
|
(currentValues, previousValues = []) => {
|
|
const refreshTokenIndex = 6;
|
|
const refreshToken = Number(currentValues[refreshTokenIndex]);
|
|
const previousRefreshToken = Number(previousValues[refreshTokenIndex]);
|
|
const animateHighlight =
|
|
Number.isFinite(refreshToken) &&
|
|
refreshToken > 0 &&
|
|
refreshToken !== previousRefreshToken;
|
|
|
|
return scheduleDraw({ animateHighlight });
|
|
},
|
|
{
|
|
deep: true,
|
|
}
|
|
);
|
|
|
|
onMounted(() => {
|
|
mountDrawTimer = setTimeout(measureAndDraw, 30);
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
drawRequestGeneration += 1;
|
|
cancelHighlightAnimation();
|
|
if (mountDrawTimer) {
|
|
clearTimeout(mountDrawTimer);
|
|
mountDrawTimer = null;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<canvas
|
|
:id="currentCanvasId"
|
|
class="target-canvas"
|
|
:canvas-id="currentCanvasId"
|
|
:width="canvasSize.width"
|
|
:height="canvasSize.height"
|
|
/>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.target-canvas {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
</style>
|