Skip to content

Commit d56ea36

Browse files
committed
image occlusion project
* basic shapes - rectangle - ellipse - polygon * some other tools - undo & redo - group & ungroup - zoom tools - copy
1 parent 88e0292 commit d56ea36

18 files changed

+1364
-6
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"codemirror": "^5.63.1",
7575
"css-browser-selector": "^0.6.5",
7676
"d3": "^7.0.0",
77+
"fabric": "^5.3.0",
7778
"fuse.js": "^6.6.2",
7879
"gemoji": "^7.1.0",
7980
"intl-pluralrules": "^1.2.2",
@@ -82,6 +83,7 @@
8283
"lodash-es": "^4.17.21",
8384
"marked": "^4.0.0",
8485
"mathjax": "^3.1.2",
86+
"panzoom": "^9.4.3",
8587
"protobufjs": "^7"
8688
},
8789
"resolutions": {
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<!--
2+
Copyright: Ankitects Pty Ltd and contributors
3+
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
4+
-->
5+
<script lang="ts">
6+
import { fabric } from "fabric";
7+
import panzoom from "panzoom";
8+
import protobuf from "protobufjs";
9+
10+
import { getImageClozeMetadata } from "./lib";
11+
import SideToolbar from "./SideToolbar.svelte";
12+
import { zoomResetValue } from "./store";
13+
import { undoRedoInit } from "./tools/tool-undo-redo";
14+
15+
16+
export let path: string;
17+
18+
let instance;
19+
let innerWidth = 0;
20+
21+
let canvas: fabric.Canvas;
22+
getImageClozeMetadata(path).then((metadata) => {
23+
const b64encoded = protobuf.util.base64.encode(
24+
metadata.data,
25+
0,
26+
metadata.data.length,
27+
);
28+
const data = "data:image/png;base64," + b64encoded;
29+
30+
canvas = new fabric.Canvas("canvas", {
31+
hoverCursor: "pointer",
32+
selectionBorderColor: "green",
33+
});
34+
35+
// for debug in devtools
36+
globalThis.canvas = canvas;
37+
38+
// get image width and height
39+
const image = new Image();
40+
image.onload = function () {
41+
canvas.setWidth(image.width);
42+
canvas.setHeight(image.height);
43+
44+
fabric.Image.fromURL(image.src, function (image) {
45+
canvas.setBackgroundImage(image, canvas.renderAll.bind(canvas), {
46+
scaleX: canvas.width! / image.width!,
47+
scaleY: canvas.height! / image.height!,
48+
});
49+
50+
const zoomRatio = innerWidth / canvas.width!;
51+
zoomResetValue.set(zoomRatio);
52+
instance!.smoothZoom(0, 0, zoomRatio);
53+
});
54+
};
55+
56+
undoRedoInit(canvas);
57+
58+
image.src = data;
59+
image.remove();
60+
});
61+
62+
function initPanzoom(node) {
63+
instance = panzoom(node, {
64+
bounds: true,
65+
maxZoom: 3,
66+
minZoom: 0.1,
67+
zoomDoubleClickSpeed: 1,
68+
});
69+
instance.pause();
70+
}
71+
</script>
72+
73+
<div><SideToolbar {instance} {canvas} /></div>
74+
<div class="editor-main" bind:clientWidth={innerWidth}>
75+
<div class="editor-container" use:initPanzoom>
76+
<canvas id="canvas" />
77+
</div>
78+
</div>
79+
80+
<style lang="scss">
81+
.editor-main {
82+
position: absolute;
83+
top: 80px;
84+
left: 36px;
85+
bottom: 2px;
86+
right: 2px;
87+
border: 1px solid rgb(96, 141, 225);
88+
overflow: auto;
89+
padding-bottom: 100px;
90+
}
91+
92+
.editor-container {
93+
width: 100%;
94+
height: 100%;
95+
}
96+
</style>
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<!--
2+
Copyright: Ankitects Pty Ltd and contributors
3+
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
4+
-->
5+
<script lang="ts">
6+
import IconButton from "../components/IconButton.svelte";
7+
import { drawRectangle, drawEllipse, drawPolygon } from "./tools/index";
8+
import { enableSelectable, fillShapeColor, stopDraw } from "./tools/lib";
9+
import { tools } from "./tools/tool-buttons";
10+
import TopToolbar from "./TopToolbar.svelte";
11+
12+
export let instance;
13+
export let canvas;
14+
15+
const iconSize = 80;
16+
let activeTool = "cursor";
17+
let toolTopPos = 0;
18+
19+
function setActive(toolId) {
20+
activeTool = toolId;
21+
disableFunctions();
22+
23+
switch (toolId) {
24+
case "cursor":
25+
enableSelectable(canvas, true);
26+
break;
27+
case "magnify":
28+
instance.resume();
29+
break;
30+
case "draw-rectangle":
31+
drawRectangle(canvas);
32+
break;
33+
case "draw-ellipse":
34+
drawEllipse(canvas);
35+
break;
36+
case "draw-polygon":
37+
drawPolygon(canvas, instance);
38+
break;
39+
case "shape-fill-color":
40+
enableSelectable(canvas, true);
41+
break;
42+
default:
43+
break;
44+
}
45+
}
46+
47+
const disableFunctions = () => {
48+
instance.pause();
49+
stopDraw(canvas);
50+
enableSelectable(canvas, false);
51+
};
52+
53+
const changeShapeFillColor = (node: any) => {
54+
if (node) {
55+
fillShapeColor(canvas, node.target.value);
56+
}
57+
};
58+
</script>
59+
60+
<TopToolbar {canvas} {activeTool} {instance} {iconSize} />
61+
62+
<div class="tool-bar-container">
63+
{#each tools as tool}
64+
<IconButton
65+
class="tool-icon-button {activeTool == tool.id ? 'active-tool' : ''}"
66+
{iconSize}
67+
active={activeTool === tool.id}
68+
on:click={(e) => {
69+
setActive(tool.id);
70+
// popup position for color dialog
71+
toolTopPos = e.pageY - 60;
72+
}}>{@html tool.icon}</IconButton
73+
>
74+
{/each}
75+
</div>
76+
77+
<style>
78+
.tool-bar-container {
79+
position: fixed;
80+
top: 42px;
81+
left: 0;
82+
height: 100%;
83+
border-right: 1px solid #e3e3e3;
84+
overflow-y: auto;
85+
width: 32px;
86+
z-index: 99;
87+
background: white;
88+
padding-bottom: 100px;
89+
}
90+
91+
:global(.tool-icon-button) {
92+
border: unset;
93+
display: block;
94+
width: 32px;
95+
height: 32px;
96+
margin: unset;
97+
padding: 6px !important;
98+
}
99+
100+
:global(.active-tool) {
101+
color: red !important;
102+
background: unset !important;
103+
}
104+
105+
::-webkit-scrollbar {
106+
width: 0.2em !important;
107+
height: 0.2em !important;
108+
}
109+
</style>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<!--
2+
Copyright: Ankitects Pty Ltd and contributors
3+
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
4+
-->
5+
<script>
6+
import IconButton from "../components/IconButton.svelte";
7+
import { cursorTools, zoomTools } from "./tools/more-tools";
8+
import { undoRedoTools } from "./tools/tool-undo-redo";
9+
10+
export let activeTool = "cursor";
11+
export let canvas;
12+
export let instance;
13+
export let iconSize;
14+
</script>
15+
16+
<div class="top-tool-bar-container">
17+
{#each undoRedoTools as undoRedoTool}
18+
<IconButton
19+
class="top-tool-icon-button"
20+
{iconSize}
21+
on:click={() => {
22+
undoRedoTool.action(canvas);
23+
}}
24+
>
25+
{@html undoRedoTool.icon}
26+
</IconButton>
27+
{/each}
28+
29+
{#each zoomTools as zoomBottomTool}
30+
<IconButton
31+
class="top-tool-icon-button"
32+
{iconSize}
33+
on:click={() => {
34+
zoomBottomTool.action(instance);
35+
}}
36+
>
37+
{@html zoomBottomTool.icon}
38+
</IconButton>
39+
{/each}
40+
41+
{#if activeTool === "cursor"}
42+
{#each cursorTools as cursorBottomTool}
43+
<IconButton
44+
class="top-tool-icon-button"
45+
{iconSize}
46+
on:click={() => {
47+
cursorBottomTool.action(canvas);
48+
}}
49+
>
50+
{@html cursorBottomTool.icon}
51+
</IconButton>
52+
{/each}
53+
{/if}
54+
</div>
55+
56+
<style>
57+
.top-tool-bar-container {
58+
position: fixed;
59+
top: 42px;
60+
left: 36px;
61+
width: 100%;
62+
border-right: 1px solid #e3e3e3;
63+
overflow-y: auto;
64+
z-index: 99;
65+
background: white;
66+
}
67+
68+
:global(.top-tool-icon-button) {
69+
border: unset;
70+
display: inline;
71+
width: 32px;
72+
height: 32px;
73+
margin: unset;
74+
padding: 6px !important;
75+
}
76+
</style>

0 commit comments

Comments
 (0)