Merge branch 'wip/rect_rework' into develop

This commit is contained in:
Jörn-Michael Miehe 2023-10-27 15:05:32 +00:00
commit a72ba92f5e
6 changed files with 143 additions and 150 deletions

View file

@ -2,9 +2,21 @@
<MultiModal @handle="modal_handle" /> <MultiModal @handle="modal_handle" />
<figure> <figure>
<figcaption class="has-text-primary-dark"> <div class="level is-mobile mb-2">
{{ figure_caption }} <div class="level-left">
</figcaption> <figcaption class="level-item has-text-primary-dark">
{{ figure_caption }}
</figcaption>
</div>
<div class="level-right">
<BulmaButton
class="level-item tag is-link"
text="Türchen anzeigen"
:icon="'fa-solid fa-toggle-' + (show_doors ? 'on' : 'off')"
@click.left="show_doors = !show_doors"
/>
</div>
</div>
<div class="image is-unselectable"> <div class="image is-unselectable">
<img :src="$advent22.api_url('user/background_image')" /> <img :src="$advent22.api_url('user/background_image')" />
<ThouCanvas> <ThouCanvas>
@ -12,6 +24,7 @@
v-for="(door, index) in doors" v-for="(door, index) in doors"
:key="`door-${index}`" :key="`door-${index}`"
:door="door" :door="door"
:visible="show_doors"
@doorClick="door_click" @doorClick="door_click"
@doorSuccess="door_success" @doorSuccess="door_success"
@doorFailure="door_failure" @doorFailure="door_failure"
@ -30,12 +43,14 @@ import { Door } from "@/lib/door";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import MultiModal from "./MultiModal.vue"; import MultiModal from "./MultiModal.vue";
import BulmaButton from "./bulma/Button.vue";
import CalendarDoor from "./calendar/CalendarDoor.vue"; import CalendarDoor from "./calendar/CalendarDoor.vue";
import ThouCanvas from "./calendar/ThouCanvas.vue"; import ThouCanvas from "./calendar/ThouCanvas.vue";
@Options({ @Options({
components: { components: {
MultiModal, MultiModal,
BulmaButton,
ThouCanvas, ThouCanvas,
CalendarDoor, CalendarDoor,
}, },
@ -47,6 +62,7 @@ export default class extends Vue {
public readonly doors!: Door[]; public readonly doors!: Door[];
private readonly idle_caption = "Finde die Türchen auf dem Adventskalender!"; private readonly idle_caption = "Finde die Türchen auf dem Adventskalender!";
public show_doors = false;
private multi_modal?: MultiModal; private multi_modal?: MultiModal;
public figure_caption = this.idle_caption; public figure_caption = this.idle_caption;

View file

@ -1,5 +1,12 @@
<template> <template>
<SVGRect :rectangle="door.position" @click.left="on_click" /> <SVGRect
style="cursor: pointer"
:variant="visible ? 'primary' : undefined"
:rectangle="door.position"
@click.left="on_click"
>
<div class="has-text-danger">{{ door.day }}</div>
</SVGRect>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -14,11 +21,16 @@ import SVGRect from "./SVGRect.vue";
}, },
props: { props: {
door: Door, door: Door,
visible: {
type: Boolean,
default: false,
},
}, },
emits: ["doorClick", "doorSuccess", "doorFailure"], emits: ["doorClick", "doorSuccess", "doorFailure"],
}) })
export default class extends Vue { export default class extends Vue {
public door!: Door; public door!: Door;
public visible!: boolean;
public on_click() { public on_click() {
this.$emit("doorClick"); this.$emit("doorClick");
@ -32,9 +44,3 @@ export default class extends Vue {
} }
} }
</script> </script>
<style scoped>
rect {
cursor: pointer;
}
</style>

View file

@ -1,5 +1,22 @@
<template> <template>
<foreignObject
:x="Math.round(parent_aspect_ratio * rectangle.left)"
:y="rectangle.top"
:width="Math.round(parent_aspect_ratio * rectangle.width)"
:height="rectangle.height"
:style="`transform: scaleX(${1 / parent_aspect_ratio})`"
>
<div
v-if="variant !== undefined"
xmlns="http://www.w3.org/1999/xhtml"
class="px-4 is-flex is-align-items-center is-justify-content-center is-size-1 has-text-weight-bold"
>
<slot name="default" />
</div>
</foreignObject>
<rect <rect
ref="rect"
v-bind="$attrs"
:class="variant !== undefined ? variant : ''" :class="variant !== undefined ? variant : ''"
:x="rectangle.left" :x="rectangle.left"
:y="rectangle.top" :y="rectangle.top"
@ -12,7 +29,13 @@
import { Rectangle } from "@/lib/rectangle"; import { Rectangle } from "@/lib/rectangle";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
type RectColor = "primary" | "link" | "info" | "success" | "warning" | "danger"; type BulmaVariant =
| "primary"
| "link"
| "info"
| "success"
| "warning"
| "danger";
@Options({ @Options({
props: { props: {
@ -24,14 +47,56 @@ type RectColor = "primary" | "link" | "info" | "success" | "warning" | "danger";
}, },
}) })
export default class extends Vue { export default class extends Vue {
public variant?: RectColor; public variant?: BulmaVariant;
public rectangle!: Rectangle; public rectangle!: Rectangle;
private refreshKey = 0;
declare $refs: {
rect: unknown;
};
private refresh() {
window.setTimeout(() => {
// don't loop endlessly
if (this.refreshKey < 10000) {
this.refreshKey++;
}
}, 100);
}
public get parent_aspect_ratio(): number {
this.refreshKey; // read it just to force recompute on change
if (
!(this.$refs.rect instanceof SVGRectElement) ||
this.$refs.rect.parentElement === null
) {
this.refresh();
return 1;
}
const parent = this.$refs.rect.parentElement;
const result = parent.clientWidth / parent.clientHeight;
// force recompute for suspicious results
if (result === 0 || result === Infinity) {
this.refresh();
return 1;
}
return result;
}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "@/bulma-vars"; @import "@/bulma-vars";
foreignObject > div {
height: inherit;
}
rect { rect {
fill: transparent; fill: transparent;
fill-opacity: 0.3; fill-opacity: 0.3;

View file

@ -8,11 +8,12 @@
@click.middle="remove_rect" @click.middle="remove_rect"
@dblclick.left="remove_rect" @dblclick.left="remove_rect"
> >
<SVGRect <CalendarDoor
v-for="(rect, index) in rectangles" v-for="(door, index) in doors"
variant="primary" :key="`door-${index}`"
:key="`rect-${index}`" :door="door"
:rectangle="rect" style="cursor: inherit"
visible
/> />
<SVGRect <SVGRect
v-if="preview_visible" v-if="preview_visible"
@ -23,10 +24,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Door } from "@/lib/door";
import { Rectangle } from "@/lib/rectangle"; import { Rectangle } from "@/lib/rectangle";
import { Vector2D } from "@/lib/vector2d"; import { Vector2D } from "@/lib/vector2d";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import CalendarDoor from "../calendar/CalendarDoor.vue";
import SVGRect from "../calendar/SVGRect.vue"; import SVGRect from "../calendar/SVGRect.vue";
import ThouCanvas from "../calendar/ThouCanvas.vue"; import ThouCanvas from "../calendar/ThouCanvas.vue";
@ -38,34 +41,34 @@ enum CanvasState {
@Options({ @Options({
components: { components: {
ThouCanvas, CalendarDoor,
SVGRect, SVGRect,
ThouCanvas,
}, },
props: { props: {
rectangles: Array, doors: Array,
}, },
emits: ["draw", "drag", "remove"],
}) })
export default class extends Vue { export default class extends Vue {
private readonly min_rect_area = 300; private readonly min_rect_area = 300;
private state = CanvasState.Idle; private state = CanvasState.Idle;
public preview_rect = new Rectangle(); public preview_rect = new Rectangle();
private drag_rect?: Rectangle; private drag_door?: Door;
private drag_origin = new Vector2D(); private drag_origin = new Vector2D();
public rectangles!: Rectangle[]; public doors!: Door[];
public get preview_visible(): boolean { public get preview_visible(): boolean {
return this.state !== CanvasState.Idle; return this.state !== CanvasState.Idle;
} }
private pop_rectangle(point: Vector2D): Rectangle | undefined { private pop_door(point: Vector2D): Door | undefined {
const idx = this.rectangles.findIndex((rect) => rect.contains(point)); const idx = this.doors.findIndex((rect) => rect.position.contains(point));
if (idx === -1) { if (idx === -1) {
return; return;
} }
return this.rectangles.splice(idx, 1)[0]; return this.doors.splice(idx, 1)[0];
} }
public draw_start(event: MouseEvent, point: Vector2D) { public draw_start(event: MouseEvent, point: Vector2D) {
@ -88,8 +91,7 @@ export default class extends Vue {
return; return;
} }
this.rectangles.push(this.preview_rect); this.doors.push(new Door(this.preview_rect));
this.$emit("draw", this.preview_rect);
} }
public drag_start(event: MouseEvent, point: Vector2D) { public drag_start(event: MouseEvent, point: Vector2D) {
@ -97,16 +99,16 @@ export default class extends Vue {
return; return;
} }
this.drag_rect = this.pop_rectangle(point); this.drag_door = this.pop_door(point);
if (this.drag_rect === undefined) { if (this.drag_door === undefined) {
return; return;
} }
this.state = CanvasState.Dragging; this.state = CanvasState.Dragging;
this.drag_origin = point; this.drag_origin = point;
this.preview_rect = this.drag_rect; this.preview_rect = this.drag_door.position;
} }
public drag_finish() { public drag_finish() {
@ -118,8 +120,7 @@ export default class extends Vue {
} }
this.state = CanvasState.Idle; this.state = CanvasState.Idle;
this.rectangles.push(this.preview_rect); this.doors.push(new Door(this.preview_rect, this.drag_door!.day));
this.$emit("drag", this.drag_rect, this.preview_rect);
} }
public on_mousemove(event: MouseEvent, point: Vector2D) { public on_mousemove(event: MouseEvent, point: Vector2D) {
@ -129,9 +130,9 @@ export default class extends Vue {
if (this.state === CanvasState.Drawing) { if (this.state === CanvasState.Drawing) {
this.preview_rect = this.preview_rect.update(undefined, point); this.preview_rect = this.preview_rect.update(undefined, point);
} else if (this.state === CanvasState.Dragging && this.drag_rect) { } else if (this.state === CanvasState.Dragging && this.drag_door) {
const movement = point.minus(this.drag_origin); const movement = point.minus(this.drag_origin);
this.preview_rect = this.drag_rect.move(movement); this.preview_rect = this.drag_door.position.move(movement);
} }
} }
@ -140,13 +141,7 @@ export default class extends Vue {
return; return;
} }
const rect = this.pop_rectangle(point); this.pop_door(point);
if (rect === undefined) {
return;
}
this.$emit("remove", rect);
} }
} }
</script> </script>

