From 72282b250b51d1f626ba12a69d575c70416cae8f Mon Sep 17 00:00:00 2001 From: Thibaud Date: Thu, 30 Jan 2025 22:55:15 +0100 Subject: [PATCH] migrate everything to typescript --- .gitignore | 2 + bezier-1.js | 111 ++++++++++++++++++++++------------------------ bezier-1.ts | 74 +++++++++++++++++++++++++++++++ bezier-2.js | 84 ++++++++++++++++------------------- bezier-2.ts | 65 +++++++++++++++++++++++++++ common.js | 51 +++++++++++---------- common.ts | 70 +++++++++++++++++++++++++++++ image.js | 43 +++++++----------- image.ts | 107 ++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 29 ++++++++++++ package.json | 10 +++++ spline.js | 104 ++++++++++++++++++++----------------------- spline.ts | 94 +++++++++++++++++++++++++++++++++++++++ tsconfig.json | 111 ++++++++++++++++++++++++++++++++++++++++++++++ 14 files changed, 746 insertions(+), 209 deletions(-) create mode 100644 .gitignore create mode 100644 bezier-1.ts create mode 100644 bezier-2.ts create mode 100644 common.ts create mode 100644 image.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 spline.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f5f2ba6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +*.js diff --git a/bezier-1.js b/bezier-1.js index f441038..e625457 100644 --- a/bezier-1.js +++ b/bezier-1.js @@ -1,74 +1,67 @@ -import { lerp, drawCircle, drawLine, drawDashedLine, resizeCanvas } from "./common.js" - -let ctx = undefined; +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const common_js_1 = require("./common.js"); let points = []; - -function update(time) { +function update(ctx, time) { ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - const p = (Math.sin(0.001 * time) + 1.0) * 0.5; if (points.length == 2) { const [start, end] = points; - drawCircle(ctx, lerp(start, end, p), 1, 0xFF0000); - } else if (points.length === 3) { - const [a, b, c] = points; - const ab = lerp(a, b, p); - const bc = lerp(b, c, p); - const abc = lerp(ab, bc, p); - drawDashedLine(ctx, ab, bc, 0xFF0000); - drawCircle(ctx, ab, 1, 0xFF0000); - drawCircle(ctx, bc, 1, 0xFF0000); - drawCircle(ctx, abc, 1, 0xFFFFFF); - } else if (points.length === 4) { - const [a, b, c, d] = points; - const ab = lerp(a, b, p); - const bc = lerp(b, c, p); - const cd = lerp(c, d, p); - const abc = lerp(ab, bc, p); - const bcd = lerp(bc, cd, p); - const abcd = lerp(abc, bcd, p); - - drawDashedLine(ctx, ab, bc, 0xFF0000); - drawDashedLine(ctx, bc, cd, 0xFF0000); - drawDashedLine(ctx, abc, bcd, 0xFFFF00); - drawCircle(ctx, ab, 1, 0xFF0000); - drawCircle(ctx, bc, 1, 0xFF0000); - drawCircle(ctx, cd, 1, 0xFF0000); - drawCircle(ctx, abc, 1, 0xFFFF00); - drawCircle(ctx, bcd, 1, 0xFFFF00); - drawCircle(ctx, abcd, 1, 0xFFFFFF); + (0, common_js_1.drawCircle)(ctx, (0, common_js_1.lerp)(start, end, p), 1, 0xFF0000); + } + else if (points.length === 3) { + const [a, b, c] = points; + const ab = (0, common_js_1.lerp)(a, b, p); + const bc = (0, common_js_1.lerp)(b, c, p); + const abc = (0, common_js_1.lerp)(ab, bc, p); + (0, common_js_1.drawDashedLine)(ctx, ab, bc, 0xFF0000); + (0, common_js_1.drawCircle)(ctx, ab, 1, 0xFF0000); + (0, common_js_1.drawCircle)(ctx, bc, 1, 0xFF0000); + (0, common_js_1.drawCircle)(ctx, abc, 1, 0xFFFFFF); + } + else if (points.length === 4) { + const [a, b, c, d] = points; + const ab = (0, common_js_1.lerp)(a, b, p); + const bc = (0, common_js_1.lerp)(b, c, p); + const cd = (0, common_js_1.lerp)(c, d, p); + const abc = (0, common_js_1.lerp)(ab, bc, p); + const bcd = (0, common_js_1.lerp)(bc, cd, p); + const abcd = (0, common_js_1.lerp)(abc, bcd, p); + (0, common_js_1.drawDashedLine)(ctx, ab, bc, 0xFF0000); + (0, common_js_1.drawDashedLine)(ctx, bc, cd, 0xFF0000); + (0, common_js_1.drawDashedLine)(ctx, abc, bcd, 0xFFFF00); + (0, common_js_1.drawCircle)(ctx, ab, 1, 0xFF0000); + (0, common_js_1.drawCircle)(ctx, bc, 1, 0xFF0000); + (0, common_js_1.drawCircle)(ctx, cd, 1, 0xFF0000); + (0, common_js_1.drawCircle)(ctx, abc, 1, 0xFFFF00); + (0, common_js_1.drawCircle)(ctx, bcd, 1, 0xFFFF00); + (0, common_js_1.drawCircle)(ctx, abcd, 1, 0xFFFFFF); } - for (let i = 0; i < points.length - 1; ++i) { const current = points[i]; - const next = points[i+1]; - drawLine(ctx, current, next, 0xFF00FF); + const next = points[i + 1]; + (0, common_js_1.drawLine)(ctx, current, next, 0xFF00FF); } - for (const point of points) { - drawCircle(ctx, point, 1, 0xFF00FF); + (0, common_js_1.drawCircle)(ctx, point, 1, 0xFF00FF); } - - window.requestAnimationFrame(update); + window.requestAnimationFrame(t => update(ctx, t)); } - function init() { - const canvas = document.getElementById("canvas"); - if (canvas.getContext) { - ctx = canvas.getContext("2d"); - resizeCanvas(ctx); // Init canvas to full window size - - canvas.addEventListener("click", (evt) => { - const {clientX, clientY} = evt; - if (points.length < 4) { - points.push({x: clientX, y: clientY}); - } - }); - - window.addEventListener('resize', () => resizeCanvas(ctx)); - window.requestAnimationFrame(update) - } + const canvas = document.getElementById("game"); + if (!canvas) + throw new Error("unable to get canvas HTML element"); + const ctx = canvas.getContext("2d"); + if (!ctx) + throw new Error("unable to get canvas 2D context"); + (0, common_js_1.resizeCanvas)(ctx); // Init canvas to full window size + canvas.addEventListener("click", (evt) => { + const { clientX, clientY } = evt; + if (points.length < 4) { + points.push({ x: clientX, y: clientY }); + } + }); + window.addEventListener('resize', () => (0, common_js_1.resizeCanvas)(ctx)); + window.requestAnimationFrame(t => update(ctx, t)); } - init(); - diff --git a/bezier-1.ts b/bezier-1.ts new file mode 100644 index 0000000..75e26c2 --- /dev/null +++ b/bezier-1.ts @@ -0,0 +1,74 @@ +import { Point, lerp, drawCircle, drawLine, drawDashedLine, resizeCanvas } from "./common.js" + +let points: Point[] = []; + +function update(ctx: CanvasRenderingContext2D, time: number) { + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + + const p = (Math.sin(0.001 * time) + 1.0) * 0.5; + if (points.length == 2) { + const [start, end] = points; + drawCircle(ctx, lerp(start, end, p), 1, 0xFF0000); + } else if (points.length === 3) { + const [a, b, c] = points; + const ab = lerp(a, b, p); + const bc = lerp(b, c, p); + const abc = lerp(ab, bc, p); + drawDashedLine(ctx, ab, bc, 0xFF0000); + drawCircle(ctx, ab, 1, 0xFF0000); + drawCircle(ctx, bc, 1, 0xFF0000); + drawCircle(ctx, abc, 1, 0xFFFFFF); + } else if (points.length === 4) { + const [a, b, c, d] = points; + const ab = lerp(a, b, p); + const bc = lerp(b, c, p); + const cd = lerp(c, d, p); + const abc = lerp(ab, bc, p); + const bcd = lerp(bc, cd, p); + const abcd = lerp(abc, bcd, p); + + drawDashedLine(ctx, ab, bc, 0xFF0000); + drawDashedLine(ctx, bc, cd, 0xFF0000); + drawDashedLine(ctx, abc, bcd, 0xFFFF00); + drawCircle(ctx, ab, 1, 0xFF0000); + drawCircle(ctx, bc, 1, 0xFF0000); + drawCircle(ctx, cd, 1, 0xFF0000); + drawCircle(ctx, abc, 1, 0xFFFF00); + drawCircle(ctx, bcd, 1, 0xFFFF00); + drawCircle(ctx, abcd, 1, 0xFFFFFF); + } + + for (let i = 0; i < points.length - 1; ++i) { + const current = points[i]; + const next = points[i+1]; + drawLine(ctx, current, next, 0xFF00FF); + } + + for (const point of points) { + drawCircle(ctx, point, 1, 0xFF00FF); + } + + window.requestAnimationFrame(t => update(ctx, t)); +} + +function init() { + const canvas = document.getElementById("game") as HTMLCanvasElement | null; + if (!canvas) throw new Error("unable to get canvas HTML element"); + const ctx = canvas.getContext("2d") as CanvasRenderingContext2D | null; + if (!ctx) throw new Error("unable to get canvas 2D context"); + + resizeCanvas(ctx); // Init canvas to full window size + + canvas.addEventListener("click", (evt) => { + const {clientX, clientY} = evt; + if (points.length < 4) { + points.push({x: clientX, y: clientY}); + } + }); + + window.addEventListener('resize', () => resizeCanvas(ctx)); + window.requestAnimationFrame(t => update(ctx, t)); +} + +init(); + diff --git a/bezier-2.js b/bezier-2.js index 20a01e7..8031ae1 100644 --- a/bezier-2.js +++ b/bezier-2.js @@ -1,23 +1,26 @@ -import { resizeCanvas, drawCircle, drawLine, cubicBezier } from "./common.js"; - +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const common_js_1 = require("./common.js"); function drawPoints(ctx, points) { const [a, b, c, d] = points; - drawLine(ctx, a, b, 0xFF00FF); - drawLine(ctx, c, d, 0xFF00FF); + (0, common_js_1.drawLine)(ctx, a, b, 0xFF00FF); + (0, common_js_1.drawLine)(ctx, c, d, 0xFF00FF); for (const p of points) { - drawCircle(ctx, p, 2, 0xFF00FF); + (0, common_js_1.drawCircle)(ctx, p, 2, 0xFF00FF); } } - function drawCurve(ctx, curve) { - for (let i=0; i < curve.length - 1; ++i) { - drawLine(ctx, curve[i], curve[i+1], 0xFFFFFF); + for (let i = 0; i < curve.length - 1; ++i) { + (0, common_js_1.drawLine)(ctx, curve[i], curve[i + 1], 0xFFFFFF); } } - - function init() { - const canvas = document.getElementById("canvas"); + const canvas = document.getElementById("game"); + if (!canvas) + throw new Error("unable to get canvas HTML element"); + const ctx = canvas.getContext("2d"); + if (!ctx) + throw new Error("unable to get canvas 2D context"); let selection = undefined; let points = [ { x: 227, y: 434 }, @@ -25,40 +28,31 @@ function init() { { x: 649, y: 255 }, { x: 765, y: 450 }, ]; - - if (canvas.getContext) { - const ctx = canvas.getContext("2d"); - resizeCanvas(ctx); // Init canvas - drawPoints(ctx, points); - const bezier = cubicBezier(...points); - drawCurve(ctx, bezier); - - canvas.onmousedown = (evt) => { - const { clientX, clientY } = evt; - for (const p of points) { - if (Math.abs(p.x - clientX) < 10 && Math.abs(p.y - clientY) < 10) { - selection = points.indexOf(p); - } + (0, common_js_1.resizeCanvas)(ctx); // Init canvas + drawPoints(ctx, points); + const bezier = (0, common_js_1.cubicBezier)(...points); + drawCurve(ctx, bezier); + canvas.onmousedown = (evt) => { + const { clientX, clientY } = evt; + for (const p of points) { + if (Math.abs(p.x - clientX) < 10 && Math.abs(p.y - clientY) < 10) { + selection = points.indexOf(p); } - }; - - canvas.onmousemove = (evt) => { - if (selection !== undefined) { - points[selection].x = evt.clientX; - points[selection].y = evt.clientY; - // redraw - ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - const bezier = cubicBezier(...points); - drawCurve(ctx, bezier); - drawPoints(ctx, points); - } - }; - - canvas.onmouseup = () => { - selection = undefined; - }; - } + } + }; + canvas.onmousemove = (evt) => { + if (selection !== undefined) { + points[selection].x = evt.clientX; + points[selection].y = evt.clientY; + // redraw + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + const bezier = (0, common_js_1.cubicBezier)(...points); + drawCurve(ctx, bezier); + drawPoints(ctx, points); + } + }; + canvas.onmouseup = () => { + selection = undefined; + }; } - init(); - diff --git a/bezier-2.ts b/bezier-2.ts new file mode 100644 index 0000000..621914e --- /dev/null +++ b/bezier-2.ts @@ -0,0 +1,65 @@ +import { Point, resizeCanvas, drawCircle, drawLine, cubicBezier } from "./common.js"; + +function drawPoints(ctx: CanvasRenderingContext2D, points: Point[]) { + const [a, b, c, d] = points; + drawLine(ctx, a, b, 0xFF00FF); + drawLine(ctx, c, d, 0xFF00FF); + for (const p of points) { + drawCircle(ctx, p, 2, 0xFF00FF); + } +} + +function drawCurve(ctx: CanvasRenderingContext2D, curve: Point[]) { + for (let i=0; i < curve.length - 1; ++i) { + drawLine(ctx, curve[i], curve[i+1], 0xFFFFFF); + } +} + + +function init() { + const canvas = document.getElementById("game") as HTMLCanvasElement | null; + if (!canvas) throw new Error("unable to get canvas HTML element"); + const ctx = canvas.getContext("2d") as CanvasRenderingContext2D | null; + if (!ctx) throw new Error("unable to get canvas 2D context"); + + let selection: number | undefined = undefined; + let points: [Point, Point, Point, Point] = [ + { x: 227, y: 434 }, + { x: 341, y: 234 }, + { x: 649, y: 255 }, + { x: 765, y: 450 }, + ]; + + resizeCanvas(ctx); // Init canvas + drawPoints(ctx, points); + const bezier = cubicBezier(...points); + drawCurve(ctx, bezier); + + canvas.onmousedown = (evt) => { + const { clientX, clientY } = evt; + for (const p of points) { + if (Math.abs(p.x - clientX) < 10 && Math.abs(p.y - clientY) < 10) { + selection = points.indexOf(p); + } + } + }; + + canvas.onmousemove = (evt) => { + if (selection !== undefined) { + points[selection].x = evt.clientX; + points[selection].y = evt.clientY; + // redraw + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + const bezier = cubicBezier(...points); + drawCurve(ctx, bezier); + drawPoints(ctx, points); + } + }; + + canvas.onmouseup = () => { + selection = undefined; + }; +} + +init(); + diff --git a/common.js b/common.js index 5fb801c..4a8cf6a 100644 --- a/common.js +++ b/common.js @@ -1,29 +1,36 @@ -export function lerp(a, b, p) { - return { +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.lerp = lerp; +exports.quadraticBezier = quadraticBezier; +exports.cubicBezier = cubicBezier; +exports.resizeCanvas = resizeCanvas; +exports.drawCircle = drawCircle; +exports.drawLine = drawLine; +exports.drawDashedLine = drawDashedLine; +function lerp(a, b, p) { + return { x: a.x + (b.x - a.x) * p, y: a.y + (b.y - a.y) * p }; } - -export function quadraticBezier(a, b, c, res=0.05) { +function quadraticBezier(a, b, c, res = 0.05) { const eps = 0.001; // to prevent issues with float comparaison (p <= 1) const curve = []; for (let p = 0; p - 1 < eps; p += res) { - const ab = lerp(a, b, p); + const ab = lerp(a, b, p); const bc = lerp(b, c, p); const abc = lerp(ab, bc, p); curve.push(abc); } return curve; } - -export function cubicBezier(a, b, c, d, res=0.05) { +function cubicBezier(a, b, c, d, res = 0.05) { const eps = 0.001; // to prevent issues with float comparaison (p <= 1) const curve = []; for (let p = 0; p - 1 < eps; p += res) { - const ab = lerp(a, b, p); - const bc = lerp(b, c, p); - const cd = lerp(c, d, p); + const ab = lerp(a, b, p); + const bc = lerp(b, c, p); + const cd = lerp(c, d, p); const abc = lerp(ab, bc, p); const bcd = lerp(bc, cd, p); const abcd = lerp(abc, bcd, p); @@ -31,35 +38,31 @@ export function cubicBezier(a, b, c, d, res=0.05) { } return curve; } - -export function resizeCanvas(ctx) { +function resizeCanvas(ctx) { ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - ctx.canvas.width = window.innerWidth; + ctx.canvas.width = window.innerWidth; ctx.canvas.height = window.innerHeight; } - -export function drawCircle(ctx, {x, y}, radius, color) { +function drawCircle(ctx, center, radius, color) { ctx.save(); ctx.beginPath(); - ctx.arc(x, y, radius, 0, 2*Math.PI); - ctx.strokeStyle = `#${color.toString(16)}` + ctx.arc(center.x, center.y, radius, 0, 2 * Math.PI); + ctx.strokeStyle = `#${color.toString(16)}`; ctx.lineWidth = 5; ctx.stroke(); ctx.restore(); } - -export function drawLine(ctx, start, end, color, dashed=false) { +function drawLine(ctx, start, end, color, dashed = false) { ctx.save(); ctx.beginPath(); - if (dashed) ctx.setLineDash([5, 5]); - ctx.strokeStyle = `#${color.toString(16)}` + if (dashed) + ctx.setLineDash([5, 5]); + ctx.strokeStyle = `#${color.toString(16)}`; ctx.moveTo(start.x, start.y); ctx.lineTo(end.x, end.y); ctx.stroke(); ctx.restore(); } - -export function drawDashedLine(ctx, start, end, color) { +function drawDashedLine(ctx, start, end, color) { drawLine(ctx, start, end, color, true); } - diff --git a/common.ts b/common.ts new file mode 100644 index 0000000..78b7c4a --- /dev/null +++ b/common.ts @@ -0,0 +1,70 @@ +export interface Point { + x: number; + y: number; +} + +export function lerp(a: Point, b: Point, p: number) { + return { + x: a.x + (b.x - a.x) * p, + y: a.y + (b.y - a.y) * p + }; +} + +export function quadraticBezier(a: Point, b: Point, c: Point, res=0.05) { + const eps = 0.001; // to prevent issues with float comparaison (p <= 1) + const curve = []; + for (let p = 0; p - 1 < eps; p += res) { + const ab = lerp(a, b, p); + const bc = lerp(b, c, p); + const abc = lerp(ab, bc, p); + curve.push(abc); + } + return curve; +} + +export function cubicBezier(a: Point, b: Point, c: Point, d: Point, res=0.05) { + const eps = 0.001; // to prevent issues with float comparaison (p <= 1) + const curve = []; + for (let p = 0; p - 1 < eps; p += res) { + const ab = lerp(a, b, p); + const bc = lerp(b, c, p); + const cd = lerp(c, d, p); + const abc = lerp(ab, bc, p); + const bcd = lerp(bc, cd, p); + const abcd = lerp(abc, bcd, p); + curve.push(abcd); + } + return curve; +} + +export function resizeCanvas(ctx: CanvasRenderingContext2D) { + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + ctx.canvas.width = window.innerWidth; + ctx.canvas.height = window.innerHeight; +} + +export function drawCircle(ctx: CanvasRenderingContext2D, center: Point, radius: number, color: number) { + ctx.save(); + ctx.beginPath(); + ctx.arc(center.x, center.y, radius, 0, 2*Math.PI); + ctx.strokeStyle = `#${color.toString(16)}` + ctx.lineWidth = 5; + ctx.stroke(); + ctx.restore(); +} + +export function drawLine(ctx: CanvasRenderingContext2D, start: Point, end: Point, color: number, dashed=false) { + ctx.save(); + ctx.beginPath(); + if (dashed) ctx.setLineDash([5, 5]); + ctx.strokeStyle = `#${color.toString(16)}` + ctx.moveTo(start.x, start.y); + ctx.lineTo(end.x, end.y); + ctx.stroke(); + ctx.restore(); +} + +export function drawDashedLine(ctx: CanvasRenderingContext2D, start: Point, end: Point, color: number) { + drawLine(ctx, start, end, color, true); +} + diff --git a/image.js b/image.js index acf1365..ef52e1f 100644 --- a/image.js +++ b/image.js @@ -1,6 +1,10 @@ -const canvas = document.getElementById("canvas"); +"use strict"; +const canvas = document.getElementById("game"); +if (!canvas) + throw new Error("unable to get canvas HTML element"); const ctx = canvas.getContext("2d"); - +if (!ctx) + throw new Error("unable to get canvas 2D context"); const img = new Image(); img.crossOrigin = "anonymous"; img.src = "/rose.png"; @@ -12,31 +16,27 @@ img.onload = () => { */ ctx.drawImage(img, 0, 0); }; - function invert(imgData) { const data = imgData.data; for (let i = 0; i < data.length; i += 4) { //const avg = (data[i] + data[i + 1] + data[i + 2]) / 3; data[i] = 255 - data[i]; - data[i+1] = 255 - data[i+1]; - data[i+2] = 255 - data[i+2]; + data[i + 1] = 255 - data[i + 1]; + data[i + 2] = 255 - data[i + 2]; } } - function greyscale(imgData) { const data = imgData.data; for (let i = 0; i < data.length; i += 4) { const avg = (data[i] + data[i + 1] + data[i + 2]) / 3; data[i] = avg; - data[i+1] = avg; - data[i+2] = avg; + data[i + 1] = avg; + data[i + 2] = avg; } } - function clamp(x, min, max) { return Math.min(Math.max(x, min), max); } - function convolve(imageData, kernel) { const width = imageData.width; const height = imageData.height; @@ -44,11 +44,9 @@ function convolve(imageData, kernel) { const out = new ImageData(width, height); const kernelSize = Math.sqrt(kernel.length); // Assuming kernel is square const halfKernel = Math.floor(kernelSize / 2); - for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { let r = 0, g = 0, b = 0; - for (let ky = -halfKernel; ky <= halfKernel; ky++) { for (let kx = -halfKernel; kx <= halfKernel; kx++) { const px = x + kx; @@ -62,44 +60,37 @@ function convolve(imageData, kernel) { } } } - - out.data[(y*width+x)*4] = r; - out.data[(y*width+x)*4 + 1] = g; - out.data[(y*width+x)*4 + 2] = b; - out.data[(y*width+x)*4 + 3] = data[(y*width+x) * 4 + 3]; // preserve original alpha + out.data[(y * width + x) * 4] = r; + out.data[(y * width + x) * 4 + 1] = g; + out.data[(y * width + x) * 4 + 2] = b; + out.data[(y * width + x) * 4 + 3] = data[(y * width + x) * 4 + 3]; // preserve original alpha } } - return out; } - // multiply two 3x3 matrices function matMult(a, b) { const result = new Array(9).fill(0); - for (let j = 0; j < 3; ++j){ + for (let j = 0; j < 3; ++j) { for (let i = 0; i < 3; ++i) { for (let k = 0; k < 3; ++k) { - result[j*3+i] += a[j*3+k] * b[k*3+i]; + result[j * 3 + i] += a[j * 3 + k] * b[k * 3 + i]; } } } return result; } - canvas.onclick = () => { const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.height, canvas.width); //invert(imgData); //greyscale(imgData); //const identityKernel = [0, 0, 0, 0, 1, 0, 0, 0, 0]; - const gaussianKernel = [0.075, 0.124, 0.075, 0.124, 0.204, 0.124, 0.075, 0.124, 0.075] + const gaussianKernel = [0.075, 0.124, 0.075, 0.124, 0.204, 0.124, 0.075, 0.124, 0.075]; const convolved = convolve(imgData, gaussianKernel); ctx.putImageData(convolved, 0, 0); }; - - const matrixA = [1, 2, 3, 4, 5, 6, 7, 8, 9]; const matrixB = [9, 8, 7, 6, 5, 4, 3, 2, 1]; - const result = matMult(matrixA, matrixB); console.log(result); diff --git a/image.ts b/image.ts new file mode 100644 index 0000000..b25a0c0 --- /dev/null +++ b/image.ts @@ -0,0 +1,107 @@ +const canvas = document.getElementById("game") as HTMLCanvasElement | null; +if (!canvas) throw new Error("unable to get canvas HTML element"); +const ctx = canvas.getContext("2d") as CanvasRenderingContext2D | null; +if (!ctx) throw new Error("unable to get canvas 2D context"); + +const img = new Image(); +img.crossOrigin = "anonymous"; +img.src = "/rose.png"; +img.onload = () => { + /*canvas.offscreenCanvas = document.createElement("canvas"); + canvas.offscreenCanvas.height = img.height; + canvas.offscreenCanvas.width = img.width; + canvas.offscreenCanvas.getContext("2d").drawImage(img, 0, 0); + */ + ctx.drawImage(img, 0, 0); +}; + +function invert(imgData: ImageData) { + const data = imgData.data; + for (let i = 0; i < data.length; i += 4) { + //const avg = (data[i] + data[i + 1] + data[i + 2]) / 3; + data[i] = 255 - data[i]; + data[i+1] = 255 - data[i+1]; + data[i+2] = 255 - data[i+2]; + } +} + +function greyscale(imgData: ImageData) { + const data = imgData.data; + for (let i = 0; i < data.length; i += 4) { + const avg = (data[i] + data[i + 1] + data[i + 2]) / 3; + data[i] = avg; + data[i+1] = avg; + data[i+2] = avg; + } +} + +function clamp(x: number, min: number, max: number) { + return Math.min(Math.max(x, min), max); +} + +function convolve(imageData: ImageData, kernel: number[]) { + const width = imageData.width; + const height = imageData.height; + const data = imageData.data; + const out = new ImageData(width, height); + const kernelSize = Math.sqrt(kernel.length); // Assuming kernel is square + const halfKernel = Math.floor(kernelSize / 2); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + let r = 0, g = 0, b = 0; + + for (let ky = -halfKernel; ky <= halfKernel; ky++) { + for (let kx = -halfKernel; kx <= halfKernel; kx++) { + const px = x + kx; + const py = y + ky; + if (px >= 0 && px < width && py >= 0 && py < height) { + const idx = (py * width + px) * 4; + const kernelValue = kernel[(ky + halfKernel) * kernelSize + (kx + halfKernel)]; + r += data[idx] * kernelValue; + g += data[idx + 1] * kernelValue; + b += data[idx + 2] * kernelValue; + } + } + } + + out.data[(y*width+x)*4] = r; + out.data[(y*width+x)*4 + 1] = g; + out.data[(y*width+x)*4 + 2] = b; + out.data[(y*width+x)*4 + 3] = data[(y*width+x) * 4 + 3]; // preserve original alpha + } + } + + return out; +} + +// multiply two 3x3 matrices +function matMult(a: number[], b: number[]) { + const result = new Array(9).fill(0); + for (let j = 0; j < 3; ++j){ + for (let i = 0; i < 3; ++i) { + for (let k = 0; k < 3; ++k) { + result[j*3+i] += a[j*3+k] * b[k*3+i]; + } + } + } + return result; +} + +canvas.onclick = () => { + const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); + ctx.clearRect(0, 0, canvas.height, canvas.width); + //invert(imgData); + //greyscale(imgData); + //const identityKernel = [0, 0, 0, 0, 1, 0, 0, 0, 0]; + const gaussianKernel = [0.075, 0.124, 0.075, 0.124, 0.204, 0.124, 0.075, 0.124, 0.075] + const convolved = convolve(imgData, gaussianKernel); + ctx.putImageData(convolved, 0, 0); +}; + + +const matrixA = [1, 2, 3, 4, 5, 6, 7, 8, 9]; +const matrixB = [9, 8, 7, 6, 5, 4, 3, 2, 1]; + +const result = matMult(matrixA, matrixB); +console.log(result); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a0b00da --- /dev/null +++ b/package-lock.json @@ -0,0 +1,29 @@ +{ + "name": "js-canvas-playground", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "js-canvas-playground", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "typescript": "^5.7.3" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..380135d --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "js-canvas-playground", + "version": "1.0.0", + "description": "", + "author": "", + "license": "ISC", + "devDependencies": { + "typescript": "^5.7.3" + } +} diff --git a/spline.js b/spline.js index 5e4b51c..2914c11 100644 --- a/spline.js +++ b/spline.js @@ -1,39 +1,43 @@ -import { resizeCanvas, drawCircle, drawLine, cubicBezier, quadraticBezier } from "./common.js" - +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const common_js_1 = require("./common.js"); function drawPoints(ctx, points) { for (const p of points) { - drawCircle(ctx, p, 2, 0xFF00FF); + (0, common_js_1.drawCircle)(ctx, p, 2, 0xFF00FF); } } - - function drawCurve(ctx, curve) { - for (let i=0; i < curve.length - 1; ++i) { - drawLine(ctx, curve[i], curve[i+1], 0xFFFFFF); + for (let i = 0; i < curve.length - 1; ++i) { + (0, common_js_1.drawLine)(ctx, curve[i], curve[i + 1], 0xFFFFFF); } } - function draw(ctx, points) { let start = 0; while (true) { const sl = points.slice(start, start + 4); if (sl.length === 4) { - const bezier = cubicBezier(...sl); + const bezier = (0, common_js_1.cubicBezier)(...sl); drawCurve(ctx, bezier); start += 3; - } else if (sl.length === 3) { - const bezier = quadraticBezier(...sl); + } + else if (sl.length === 3) { + const bezier = (0, common_js_1.quadraticBezier)(...sl); drawCurve(ctx, bezier); start += 2; - } else { + } + else { break; } } drawPoints(ctx, points); } - function init() { - const canvas = document.getElementById("canvas"); + const canvas = document.getElementById("game"); + if (!canvas) + throw new Error("unable to get canvas HTML element"); + const ctx = canvas.getContext("2d"); + if (!ctx) + throw new Error("unable to get canvas 2D context"); let selection = undefined; let points = [ { x: 227, y: 434 }, @@ -44,50 +48,40 @@ function init() { { x: 850, y: 450 }, { x: 900, y: 550 }, ]; - - if (canvas.getContext) { - const ctx = canvas.getContext("2d"); - resizeCanvas(ctx); // Init canvas + (0, common_js_1.resizeCanvas)(ctx); // Init canvas + draw(ctx, points); + canvas.oncontextmenu = (evt) => { + evt.preventDefault(); + points = []; + // redraw + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); draw(ctx, points); - - canvas.oncontextmenu = (evt) => { - evt.preventDefault(); - points = []; + }; + canvas.onmousedown = (evt) => { + const { clientX, clientY } = evt; + for (const p of points) { + if (Math.abs(p.x - clientX) < 10 && Math.abs(p.y - clientY) < 10) { + selection = points.indexOf(p); + } + } + if (selection === undefined) { + points.push({ x: clientX, y: clientY }); // redraw ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); draw(ctx, points); - }; - canvas.onmousedown = (evt) => { - const { clientX, clientY } = evt; - for (const p of points) { - if (Math.abs(p.x - clientX) < 10 && Math.abs(p.y - clientY) < 10) { - selection = points.indexOf(p); - } - } - if (selection === undefined) { - points.push({ x: clientX, y: clientY }); - // redraw - ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - draw(ctx, points); - - } - }; - - canvas.onmousemove = (evt) => { - if (selection !== undefined) { - points[selection].x = evt.clientX; - points[selection].y = evt.clientY; - // redraw - ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - draw(ctx, points); - } - }; - - canvas.onmouseup = () => { - selection = undefined; - }; - - } + } + }; + canvas.onmousemove = (evt) => { + if (selection !== undefined) { + points[selection].x = evt.clientX; + points[selection].y = evt.clientY; + // redraw + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + draw(ctx, points); + } + }; + canvas.onmouseup = () => { + selection = undefined; + }; } - init(); diff --git a/spline.ts b/spline.ts new file mode 100644 index 0000000..5502418 --- /dev/null +++ b/spline.ts @@ -0,0 +1,94 @@ +import { Point, resizeCanvas, drawCircle, drawLine, cubicBezier, quadraticBezier } from "./common.js" + +function drawPoints(ctx: CanvasRenderingContext2D, points: Point[]) { + for (const p of points) { + drawCircle(ctx, p, 2, 0xFF00FF); + } +} + + +function drawCurve(ctx: CanvasRenderingContext2D, curve: Point[]) { + for (let i=0; i < curve.length - 1; ++i) { + drawLine(ctx, curve[i], curve[i+1], 0xFFFFFF); + } +} + +function draw(ctx: CanvasRenderingContext2D, points: Point[]) { + let start = 0; + while (true) { + const sl = points.slice(start, start + 4); + if (sl.length === 4) { + const bezier = cubicBezier(...(sl as [Point, Point, Point, Point])); + drawCurve(ctx, bezier); + start += 3; + } else if (sl.length === 3) { + const bezier = quadraticBezier(...(sl as [Point, Point, Point])); + drawCurve(ctx, bezier); + start += 2; + } else { + break; + } + } + drawPoints(ctx, points); +} + +function init() { + const canvas = document.getElementById("game") as HTMLCanvasElement | null; + if (!canvas) throw new Error("unable to get canvas HTML element"); + + const ctx = canvas.getContext("2d") as CanvasRenderingContext2D | null; + if (!ctx) throw new Error("unable to get canvas 2D context"); + + let selection: number | undefined = undefined; + let points = [ + { x: 227, y: 434 }, + { x: 341, y: 234 }, + { x: 649, y: 255 }, + { x: 765, y: 450 }, + { x: 800, y: 500 }, + { x: 850, y: 450 }, + { x: 900, y: 550 }, + ]; + + resizeCanvas(ctx); // Init canvas + draw(ctx, points); + + canvas.oncontextmenu = (evt) => { + evt.preventDefault(); + points = []; + // redraw + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + draw(ctx, points); + }; + canvas.onmousedown = (evt) => { + const { clientX, clientY } = evt; + for (const p of points) { + if (Math.abs(p.x - clientX) < 10 && Math.abs(p.y - clientY) < 10) { + selection = points.indexOf(p); + } + } + if (selection === undefined) { + points.push({ x: clientX, y: clientY }); + // redraw + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + draw(ctx, points); + + } + }; + + canvas.onmousemove = (evt) => { + if (selection !== undefined) { + points[selection].x = evt.clientX; + points[selection].y = evt.clientY; + // redraw + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + draw(ctx, points); + } + }; + + canvas.onmouseup = () => { + selection = undefined; + }; +} + +init(); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c9c555d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,111 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +}