diff --git a/images/v0.6.0.gif b/images/v0.6.0.gif new file mode 100644 index 0000000..0670013 Binary files /dev/null and b/images/v0.6.0.gif differ diff --git a/package.json b/package.json index 45ac40f..1e5e1a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mizu-ts", - "version": "0.5.0", + "version": "0.6.0", "description": "Mizu-ts is a joke script that simulates water(H2o) generation in TypeScript.", "type": "module", "scripts": { diff --git a/src/atoms/O.ts b/src/atoms/O.ts new file mode 100644 index 0000000..9e32e55 --- /dev/null +++ b/src/atoms/O.ts @@ -0,0 +1,86 @@ +import type { Coordinate } from './Coordinate'; + +export class O { + public x = 0; + public y = 0; + public w = 0; + public h = 0; + public r = 0; + public color = ''; + + private name = 'O'; + private vx = 0; + private vy = 0; + + constructor( + private sw: number, + private sh: number, + ) {} + + public initializeDrawingProperties(coord: Coordinate): void { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('Failed to get canvas 2D context'); + } + const fontSize = 24 * this.getScale(); + ctx.font = `${fontSize}px sans-serif`; + const txtSize = ctx.measureText(this.getName()).width; + + this.x = coord.x; + this.y = coord.y; + this.w = txtSize; + this.h = txtSize; + this.r = txtSize / 2; + this.color = this.getColor(); + } + + public getName(): string { + return this.name; + } + + public getColor(): string { + return `#${Math.random().toString(16).slice(-6)}`; + } + + public getScale(): number { + return this.sw < 768 ? 1.0 : 1.2; + } + + public updatePosition(): void { + const randomAngle = 2 * Math.PI * Math.random(); + const speedFactor = 0.075; + + this.vx += speedFactor * Math.cos(randomAngle); + this.vy += speedFactor * Math.sin(randomAngle); + + const maxSpeed = 1.05; + const currentSpeed = Math.sqrt(this.vx ** 2 + this.vy ** 2); + if (currentSpeed > maxSpeed) { + this.vx = (this.vx / currentSpeed) * maxSpeed; + this.vy = (this.vy / currentSpeed) * maxSpeed; + } + + this.x += this.vx; + this.y += this.vy; + + if (this.x > this.sw + this.w / 2) this.x = -(this.w / 2); + if (this.x + this.w < 0) this.x = this.sw + this.w / 2; + if (this.y > this.sh + this.h / 2) this.y = -(this.h / 2); + if (this.y + this.h < 0) this.y = this.sh + this.h / 2; + } + + public render(ctx: CanvasRenderingContext2D): void { + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + const fontSize = 24 * this.getScale(); + ctx.font = `${fontSize}px sans-serif`; + ctx.fillStyle = this.color; + ctx.shadowColor = '#888'; + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 1; + ctx.shadowBlur = 1; + + ctx.fillText(this.getName(), this.x, this.y); + } +} diff --git a/src/main.ts b/src/main.ts index 3ee719a..af245af 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,7 +3,7 @@ import { MizuSimulator } from './simulator/MizuSimulator'; document.addEventListener('DOMContentLoaded', () => { const simulator = new MizuSimulator(); const scale = simulator.getScale(); - simulator.init(30 * scale); + simulator.init(30 * scale, 20 * scale); const loop = () => { simulator.renderFrame(); diff --git a/src/simulator/MizuSimulator.ts b/src/simulator/MizuSimulator.ts index 1adff97..6a65860 100644 --- a/src/simulator/MizuSimulator.ts +++ b/src/simulator/MizuSimulator.ts @@ -1,8 +1,10 @@ import { Coordinate } from '../atoms/Coordinate'; import { H } from '../atoms/H'; +import { O } from '../atoms/O'; export class MizuSimulator { private h: H[] = []; + private o: O[] = []; private cw: number; private ch: number; private ctx: CanvasRenderingContext2D; @@ -37,9 +39,12 @@ export class MizuSimulator { this.bufferCtx = bufferCtx; } - public init(hCount: number): void { + public init(hCount: number, oCount: number): void { for (let i = 0; i < hCount; i++) { - this.h.push(this.createAtom()); + this.h.push(this.createHAtom()); + } + for (let i = 0; i < oCount; i++) { + this.o.push(this.createOAtom()); } } @@ -48,6 +53,7 @@ export class MizuSimulator { this.bufferCtx.fillRect(0, 0, this.cw, this.ch); this.renderH(this.h); + this.renderO(this.o); this.ctx.drawImage(this.bufferCanvas, 0, 0); } @@ -62,7 +68,7 @@ export class MizuSimulator { return 1.5; } - private createAtom(): H { + private createHAtom(): H { const x = this.cw * Math.random(); const y = this.ch * Math.random(); const h = new H(this.cw, this.ch); @@ -70,6 +76,14 @@ export class MizuSimulator { return h; } + private createOAtom(): O { + const x = this.cw * Math.random(); + const y = this.ch * Math.random(); + const o = new O(this.cw, this.ch); + o.initializeDrawingProperties(new Coordinate(x, y)); + return o; + } + private renderH(atoms: H[]): void { for (let i = 0; i < atoms.length; i++) { const _h = atoms[i]; @@ -90,10 +104,18 @@ export class MizuSimulator { _h.mergeAndRender(this.bufferCtx, new Coordinate(_h.x, _h.y)); // 衝突した相手は新しい H に差し替え - atoms[j] = this.createAtom(); + atoms[j] = this.createHAtom(); break; } } } + + private renderO(atoms: O[]): void { + for (let i = 0; i < atoms.length; i++) { + const _o = atoms[i]; + _o.updatePosition(); + _o.render(this.bufferCtx); + } + } } diff --git a/tests/atoms/O.test.ts b/tests/atoms/O.test.ts new file mode 100644 index 0000000..81a4ade --- /dev/null +++ b/tests/atoms/O.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; +import { Coordinate } from '../../src/atoms/Coordinate'; +import { O } from '../../src/atoms/O'; + +describe('O クラスのテスト', () => { + const sw = 800; + const sh = 600; + + it('プロパティが初期化されること', () => { + const o = new O(sw, sh); + o.initializeDrawingProperties(new Coordinate(200, 300)); + + expect(o.x).toBe(200); + expect(o.y).toBe(300); + expect(o.getName()).toBe('O'); + }); + + it('位置がランダムに更新され、範囲内に収まること', () => { + const o = new O(sw, sh); + o.initializeDrawingProperties(new Coordinate(200, 300)); + + for (let i = 0; i < 100; i++) { + o.updatePosition(); + + expect(o.x).toBeGreaterThanOrEqual(0); + expect(o.x).toBeLessThanOrEqual(sw); + expect(o.y).toBeGreaterThanOrEqual(0); + expect(o.y).toBeLessThanOrEqual(sh); + } + }); + + it('描画処理がエラーなく実行されること', () => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) throw new Error('Canvas context not available'); + + const o = new O(sw, sh); + o.initializeDrawingProperties(new Coordinate(300, 400)); + + expect(() => o.render(ctx)).not.toThrow(); + }); +}); diff --git a/tests/simulator/MizuSimulator.test.ts b/tests/simulator/MizuSimulator.test.ts index 8549bdd..bd20111 100644 --- a/tests/simulator/MizuSimulator.test.ts +++ b/tests/simulator/MizuSimulator.test.ts @@ -9,40 +9,53 @@ describe('MizuSimulator クラスのテスト', () => { simulator = new MizuSimulator(); }); - it('初期化時に指定された数の H が生成されること', () => { - simulator.init(10); + it('初期化時に指定された数の H と O が生成されること', () => { + simulator.init(10, 5); expect(simulator['h'].length).toBe(10); + expect(simulator['o'].length).toBe(5); }); - it('H がランダムな座標で初期化されること', () => { - simulator.init(1); + it('H と O がランダムな座標で初期化されること', () => { + simulator.init(1, 1); + const h = simulator['h'][0]; expect(h.x).toBeGreaterThanOrEqual(0); expect(h.x).toBeLessThanOrEqual(simulator['cw']); expect(h.y).toBeGreaterThanOrEqual(0); expect(h.y).toBeLessThanOrEqual(simulator['ch']); + + const o = simulator['o'][0]; + expect(o.x).toBeGreaterThanOrEqual(0); + expect(o.x).toBeLessThanOrEqual(simulator['cw']); + expect(o.y).toBeGreaterThanOrEqual(0); + expect(o.y).toBeLessThanOrEqual(simulator['ch']); }); it('フレームの描画がエラーなく実行されること', () => { - simulator.init(5); + simulator.init(5, 5); expect(() => simulator.renderFrame()).not.toThrow(); }); - it('フレーム描画時に H が正しく移動すること', () => { - simulator.init(1); - const initialX = simulator['h'][0].x; - const initialY = simulator['h'][0].y; + it('フレーム描画時に H と O が正しく移動すること', () => { + simulator.init(1, 1); + + const initialHX = simulator['h'][0].x; + const initialHY = simulator['h'][0].y; + const initialOX = simulator['o'][0].x; + const initialOY = simulator['o'][0].y; simulator.renderFrame(); - expect(simulator['h'][0].x).not.toBe(initialX); - expect(simulator['h'][0].y).not.toBe(initialY); + expect(simulator['h'][0].x).not.toBe(initialHX); + expect(simulator['h'][0].y).not.toBe(initialHY); + expect(simulator['o'][0].x).not.toBe(initialOX); + expect(simulator['o'][0].y).not.toBe(initialOY); }); - it('H同士の衝突時に結合が正しく行われること', () => { - simulator.init(2); + it('H 同士が衝突時に正しく結合されること', () => { + simulator.init(2, 0); simulator['h'][0].initializeDrawingProperties(new Coordinate(100, 100)); - simulator['h'][1].initializeDrawingProperties(new Coordinate(105, 105)); // 衝突する位置 + simulator['h'][1].initializeDrawingProperties(new Coordinate(105, 105)); simulator.renderFrame();