major refactor (add segments) in preparation for recursive collages
This commit is contained in:
parent
3866522404
commit
7cf393af71
4 changed files with 189 additions and 135 deletions
241
src/collages.ts
241
src/collages.ts
|
@ -1,5 +1,5 @@
|
||||||
import {CollageMode} from "@/types";
|
import {CollageConfig, CollageMode, Segment} from "@/types";
|
||||||
import {choice, randint, shuffle} from "@/utils";
|
import {choice, randint, range, shuffle} from "@/utils";
|
||||||
|
|
||||||
const collageModeType = [
|
const collageModeType = [
|
||||||
"clean_grid", "chaos_grid",
|
"clean_grid", "chaos_grid",
|
||||||
|
@ -22,27 +22,40 @@ function cleanDraw(ctx: CanvasRenderingContext2D, image: ImageBitmap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGridPoints(ctx: CanvasRenderingContext2D, idx: number) {
|
function getGridSegments(ctx: CanvasRenderingContext2D, config?: CollageConfig) {
|
||||||
let x!: number, y!: number;
|
return [0, 1, 2, 3].map((idx) => {
|
||||||
switch (idx) {
|
let x!: number, y!: number;
|
||||||
case 0:
|
switch (idx) {
|
||||||
x = ctx.canvas.width * .25;
|
case 0:
|
||||||
y = ctx.canvas.height * .25;
|
x = ctx.canvas.width * .25;
|
||||||
break;
|
y = ctx.canvas.height * .25;
|
||||||
case 1:
|
break;
|
||||||
x = ctx.canvas.width * .75;
|
case 1:
|
||||||
y = ctx.canvas.height * .25;
|
x = ctx.canvas.width * .75;
|
||||||
break;
|
y = ctx.canvas.height * .25;
|
||||||
case 2:
|
break;
|
||||||
x = ctx.canvas.width * .25;
|
case 2:
|
||||||
y = ctx.canvas.height * .75;
|
x = ctx.canvas.width * .25;
|
||||||
break;
|
y = ctx.canvas.height * .75;
|
||||||
case 3:
|
break;
|
||||||
x = ctx.canvas.width * .75;
|
case 3:
|
||||||
y = ctx.canvas.height * .75;
|
x = ctx.canvas.width * .75;
|
||||||
break;
|
y = ctx.canvas.height * .75;
|
||||||
}
|
break;
|
||||||
return [x, y];
|
}
|
||||||
|
return {
|
||||||
|
x, y,
|
||||||
|
w: ctx.canvas.width / 2,
|
||||||
|
h: ctx.canvas.height / 2
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function cleanPlace(ctx: CanvasRenderingContext2D, images: ImageBitmap[], segments: Segment[]) {
|
||||||
|
segments.forEach((segment, idx) => {
|
||||||
|
cleanDraw(ctx, images[idx], segment.x, segment.y, segment.w, segment.h);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const modes: { [key in CollageModeType]: CollageMode } = {
|
const modes: { [key in CollageModeType]: CollageMode } = {
|
||||||
|
@ -52,14 +65,8 @@ const modes: { [key in CollageModeType]: CollageMode } = {
|
||||||
forceConfig: {
|
forceConfig: {
|
||||||
numImages: 4
|
numImages: 4
|
||||||
},
|
},
|
||||||
place: (ctx, images, config) => {
|
getSegments: getGridSegments,
|
||||||
const quadrantSize = [ctx.canvas.width / 2, ctx.canvas.height / 2];
|
place: cleanPlace
|
||||||
const selectedImages = shuffle(images).slice(0, 4);
|
|
||||||
selectedImages.forEach((image, idx) => {
|
|
||||||
const [x, y] = getGridPoints(ctx, idx);
|
|
||||||
cleanDraw(ctx, image, x, y, quadrantSize[0], quadrantSize[1]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"chaos_grid": {
|
"chaos_grid": {
|
||||||
name: "Irregular Grid",
|
name: "Irregular Grid",
|
||||||
|
@ -67,125 +74,149 @@ const modes: { [key in CollageModeType]: CollageMode } = {
|
||||||
forceConfig: {
|
forceConfig: {
|
||||||
numImages: 4
|
numImages: 4
|
||||||
},
|
},
|
||||||
place: (ctx, images, config) => {
|
getSegments: getGridSegments,
|
||||||
const quadrantSize = [ctx.canvas.width / 2, ctx.canvas.height / 2];
|
place: (ctx, images, segments) => {
|
||||||
const selectedImages = shuffle(images).slice(0, 4);
|
const shuffledImages = shuffle(images);
|
||||||
shuffle(selectedImages.map((image, idx) => [image, idx] as [ImageBitmap, number]))
|
shuffle(segments.map((segment, idx) => [segment, idx] as [Segment, number]))
|
||||||
.forEach(([image, idx]) => {
|
.forEach(([segment, idx]) => {
|
||||||
const [x, y] = getGridPoints(ctx, idx);
|
const image = shuffledImages[idx];
|
||||||
const scaleRatio = Math.max(quadrantSize[0] / image.width, quadrantSize[1] / image.height);
|
const scaleRatio = Math.max(segment.w / image.width, segment.h / image.height);
|
||||||
ctx.drawImage(image,
|
ctx.drawImage(image,
|
||||||
x - (image.width * scaleRatio / 2), y - (image.height * scaleRatio / 2),
|
segment.x - (image.width * scaleRatio / 2), segment.y - (image.height * scaleRatio / 2),
|
||||||
image.width * scaleRatio, image.height * scaleRatio);
|
image.width * scaleRatio, image.height * scaleRatio);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"row": {
|
"row": {
|
||||||
name: "Regular Row",
|
name: "Regular Row",
|
||||||
minImages: 2,
|
minImages: 2,
|
||||||
place: (ctx, images, config) => {
|
getSegments: (ctx, config, images) => {
|
||||||
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
|
const numImages = Math.min(images?.length || 0, config?.numImages || randint(4) + 2);
|
||||||
const quadrantSize = [ctx.canvas.width / selectedImages.length, ctx.canvas.height];
|
const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
|
||||||
selectedImages.forEach((image, idx) => {
|
return range(numImages).map((idx) => {
|
||||||
const x = idx * quadrantSize[0] + quadrantSize[0] / 2;
|
return {
|
||||||
const y = quadrantSize[1] / 2;
|
x: idx * segmentSize[0] + segmentSize[0] / 2,
|
||||||
cleanDraw(ctx, image, x, y, quadrantSize[0], quadrantSize[1]);
|
y: segmentSize[1] / 2,
|
||||||
|
w: segmentSize[0],
|
||||||
|
h: segmentSize[1]
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
place: cleanPlace
|
||||||
},
|
},
|
||||||
"irow": {
|
"irow": {
|
||||||
name: "Irregular Row",
|
name: "Irregular Row",
|
||||||
minImages: 2,
|
minImages: 2,
|
||||||
place: (ctx, images, config) => {
|
getSegments: (ctx, config, images) => {
|
||||||
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
|
const numImages = Math.min(images?.length || 0, config?.numImages || randint(4) + 2);
|
||||||
const quadrantSize = [ctx.canvas.width / selectedImages.length, ctx.canvas.height];
|
const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
|
||||||
selectedImages.forEach((image, idx) => {
|
return range(numImages).map((idx) => {
|
||||||
const x = idx * quadrantSize[0] + quadrantSize[0] / 2;
|
const irregularWidth = images ?
|
||||||
const y = quadrantSize[1] / 2;
|
segmentSize[0] + Math.random() * ((segmentSize[1] / images[idx].height * images[idx].width) - segmentSize[0]) :
|
||||||
const w = Math.min(ctx.canvas.width / 2,
|
segmentSize[0] + Math.random() * segmentSize[0] * .5;
|
||||||
quadrantSize[1] + Math.random() * (quadrantSize[1] - (quadrantSize[1] / image.height) * image.width));
|
return {
|
||||||
cleanDraw(ctx, image, x, y, w, quadrantSize[1]);
|
x: idx * segmentSize[0] + segmentSize[0] / 2,
|
||||||
|
y: segmentSize[1] / 2,
|
||||||
|
w: Math.min(ctx.canvas.width / 2, irregularWidth),
|
||||||
|
h: segmentSize[1],
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
place: cleanPlace
|
||||||
},
|
},
|
||||||
"col": {
|
"col": {
|
||||||
name: "Regular Column",
|
name: "Regular Column",
|
||||||
minImages: 2,
|
minImages: 2,
|
||||||
place: (ctx, images, config) => {
|
getSegments: (ctx, config, images) => {
|
||||||
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
|
const numImages = Math.min(images?.length || 0, config?.numImages || randint(4) + 2);
|
||||||
const quadrantSize = [ctx.canvas.width, ctx.canvas.height / selectedImages.length];
|
const segmentSize = [ctx.canvas.width, ctx.canvas.height / numImages];
|
||||||
selectedImages.forEach((image, idx) => {
|
return range(numImages).map((idx) => {
|
||||||
const x = quadrantSize[0] / 2;
|
return {
|
||||||
const y = idx * quadrantSize[1] + quadrantSize[1] / 2;
|
x: segmentSize[0] / 2,
|
||||||
cleanDraw(ctx, image, x, y, quadrantSize[0], quadrantSize[1]);
|
y: idx * segmentSize[1] + segmentSize[1] / 2,
|
||||||
|
w: segmentSize[0],
|
||||||
|
h: segmentSize[1]
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
place: cleanPlace
|
||||||
},
|
},
|
||||||
"icol": {
|
"icol": {
|
||||||
name: "Irregular Column",
|
name: "Irregular Column",
|
||||||
minImages: 2,
|
minImages: 2,
|
||||||
place: (ctx, images, config) => {
|
getSegments: (ctx, config, images) => {
|
||||||
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
|
const numImages = Math.min(images?.length || 0, config?.numImages || randint(4) + 2);
|
||||||
const quadrantSize = [ctx.canvas.width, ctx.canvas.height / selectedImages.length];
|
const segmentSize = [ctx.canvas.width, ctx.canvas.height / numImages];
|
||||||
selectedImages.forEach((image, idx) => {
|
return range(numImages).map((idx) => {
|
||||||
const x = quadrantSize[0] / 2;
|
const irregularHeight = images ?
|
||||||
const y = idx * quadrantSize[1] + quadrantSize[1] / 2;
|
segmentSize[1] + Math.random() * ((segmentSize[0] / images[idx].width * images[idx].height) - segmentSize[1]) :
|
||||||
const h = Math.min(ctx.canvas.height / 2,
|
segmentSize[1] + Math.random() * segmentSize[1] * .5;
|
||||||
quadrantSize[0] + Math.random() * (quadrantSize[0] - (quadrantSize[0] / image.width) * image.height));
|
return {
|
||||||
cleanDraw(ctx, image, x, y, quadrantSize[0], h);
|
x: segmentSize[0] / 2,
|
||||||
|
y: idx * segmentSize[1] + segmentSize[1] / 2,
|
||||||
|
w: segmentSize[0],
|
||||||
|
h: Math.min(ctx.canvas.height / 2, irregularHeight),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
place: cleanPlace
|
||||||
},
|
},
|
||||||
"concentric_factor": {
|
"concentric_factor": {
|
||||||
name: "Constant factor concentric",
|
name: "Constant factor concentric",
|
||||||
minImages: 2,
|
minImages: 2,
|
||||||
place: (ctx, images, config) => {
|
getSegments: (ctx, config, images) => {
|
||||||
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
|
const numImages = Math.min(images?.length || 0, config?.numImages || randint(4) + 2);
|
||||||
const x = ctx.canvas.width / 2;
|
|
||||||
const y = ctx.canvas.height / 2;
|
|
||||||
|
|
||||||
let factor: number;
|
let factor: number;
|
||||||
if (Math.random() > .5) {
|
if (Math.random() > .5) {
|
||||||
factor = choice([1 / Math.sqrt(2), .5, .88]);
|
factor = choice([1 / Math.sqrt(2), .5, .88]);
|
||||||
} else {
|
} else {
|
||||||
factor = 1 - (1 / selectedImages.length);
|
factor = 1 - (1 / numImages);
|
||||||
}
|
}
|
||||||
selectedImages.forEach((image, idx) => {
|
return range(numImages).map((idx) => {
|
||||||
const ratio = Math.pow(factor, idx);
|
const ratio = Math.pow(factor, idx);
|
||||||
cleanDraw(ctx, image, x, y, ctx.canvas.width * ratio, ctx.canvas.height * ratio);
|
return {
|
||||||
|
x: ctx.canvas.width / 2,
|
||||||
|
y: ctx.canvas.height / 2,
|
||||||
|
w: ctx.canvas.width * ratio,
|
||||||
|
h: ctx.canvas.height * ratio
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
place: cleanPlace
|
||||||
},
|
},
|
||||||
"concentric_spaced": {
|
"concentric_spaced": {
|
||||||
name: "Equally spaced concentric",
|
name: "Equally spaced concentric",
|
||||||
minImages: 2,
|
minImages: 2,
|
||||||
place: (ctx, images, config) => {
|
getSegments: (ctx, config, images) => {
|
||||||
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
|
const numImages = Math.min(images?.length || 0, config?.numImages || randint(2) + 2);
|
||||||
|
return range(numImages).map((idx) => {
|
||||||
selectedImages.forEach((image, idx) => {
|
return {
|
||||||
cleanDraw(
|
x: ctx.canvas.width / 2,
|
||||||
ctx, image,
|
y: ctx.canvas.height / 2,
|
||||||
ctx.canvas.width / 2,
|
w: ctx.canvas.width - (ctx.canvas.width / numImages * idx),
|
||||||
ctx.canvas.height / 2,
|
h: ctx.canvas.height - (ctx.canvas.height / numImages * idx),
|
||||||
ctx.canvas.width - (ctx.canvas.width / selectedImages.length * idx),
|
};
|
||||||
ctx.canvas.height - (ctx.canvas.height / selectedImages.length * idx),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
place: cleanPlace
|
||||||
},
|
},
|
||||||
"blend": {
|
"blend": {
|
||||||
name: "Blending",
|
name: "Blending",
|
||||||
minImages: 2,
|
minImages: 2,
|
||||||
place: (ctx, images, config) => {
|
getSegments: (ctx, config, images) => {
|
||||||
const selectedImages = shuffle(images).slice(0, config.numImages || randint(2) + 2);
|
const numImages = Math.min(images?.length || 0, config?.numImages || randint(2) + 2);
|
||||||
ctx.globalCompositeOperation = choice(["difference", "saturation", "soft-light", "overlay"]);
|
return range(numImages).map((_) => {
|
||||||
selectedImages.forEach((image) => {
|
return {
|
||||||
cleanDraw(
|
x: ctx.canvas.width / 2,
|
||||||
ctx, image,
|
y: ctx.canvas.height / 2,
|
||||||
ctx.canvas.width / 2, ctx.canvas.height / 2,
|
w: ctx.canvas.width,
|
||||||
ctx.canvas.width, ctx.canvas.height
|
h: ctx.canvas.height
|
||||||
);
|
};
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
place: (ctx, images, segments) => {
|
||||||
|
ctx.globalCompositeOperation = choice(["difference", "saturation", "soft-light", "overlay"]);
|
||||||
|
cleanPlace(ctx, images, segments);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,13 +22,19 @@
|
||||||
{{mode.name}}
|
{{mode.name}}
|
||||||
<input type="radio" :value="idx" v-model="currentModeType">
|
<input type="radio" :value="idx" v-model="currentModeType">
|
||||||
</label>
|
</label>
|
||||||
|
<hr>
|
||||||
|
<label :class="{disabled: images.length < minImages,
|
||||||
|
selected: 'shuffle' === currentModeType}">
|
||||||
|
!SHUFFLE ALL!
|
||||||
|
<input type="radio" value="shuffle" v-model="currentModeType">
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button :disabled="images.length < currentMode.minImages" @click="renderCollage">REPAINT</button>
|
<button :disabled="images.length < minImages" @click="renderCollage">REPAINT</button>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="config">
|
<div class="config">
|
||||||
<label class="config-numimages">
|
<label class="config-numimages">
|
||||||
#N of images:
|
#N of images:
|
||||||
<input type="number" :min="currentMode.minImages" :max="images.length"
|
<input type="number" :min="minImages" :max="images.length"
|
||||||
placeholder="RND"
|
placeholder="RND"
|
||||||
:disabled="Object.keys(forceConfig).includes('numImages')"
|
:disabled="Object.keys(forceConfig).includes('numImages')"
|
||||||
v-model="forceConfig.numImages || collageConfig.numImages">
|
v-model="forceConfig.numImages || collageConfig.numImages">
|
||||||
|
@ -42,6 +48,7 @@
|
||||||
import {Component, Prop, Vue, Watch} from "vue-property-decorator";
|
import {Component, Prop, Vue, Watch} from "vue-property-decorator";
|
||||||
import collageModes, {CollageModeType} from "../collages";
|
import collageModes, {CollageModeType} from "../collages";
|
||||||
import {CollageConfig, CollageMode} from "@/types";
|
import {CollageConfig, CollageMode} from "@/types";
|
||||||
|
import {choice, shuffle} from "@/utils";
|
||||||
|
|
||||||
type DisplayCollageModeType = CollageModeType | & "shuffle";
|
type DisplayCollageModeType = CollageModeType | & "shuffle";
|
||||||
|
|
||||||
|
@ -57,30 +64,15 @@ export default class Collage extends Vue {
|
||||||
numImages: undefined
|
numImages: undefined
|
||||||
};
|
};
|
||||||
private currentModeType: DisplayCollageModeType = "shuffle";
|
private currentModeType: DisplayCollageModeType = "shuffle";
|
||||||
private lastActiveModeType: DisplayCollageModeType | null = null;
|
private lastActiveModeType: CollageModeType | null = null;
|
||||||
private modes: { [key in DisplayCollageModeType]: CollageMode } = {
|
private modes = collageModes;
|
||||||
...collageModes,
|
|
||||||
"shuffle": {
|
private get minImages() {
|
||||||
name: "Shuffle all!",
|
if (this.currentModeType === "shuffle") {
|
||||||
minImages: Math.min(...Object.values(collageModes).map(m => m.minImages)),
|
return Math.min(...Object.values(this.modes).map((mode) => mode.minImages));
|
||||||
place: (ctx, images, config) => {
|
} else {
|
||||||
const permissibleModeKeys = Object.keys(collageModes)
|
return this.modes[this.currentModeType].minImages;
|
||||||
.filter(k => collageModes[k as CollageModeType].minImages <= images.length) as CollageModeType[];
|
|
||||||
const randomModeType = permissibleModeKeys[Math.floor(Math.random() * permissibleModeKeys.length)];
|
|
||||||
const randomMode = collageModes[randomModeType];
|
|
||||||
this.setLastActiveModeType(randomModeType);
|
|
||||||
randomMode.place(ctx, images, config);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// wtf vue?
|
|
||||||
private setLastActiveModeType(lastActiveModeType: any) {
|
|
||||||
this.lastActiveModeType = lastActiveModeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
private get currentMode() {
|
|
||||||
return this.modes[this.currentModeType];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private get lastMode() {
|
private get lastMode() {
|
||||||
|
@ -97,13 +89,26 @@ export default class Collage extends Vue {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch("images")
|
@Watch("images")
|
||||||
@Watch("currentMode")
|
@Watch("currentModeType")
|
||||||
@Watch("collageConfig", {deep: true})
|
@Watch("collageConfig", {deep: true})
|
||||||
private renderCollage() {
|
private renderCollage() {
|
||||||
if (this.images.length >= this.currentMode.minImages) {
|
if (this.images.length >= this.minImages) {
|
||||||
this.lastActiveModeType = this.currentModeType;
|
|
||||||
this.reset();
|
this.reset();
|
||||||
this.currentMode.place(this.context, this.images, this.collageConfig);
|
this.lastActiveModeType = this.currentModeType === "shuffle" ? null : this.currentModeType;
|
||||||
|
|
||||||
|
let mode: CollageMode;
|
||||||
|
if (this.currentModeType === "shuffle") {
|
||||||
|
const permissibleModeKeys = Object.keys(collageModes)
|
||||||
|
.filter(k => collageModes[k as CollageModeType].minImages <= this.images.length) as CollageModeType[];
|
||||||
|
const randomModeType = choice(permissibleModeKeys);
|
||||||
|
this.lastActiveModeType = randomModeType;
|
||||||
|
mode = collageModes[randomModeType];
|
||||||
|
} else {
|
||||||
|
mode = this.modes[this.currentModeType];
|
||||||
|
}
|
||||||
|
const shuffledImages = shuffle(this.images);
|
||||||
|
const segments = mode.getSegments(this.context, this.collageConfig, shuffledImages);
|
||||||
|
mode.place(this.context, shuffledImages, segments);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,6 +161,12 @@ export default class Collage extends Vue {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.controls .modes hr {
|
||||||
|
margin-top: .5rem;
|
||||||
|
width: 100%;
|
||||||
|
color: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
.controls label {
|
.controls label {
|
||||||
font-size: 14pt;
|
font-size: 14pt;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
10
src/types.d.ts
vendored
10
src/types.d.ts
vendored
|
@ -1,10 +1,18 @@
|
||||||
export interface CollageMode {
|
export interface CollageMode {
|
||||||
name: string;
|
name: string;
|
||||||
minImages: number;
|
minImages: number;
|
||||||
place: (ctx: CanvasRenderingContext2D, images: ImageBitmap[], config: CollageConfig) => void;
|
getSegments: (ctx: CanvasRenderingContext2D, config?: CollageConfig, images?: ImageBitmap[]) => Segment[];
|
||||||
|
place: (ctx: CanvasRenderingContext2D, images: ImageBitmap[], segments: Segment[]) => void;
|
||||||
forceConfig?: CollageConfig;
|
forceConfig?: CollageConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollageConfig {
|
export interface CollageConfig {
|
||||||
numImages?: number;
|
numImages?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Segment {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
}
|
||||||
|
|
|
@ -21,3 +21,7 @@ export function randint(n: number) {
|
||||||
export function choice<T>(arr: T[]): T {
|
export function choice<T>(arr: T[]): T {
|
||||||
return arr[randint(arr.length)];
|
return arr[randint(arr.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function range(n: number): number[] {
|
||||||
|
return Array.from({length: n}, (x, i) => i);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue