固定字体大小 html canvas,如何在缩放时改变它 in/out
fixed font size html canvas, how to make it change while zooming in/out
我以前用htmlcanvas做过笛卡尔坐标系。有用户帮我添加了鼠标缩放功能
但是我遇到了一个问题。轴号的字体大小是固定的,所以在缩小的同时,字体也会变小。
我想要固定的字体大小但数字之间的间隔可变。
例如,如果放大,您会看到 x 轴上的数字 0、1、2、3、4、5
但是一旦缩小,它应该是 0、5、10、15
喜欢地理https://www.geogebra.org/classic
我需要制作自己的坐标系,项目不能使用小程序或内嵌代码
目前得到的代码
class ViewPort {
constructor(canvas) {
this.canvas = canvas
/**
* Point used to calculate the change of every point's position on
* canvas after view port is zoomed and panned
*/
this.center = this.basicCenter
this.zoom = 1
this.shouldPan = false
this.prevZoomingPoint = null
}
get canvasWidth() {
return this.canvas.getBoundingClientRect().width
}
get canvasHeight() {
return this.canvas.getBoundingClientRect().height
}
get canvasLeft() {
return this.canvas.getBoundingClientRect().left
}
get canvasTop() {
return this.canvas.getBoundingClientRect().top
}
get context() {
return this.canvas.getContext('2d')
}
get basicCenter() {
const { canvasWidth, canvasHeight } = this
const point = {
x: canvasWidth / 2,
y: canvasHeight / 2
}
return point
}
get basicWidth() {
const width = this.canvasWidth
return width
}
get basicHeight() {
const height = this.canvasHeight
return height
}
get width() {
const { basicWidth, zoom } = this
const width = basicWidth * zoom
return width
}
get height() {
const { basicHeight, zoom } = this
const height = basicHeight * zoom
return height
}
get movement() {
const { width, height, basicWidth, basicHeight } = this
const { x: cx, y: cy } = this.center
const { x: basicCX, y: basicCY } = this.basicCenter
const deltaX = cx - basicCX - ((width - basicWidth) / 2)
const deltaY = cy - basicCY - ((height - basicHeight) / 2)
const res = {
x: deltaX,
y: deltaY
}
return res
}
get pan() {
const { center, zoom, basicCenter } = this
const res = {
x: center.x - basicCenter.x,
y: center.y - basicCenter.y
}
return res
}
zoomBy(center, deltaZoom) {
const prevZoom = this.zoom
this.zoom = this.zoom + deltaZoom
this.center = this.zoomPoint(center, this.zoom / prevZoom, this.center)
}
zoomIn(point) {
this.zoomBy(point, 0.1)
}
zoomOut(point) {
this.zoom > 0.25 && this.zoomBy(point, -0.1)
}
zoomPoint(center, rate, point) {
const { x: cx, y: cy } = center
const { x, y } = point
const deltaX = (x - cx) * rate
const deltaY = (y - cy) * rate
const newPoint = {
x: cx + deltaX,
y: cy + deltaY
}
return newPoint
}
panBy(deltaX, deltaY) {
const { x: centerX, y: centerY } = this.center
this.center = {
x: centerX + deltaX,
y: centerY + deltaY
}
}
getDeltaPointToPrevPanningPoint(point) {
const { x, y } = point
const { x: prevX, y: prevY } = this.prevZoomingPoint
const deltaPoint = {
x: x - prevX,
y: y - prevY
}
return deltaPoint
}
startPan(event) {
const point = {
x: event.x - this.canvasLeft,
y: event.y - this.canvasTop,
}
this.shouldPan = true
this.prevZoomingPoint = point
}
panning(event) {
const point = {
x: event.x - this.canvasLeft,
y: event.y - this.canvasTop,
}
const deltaX = this.getDeltaPointToPrevPanningPoint(point).x
const deltaY = this.getDeltaPointToPrevPanningPoint(point).y
this.prevZoomingPoint = point
this.panBy(deltaX, deltaY)
}
stopPan() {
this.shouldPan = false
}
transformToInitial(point) {
const { x, y } = point
const { movement, zoom } = this
const res = {
x: (x - movement.x) / zoom,
y: (y - movement.y) / zoom
}
return res
}
transform(point) {
const { x, y } = point
const { movement, zoom } = this
const res = {
x: x * zoom + movement.x,
y: y * zoom + movement.y
}
return res
}
clearCanvas() {
this.context.setTransform(1, 0, 0, 1, 0, 0)
this.context.clearRect(
0,
0,
viewPort.canvasWidth,
viewPort.canvasHeight
)
}
}
class Interaction {
constructor({
canvas,
viewPort,
dragger
}) {
canvas.removeEventListener("mousewheel", mousewheelListener)
canvas.addEventListener("mousewheel", mousewheelListener)
canvas.removeEventListener("mousedown", mousedownListener)
canvas.addEventListener("mousedown", mousedownListener)
canvas.removeEventListener("mousemove", mousemoveListener)
canvas.addEventListener("mousemove", mousemoveListener)
canvas.removeEventListener("mouseup", mouseupListener)
canvas.addEventListener("mouseup", mouseupListener)
function mousewheelListener(event) {
event.preventDefault()
const point = {
x: event.x - canvas.getBoundingClientRect().left,
y: event.y - canvas.getBoundingClientRect().top,
}
const { deltaX, deltaY } = event
if (isDecreasing()) {
viewPort.zoomIn(point)
}
if (isIncreasing()) {
viewPort.zoomOut(point)
}
function isIncreasing() {
const res = deltaX > 0 || deltaY > 0
return res
}
function isDecreasing() {
const res = deltaX < 0 || deltaY < 0
return res
}
render()
}
function mousedownListener(event) {
viewPort.startPan(event)
}
function mousemoveListener(event) {
viewPort.shouldPan && viewPort.panning(event)
viewPort.shouldPan && render()
}
function mouseupListener(event) {
viewPort.stopPan(event)
}
}
}
const canvas = document.getElementById("myCanvas")
const viewPort = new ViewPort(canvas)
const interaction = new Interaction({ viewPort, canvas })
function render() {
const { abs, max } = Math
const { zoom, movement, context: ctx, pan, center, basicCenter } = viewPort
viewPort.clearCanvas()
ctx.setTransform(zoom, 0, 0, zoom, movement.x, movement.y)
// Original codes are rewrote
const { canvasWidth, canvasHeight } = viewPort
const interval = 20
const basicWidth = canvasWidth
const basicHeight = canvasHeight
const potentialWidth = 2 * max(abs(viewPort.transformToInitial({ x: 0, y: 0 }).x - basicCenter.x), abs(viewPort.transformToInitial({ x: basicWidth, y: 0 }).x - basicCenter.x))
const width = potentialWidth > basicWidth ? potentialWidth : basicWidth
const potentialHeight = 2 * max(abs(viewPort.transformToInitial({ x: 0, y: 0 }).y - basicCenter.y), abs(viewPort.transformToInitial({ x: 0, y: basicHeight }).y - basicCenter.y))
const height = potentialHeight > basicHeight ? potentialHeight : basicHeight
drawXAxis()
drawYAxis()
drawOriginCoordinate()
drawXCoordinates()
drawYCoordinates()
function drawXAxis() {
const path = new Path2D
path.moveTo(basicCenter.x - width / 2, basicHeight / 2)
path.lineTo(basicCenter.x + width / 2, basicHeight / 2)
ctx.stroke(path)
}
function drawYAxis() {
const path = new Path2D
path.moveTo(basicWidth / 2, basicCenter.y - height / 2)
path.lineTo(basicWidth / 2, basicCenter.y + height / 2)
ctx.stroke(path)
}
function drawOriginCoordinate() {
ctx.fillText(`O`, basicCenter.x + 5, basicCenter.y - 5)
}
function drawXCoordinates() {
for (let i = 1; i <= width / 2 / interval; i++) {
total = i * interval
ctx.fillText(` ${i} `, basicCenter.x + total, basicHeight / 2)
}
for (let i = 1; i <= width / 2 / interval; i++) {
total = i * interval
ctx.fillText(` -${i} `, basicCenter.x - total, basicHeight / 2)
}
}
function drawYCoordinates() {
for (let i = 1; i <= height / 2 / interval; i++) {
total = i * interval
ctx.fillText(` ${i} `, basicWidth / 2, basicCenter.y + total)
}
for (let i = 1; i <= height / 2 / interval; i++) {
total = i * interval
ctx.fillText(` -${i} `, basicWidth / 2, basicCenter.y - total)
}
}
}
render()
<canvas id="myCanvas" width="300" height="300" style="border:1px solid #d3d3d3;"></canvas>
字体大小
对于字体大小,您希望字体大小与 canvas 的缩放值成反比。说:
ctx.font = 12 / zoom + "px Arial";
其中 12 是当比例 (zoom
) 为 1 时的字体大小。如果您放大所有内容都拉伸两倍 (zoom = 2
),则字体大小将为 6。由于字体大小是线性而非面积度量,因此我们不需要在此处对缩放进行平方。
更新轴
要更新显示的数字以便适当地缩放它们,可以使用几种不同的方法。
作为一个简单的例子,我们可以找出缩放的数量级(或者本质上它有多少位数或它有多少小数点)并根据这个因素缩放显示的数字。例如,如果缩放比例为 10,那么我们将以 1/10 的增量显示轴号。如果缩放为 0.1,那么我们将以 1/0.1 或 10 的增量显示轴号。
首先让我们找出缩放的数量级:
const orderMagnitude = Math.pow(10,Math.floor(Math.log(zoom) / Math.LN10));
缩放值为 1(起始值)产生数量级 0。缩放值为 10 产生数量级 1。
现在我们可以采用该数量级并将其转换为以 10 为基数的整数:
const every = 1 / Math.pow(10,orderMagnitude);
这里取一个数量级,比如1,换算成1/10,1/10就是轴上显示的增量(变量名你已经用过了increment
,所以我称它为 every
,因为它经常代表一个轴刻度)。这种间隔 1/10 个单位的刻度是合适的,因为一个数量级代表 10 倍缩放。
现在我们需要将其应用于代码中的几个位置:
const inverval = 20 * every; // scale the interval to reflect the density of ticks
当然,当你设置坐标轴时,例如:
for (let i = 1; i <= width / 2 / interval; i++) {
total = i * interval
ctx.fillText(` ${i*every} `, basicCenter.x + total, basicHeight / 2)
}
这是目前为止的一个示例(缩小显示效果更快):
class ViewPort {
constructor(canvas) {
this.canvas = canvas
/**
* Point used to calculate the change of every point's position on
* canvas after view port is zoomed and panned
*/
this.center = this.basicCenter
this.zoom = 1
this.shouldPan = false
this.prevZoomingPoint = null
}
get canvasWidth() {
return this.canvas.getBoundingClientRect().width
}
get canvasHeight() {
return this.canvas.getBoundingClientRect().height
}
get canvasLeft() {
return this.canvas.getBoundingClientRect().left
}
get canvasTop() {
return this.canvas.getBoundingClientRect().top
}
get context() {
return this.canvas.getContext('2d')
}
get basicCenter() {
const { canvasWidth, canvasHeight } = this
const point = {
x: canvasWidth / 2,
y: canvasHeight / 2
}
return point
}
get basicWidth() {
const width = this.canvasWidth
return width
}
get basicHeight() {
const height = this.canvasHeight
return height
}
get width() {
const { basicWidth, zoom } = this
const width = basicWidth * zoom
return width
}
get height() {
const { basicHeight, zoom } = this
const height = basicHeight * zoom
return height
}
get movement() {
const { width, height, basicWidth, basicHeight } = this
const { x: cx, y: cy } = this.center
const { x: basicCX, y: basicCY } = this.basicCenter
const deltaX = cx - basicCX - ((width - basicWidth) / 2)
const deltaY = cy - basicCY - ((height - basicHeight) / 2)
const res = {
x: deltaX,
y: deltaY
}
return res
}
get pan() {
const { center, zoom, basicCenter } = this
const res = {
x: center.x - basicCenter.x,
y: center.y - basicCenter.y
}
return res
}
zoomBy(center, deltaZoom) {
const prevZoom = this.zoom
this.zoom = this.zoom + deltaZoom
this.center = this.zoomPoint(center, this.zoom / prevZoom, this.center)
}
zoomIn(point) {
this.zoomBy(point, 0.1)
}
zoomOut(point) {
this.zoom > 0.25 && this.zoomBy(point, -0.1)
}
zoomPoint(center, rate, point) {
const { x: cx, y: cy } = center
const { x, y } = point
const deltaX = (x - cx) * rate
const deltaY = (y - cy) * rate
const newPoint = {
x: cx + deltaX,
y: cy + deltaY
}
return newPoint
}
panBy(deltaX, deltaY) {
const { x: centerX, y: centerY } = this.center
this.center = {
x: centerX + deltaX,
y: centerY + deltaY
}
}
getDeltaPointToPrevPanningPoint(point) {
const { x, y } = point
const { x: prevX, y: prevY } = this.prevZoomingPoint
const deltaPoint = {
x: x - prevX,
y: y - prevY
}
return deltaPoint
}
startPan(event) {
const point = {
x: event.x - this.canvasLeft,
y: event.y - this.canvasTop,
}
this.shouldPan = true
this.prevZoomingPoint = point
}
panning(event) {
const point = {
x: event.x - this.canvasLeft,
y: event.y - this.canvasTop,
}
const deltaX = this.getDeltaPointToPrevPanningPoint(point).x
const deltaY = this.getDeltaPointToPrevPanningPoint(point).y
this.prevZoomingPoint = point
this.panBy(deltaX, deltaY)
}
stopPan() {
this.shouldPan = false
}
transformToInitial(point) {
const { x, y } = point
const { movement, zoom } = this
const res = {
x: (x - movement.x) / zoom,
y: (y - movement.y) / zoom
}
return res
}
transform(point) {
const { x, y } = point
const { movement, zoom } = this
const res = {
x: x * zoom + movement.x,
y: y * zoom + movement.y
}
return res
}
clearCanvas() {
this.context.setTransform(1, 0, 0, 1, 0, 0)
this.context.clearRect(
0,
0,
viewPort.canvasWidth,
viewPort.canvasHeight
)
}
}
class Interaction {
constructor({
canvas,
viewPort,
dragger
}) {
canvas.removeEventListener("mousewheel", mousewheelListener)
canvas.addEventListener("mousewheel", mousewheelListener)
canvas.removeEventListener("mousedown", mousedownListener)
canvas.addEventListener("mousedown", mousedownListener)
canvas.removeEventListener("mousemove", mousemoveListener)
canvas.addEventListener("mousemove", mousemoveListener)
canvas.removeEventListener("mouseup", mouseupListener)
canvas.addEventListener("mouseup", mouseupListener)
function mousewheelListener(event) {
event.preventDefault()
const point = {
x: event.x - canvas.getBoundingClientRect().left,
y: event.y - canvas.getBoundingClientRect().top,
}
const { deltaX, deltaY } = event
if (isDecreasing()) {
viewPort.zoomIn(point)
}
if (isIncreasing()) {
viewPort.zoomOut(point)
}
function isIncreasing() {
const res = deltaX > 0 || deltaY > 0
return res
}
function isDecreasing() {
const res = deltaX < 0 || deltaY < 0
return res
}
render()
}
function mousedownListener(event) {
viewPort.startPan(event)
}
function mousemoveListener(event) {
viewPort.shouldPan && viewPort.panning(event)
viewPort.shouldPan && render()
}
function mouseupListener(event) {
viewPort.stopPan(event)
}
}
}
const canvas = document.getElementById("myCanvas")
const viewPort = new ViewPort(canvas)
const interaction = new Interaction({ viewPort, canvas })
function render() {
const { abs, max } = Math
const { zoom, movement, context: ctx, pan, center, basicCenter } = viewPort
viewPort.clearCanvas()
ctx.setTransform(zoom, 0, 0, zoom, movement.x, movement.y)
// modify font based on zoom:
ctx.font = 12 / zoom + "px Arial";
// modify number interval based on zoom:
const orderMagnitude = Math.floor(Math.log(zoom) / Math.LN10);
const every = 1 / Math.pow(10,orderMagnitude);
// Original codes are rewrote
const { canvasWidth, canvasHeight } = viewPort
const interval = 20 * every;
const basicWidth = canvasWidth
const basicHeight = canvasHeight
const potentialWidth = 2 * max(abs(viewPort.transformToInitial({ x: 0, y: 0 }).x - basicCenter.x), abs(viewPort.transformToInitial({ x: basicWidth, y: 0 }).x - basicCenter.x))
const width = potentialWidth > basicWidth ? potentialWidth : basicWidth
const potentialHeight = 2 * max(abs(viewPort.transformToInitial({ x: 0, y: 0 }).y - basicCenter.y), abs(viewPort.transformToInitial({ x: 0, y: basicHeight }).y - basicCenter.y))
const height = potentialHeight > basicHeight ? potentialHeight : basicHeight
drawXAxis()
drawYAxis()
drawOriginCoordinate()
drawXCoordinates()
drawYCoordinates()
function drawXAxis() {
const path = new Path2D
path.moveTo(basicCenter.x - width / 2, basicHeight / 2)
path.lineTo(basicCenter.x + width / 2, basicHeight / 2)
ctx.stroke(path)
}
function drawYAxis() {
const path = new Path2D
path.moveTo(basicWidth / 2, basicCenter.y - height / 2)
path.lineTo(basicWidth / 2, basicCenter.y + height / 2)
ctx.stroke(path)
}
function drawOriginCoordinate() {
ctx.fillText(`O`, basicCenter.x + 5, basicCenter.y - 5)
}
function drawXCoordinates() {
for (let i = 1; i <= width / 2 / interval; i++) {
total = i * interval
ctx.fillText(` ${i*every} `, basicCenter.x + total, basicHeight / 2)
}
for (let i = 1; i <= width / 2 / interval; i++) {
total = i * interval
ctx.fillText(` -${i*every} `, basicCenter.x - total, basicHeight / 2)
}
}
function drawYCoordinates() {
for (let i = 1; i <= height / 2 / interval; i++) {
total = i * interval
ctx.fillText(` ${i*every} `, basicWidth / 2, basicCenter.y + total)
}
for (let i = 1; i <= height / 2 / interval; i++) {
total = i * interval
ctx.fillText(` -${i*every} `, basicWidth / 2, basicCenter.y - total)
}
}
}
render()
<canvas id="myCanvas" width="300" height="300" style="border:1px solid #d3d3d3;"></canvas>
细化坐标轴
这没问题,但 zoom = 1
处的刻度阈值位置不理想。也许我们可以通过偏移输入值来稍微修改一下计算出的数量级:
const orderMagnitude = Math.pow(10,Math.floor(Math.log(zoom*1.5) / Math.LN10));
这将为不同轴的刻度产生稍微更好的间隔阈值。
进一步细化
我们可以使用 2 或 5 作为中间值,而不是让原点旁边的每个刻度都从 1 x 10^n
开始,因为仅当缩放比例变化 10 倍时才重置刻度并不是最常见的理想。
一个可能的解决方案是,随着缩放比例因子相对于给定数量级的增加,我们减少刻度之间的间隔(减少 every
):
// Modify how every often we want to show an axis tick:
var every;
if (zoom/Math.pow(10,orderMagnitude) > 4) {
every = 1 / Math.pow(10,orderMagnitude) * 0.2;
}
else if (zoom/Math.pow(10,orderMagnitude) > 2) {
every = 1 / Math.pow(10,orderMagnitude) * 0.5;
}
else {
every = 1 / Math.pow(10,orderMagnitude);
}
这给了我们:
class ViewPort {
constructor(canvas) {
this.canvas = canvas
/**
* Point used to calculate the change of every point's position on
* canvas after view port is zoomed and panned
*/
this.center = this.basicCenter
this.zoom = 1
this.shouldPan = false
this.prevZoomingPoint = null
}
get canvasWidth() {
return this.canvas.getBoundingClientRect().width
}
get canvasHeight() {
return this.canvas.getBoundingClientRect().height
}
get canvasLeft() {
return this.canvas.getBoundingClientRect().left
}
get canvasTop() {
return this.canvas.getBoundingClientRect().top
}
get context() {
return this.canvas.getContext('2d')
}
get basicCenter() {
const { canvasWidth, canvasHeight } = this
const point = {
x: canvasWidth / 2,
y: canvasHeight / 2
}
return point
}
get basicWidth() {
const width = this.canvasWidth
return width
}
get basicHeight() {
const height = this.canvasHeight
return height
}
get width() {
const { basicWidth, zoom } = this
const width = basicWidth * zoom
return width
}
get height() {
const { basicHeight, zoom } = this
const height = basicHeight * zoom
return height
}
get movement() {
const { width, height, basicWidth, basicHeight } = this
const { x: cx, y: cy } = this.center
const { x: basicCX, y: basicCY } = this.basicCenter
const deltaX = cx - basicCX - ((width - basicWidth) / 2)
const deltaY = cy - basicCY - ((height - basicHeight) / 2)
const res = {
x: deltaX,
y: deltaY
}
return res
}
get pan() {
const { center, zoom, basicCenter } = this
const res = {
x: center.x - basicCenter.x,
y: center.y - basicCenter.y
}
return res
}
zoomBy(center, deltaZoom) {
const prevZoom = this.zoom
this.zoom = this.zoom + deltaZoom
this.center = this.zoomPoint(center, this.zoom / prevZoom, this.center)
}
zoomIn(point) {
this.zoomBy(point, 0.1)
}
zoomOut(point) {
this.zoom > 0.25 && this.zoomBy(point, -0.1)
}
zoomPoint(center, rate, point) {
const { x: cx, y: cy } = center
const { x, y } = point
const deltaX = (x - cx) * rate
const deltaY = (y - cy) * rate
const newPoint = {
x: cx + deltaX,
y: cy + deltaY
}
return newPoint
}
panBy(deltaX, deltaY) {
const { x: centerX, y: centerY } = this.center
this.center = {
x: centerX + deltaX,
y: centerY + deltaY
}
}
getDeltaPointToPrevPanningPoint(point) {
const { x, y } = point
const { x: prevX, y: prevY } = this.prevZoomingPoint
const deltaPoint = {
x: x - prevX,
y: y - prevY
}
return deltaPoint
}
startPan(event) {
const point = {
x: event.x - this.canvasLeft,
y: event.y - this.canvasTop,
}
this.shouldPan = true
this.prevZoomingPoint = point
}
panning(event) {
const point = {
x: event.x - this.canvasLeft,
y: event.y - this.canvasTop,
}
const deltaX = this.getDeltaPointToPrevPanningPoint(point).x
const deltaY = this.getDeltaPointToPrevPanningPoint(point).y
this.prevZoomingPoint = point
this.panBy(deltaX, deltaY)
}
stopPan() {
this.shouldPan = false
}
transformToInitial(point) {
const { x, y } = point
const { movement, zoom } = this
const res = {
x: (x - movement.x) / zoom,
y: (y - movement.y) / zoom
}
return res
}
transform(point) {
const { x, y } = point
const { movement, zoom } = this
const res = {
x: x * zoom + movement.x,
y: y * zoom + movement.y
}
return res
}
clearCanvas() {
this.context.setTransform(1, 0, 0, 1, 0, 0)
this.context.clearRect(
0,
0,
viewPort.canvasWidth,
viewPort.canvasHeight
)
}
}
class Interaction {
constructor({
canvas,
viewPort,
dragger
}) {
canvas.removeEventListener("mousewheel", mousewheelListener)
canvas.addEventListener("mousewheel", mousewheelListener)
canvas.removeEventListener("mousedown", mousedownListener)
canvas.addEventListener("mousedown", mousedownListener)
canvas.removeEventListener("mousemove", mousemoveListener)
canvas.addEventListener("mousemove", mousemoveListener)
canvas.removeEventListener("mouseup", mouseupListener)
canvas.addEventListener("mouseup", mouseupListener)
function mousewheelListener(event) {
event.preventDefault()
const point = {
x: event.x - canvas.getBoundingClientRect().left,
y: event.y - canvas.getBoundingClientRect().top,
}
const { deltaX, deltaY } = event
if (isDecreasing()) {
viewPort.zoomIn(point)
}
if (isIncreasing()) {
viewPort.zoomOut(point)
}
function isIncreasing() {
const res = deltaX > 0 || deltaY > 0
return res
}
function isDecreasing() {
const res = deltaX < 0 || deltaY < 0
return res
}
render()
}
function mousedownListener(event) {
viewPort.startPan(event)
}
function mousemoveListener(event) {
viewPort.shouldPan && viewPort.panning(event)
viewPort.shouldPan && render()
}
function mouseupListener(event) {
viewPort.stopPan(event)
}
}
}
const canvas = document.getElementById("myCanvas")
const viewPort = new ViewPort(canvas)
const interaction = new Interaction({ viewPort, canvas })
function render() {
const { abs, max } = Math
const { zoom, movement, context: ctx, pan, center, basicCenter } = viewPort
viewPort.clearCanvas()
ctx.setTransform(zoom, 0, 0, zoom, movement.x, movement.y)
// modify font based on zoom:
ctx.font = 12 / zoom + "px Arial";
// modify number interval based on zoom:
const orderMagnitude = Math.floor(Math.log(zoom*1.5) / Math.LN10);
// Modify how every often we want to show an axis tick:
var every;
if (zoom/Math.pow(10,orderMagnitude) > 4) {
every = 1 / Math.pow(10,orderMagnitude) * 0.2;
}
else if (zoom/Math.pow(10,orderMagnitude) > 2) {
every = 1 / Math.pow(10,orderMagnitude) * 0.5;
}
else {
every = 1 / Math.pow(10,orderMagnitude);
}
// Original codes are rewrote
const { canvasWidth, canvasHeight } = viewPort
const interval = 30 * every;
const basicWidth = canvasWidth
const basicHeight = canvasHeight
const potentialWidth = 2 * max(abs(viewPort.transformToInitial({ x: 0, y: 0 }).x - basicCenter.x), abs(viewPort.transformToInitial({ x: basicWidth, y: 0 }).x - basicCenter.x))
const width = potentialWidth > basicWidth ? potentialWidth : basicWidth
const potentialHeight = 2 * max(abs(viewPort.transformToInitial({ x: 0, y: 0 }).y - basicCenter.y), abs(viewPort.transformToInitial({ x: 0, y: basicHeight }).y - basicCenter.y))
const height = potentialHeight > basicHeight ? potentialHeight : basicHeight
drawXAxis()
drawYAxis()
drawOriginCoordinate()
drawXCoordinates()
drawYCoordinates()
function drawXAxis() {
const path = new Path2D
path.moveTo(basicCenter.x - width / 2, basicHeight / 2)
path.lineTo(basicCenter.x + width / 2, basicHeight / 2)
ctx.stroke(path)
}
function drawYAxis() {
const path = new Path2D
path.moveTo(basicWidth / 2, basicCenter.y - height / 2)
path.lineTo(basicWidth / 2, basicCenter.y + height / 2)
ctx.stroke(path)
}
function drawOriginCoordinate() {
ctx.fillText(`O`, basicCenter.x + 5, basicCenter.y - 5)
}
function drawXCoordinates() {
for (let i = 1; i <= width / 2 / interval; i++) {
total = i * interval
ctx.fillText(` ${i*every} `, basicCenter.x + total, basicHeight / 2)
}
for (let i = 1; i <= width / 2 / interval; i++) {
total = i * interval
ctx.fillText(` -${i*every} `, basicCenter.x - total, basicHeight / 2)
}
}
function drawYCoordinates() {
for (let i = 1; i <= height / 2 / interval; i++) {
total = i * interval
ctx.fillText(` ${i*every} `, basicWidth / 2, basicCenter.y + total)
}
for (let i = 1; i <= height / 2 / interval; i++) {
total = i * interval
ctx.fillText(` -${i*every} `, basicWidth / 2, basicCenter.y - total)
}
}
}
render()
<canvas id="myCanvas" width="300" height="300" style="border:1px solid #d3d3d3;"></canvas>
进一步完善
我没有触及数字格式,但放大时您会看到一些浮点问题。此外,坐标轴的条形宽度随着我们放大而增大,随着我们缩小而缩小,这会影响文本定位。
我以前用htmlcanvas做过笛卡尔坐标系。有用户帮我添加了鼠标缩放功能
但是我遇到了一个问题。轴号的字体大小是固定的,所以在缩小的同时,字体也会变小。
我想要固定的字体大小但数字之间的间隔可变。
例如,如果放大,您会看到 x 轴上的数字 0、1、2、3、4、5
但是一旦缩小,它应该是 0、5、10、15
喜欢地理https://www.geogebra.org/classic
我需要制作自己的坐标系,项目不能使用小程序或内嵌代码
目前得到的代码
class ViewPort {
constructor(canvas) {
this.canvas = canvas
/**
* Point used to calculate the change of every point's position on
* canvas after view port is zoomed and panned
*/
this.center = this.basicCenter
this.zoom = 1
this.shouldPan = false
this.prevZoomingPoint = null
}
get canvasWidth() {
return this.canvas.getBoundingClientRect().width
}
get canvasHeight() {
return this.canvas.getBoundingClientRect().height
}
get canvasLeft() {
return this.canvas.getBoundingClientRect().left
}
get canvasTop() {
return this.canvas.getBoundingClientRect().top
}
get context() {
return this.canvas.getContext('2d')
}
get basicCenter() {
const { canvasWidth, canvasHeight } = this
const point = {
x: canvasWidth / 2,
y: canvasHeight / 2
}
return point
}
get basicWidth() {
const width = this.canvasWidth
return width
}
get basicHeight() {
const height = this.canvasHeight
return height
}
get width() {
const { basicWidth, zoom } = this
const width = basicWidth * zoom
return width
}
get height() {
const { basicHeight, zoom } = this
const height = basicHeight * zoom
return height
}
get movement() {
const { width, height, basicWidth, basicHeight } = this
const { x: cx, y: cy } = this.center
const { x: basicCX, y: basicCY } = this.basicCenter
const deltaX = cx - basicCX - ((width - basicWidth) / 2)
const deltaY = cy - basicCY - ((height - basicHeight) / 2)
const res = {
x: deltaX,
y: deltaY
}
return res
}
get pan() {
const { center, zoom, basicCenter } = this
const res = {
x: center.x - basicCenter.x,
y: center.y - basicCenter.y
}
return res
}
zoomBy(center, deltaZoom) {
const prevZoom = this.zoom
this.zoom = this.zoom + deltaZoom
this.center = this.zoomPoint(center, this.zoom / prevZoom, this.center)
}
zoomIn(point) {
this.zoomBy(point, 0.1)
}
zoomOut(point) {
this.zoom > 0.25 && this.zoomBy(point, -0.1)
}
zoomPoint(center, rate, point) {
const { x: cx, y: cy } = center
const { x, y } = point
const deltaX = (x - cx) * rate
const deltaY = (y - cy) * rate
const newPoint = {
x: cx + deltaX,
y: cy + deltaY
}
return newPoint
}
panBy(deltaX, deltaY) {
const { x: centerX, y: centerY } = this.center
this.center = {
x: centerX + deltaX,
y: centerY + deltaY
}
}
getDeltaPointToPrevPanningPoint(point) {
const { x, y } = point
const { x: prevX, y: prevY } = this.prevZoomingPoint
const deltaPoint = {
x: x - prevX,
y: y - prevY
}
return deltaPoint
}
startPan(event) {
const point = {
x: event.x - this.canvasLeft,
y: event.y - this.canvasTop,
}
this.shouldPan = true
this.prevZoomingPoint = point
}
panning(event) {
const point = {
x: event.x - this.canvasLeft,
y: event.y - this.canvasTop,
}
const deltaX = this.getDeltaPointToPrevPanningPoint(point).x
const deltaY = this.getDeltaPointToPrevPanningPoint(point).y
this.prevZoomingPoint = point
this.panBy(deltaX, deltaY)
}
stopPan() {
this.shouldPan = false
}
transformToInitial(point) {
const { x, y } = point
const { movement, zoom } = this
const res = {
x: (x - movement.x) / zoom,
y: (y - movement.y) / zoom
}
return res
}
transform(point) {
const { x, y } = point
const { movement, zoom } = this
const res = {
x: x * zoom + movement.x,
y: y * zoom + movement.y
}
return res
}
clearCanvas() {
this.context.setTransform(1, 0, 0, 1, 0, 0)
this.context.clearRect(
0,
0,
viewPort.canvasWidth,
viewPort.canvasHeight
)
}
}
class Interaction {
constructor({
canvas,
viewPort,
dragger
}) {
canvas.removeEventListener("mousewheel", mousewheelListener)
canvas.addEventListener("mousewheel", mousewheelListener)
canvas.removeEventListener("mousedown", mousedownListener)
canvas.addEventListener("mousedown", mousedownListener)
canvas.removeEventListener("mousemove", mousemoveListener)
canvas.addEventListener("mousemove", mousemoveListener)
canvas.removeEventListener("mouseup", mouseupListener)
canvas.addEventListener("mouseup", mouseupListener)
function mousewheelListener(event) {
event.preventDefault()
const point = {
x: event.x - canvas.getBoundingClientRect().left,
y: event.y - canvas.getBoundingClientRect().top,
}
const { deltaX, deltaY } = event
if (isDecreasing()) {
viewPort.zoomIn(point)
}
if (isIncreasing()) {
viewPort.zoomOut(point)
}
function isIncreasing() {
const res = deltaX > 0 || deltaY > 0
return res
}
function isDecreasing() {
const res = deltaX < 0 || deltaY < 0
return res
}
render()
}
function mousedownListener(event) {
viewPort.startPan(event)
}
function mousemoveListener(event) {
viewPort.shouldPan && viewPort.panning(event)
viewPort.shouldPan && render()
}
function mouseupListener(event) {
viewPort.stopPan(event)
}
}
}
const canvas = document.getElementById("myCanvas")
const viewPort = new ViewPort(canvas)
const interaction = new Interaction({ viewPort, canvas })
function render() {
const { abs, max } = Math
const { zoom, movement, context: ctx, pan, center, basicCenter } = viewPort
viewPort.clearCanvas()
ctx.setTransform(zoom, 0, 0, zoom, movement.x, movement.y)
// Original codes are rewrote
const { canvasWidth, canvasHeight } = viewPort
const interval = 20
const basicWidth = canvasWidth
const basicHeight = canvasHeight
const potentialWidth = 2 * max(abs(viewPort.transformToInitial({ x: 0, y: 0 }).x - basicCenter.x), abs(viewPort.transformToInitial({ x: basicWidth, y: 0 }).x - basicCenter.x))
const width = potentialWidth > basicWidth ? potentialWidth : basicWidth
const potentialHeight = 2 * max(abs(viewPort.transformToInitial({ x: 0, y: 0 }).y - basicCenter.y), abs(viewPort.transformToInitial({ x: 0, y: basicHeight }).y - basicCenter.y))
const height = potentialHeight > basicHeight ? potentialHeight : basicHeight
drawXAxis()
drawYAxis()
drawOriginCoordinate()
drawXCoordinates()
drawYCoordinates()
function drawXAxis() {
const path = new Path2D
path.moveTo(basicCenter.x - width / 2, basicHeight / 2)
path.lineTo(basicCenter.x + width / 2, basicHeight / 2)
ctx.stroke(path)
}
function drawYAxis() {
const path = new Path2D
path.moveTo(basicWidth / 2, basicCenter.y - height / 2)
path.lineTo(basicWidth / 2, basicCenter.y + height / 2)
ctx.stroke(path)
}
function drawOriginCoordinate() {
ctx.fillText(`O`, basicCenter.x + 5, basicCenter.y - 5)
}
function drawXCoordinates() {
for (let i = 1; i <= width / 2 / interval; i++) {
total = i * interval
ctx.fillText(` ${i} `, basicCenter.x + total, basicHeight / 2)
}
for (let i = 1; i <= width / 2 / interval; i++) {
total = i * interval
ctx.fillText(` -${i} `, basicCenter.x - total, basicHeight / 2)
}
}
function drawYCoordinates() {
for (let i = 1; i <= height / 2 / interval; i++) {
total = i * interval
ctx.fillText(` ${i} `, basicWidth / 2, basicCenter.y + total)
}
for (let i = 1; i <= height / 2 / interval; i++) {
total = i * interval
ctx.fillText(` -${i} `, basicWidth / 2, basicCenter.y - total)
}
}
}
render()
<canvas id="myCanvas" width="300" height="300" style="border:1px solid #d3d3d3;"></canvas>
字体大小
对于字体大小,您希望字体大小与 canvas 的缩放值成反比。说:
ctx.font = 12 / zoom + "px Arial";
其中 12 是当比例 (zoom
) 为 1 时的字体大小。如果您放大所有内容都拉伸两倍 (zoom = 2
),则字体大小将为 6。由于字体大小是线性而非面积度量,因此我们不需要在此处对缩放进行平方。
更新轴
要更新显示的数字以便适当地缩放它们,可以使用几种不同的方法。
作为一个简单的例子,我们可以找出缩放的数量级(或者本质上它有多少位数或它有多少小数点)并根据这个因素缩放显示的数字。例如,如果缩放比例为 10,那么我们将以 1/10 的增量显示轴号。如果缩放为 0.1,那么我们将以 1/0.1 或 10 的增量显示轴号。
首先让我们找出缩放的数量级:
const orderMagnitude = Math.pow(10,Math.floor(Math.log(zoom) / Math.LN10));
缩放值为 1(起始值)产生数量级 0。缩放值为 10 产生数量级 1。
现在我们可以采用该数量级并将其转换为以 10 为基数的整数:
const every = 1 / Math.pow(10,orderMagnitude);
这里取一个数量级,比如1,换算成1/10,1/10就是轴上显示的增量(变量名你已经用过了increment
,所以我称它为 every
,因为它经常代表一个轴刻度)。这种间隔 1/10 个单位的刻度是合适的,因为一个数量级代表 10 倍缩放。
现在我们需要将其应用于代码中的几个位置:
const inverval = 20 * every; // scale the interval to reflect the density of ticks
当然,当你设置坐标轴时,例如:
for (let i = 1; i <= width / 2 / interval; i++) {
total = i * interval
ctx.fillText(` ${i*every} `, basicCenter.x + total, basicHeight / 2)
}
这是目前为止的一个示例(缩小显示效果更快):
class ViewPort {
constructor(canvas) {
this.canvas = canvas
/**
* Point used to calculate the change of every point's position on
* canvas after view port is zoomed and panned
*/
this.center = this.basicCenter
this.zoom = 1
this.shouldPan = false
this.prevZoomingPoint = null
}
get canvasWidth() {
return this.canvas.getBoundingClientRect().width
}
get canvasHeight() {
return this.canvas.getBoundingClientRect().height
}
get canvasLeft() {
return this.canvas.getBoundingClientRect().left
}
get canvasTop() {
return this.canvas.getBoundingClientRect().top
}
get context() {
return this.canvas.getContext('2d')
}
get basicCenter() {
const { canvasWidth, canvasHeight } = this
const point = {
x: canvasWidth / 2,
y: canvasHeight / 2
}
return point
}
get basicWidth() {
const width = this.canvasWidth
return width
}
get basicHeight() {
const height = this.canvasHeight
return height
}
get width() {
const { basicWidth, zoom } = this
const width = basicWidth * zoom
return width
}
get height() {
const { basicHeight, zoom } = this
const height = basicHeight * zoom
return height
}
get movement() {
const { width, height, basicWidth, basicHeight } = this
const { x: cx, y: cy } = this.center
const { x: basicCX, y: basicCY } = this.basicCenter
const deltaX = cx - basicCX - ((width - basicWidth) / 2)
const deltaY = cy - basicCY - ((height - basicHeight) / 2)
const res = {
x: deltaX,
y: deltaY
}
return res
}
get pan() {
const { center, zoom, basicCenter } = this
const res = {
x: center.x - basicCenter.x,
y: center.y - basicCenter.y
}
return res
}
zoomBy(center, deltaZoom) {
const prevZoom = this.zoom
this.zoom = this.zoom + deltaZoom
this.center = this.zoomPoint(center, this.zoom / prevZoom, this.center)
}
zoomIn(point) {
this.zoomBy(point, 0.1)
}
zoomOut(point) {
this.zoom > 0.25 && this.zoomBy(point, -0.1)
}
zoomPoint(center, rate, point) {
const { x: cx, y: cy } = center
const { x, y } = point
const deltaX = (x - cx) * rate
const deltaY = (y - cy) * rate
const newPoint = {
x: cx + deltaX,
y: cy + deltaY
}
return newPoint
}
panBy(deltaX, deltaY) {
const { x: centerX, y: centerY } = this.center
this.center = {
x: centerX + deltaX,
y: centerY + deltaY
}
}
getDeltaPointToPrevPanningPoint(point) {
const { x, y } = point
const { x: prevX, y: prevY } = this.prevZoomingPoint
const deltaPoint = {
x: x - prevX,
y: y - prevY
}
return deltaPoint
}
startPan(event) {
const point = {
x: event.x - this.canvasLeft,
y: event.y - this.canvasTop,
}
this.shouldPan = true
this.prevZoomingPoint = point
}
panning(event) {
const point = {
x: event.x - this.canvasLeft,
y: event.y - this.canvasTop,
}
const deltaX = this.getDeltaPointToPrevPanningPoint(point).x
const deltaY = this.getDeltaPointToPrevPanningPoint(point).y
this.prevZoomingPoint = point
this.panBy(deltaX, deltaY)
}
stopPan() {
this.shouldPan = false
}
transformToInitial(point) {
const { x, y } = point
const { movement, zoom } = this
const res = {
x: (x - movement.x) / zoom,
y: (y - movement.y) / zoom
}
return res
}
transform(point) {
const { x, y } = point
const { movement, zoom } = this
const res = {
x: x * zoom + movement.x,
y: y * zoom + movement.y
}
return res
}
clearCanvas() {
this.context.setTransform(1, 0, 0, 1, 0, 0)
this.context.clearRect(
0,
0,
viewPort.canvasWidth,
viewPort.canvasHeight
)
}
}
class Interaction {
constructor({
canvas,
viewPort,
dragger
}) {
canvas.removeEventListener("mousewheel", mousewheelListener)
canvas.addEventListener("mousewheel", mousewheelListener)
canvas.removeEventListener("mousedown", mousedownListener)
canvas.addEventListener("mousedown", mousedownListener)
canvas.removeEventListener("mousemove", mousemoveListener)
canvas.addEventListener("mousemove", mousemoveListener)
canvas.removeEventListener("mouseup", mouseupListener)
canvas.addEventListener("mouseup", mouseupListener)
function mousewheelListener(event) {
event.preventDefault()
const point = {
x: event.x - canvas.getBoundingClientRect().left,
y: event.y - canvas.getBoundingClientRect().top,
}
const { deltaX, deltaY } = event
if (isDecreasing()) {
viewPort.zoomIn(point)
}
if (isIncreasing()) {
viewPort.zoomOut(point)
}
function isIncreasing() {
const res = deltaX > 0 || deltaY > 0
return res
}
function isDecreasing() {
const res = deltaX < 0 || deltaY < 0
return res
}
render()
}
function mousedownListener(event) {
viewPort.startPan(event)
}
function mousemoveListener(event) {
viewPort.shouldPan && viewPort.panning(event)
viewPort.shouldPan && render()
}
function mouseupListener(event) {
viewPort.stopPan(event)
}
}
}
const canvas = document.getElementById("myCanvas")
const viewPort = new ViewPort(canvas)
const interaction = new Interaction({ viewPort, canvas })
function render() {
const { abs, max } = Math
const { zoom, movement, context: ctx, pan, center, basicCenter } = viewPort
viewPort.clearCanvas()
ctx.setTransform(zoom, 0, 0, zoom, movement.x, movement.y)
// modify font based on zoom:
ctx.font = 12 / zoom + "px Arial";
// modify number interval based on zoom:
const orderMagnitude = Math.floor(Math.log(zoom) / Math.LN10);
const every = 1 / Math.pow(10,orderMagnitude);
// Original codes are rewrote
const { canvasWidth, canvasHeight } = viewPort
const interval = 20 * every;
const basicWidth = canvasWidth
const basicHeight = canvasHeight
const potentialWidth = 2 * max(abs(viewPort.transformToInitial({ x: 0, y: 0 }).x - basicCenter.x), abs(viewPort.transformToInitial({ x: basicWidth, y: 0 }).x - basicCenter.x))
const width = potentialWidth > basicWidth ? potentialWidth : basicWidth
const potentialHeight = 2 * max(abs(viewPort.transformToInitial({ x: 0, y: 0 }).y - basicCenter.y), abs(viewPort.transformToInitial({ x: 0, y: basicHeight }).y - basicCenter.y))
const height = potentialHeight > basicHeight ? potentialHeight : basicHeight
drawXAxis()
drawYAxis()
drawOriginCoordinate()
drawXCoordinates()
drawYCoordinates()
function drawXAxis() {
const path = new Path2D
path.moveTo(basicCenter.x - width / 2, basicHeight / 2)
path.lineTo(basicCenter.x + width / 2, basicHeight / 2)
ctx.stroke(path)
}
function drawYAxis() {
const path = new Path2D
path.moveTo(basicWidth / 2, basicCenter.y - height / 2)
path.lineTo(basicWidth / 2, basicCenter.y + height / 2)
ctx.stroke(path)
}
function drawOriginCoordinate() {
ctx.fillText(`O`, basicCenter.x + 5, basicCenter.y - 5)
}
function drawXCoordinates() {
for (let i = 1; i <= width / 2 / interval; i++) {
total = i * interval
ctx.fillText(` ${i*every} `, basicCenter.x + total, basicHeight / 2)
}
for (let i = 1; i <= width / 2 / interval; i++) {
total = i * interval
ctx.fillText(` -${i*every} `, basicCenter.x - total, basicHeight / 2)
}
}
function drawYCoordinates() {
for (let i = 1; i <= height / 2 / interval; i++) {
total = i * interval
ctx.fillText(` ${i*every} `, basicWidth / 2, basicCenter.y + total)
}
for (let i = 1; i <= height / 2 / interval; i++) {
total = i * interval
ctx.fillText(` -${i*every} `, basicWidth / 2, basicCenter.y - total)
}
}
}
render()
<canvas id="myCanvas" width="300" height="300" style="border:1px solid #d3d3d3;"></canvas>
细化坐标轴
这没问题,但 zoom = 1
处的刻度阈值位置不理想。也许我们可以通过偏移输入值来稍微修改一下计算出的数量级:
const orderMagnitude = Math.pow(10,Math.floor(Math.log(zoom*1.5) / Math.LN10));
这将为不同轴的刻度产生稍微更好的间隔阈值。
进一步细化
我们可以使用 2 或 5 作为中间值,而不是让原点旁边的每个刻度都从 1 x 10^n
开始,因为仅当缩放比例变化 10 倍时才重置刻度并不是最常见的理想。
一个可能的解决方案是,随着缩放比例因子相对于给定数量级的增加,我们减少刻度之间的间隔(减少 every
):
// Modify how every often we want to show an axis tick:
var every;
if (zoom/Math.pow(10,orderMagnitude) > 4) {
every = 1 / Math.pow(10,orderMagnitude) * 0.2;
}
else if (zoom/Math.pow(10,orderMagnitude) > 2) {
every = 1 / Math.pow(10,orderMagnitude) * 0.5;
}
else {
every = 1 / Math.pow(10,orderMagnitude);
}
这给了我们:
class ViewPort {
constructor(canvas) {
this.canvas = canvas
/**
* Point used to calculate the change of every point's position on
* canvas after view port is zoomed and panned
*/
this.center = this.basicCenter
this.zoom = 1
this.shouldPan = false
this.prevZoomingPoint = null
}
get canvasWidth() {
return this.canvas.getBoundingClientRect().width
}
get canvasHeight() {
return this.canvas.getBoundingClientRect().height
}
get canvasLeft() {
return this.canvas.getBoundingClientRect().left
}
get canvasTop() {
return this.canvas.getBoundingClientRect().top
}
get context() {
return this.canvas.getContext('2d')
}
get basicCenter() {
const { canvasWidth, canvasHeight } = this
const point = {
x: canvasWidth / 2,
y: canvasHeight / 2
}
return point
}
get basicWidth() {
const width = this.canvasWidth
return width
}
get basicHeight() {
const height = this.canvasHeight
return height
}
get width() {
const { basicWidth, zoom } = this
const width = basicWidth * zoom
return width
}
get height() {
const { basicHeight, zoom } = this
const height = basicHeight * zoom
return height
}
get movement() {
const { width, height, basicWidth, basicHeight } = this
const { x: cx, y: cy } = this.center
const { x: basicCX, y: basicCY } = this.basicCenter
const deltaX = cx - basicCX - ((width - basicWidth) / 2)
const deltaY = cy - basicCY - ((height - basicHeight) / 2)
const res = {
x: deltaX,
y: deltaY
}
return res
}
get pan() {
const { center, zoom, basicCenter } = this
const res = {
x: center.x - basicCenter.x,
y: center.y - basicCenter.y
}
return res
}
zoomBy(center, deltaZoom) {
const prevZoom = this.zoom
this.zoom = this.zoom + deltaZoom
this.center = this.zoomPoint(center, this.zoom / prevZoom, this.center)
}
zoomIn(point) {
this.zoomBy(point, 0.1)
}
zoomOut(point) {
this.zoom > 0.25 && this.zoomBy(point, -0.1)
}
zoomPoint(center, rate, point) {
const { x: cx, y: cy } = center
const { x, y } = point
const deltaX = (x - cx) * rate
const deltaY = (y - cy) * rate
const newPoint = {
x: cx + deltaX,
y: cy + deltaY
}
return newPoint
}
panBy(deltaX, deltaY) {
const { x: centerX, y: centerY } = this.center
this.center = {
x: centerX + deltaX,
y: centerY + deltaY
}
}
getDeltaPointToPrevPanningPoint(point) {
const { x, y } = point
const { x: prevX, y: prevY } = this.prevZoomingPoint
const deltaPoint = {
x: x - prevX,
y: y - prevY
}
return deltaPoint
}
startPan(event) {
const point = {
x: event.x - this.canvasLeft,
y: event.y - this.canvasTop,
}
this.shouldPan = true
this.prevZoomingPoint = point
}
panning(event) {
const point = {
x: event.x - this.canvasLeft,
y: event.y - this.canvasTop,
}
const deltaX = this.getDeltaPointToPrevPanningPoint(point).x
const deltaY = this.getDeltaPointToPrevPanningPoint(point).y
this.prevZoomingPoint = point
this.panBy(deltaX, deltaY)
}
stopPan() {
this.shouldPan = false
}
transformToInitial(point) {
const { x, y } = point
const { movement, zoom } = this
const res = {
x: (x - movement.x) / zoom,
y: (y - movement.y) / zoom
}
return res
}
transform(point) {
const { x, y } = point
const { movement, zoom } = this
const res = {
x: x * zoom + movement.x,
y: y * zoom + movement.y
}
return res
}
clearCanvas() {
this.context.setTransform(1, 0, 0, 1, 0, 0)
this.context.clearRect(
0,
0,
viewPort.canvasWidth,
viewPort.canvasHeight
)
}
}
class Interaction {
constructor({
canvas,
viewPort,
dragger
}) {
canvas.removeEventListener("mousewheel", mousewheelListener)
canvas.addEventListener("mousewheel", mousewheelListener)
canvas.removeEventListener("mousedown", mousedownListener)
canvas.addEventListener("mousedown", mousedownListener)
canvas.removeEventListener("mousemove", mousemoveListener)
canvas.addEventListener("mousemove", mousemoveListener)
canvas.removeEventListener("mouseup", mouseupListener)
canvas.addEventListener("mouseup", mouseupListener)
function mousewheelListener(event) {
event.preventDefault()
const point = {
x: event.x - canvas.getBoundingClientRect().left,
y: event.y - canvas.getBoundingClientRect().top,
}
const { deltaX, deltaY } = event
if (isDecreasing()) {
viewPort.zoomIn(point)
}
if (isIncreasing()) {
viewPort.zoomOut(point)
}
function isIncreasing() {
const res = deltaX > 0 || deltaY > 0
return res
}
function isDecreasing() {
const res = deltaX < 0 || deltaY < 0
return res
}
render()
}
function mousedownListener(event) {
viewPort.startPan(event)
}
function mousemoveListener(event) {
viewPort.shouldPan && viewPort.panning(event)
viewPort.shouldPan && render()
}
function mouseupListener(event) {
viewPort.stopPan(event)
}
}
}
const canvas = document.getElementById("myCanvas")
const viewPort = new ViewPort(canvas)
const interaction = new Interaction({ viewPort, canvas })
function render() {
const { abs, max } = Math
const { zoom, movement, context: ctx, pan, center, basicCenter } = viewPort
viewPort.clearCanvas()
ctx.setTransform(zoom, 0, 0, zoom, movement.x, movement.y)
// modify font based on zoom:
ctx.font = 12 / zoom + "px Arial";
// modify number interval based on zoom:
const orderMagnitude = Math.floor(Math.log(zoom*1.5) / Math.LN10);
// Modify how every often we want to show an axis tick:
var every;
if (zoom/Math.pow(10,orderMagnitude) > 4) {
every = 1 / Math.pow(10,orderMagnitude) * 0.2;
}
else if (zoom/Math.pow(10,orderMagnitude) > 2) {
every = 1 / Math.pow(10,orderMagnitude) * 0.5;
}
else {
every = 1 / Math.pow(10,orderMagnitude);
}
// Original codes are rewrote
const { canvasWidth, canvasHeight } = viewPort
const interval = 30 * every;
const basicWidth = canvasWidth
const basicHeight = canvasHeight
const potentialWidth = 2 * max(abs(viewPort.transformToInitial({ x: 0, y: 0 }).x - basicCenter.x), abs(viewPort.transformToInitial({ x: basicWidth, y: 0 }).x - basicCenter.x))
const width = potentialWidth > basicWidth ? potentialWidth : basicWidth
const potentialHeight = 2 * max(abs(viewPort.transformToInitial({ x: 0, y: 0 }).y - basicCenter.y), abs(viewPort.transformToInitial({ x: 0, y: basicHeight }).y - basicCenter.y))
const height = potentialHeight > basicHeight ? potentialHeight : basicHeight
drawXAxis()
drawYAxis()
drawOriginCoordinate()
drawXCoordinates()
drawYCoordinates()
function drawXAxis() {
const path = new Path2D
path.moveTo(basicCenter.x - width / 2, basicHeight / 2)
path.lineTo(basicCenter.x + width / 2, basicHeight / 2)
ctx.stroke(path)
}
function drawYAxis() {
const path = new Path2D
path.moveTo(basicWidth / 2, basicCenter.y - height / 2)
path.lineTo(basicWidth / 2, basicCenter.y + height / 2)
ctx.stroke(path)
}
function drawOriginCoordinate() {
ctx.fillText(`O`, basicCenter.x + 5, basicCenter.y - 5)
}
function drawXCoordinates() {
for (let i = 1; i <= width / 2 / interval; i++) {
total = i * interval
ctx.fillText(` ${i*every} `, basicCenter.x + total, basicHeight / 2)
}
for (let i = 1; i <= width / 2 / interval; i++) {
total = i * interval
ctx.fillText(` -${i*every} `, basicCenter.x - total, basicHeight / 2)
}
}
function drawYCoordinates() {
for (let i = 1; i <= height / 2 / interval; i++) {
total = i * interval
ctx.fillText(` ${i*every} `, basicWidth / 2, basicCenter.y + total)
}
for (let i = 1; i <= height / 2 / interval; i++) {
total = i * interval
ctx.fillText(` -${i*every} `, basicWidth / 2, basicCenter.y - total)
}
}
}
render()
<canvas id="myCanvas" width="300" height="300" style="border:1px solid #d3d3d3;"></canvas>
进一步完善
我没有触及数字格式,但放大时您会看到一些浮点问题。此外,坐标轴的条形宽度随着我们放大而增大,随着我们缩小而缩小,这会影响文本定位。