View file

@ -10,64 +10,26 @@
</div> </div>
<figure class="image is-unselectable"> <figure class="image is-unselectable">
<img :src="$advent22.api_url('user/background_image')" /> <img :src="$advent22.api_url('user/background_image')" />
<RectangleCanvas <DoorCanvas :doors="doors" />
:rectangles="rectangles"
@draw="on_draw"
@drag="on_drag"
@remove="on_remove"
/>
</figure> </figure>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Door } from "@/lib/door"; import { Door } from "@/lib/door";
import { Rectangle } from "@/lib/rectangle";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import RectangleCanvas from "./RectangleCanvas.vue"; import DoorCanvas from "./DoorCanvas.vue";
@Options({ @Options({
components: { components: {
RectangleCanvas, DoorCanvas,
}, },
props: { props: {
doors: Array, doors: Array,
}, },
}) })
export default class extends Vue { export default class extends Vue {
private doors!: Door[]; public doors!: Door[];
public get rectangles() {
return this.doors.map((door) => door.position);
}
public on_draw(position: Rectangle) {
this.doors.push(new Door(position));
}
public find_door_index(position: Rectangle): number {
return this.doors.findIndex((door) => door.position.equals(position));
}
public on_drag(old_pos: Rectangle, new_pos: Rectangle) {
const idx = this.find_door_index(old_pos);
if (idx === -1) {
return;
}
this.doors[idx].position = new_pos;
}
public on_remove(position: Rectangle) {
const idx = this.find_door_index(position);
if (idx === -1) {
return;
}
this.doors.splice(idx, 1);
}
} }
</script> </script>

