autoformat, fix lint errors
This commit is contained in:
parent
054f9a9218
commit
5734774366
1 changed files with 306 additions and 229 deletions
|
@ -1,207 +1,281 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="collage">
|
<div class="collage">
|
||||||
<div class="canvas">
|
<div class="canvas">
|
||||||
<canvas id="canvas" ref="canvas" :width="canvasSize.width" :height="canvasSize.height"></canvas>
|
<canvas
|
||||||
<div class="canvas-size">
|
id="canvas"
|
||||||
<label>
|
ref="canvas"
|
||||||
Width:
|
:width="canvasSize.width"
|
||||||
<input type="number" step="16" min="128" v-model="canvasSize.width">
|
:height="canvasSize.height"
|
||||||
</label>
|
></canvas>
|
||||||
<label>
|
<div class="canvas-size">
|
||||||
Height:
|
<label>
|
||||||
<input type="number" step="16" min="128" v-model="canvasSize.height">
|
Width:
|
||||||
</label>
|
<input type="number" step="16" min="128" v-model="canvasSize.width" />
|
||||||
</div>
|
</label>
|
||||||
|
<label>
|
||||||
|
Height:
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="16"
|
||||||
|
min="128"
|
||||||
|
v-model="canvasSize.height"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="modes">
|
<div class="modes">
|
||||||
<ul class="modes-list">
|
<ul class="modes-list">
|
||||||
<li v-for="(mode, idx) in modes.modes"
|
<li
|
||||||
:class="['mode', {
|
v-for="(mode, idx) in modes.modes"
|
||||||
disabled: images.length < mode.minImages,
|
:class="[
|
||||||
excluded: excludedModes.includes(idx),
|
'mode',
|
||||||
selected: idx === currentModeType,
|
{
|
||||||
lastActive: lastActiveModeTypes.includes(idx)}]">
|
disabled: images.length < mode.minImages,
|
||||||
<label class="handle">{{mode.name}}<input type="radio" :value="idx"
|
excluded: excludedModes.includes(idx),
|
||||||
v-model="currentModeType"></label>
|
selected: idx === currentModeType,
|
||||||
<span class="mode-plus"
|
lastActive: lastActiveModeTypes.includes(idx),
|
||||||
v-if="['recursive', 'shuffle'].includes(currentModeType) && images.length >= minImages">
|
},
|
||||||
<span class="separator"></span>
|
]"
|
||||||
<label>{{ excludedModes.includes(idx) ? 'o' : 'x'}}<input type="checkbox" :value="idx"
|
:key="idx"
|
||||||
v-model="excludedModes"></label>
|
>
|
||||||
</span>
|
<label class="handle"
|
||||||
</li>
|
>{{ mode.name
|
||||||
</ul>
|
}}<input type="radio" :value="idx" v-model="currentModeType"
|
||||||
<hr>
|
/></label>
|
||||||
<label :class="{disabled: images.length < minImages,
|
<span
|
||||||
selected: 'shuffle' === currentModeType}">
|
class="mode-plus"
|
||||||
!SHUFFLE ALL!
|
v-if="
|
||||||
<input type="radio" value="shuffle" v-model="currentModeType">
|
['recursive', 'shuffle'].includes(currentModeType) &&
|
||||||
</label>
|
images.length >= minImages
|
||||||
<label :class="{disabled: images.length < minImages,
|
"
|
||||||
selected: 'recursive' === currentModeType}">
|
>
|
||||||
#RECURSIVE#
|
<span class="separator"></span>
|
||||||
<input type="radio" value="recursive" v-model="currentModeType">
|
<label
|
||||||
</label>
|
>{{ excludedModes.includes(idx) ? "o" : "x"
|
||||||
</div>
|
}}<input type="checkbox" :value="idx" v-model="excludedModes"
|
||||||
<button :disabled="images.length < minImages" @click="renderCollage">REPAINT</button>
|
/></label>
|
||||||
<hr>
|
</span>
|
||||||
<div class="config">
|
</li>
|
||||||
<template v-if="currentModeType !== 'recursive'">
|
</ul>
|
||||||
<label class="config-numimages">
|
<hr />
|
||||||
#N of images:
|
<label
|
||||||
<input type="number" :min="minImages" :max="images.length"
|
:class="{
|
||||||
placeholder="RND"
|
disabled: images.length < minImages,
|
||||||
:disabled="Object.keys(forceConfig).includes('numImages')"
|
selected: 'shuffle' === currentModeType,
|
||||||
v-model="forceConfig.numImages || collageConfig.numImages">
|
}"
|
||||||
</label>
|
>
|
||||||
</template>
|
!SHUFFLE ALL!
|
||||||
<template v-else>
|
<input type="radio" value="shuffle" v-model="currentModeType" />
|
||||||
<label>
|
</label>
|
||||||
Recursion levels:
|
<label
|
||||||
<input type="number" :min="1" :max="10" v-model="recursiveConfig.level">
|
:class="{
|
||||||
</label>
|
disabled: images.length < minImages,
|
||||||
<label>
|
selected: 'recursive' === currentModeType,
|
||||||
<input type="checkbox" v-model="recursiveConfig.repeat">
|
}"
|
||||||
Repeat images?
|
>
|
||||||
</label>
|
#RECURSIVE#
|
||||||
</template>
|
<input type="radio" value="recursive" v-model="currentModeType" />
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<button :disabled="images.length < minImages" @click="renderCollage">
|
||||||
|
REPAINT
|
||||||
|
</button>
|
||||||
|
<hr />
|
||||||
|
<div class="config">
|
||||||
|
<template v-if="currentModeType !== 'recursive'">
|
||||||
|
<label class="config-numimages">
|
||||||
|
#N of images:
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
:min="minImages"
|
||||||
|
:max="images.length"
|
||||||
|
placeholder="RND"
|
||||||
|
:disabled="Object.keys(forceConfig).includes('numImages')"
|
||||||
|
:value="forceConfig.numImages || collageConfig.numImages"
|
||||||
|
@input="(n) => (collageConfig.numImages = n)"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<label>
|
||||||
|
Recursion levels:
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
:min="1"
|
||||||
|
:max="10"
|
||||||
|
v-model="recursiveConfig.level"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" v-model="recursiveConfig.repeat" />
|
||||||
|
Repeat images?
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Component, Prop, Vue, Watch} from "vue-property-decorator";
|
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||||
import {CollageModeType} from "../common/collages";
|
|
||||||
import {CollageConfig, CollageMode, Segment} from "../common/types";
|
|
||||||
import {choice, shuffle} from "../common/utils";
|
|
||||||
import BrowserCollageModes from "../collages";
|
import BrowserCollageModes from "../collages";
|
||||||
|
import { CollageModeType } from "../common/collages";
|
||||||
|
import { CollageConfig, CollageMode, Segment } from "../common/types";
|
||||||
|
import { choice, shuffle } from "../common/utils";
|
||||||
|
|
||||||
type DisplayCollageModeType = CollageModeType | & "shuffle" | & "recursive";
|
type DisplayCollageModeType = CollageModeType | "shuffle" | "recursive";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class Collage extends Vue {
|
export default class Collage extends Vue {
|
||||||
@Prop({required: true}) private images!: ImageBitmap[];
|
@Prop({ required: true }) private images!: ImageBitmap[];
|
||||||
private context!: CanvasRenderingContext2D;
|
private context!: CanvasRenderingContext2D;
|
||||||
private canvasSize = {
|
private canvasSize = {
|
||||||
width: 640,
|
width: 640,
|
||||||
height: 640
|
height: 640,
|
||||||
};
|
};
|
||||||
private collageConfig: CollageConfig = {
|
private collageConfig: CollageConfig = {
|
||||||
numImages: undefined
|
numImages: undefined,
|
||||||
};
|
};
|
||||||
private recursiveConfig = {
|
private recursiveConfig = {
|
||||||
level: 2,
|
level: 2,
|
||||||
repeat: true
|
repeat: true,
|
||||||
};
|
};
|
||||||
private currentModeType: DisplayCollageModeType = "shuffle";
|
private currentModeType: DisplayCollageModeType = "shuffle";
|
||||||
private lastActiveModeTypes: CollageModeType[] = [];
|
private lastActiveModeTypes: CollageModeType[] = [];
|
||||||
private excludedModes: CollageModeType[] = [];
|
private excludedModes: CollageModeType[] = [];
|
||||||
private modes = new BrowserCollageModes();
|
private modes = new BrowserCollageModes();
|
||||||
|
|
||||||
private get minImages() {
|
private get minImages() {
|
||||||
if (this.currentModeType === "shuffle" || this.currentModeType === "recursive") {
|
if (
|
||||||
return Math.min(...Object.values(this.modes.modes).map((mode) => mode.minImages));
|
this.currentModeType === "shuffle" ||
|
||||||
|
this.currentModeType === "recursive"
|
||||||
|
) {
|
||||||
|
return Math.min(
|
||||||
|
...Object.values(this.modes.modes).map((mode) => mode.minImages)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return this.modes.modes[this.currentModeType].minImages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get lastMode() {
|
||||||
|
if (this.lastActiveModeTypes.length === 1) {
|
||||||
|
return this.modes.modes[this.lastActiveModeTypes[0]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get forceConfig() {
|
||||||
|
return this.lastMode ? this.lastMode.forceConfig || {} : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private mounted() {
|
||||||
|
const canvas = this.$refs.canvas as HTMLCanvasElement;
|
||||||
|
this.context = canvas.getContext("2d") as CanvasRenderingContext2D;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Watch("images")
|
||||||
|
@Watch("currentModeType")
|
||||||
|
@Watch("collageConfig", { deep: true })
|
||||||
|
@Watch("recursiveConfig", { deep: true })
|
||||||
|
private renderCollage() {
|
||||||
|
if (this.images.length >= this.minImages) {
|
||||||
|
this.reset();
|
||||||
|
|
||||||
|
const permissibleModeKeys = (
|
||||||
|
Object.keys(this.modes.modes) as CollageModeType[]
|
||||||
|
).filter(
|
||||||
|
(k) =>
|
||||||
|
!this.excludedModes.includes(k) &&
|
||||||
|
this.modes.modes[k].minImages <= this.images.length
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.currentModeType !== "recursive") {
|
||||||
|
let mode: CollageMode<any, any>;
|
||||||
|
if (this.currentModeType === "shuffle") {
|
||||||
|
const randomModeType = choice(permissibleModeKeys);
|
||||||
|
this.lastActiveModeTypes = [randomModeType];
|
||||||
|
mode = this.modes.modes[randomModeType];
|
||||||
} else {
|
} else {
|
||||||
return this.modes.modes[this.currentModeType].minImages;
|
this.lastActiveModeTypes = [this.currentModeType];
|
||||||
|
mode = this.modes.modes[this.currentModeType];
|
||||||
}
|
}
|
||||||
}
|
const shuffledImages = shuffle(this.images);
|
||||||
|
const segments = mode.getSegments(
|
||||||
private get lastMode() {
|
this.context,
|
||||||
if (this.lastActiveModeTypes.length === 1) {
|
this.collageConfig,
|
||||||
return this.modes.modes[this.lastActiveModeTypes[0]];
|
shuffledImages
|
||||||
}
|
);
|
||||||
}
|
mode.place(this.context, shuffledImages, segments);
|
||||||
|
} else {
|
||||||
private get forceConfig() {
|
this.lastActiveModeTypes = [];
|
||||||
return this.lastMode ? this.lastMode.forceConfig || {} : {};
|
const shuffledImages = shuffle(this.images);
|
||||||
}
|
const rootSegment: Segment = {
|
||||||
|
x: 0,
|
||||||
private mounted() {
|
y: 0,
|
||||||
const canvas = (this.$refs.canvas as HTMLCanvasElement);
|
w: this.context.canvas.width,
|
||||||
this.context = canvas.getContext("2d") as CanvasRenderingContext2D;
|
h: this.context.canvas.height,
|
||||||
}
|
};
|
||||||
|
const processSegment = async (
|
||||||
@Watch("images")
|
segment: Segment,
|
||||||
@Watch("currentModeType")
|
level: number
|
||||||
@Watch("collageConfig", {deep: true})
|
): Promise<ImageBitmap> => {
|
||||||
@Watch("recursiveConfig", {deep: true})
|
console.debug(segment, level);
|
||||||
private renderCollage() {
|
if (
|
||||||
if (this.images.length >= this.minImages) {
|
segment === rootSegment ||
|
||||||
this.reset();
|
level <= this.recursiveConfig.level - 1
|
||||||
|
) {
|
||||||
const permissibleModeKeys = (Object.keys(this.modes.modes) as CollageModeType[])
|
let canvas = document.createElement("canvas");
|
||||||
.filter(k => !this.excludedModes.includes(k) && this.modes.modes[k].minImages <= this.images.length);
|
canvas.width = segment.w;
|
||||||
|
canvas.height = segment.h;
|
||||||
if (this.currentModeType !== "recursive") {
|
let modeKey = choice(permissibleModeKeys);
|
||||||
let mode: CollageMode<any, any>;
|
console.debug(modeKey);
|
||||||
if (this.currentModeType === "shuffle") {
|
this.lastActiveModeTypes.push(modeKey);
|
||||||
const randomModeType = choice(permissibleModeKeys);
|
let mode = this.modes.modes[modeKey];
|
||||||
this.lastActiveModeTypes = [randomModeType];
|
let ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
|
||||||
mode = this.modes.modes[randomModeType];
|
let segments = mode.getSegments(ctx);
|
||||||
} else {
|
console.debug(segments);
|
||||||
this.lastActiveModeTypes = [this.currentModeType];
|
let bitmaps = await Promise.all(
|
||||||
mode = this.modes.modes[this.currentModeType];
|
segments.map((segment) => processSegment(segment, level + 1))
|
||||||
}
|
);
|
||||||
const shuffledImages = shuffle(this.images);
|
mode.place(ctx, bitmaps, segments);
|
||||||
const segments = mode.getSegments(this.context, this.collageConfig, shuffledImages);
|
return await createImageBitmap(canvas);
|
||||||
mode.place(this.context, shuffledImages, segments);
|
} else {
|
||||||
|
if (this.recursiveConfig.repeat) {
|
||||||
|
return choice(shuffledImages);
|
||||||
} else {
|
} else {
|
||||||
this.lastActiveModeTypes = [];
|
if (shuffledImages.length > 0) {
|
||||||
const shuffledImages = shuffle(this.images);
|
return shuffledImages.pop() as ImageBitmap;
|
||||||
const rootSegment: Segment = {x: 0, y: 0, w: this.context.canvas.width, h: this.context.canvas.height};
|
} else {
|
||||||
const processSegment = async (segment: Segment, level: number): Promise<ImageBitmap> => {
|
throw "RAN OUT OF IMAGES";
|
||||||
console.debug(segment, level);
|
}
|
||||||
if (segment === rootSegment || level <= this.recursiveConfig.level - 1) {
|
|
||||||
let canvas = document.createElement("canvas");
|
|
||||||
canvas.width = segment.w;
|
|
||||||
canvas.height = segment.h;
|
|
||||||
let modeKey = choice(permissibleModeKeys);
|
|
||||||
console.debug(modeKey);
|
|
||||||
this.lastActiveModeTypes.push(modeKey);
|
|
||||||
let mode = this.modes.modes[modeKey];
|
|
||||||
let ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
|
|
||||||
let segments = mode.getSegments(ctx);
|
|
||||||
console.debug(segments);
|
|
||||||
let bitmaps = await Promise.all(segments.map((segment) => processSegment(segment, level + 1)));
|
|
||||||
mode.place(ctx, bitmaps, segments);
|
|
||||||
return await createImageBitmap(canvas);
|
|
||||||
} else {
|
|
||||||
if (this.recursiveConfig.repeat) {
|
|
||||||
return choice(shuffledImages);
|
|
||||||
} else {
|
|
||||||
if (shuffledImages.length > 0) {
|
|
||||||
return shuffledImages.pop() as ImageBitmap;
|
|
||||||
} else {
|
|
||||||
throw "RAN OUT OF IMAGES";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
processSegment(rootSegment, 0).then((finalCollage) => {
|
|
||||||
console.debug(finalCollage);
|
|
||||||
this.context.drawImage(finalCollage, 0, 0);
|
|
||||||
}).catch((error) => {
|
|
||||||
alert(error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
processSegment(rootSegment, 0)
|
||||||
|
.then((finalCollage) => {
|
||||||
|
console.debug(finalCollage);
|
||||||
|
this.context.drawImage(finalCollage, 0, 0);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
alert(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Watch("canvasSize", {deep: true})
|
@Watch("canvasSize", { deep: true })
|
||||||
private onCanvasSizeChange() {
|
private onCanvasSizeChange() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.renderCollage();
|
this.renderCollage();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private reset() {
|
private reset() {
|
||||||
this.context.globalCompositeOperation = "source-over";
|
this.context.globalCompositeOperation = "source-over";
|
||||||
const canvas = (this.$refs.canvas as HTMLCanvasElement);
|
const canvas = this.$refs.canvas as HTMLCanvasElement;
|
||||||
this.context.clearRect(0, 0, canvas.width, canvas.height);
|
this.context.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -209,112 +283,115 @@ export default class Collage extends Vue {
|
||||||
<!--suppress CssUnusedSymbol -->
|
<!--suppress CssUnusedSymbol -->
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.collage {
|
.collage {
|
||||||
margin: 2rem;
|
margin: 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas {
|
.canvas {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#canvas {
|
#canvas {
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas-size {
|
.canvas-size {
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls button {
|
.controls button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modes {
|
.modes {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modes-list {
|
.modes-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode {
|
.mode {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.separator {
|
.separator {
|
||||||
border: 1px solid gray;
|
border: 1px solid gray;
|
||||||
width: 0;
|
width: 0;
|
||||||
margin: 0 .5em;
|
margin: 0 0.5em;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode .mode-plus, .mode .mode-plus label {
|
.mode .mode-plus,
|
||||||
color: gray;
|
.mode .mode-plus label {
|
||||||
font-size: 14px;
|
color: gray;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-plus {
|
.mode-plus {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls .modes hr {
|
.controls .modes hr {
|
||||||
margin-top: .5rem;
|
margin-top: 0.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: lightgray;
|
color: lightgray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls label {
|
.controls label {
|
||||||
font-size: 14pt;
|
font-size: 14pt;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: .25rem;
|
margin: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modes input {
|
.modes input {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls button, .controls hr, .controls .config {
|
.controls button,
|
||||||
margin-top: 1rem;
|
.controls hr,
|
||||||
|
.controls .config {
|
||||||
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls .config {
|
.controls .config {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config label {
|
.config label {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-numimages input {
|
.config-numimages input {
|
||||||
width: 4em;
|
width: 4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled {
|
.disabled {
|
||||||
color: gray;
|
color: gray;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.excluded .handle {
|
.excluded .handle {
|
||||||
color: gray;
|
color: gray;
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected {
|
.selected {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lastActive .handle {
|
.lastActive .handle {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in a new issue