Compare commits

...

8 Commits

Author SHA1 Message Date
cha0s
5fe346372b feat: sprite alpha and rotation 2024-07-25 10:48:22 -05:00
cha0s
0439c52076 feat: positional usage 2024-07-25 10:47:52 -05:00
cha0s
557c8285ba feat: vertice transformation and collider aabb updates 2024-07-25 10:45:23 -05:00
cha0s
b9985a573d feat: new 2024-07-25 09:00:54 -05:00
cha0s
ce802a8499 fix: entities iterator 2024-07-25 06:44:02 -05:00
cha0s
443797017f feat: delete 2024-07-25 06:29:57 -05:00
cha0s
62eaa16b28 fix: directional control 2024-07-25 02:07:36 -05:00
cha0s
14effd2455 fun: stuff in the chest 2024-07-25 00:37:59 -05:00
19 changed files with 456 additions and 111 deletions

View File

@ -35,6 +35,8 @@ export default function evaluate(node, {scope} = {}) {
return evaluators.binary(node, {evaluate, scope}); return evaluators.binary(node, {evaluate, scope});
case 'MemberExpression': case 'MemberExpression':
return evaluators.member(node, {evaluate, scope}); return evaluators.member(node, {evaluate, scope});
case 'NewExpression':
return evaluators.new(node, {evaluate, scope});
case 'ObjectExpression': case 'ObjectExpression':
return evaluators.object(node, {evaluate, scope}); return evaluators.object(node, {evaluate, scope});
case 'UnaryExpression': case 'UnaryExpression':

View File

@ -0,0 +1,21 @@
export default function(node, {evaluate, scope}) {
let asyncArgs = false;
const args = [];
for (let i = 0; i < node.arguments.length; i++) {
const arg = node.arguments[i];
const {async, value} = evaluate(arg, {scope});
asyncArgs ||= async;
args.push(value);
}
const {callee} = node;
const {async, value} = evaluate(callee, {scope});
if (asyncArgs || async) {
return {
async: true,
value: Promise
.all([value, Promise.all(args)])
.then(([callee, args]) => new callee(...args)),
};
}
return {value: new value(...args)};
}

View File

@ -0,0 +1,48 @@
import {expect, test} from 'vitest';
import evaluate from '@/astride/evaluate.js';
import expression from '@/astride/test/expression.js';
const scopeTest = test.extend({
scope: async ({}, use) => {
await use({
S: {O: {}},
get(k) { return this.S[k]; },
set(k, v) { return this.S[k] = v; }
});
},
});
class C {
foo = 'bar';
constructor(a, b) {
this.a = a;
this.b = b;
}
}
scopeTest('creates instances', async ({scope}) => {
scope.set('C', C);
const evaluated = evaluate(await expression('new C(1, 2)'), {scope});
expect(evaluated.value)
.to.deep.include({
a: 1,
b: 2,
foo: 'bar',
});
});
scopeTest('creates instances with async dependencies', async ({scope}) => {
scope.set('C', C);
scope.set('a', Promise.resolve(1));
scope.set('b', Promise.resolve(2));
const evaluated = evaluate(await expression('new C(await a, await b)'), {scope});
expect(evaluated.async)
.to.equal(true);
expect(await evaluated.value)
.to.deep.include({
a: 1,
b: 2,
foo: 'bar',
});
});

View File

@ -150,7 +150,7 @@ export default class Sandbox {
case 'ObjectExpression': case 'ObjectExpression':
case 'Identifier': case 'Identifier':
case 'MemberExpression': case 'MemberExpression':
case 'UnaryExpression': case 'NewExpression':
case 'UpdateExpression': { case 'UpdateExpression': {
result = this.evaluateToResult(node); result = this.evaluateToResult(node);
if (result.yield) { if (result.yield) {
@ -523,6 +523,31 @@ export default class Sandbox {
}; };
break; break;
} }
case 'UnaryExpression': {
if ('delete' === node.operator) {
let property;
if (node.argument.computed) {
property = this.executeSync(node.argument.property, depth + 1);
if (property.yield) {
return property;
}
}
else {
property = {value: node.argument.property.name, yield: YIELD_NONE};
}
const scope = this.scopes.get(node);
const object = scope.get(node.argument.object.name, undefined);
delete object[property.value];
result = {value: true, yield: YIELD_NONE};
}
else {
result = this.evaluateToResult(node);
if (result.yield) {
return result;
}
}
break;
}
case 'VariableDeclaration': { case 'VariableDeclaration': {
let skipping = isReplaying; let skipping = isReplaying;
for (const child of node.declarations) { for (const child of node.declarations) {

View File

@ -121,6 +121,69 @@ test('runs arbitrary number of ops', async () => {
.to.equal(150); .to.equal(150);
}); });
test('instantiates', async () => {
const sandbox = new Sandbox(
await parse(`
const x = new C(1, 2);
const y = new C(await a, await b);
`),
{
a: Promise.resolve(1),
b: Promise.resolve(2),
C: class {
foo = 'bar';
constructor(a, b) {
this.a = a;
this.b = b;
}
},
},
);
await finish(sandbox);
expect(sandbox.context.x)
.to.deep.include({
a: 1,
b: 2,
foo: 'bar',
});
expect(sandbox.context.y)
.to.deep.include({
a: 1,
b: 2,
foo: 'bar',
});
});
test('deletes', async () => {
const sandbox = new Sandbox(
await parse(`
delete foo.one;
delete foo['two'];
const x = 'three';
delete foo[x];
const y = 'four';
delete foo[await y];
`),
{
foo: {
one: 1,
two: 2,
three: 3,
four: 4,
},
},
);
await finish(sandbox);
expect(sandbox.context.foo.one)
.to.be.undefined;
expect(sandbox.context.foo.two)
.to.be.undefined;
expect(sandbox.context.foo.three)
.to.be.undefined;
expect(sandbox.context.foo.four)
.to.be.undefined;
});
test('evaluates conditional branches', async () => { test('evaluates conditional branches', async () => {
const sandbox = new Sandbox( const sandbox = new Sandbox(
await parse(` await parse(`

View File

@ -16,6 +16,7 @@ export const TRAVERSAL_PATH = {
Identifier: [], Identifier: [],
IfStatement: ['alternate', 'consequent', 'test'], IfStatement: ['alternate', 'consequent', 'test'],
MemberExpression: ['object', 'property'], MemberExpression: ['object', 'property'],
NewExpression: ['arguments', 'callee'],
Literal: [], Literal: [],
LogicalExpression: ['left', 'right'], LogicalExpression: ['left', 'right'],
ObjectExpression: ['properties'], ObjectExpression: ['properties'],

View File

@ -1,5 +1,5 @@
import Component from '@/ecs/component.js'; import Component from '@/ecs/component.js';
import {distance, intersects} from '@/util/math.js'; import {distance, intersects, transform} from '@/util/math.js';
import vector2d from './helpers/vector-2d'; import vector2d from './helpers/vector-2d';
@ -7,6 +7,8 @@ export default class Collider extends Component {
instanceFromSchema() { instanceFromSchema() {
const {ecs} = this; const {ecs} = this;
return class ColliderInstance extends super.instanceFromSchema() { return class ColliderInstance extends super.instanceFromSchema() {
$$aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity};
$$aabbs = [];
collidingWith = {}; collidingWith = {};
get aabb() { get aabb() {
const {Position: {x: px, y: py}} = ecs.get(this.entity); const {Position: {x: px, y: py}} = ecs.get(this.entity);
@ -76,34 +78,36 @@ export default class Collider extends Component {
} }
return false; return false;
} }
updateAabbs() {
this.$$aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity};
this.$$aabbs = [];
const {bodies} = this;
const {Direction: {direction = 0} = {}} = ecs.get(this.entity);
for (const body of bodies) {
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
for (const point of transform(body.points, {rotation: direction})) {
const {x, y} = point;
if (x < x0) x0 = x;
if (x < this.$$aabb.x0) this.$$aabb.x0 = x;
if (x > x1) x1 = x;
if (x > this.$$aabb.x1) this.$$aabb.x1 = x;
if (y < y0) y0 = y;
if (y < this.$$aabb.y0) this.$$aabb.y0 = y;
if (y > y1) y1 = y;
if (y > this.$$aabb.y1) this.$$aabb.y1 = y;
}
this.$$aabbs.push({
x0: x0 > x1 ? x1 : x0,
x1: x0 > x1 ? x0 : x1,
y0: y0 > y1 ? y1 : y0,
y1: y0 > y1 ? y0 : y1,
});
}
}
} }
} }
async load(instance) { async load(instance) {
instance.$$aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity}; instance.updateAabbs();
instance.$$aabbs = [];
const {bodies} = instance;
for (const body of bodies) {
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
for (const point of body.points) {
const x = point.x;
const y = point.y;
if (x < x0) x0 = x;
if (x < instance.$$aabb.x0) instance.$$aabb.x0 = x;
if (x > x1) x1 = x;
if (x > instance.$$aabb.x1) instance.$$aabb.x1 = x;
if (y < y0) y0 = y;
if (y < instance.$$aabb.y0) instance.$$aabb.y0 = y;
if (y > y1) y1 = y;
if (y > instance.$$aabb.y1) instance.$$aabb.y1 = y;
}
instance.$$aabbs.push({
x0: x0 > x1 ? x1 : x0,
x1: x0 > x1 ? x0 : x1,
y0: y0 > y1 ? y1 : y0,
y1: y0 > y1 ? y0 : y1,
});
}
// heavy handed... // heavy handed...
if ('undefined' !== typeof window) { if ('undefined' !== typeof window) {
return; return;

View File

@ -11,6 +11,9 @@ export default class Sprite extends Component {
return super.animation; return super.animation;
} }
set animation(animation) { set animation(animation) {
if (this.$$animation === animation) {
return;
}
super.animation = animation; super.animation = animation;
// eslint-disable-next-line no-self-assign // eslint-disable-next-line no-self-assign
this.frame = this.frame; this.frame = this.frame;
@ -31,6 +34,27 @@ export default class Sprite extends Component {
} }
return this.$$sourceJson.animations[this.animation].length; return this.$$sourceJson.animations[this.animation].length;
} }
hasAnimation(animation) {
if (
!this.$$sourceJson.animations
|| !(animation in this.$$sourceJson.animations)
) {
return false;
}
return true;
}
get rotates() {
if (!this.$$sourceJson.meta) {
return false;
}
return 'rotation' in this.$$sourceJson.meta;
}
get rotation() {
if (!this.$$sourceJson.meta) {
return 0;
}
return this.$$sourceJson.meta.rotation;
}
get scale() { get scale() {
return {x: this.scaleX, y: this.scaleY}; return {x: this.scaleX, y: this.scaleY};
} }
@ -51,6 +75,7 @@ export default class Sprite extends Component {
super.markChange(entityId, key, value); super.markChange(entityId, key, value);
} }
static properties = { static properties = {
alpha: {defaultValue: 1, type: 'float32'},
anchorX: {defaultValue: 0.5, type: 'float32'}, anchorX: {defaultValue: 0.5, type: 'float32'},
anchorY: {defaultValue: 0.5, type: 'float32'}, anchorY: {defaultValue: 0.5, type: 'float32'},
animation: {type: 'string'}, animation: {type: 'string'},

View File

@ -8,7 +8,7 @@ export default class Wielder extends Component {
const {Inventory, Wielder} = ecs.get(this.entity); const {Inventory, Wielder} = ecs.get(this.entity);
return Inventory.item(Wielder.activeSlot + 1); return Inventory.item(Wielder.activeSlot + 1);
} }
useActiveItem(state) { useActiveItem([state, where]) {
const entity = ecs.get(this.entity); const entity = ecs.get(this.entity);
const {Ticking} = entity; const {Ticking} = entity;
const activeItem = this.activeItem(); const activeItem = this.activeItem();
@ -19,6 +19,7 @@ export default class Wielder extends Component {
script = script.clone(); script = script.clone();
script.context.ecs = ecs; script.context.ecs = ecs;
script.context.item = activeItem; script.context.item = activeItem;
script.context.where = where;
script.context.wielder = entity; script.context.wielder = entity;
Ticking.add(script.ticker()); Ticking.add(script.ticker());
} }

View File

@ -242,28 +242,17 @@ export default class Ecs {
this.Components[i].destroyMany(destroying[i]); this.Components[i].destroyMany(destroying[i]);
} }
for (const entityId of entityIds) { for (const entityId of entityIds) {
this.$$entities[entityId] = undefined; delete this.$$entities[entityId];
this.diff[entityId] = false; this.diff[entityId] = false;
} }
} }
get entities() { get entities() {
const it = Object.values(this.$$entities).values(); const ids = [];
return { for (const entity of Object.values(this.$$entities)) {
[Symbol.iterator]() { ids.push(entity.id);
return this; }
}, return ids;
next: () => {
let result = it.next();
while (!result.done && !result.value) {
result = it.next();
}
if (result.done) {
return {done: true, value: undefined};
}
return {done: false, value: result.value.id};
},
};
} }
get(entityId) { get(entityId) {

View File

@ -40,7 +40,16 @@ export default class Colliders extends System {
tick() { tick() {
const collisions = new Map(); const collisions = new Map();
for (const entity of this.ecs.changed(['Direction'])) {
if (!entity.Collider) {
continue;
}
entity.Collider.updateAabbs();
}
for (const entity of this.ecs.changed(['Position'])) { for (const entity of this.ecs.changed(['Position'])) {
if (!entity.Collider) {
continue;
}
this.updateHash(entity); this.updateHash(entity);
} }
for (const entity of this.ecs.changed(['Position'])) { for (const entity of this.ecs.changed(['Position'])) {

View File

@ -1,5 +1,5 @@
import {System} from '@/ecs/index.js'; import {System} from '@/ecs/index.js';
import {HALF_PI} from '@/util/math.js'; import {TAU} from '@/util/math.js';
export default class ControlDirection extends System { export default class ControlDirection extends System {
@ -9,18 +9,20 @@ export default class ControlDirection extends System {
if (locked) { if (locked) {
continue; continue;
} }
if (moveUp > 0) { if (
Direction.direction = HALF_PI * 3; 0 === moveRight
} && 0 === moveDown
if (moveDown > 0) { && 0 === moveLeft
Direction.direction = HALF_PI * 1; && 0 === moveUp
} ) {
if (moveLeft > 0) { continue;
Direction.direction = HALF_PI * 2;
}
if (moveRight > 0) {
Direction.direction = HALF_PI * 0;
} }
Direction.direction = (
TAU + Math.atan2(
moveDown - moveUp,
moveRight - moveLeft,
)
) % TAU;
} }
} }

View File

@ -24,16 +24,20 @@ export default class SpriteDirection extends System {
} }
} }
if (Direction) { if (Direction) {
const name = { if (!Sprite.rotates) {
0: 'right', const name = {
1: 'down', 0: 'right',
2: 'left', 1: 'down',
3: 'up', 2: 'left',
}; 3: 'up',
parts.push(name[Direction.quantize(4)]); };
parts.push(name[Direction.quantize(4)]);
}
} }
if (parts.length > 0) { if (parts.length > 0) {
Sprite.animation = parts.join(':'); if (Sprite.hasAnimation(parts.join(':'))) {
Sprite.animation = parts.join(':');
}
} }
} }
} }

View File

@ -11,6 +11,9 @@ function textureFromAsset(asset, animation, frame) {
} }
let texture; let texture;
if (asset.data.animations) { if (asset.data.animations) {
if (!animation) {
return undefined;
}
texture = asset.animations[animation][frame]; texture = asset.animations[animation][frame];
} }
else { else {
@ -23,7 +26,7 @@ export default function Sprite({entity, ...rest}) {
const [mounted, setMounted] = useState(); const [mounted, setMounted] = useState();
const [normals, setNormals] = useState(); const [normals, setNormals] = useState();
const [normalsMounted, setNormalsMounted] = useState(); const [normalsMounted, setNormalsMounted] = useState();
const {anchor, animation, frame, scale, source} = entity.Sprite; const {alpha, anchor, animation, frame, scale, rotates, rotation, source} = entity.Sprite;
const asset = useAsset(source); const asset = useAsset(source);
const normalsAsset = useAsset(normals); const normalsAsset = useAsset(normals);
useEffect(() => { useEffect(() => {
@ -59,8 +62,10 @@ export default function Sprite({entity, ...rest}) {
<> <>
{texture && ( {texture && (
<PixiSprite <PixiSprite
alpha={alpha}
anchor={anchor} anchor={anchor}
ref={setMounted} ref={setMounted}
{...(rotates ? {rotation: entity.Direction.direction + rotation} : {})}
scale={scale} scale={scale}
texture={texture} texture={texture}
x={Math.round(entity.Position.x)} x={Math.round(entity.Position.x)}
@ -70,8 +75,10 @@ export default function Sprite({entity, ...rest}) {
)} )}
{normalsTexture && ( {normalsTexture && (
<PixiSprite <PixiSprite
alpha={alpha}
anchor={anchor} anchor={anchor}
ref={setNormalsMounted} ref={setNormalsMounted}
{...(rotates ? {rotation: entity.Direction.direction + rotation} : {})}
scale={scale} scale={scale}
texture={normalsTexture} texture={normalsTexture}
x={Math.round(entity.Position.x)} x={Math.round(entity.Position.x)}

View File

@ -1,4 +1,4 @@
import {memo, useEffect, useRef, useState} from 'react'; import {memo, useCallback, useEffect, useRef, useState} from 'react';
import {useClient, usePacket} from '@/react/context/client.js'; import {useClient, usePacket} from '@/react/context/client.js';
import {useDebug} from '@/react/context/debug.js'; import {useDebug} from '@/react/context/debug.js';
@ -166,10 +166,6 @@ function Ui({disconnected}) {
} }
break; break;
} }
case ' ': {
actionPayload = {type: 'use', value: KEY_MAP[type]};
break;
}
case 'Tab': { case 'Tab': {
if ('keyDown' === type) { if ('keyDown' === type) {
if (isInventoryOpen) { if (isInventoryOpen) {
@ -390,6 +386,9 @@ function Ui({disconnected}) {
if (!update) { if (!update) {
continue; continue;
} }
if (update.Direction && entity.Collider) {
entity.Collider.updateAabbs();
}
if (update.Sound?.play) { if (update.Sound?.play) {
for (const sound of update.Sound.play) { for (const sound of update.Sound.play) {
(new Audio(sound)).play(); (new Audio(sound)).play();
@ -430,6 +429,30 @@ function Ui({disconnected}) {
document.body.removeEventListener('contextmenu', onContextMenu); document.body.removeEventListener('contextmenu', onContextMenu);
}; };
}, []); }, []);
const computePosition = useCallback(({clientX, clientY}) => {
if (!gameRef.current || !mainEntity) {
return;
}
const {top, left, width} = gameRef.current.getBoundingClientRect();
const master = ecs.get(1);
if (!master) {
return;
}
const {Camera} = ecs.get(mainEntity);
const size = width / RESOLUTION.x;
const camera = {
x: ((Camera.x * scale) - (RESOLUTION.x / 2)),
y: ((Camera.y * scale) - (RESOLUTION.y / 2)),
}
return {
x: (((clientX - left) / size) + camera.x) / scale,
y: (((clientY - top) / size) + camera.y) / scale,
};
}, [
ecs,
mainEntity,
scale,
]);
return ( return (
<div <div
className={styles.ui} className={styles.ui}
@ -455,39 +478,19 @@ function Ui({disconnected}) {
event.preventDefault(); event.preventDefault();
return; return;
} }
const where = computePosition(event);
switch (event.button) { switch (event.button) {
case 0: case 0:
if (devtoolsIsOpen) { if (devtoolsIsOpen) {
if (!gameRef.current || !mainEntity) {
return;
}
const {top, left, width} = gameRef.current.getBoundingClientRect();
const master = ecs.get(1);
if (!master) {
return;
}
const {Camera} = ecs.get(mainEntity);
const size = width / RESOLUTION.x;
const client = {
x: (event.clientX - left) / size,
y: (event.clientY - top) / size,
};
const camera = {
x: ((Camera.x * scale) - (RESOLUTION.x / 2)),
y: ((Camera.y * scale) - (RESOLUTION.y / 2)),
}
devEventsChannel.invoke( devEventsChannel.invoke(
'click', 'click',
{ where,
x: (client.x + camera.x) / scale,
y: (client.y + camera.y) / scale,
},
); );
} }
else { else {
client.send({ client.send({
type: 'Action', type: 'Action',
payload: {type: 'use', value: 1}, payload: {type: 'use', value: [1, where]},
}); });
} }
break; break;
@ -508,11 +511,12 @@ function Ui({disconnected}) {
event.preventDefault(); event.preventDefault();
return; return;
} }
const where = computePosition(event);
switch (event.button) { switch (event.button) {
case 0: case 0:
client.send({ client.send({
type: 'Action', type: 'Action',
payload: {type: 'use', value: 0}, payload: {type: 'use', value: [0, where]},
}); });
break; break;
case 2: case 2:

View File

@ -88,6 +88,26 @@ export default async function createHomestead(id) {
`, `,
}, },
Interlocutor: {}, Interlocutor: {},
Inventory: {
slots: {
2: {
qty: 1,
source: '/assets/watering-can/watering-can.json',
},
3: {
qty: 1,
source: '/assets/tomato-seeds/tomato-seeds.json',
},
4: {
qty: 1,
source: '/assets/hoe/hoe.json',
},
5: {
qty: 1,
source: '/assets/brush/brush.json',
},
},
},
Position: {x: 200, y: 200}, Position: {x: 200, y: 200},
Sprite: { Sprite: {
anchorX: 0.5, anchorX: 0.5,

View File

@ -26,22 +26,6 @@ export default async function createPlayer(id) {
qty: 100, qty: 100,
source: '/assets/potion/potion.json', source: '/assets/potion/potion.json',
}, },
2: {
qty: 1,
source: '/assets/watering-can/watering-can.json',
},
3: {
qty: 1,
source: '/assets/tomato-seeds/tomato-seeds.json',
},
4: {
qty: 1,
source: '/assets/hoe/hoe.json',
},
5: {
qty: 1,
source: '/assets/brush/brush.json',
},
}, },
}, },
Health: {health: 100}, Health: {health: 100},

View File

@ -316,3 +316,52 @@ export function removeCollinear([...vertices]) {
} }
return trimmed; return trimmed;
} }
export function transform(
vertices,
{
rotation = 0,
scale = 1,
translation = {x: 0, y: 0},
origin = {x: 0, y: 0},
},
) {
// nop
if (0 === rotation && 1 === scale && 0 === translation.x && 0 === translation.y) {
return vertices;
}
const transformed = [];
// scale
for (const vertice of vertices) {
if (1 === scale) {
transformed.push({x: vertice.x, y: vertice.y});
continue;
}
transformed.push({
x: origin.x + (vertice.x - origin.x) * scale,
y: origin.y + (vertice.y - origin.y) * scale,
});
}
// rotation
rotation = rotation % TAU;
if (0 !== rotation) {
for (const vertice of transformed) {
let a = rotation + Math.atan2(
vertice.y - origin.y,
vertice.x - origin.x,
);
a = (a >= 0 || a < TAU) ? a : (a % TAU + TAU) % TAU;
const d = distance(vertice, origin);
vertice.x = origin.x + d * Math.cos(a);
vertice.y = origin.y + d * Math.sin(a);
}
}
// translation
if (0 !== translation.x || 0 !== translation.y) {
for (const vertice of transformed) {
vertice.x += translation.x;
vertice.y += translation.y;
}
}
return transformed;
}

87
app/util/math.test.js Normal file
View File

@ -0,0 +1,87 @@
import {expect, test} from 'vitest';
import * as MathUtil from './math.js';
test('transforms vertices', async () => {
const expectCloseTo = (l, r) => {
expect(l.length)
.to.equal(r.length);
for (let i = 0; i < l.length; ++i) {
expect(l[i].x)
.to.be.closeTo(r[i].x, 0.0001);
expect(l[i].y)
.to.be.closeTo(r[i].y, 0.0001);
}
}
const vertices = [
{x: -1, y: -1},
{x: 1, y: -1},
{x: 1, y: 1},
{x: -1, y: 1},
];
expect(MathUtil.transform(vertices, {}))
.to.deep.equal(vertices);
expectCloseTo(
MathUtil.transform(vertices, {scale: 2}),
[
{x: -2, y: -2},
{x: 2, y: -2},
{x: 2, y: 2},
{x: -2, y: 2},
],
);
expectCloseTo(
MathUtil.transform(vertices, {rotation: MathUtil.QUARTER_PI}),
[
{x: 0, y: -Math.sqrt(2)},
{x: Math.sqrt(2), y: 0},
{x: 0, y: Math.sqrt(2)},
{x: -Math.sqrt(2), y: 0},
],
);
expectCloseTo(
MathUtil.transform(vertices, {rotation: MathUtil.QUARTER_PI, scale: 2}),
[
{x: 0, y: -Math.sqrt(2) * 2},
{x: Math.sqrt(2) * 2, y: 0},
{x: 0, y: Math.sqrt(2) * 2},
{x: -Math.sqrt(2) * 2, y: 0},
],
);
expectCloseTo(
MathUtil.transform(vertices, {translation: {x: 10, y: 10}}),
[
{x: 9, y: 9},
{x: 11, y: 9},
{x: 11, y: 11},
{x: 9, y: 11},
],
);
expectCloseTo(
MathUtil.transform(vertices, {rotation: MathUtil.QUARTER_PI, translation: {x: 10, y: 10}}),
[
{x: 10, y: 10 - Math.sqrt(2)},
{x: 10 + Math.sqrt(2), y: 10},
{x: 10, y: 10 + Math.sqrt(2)},
{x: 10 - Math.sqrt(2), y: 10},
],
);
expectCloseTo(
MathUtil.transform(vertices, {scale: 2, translation: {x: 10, y: 10}}),
[
{x: 8, y: 8},
{x: 12, y: 8},
{x: 12, y: 12},
{x: 8, y: 12},
],
);
expectCloseTo(
MathUtil.transform(vertices, {rotation: MathUtil.QUARTER_PI, scale: 2, translation: {x: 10, y: 10}}),
[
{x: 10, y: 10 - Math.sqrt(2) * 2},
{x: 10 + Math.sqrt(2) * 2, y: 10},
{x: 10, y: 10 + Math.sqrt(2) * 2},
{x: 10 - Math.sqrt(2) * 2, y: 10},
],
);
});