witch@&*weaves

30daysJS-07 - HTML5 Canvas

路径生命周期必须和一次绘画行为对齐,而不是和mousemove对齐。

没学过canvas,所以这次跟着教程写代码。

问题

先写了这样的

<script>
       
const canvas = document.querySelector("#draw");
const ctx = canvas.getContext("2d");

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.strokeStyle = "#aec600";
ctx.lineJoin = "round";
ctx.lineCap = 'round';

let isDrawing = false;
let lastX = 0;
let lastY = 0;

function draw(e) {
  if (!isDrawing) return;
    console.log(e);
  ctx.beginPath();
  ctx.moveTo(lastX, lastY);
  ctx.lineTo(e.offsetX, e.offsetY);
  ctx.stroke()
  [lastX, lastY] = [e.offsetX, e.offsetY];
}


canvas.addEventListener("mousedown", (e) => {
  isDrawing = true
  [lastX, lastY] = [e.offsetX, e.offsetY];
});

canvas.addEventListener("mousemove", draw);
      
canvas.addEventListener("mouseup", () => isDrawing = false);
canvas.addEventListener("mouseout", () => isDrawing = false);
console.log('Canvas size:', canvas.width, canvas.height);
console.log('Canvas CSS size:', canvas.clientWidth, canvas.clientHeight);
console.log('Canvas offset:', canvas.offsetLeft, canvas.offsetTop);
</script>

结果:

![[Pasted image 20260126114524.png]]

我仔细和老师的代码对比了好多遍,怎么看都是一模一样啊,哪里不对了?最后发现原来是我有两个地方没有加分号😓(是的我自己写就习惯不加分号)难怪使用prettier优化后总是把 ctx.lineTo(e.offsetX, e.offsetY)ctx.stroke()黏在一起,我还以为是插件出错······

最后去查了自动插入分号([[ASI]], Automatic Semicolon Insertion)机制,了解了下js里面什么时候必须加分号,什么时候不能换行。

JavaScript 语法:到底要不要写分号?一文吃透 ASI 与坑点清单

什么时候必须加分号?

以下列符号开头的

() // 可能会与上一行形成IIFE调用
[] // 可能会被当作上行表达式的下标/逗号表达式
/  // 可能会被当成除号
`  // 可能与上一行函数/变量粘连

什么时候不能换行?

//后置自增/自减:
i /* no LT here */ ++ 
i /* no LT here */ --

//return、throw、yield、await、async 后面紧跟的实体:
return /* no LT here */ value
throw  /* no LT here */ new Error()
yield  /* no LT here */ i++
async  /* no LT here */ function f() {}
const f = async /* no LT here */ x => x * x

//箭头函数箭头前:
const f = x /* no LT here */ => x * x

//带标签的 break/continue 与标签名之间:
break   /* no LT here */ outer
continue/* no LT here */ outer

答案

const canvas = document.querySelector("canvas")!;
const ctx = canvas?.getContext("2d")!;

let isDrawing = false;
let lineStart = 0;
let lineEnd = 0;
ctx.strokeStyle = "orange";
let hue = 0;
let direction = true;
ctx.lineWidth = 100;

function draw(e: MouseEvent) {
  if (!isDrawing) return;
  let x = position(canvas, e).x;
  let y = position(canvas, e).y;
  ctx.strokeStyle = `hsl(${hue} 50% 70%)`;

  ctx.beginPath();
  ctx.moveTo(lineStart, lineEnd);
  ctx.lineTo(x, y);

  ctx.stroke();
  [lineStart, lineEnd] = [x, y];
  hue++;
  if (hue > 360) {
    hue = 0;
  }
  if (ctx.lineWidth > 100 || ctx.lineWidth <= 1) {
    direction = !direction;
  }
  if (direction) {
    ctx.lineWidth--;
  } else if (!direction) {
    ctx.lineWidth++;
  }
  console.log(hue);
}

function position(canva: HTMLCanvasElement, e: MouseEvent) {
  const pos = canva.getBoundingClientRect();
  return {
    x: e.clientX - pos.left,
    y: e.clientY - pos.top,
  };
}

canvas.addEventListener("mousemove", draw);
canvas.addEventListener("mousedown", (e: MouseEvent) => {
  isDrawing = true;
  ctx.beginPath();

  [lineStart, lineEnd] = [position(canvas, e).x, position(canvas, e).y];
});
canvas.addEventListener("mouseup", () => {
  isDrawing = false;
});

试着用新学的ts写了一下.

思路:

  1. 功能:①画线功能,②颜色宽度变化功能;
  2. 方法:①状态机管理,isDrawing布尔值控制绘制状态,②鼠标事件(mousedown, mousemove, mouseup)驱动。

其他都很顺利,到了宽度变化部分卡住了,仔细研究了下老师代码发现实在巧妙。

let direction = true

if(ctx.lineWidth > 100 || ctx.lineWidth < 1){
	direction = !direction
}

if(direction){
	ctx.lineWidth++
} else if (!direction){
	ctx.lineWidth--
}

有两个条件,一个是ctx.lineWidth > 100,一个是ctx.lineWidth < 1,只要满足其一就使direction = !direction,不论此刻的direction是true还是false,总之只需要它反转。

接下来就是根据direction是T还是F进行数值增减,非常清晰明了。

借用ai的说法,这是双向边界控制:

上限检查:ctx.lineWidth > 100 → 超过最大值就反向

下限检查:ctx.lineWidth <= 1 → 低于最小值就反向

这样就创建了一个自动在1-100之间来回振荡的线宽变化。

发散一下,这个可以用在颜色明暗循环,图像大小变化,透明度闪烁上,真聪明啊。

结果:

ye3C0X.png

#code