View file

@ -1,35 +1,24 @@
<template> <template>
<SVGRect <SVGRect
style="cursor: text"
:rectangle="door.position" :rectangle="door.position"
:variant="editing ? 'success' : 'primary'" :variant="editing ? 'success' : 'primary'"
/> @click.left="on_click"
<foreignObject
:x="Math.round(parent_aspect_ratio * door.position.left)"
:y="door.position.top"
:width="Math.round(parent_aspect_ratio * door.position.width)"
:height="door.position.height"
:style="`transform: scaleX(${1 / parent_aspect_ratio})`"
> >
<div <input
xmlns="http://www.w3.org/1999/xhtml" v-if="editing"
class="px-4 is-flex is-align-items-center is-justify-content-center" v-model="day_str"
@click.left="on_click" ref="day_input"
> class="input is-large"
<input type="number"
v-if="editing" :min="MIN_DAY"
v-model="day_str" placeholder="Tag"
ref="day_input" @keydown="on_keydown"
class="input is-large" />
type="number" <div v-else class="has-text-danger">
:min="MIN_DAY" {{ door.day > 0 ? door.day : "*" }}
placeholder="Tag"
@keydown="on_keydown"
/>
<div v-else class="is-size-1 has-text-weight-bold has-text-danger">
{{ door.day > 0 ? door.day : "*" }}
</div>
</div> </div>
</foreignObject> </SVGRect>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -52,48 +41,18 @@ export default class extends Vue {
public day_str = ""; public day_str = "";
public editing = false; public editing = false;
private refreshKey = 0;
declare $refs: { declare $refs: {
day_input: HTMLInputElement | unknown; day_input: HTMLInputElement | unknown;
}; };
private refresh() {
window.setTimeout(() => {
// don't loop endlessly
if (this.refreshKey < 10000) {
this.refreshKey++;
}
}, 100);
}
public get parent_aspect_ratio(): number {
this.refreshKey; // read it just to force recompute on change
if (!(this.$el instanceof Text) || this.$el.parentElement === null) {
this.refresh();
return 1;
}
const parent = this.$el.parentElement;
const result = parent.clientWidth / parent.clientHeight;
// force recompute for suspicious results
if (result === 0 || result === Infinity) {
this.refresh();
return 1;
}
return result;
}
private toggle_editing() { private toggle_editing() {
this.day_str = String(this.door.day); this.day_str = String(this.door.day);
this.editing = !this.editing; this.editing = !this.editing;
} }
public on_click(event: MouseEvent) { public on_click(event: MouseEvent) {
if (event.target === null || !(event.target instanceof HTMLDivElement)) { if (event.target === null || !(event.target instanceof SVGRectElement)) {
return; return;
} }
@ -131,13 +90,3 @@ export default class extends Vue {
} }
} }
</script> </script>
<style lang="scss" scoped>
foreignObject {
cursor: text;
> div {
height: inherit;
}
}
</style>