Skip to content

Commit

Permalink
Merge pull request #75 from sohnjunior/canvas-preview
Browse files Browse the repository at this point in the history
캔버스 미리보기를 추가합니다.
  • Loading branch information
sohnjunior authored Jun 13, 2023
2 parents 417459d + 10f1c44 commit f50d1cf
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 11 deletions.
Binary file added client/public/assets/sample/humor.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/public/assets/sample/robot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { html } from 'lit-html'
import VCanvasPreview from './canvas-preview'

export default {
title: 'Elements / Canvas Preview',
Expand All @@ -9,9 +10,51 @@ interface Props {
selected: boolean
}

export const Basic = ({ caption, selected }: Props) =>
html`<v-canvas-preview caption="${caption}" selected="${selected}"></v-canvas-preview>`
export const Basic = (
{ caption, selected }: Props,
{ loaded: { imageData } }: { loaded: { imageData: ImageData | undefined } }
) => {
const lazyLoadImageData = () => {
setTimeout(() => {
const $preview = document.querySelector<VCanvasPreview>('v-canvas-preview')
if ($preview) {
$preview.imageData = { image: imageData }
}
}, 0)
}

lazyLoadImageData()

return html`<v-canvas-preview caption="${caption}" selected="${selected}"></v-canvas-preview>`
}
Basic.args = {
caption: '프리뷰 설명',
selected: true,
}
Basic.loaders = [
async () => ({
imageData: await getImageData(),
}),
]

async function getImageData(): Promise<ImageData | undefined> {
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas')
canvas.width = 1000
canvas.height = 1000

const image = new Image()
image.src = 'assets/sample/robot.png'
image.addEventListener('load', () => {
const ctx = canvas.getContext('2d')
ctx?.drawImage(image, 0, 0, 900, 900)

const imageData = ctx?.getImageData(0, 0, 900, 900)
if (imageData) {
resolve(imageData)
} else {
reject()
}
})
})
}
93 changes: 91 additions & 2 deletions client/src/components/atoms/canvas-preview/canvas-preview.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { VComponent } from '@/modules/v-component'
import type { ReflectAttributeParam } from '@/modules/v-component/types'

import { reflectSnapshot } from '@/modules/canvas-engine'

const PREVIEW_CONTENT_SIZE = 100

const template = document.createElement('template')
template.innerHTML = `
<style>
Expand All @@ -22,8 +26,17 @@ template.innerHTML = `
box-sizing: border-box;
border: 2px solid var(--color-gray);
border-radius: 13px;
width: 100px;
height: 100px;
width: ${PREVIEW_CONTENT_SIZE}px;
height: ${PREVIEW_CONTENT_SIZE}px;
}
:host figure > .figure-content > .canvas-container {
position: relative;
}
:host figure > .figure-content > .canvas-container > canvas {
position: absolute;
top: 0;
}
:host figure > .figure-content > v-icon {
Expand All @@ -40,6 +53,10 @@ template.innerHTML = `
</style>
<figure>
<div class="figure-content">
<div class="canvas-container">
<canvas id="image-canvas" width="100%" height="100%"></canvas>
<canvas id="drawing-canvas" width="100%" height="100%"></canvas>
</div>
<v-icon icon="close-circle" size="xlarge"></v-icon>
</div>
<figcaption></figcaption>
Expand Down Expand Up @@ -71,6 +88,41 @@ export default class VCanvasPreview extends VComponent {
this.setAttribute('selected', `${newValue}`)
}

private _imageData: { drawing?: ImageData; image?: ImageData } = {}
get imageData() {
return this._imageData
}
set imageData(newValue: { drawing?: ImageData; image?: ImageData }) {
this._imageData = newValue
this.reflectImageData(newValue)
}

private async reflectImageData(snapshots: { drawing?: ImageData; image?: ImageData }) {
const $imageCanvas = this.$root.querySelector<HTMLCanvasElement>('#image-canvas')
const $drawingCanvas = this.$root.querySelector<HTMLCanvasElement>('#drawing-canvas')
if (!$imageCanvas || !$drawingCanvas) {
return
}

if (snapshots.image) {
const resizedImageData = await rescaleImageData(
snapshots.image,
PREVIEW_CONTENT_SIZE - 10,
PREVIEW_CONTENT_SIZE - 10
)
reflectSnapshot($imageCanvas, resizedImageData)
}

if (snapshots.drawing) {
const resizedImageData = await rescaleImageData(
snapshots.drawing,
PREVIEW_CONTENT_SIZE - 10,
PREVIEW_CONTENT_SIZE - 10
)
reflectSnapshot($drawingCanvas, resizedImageData)
}
}

protected bindEventListener() {
this.$root
.querySelector('v-icon[icon="close-circle"]')
Expand Down Expand Up @@ -108,3 +160,40 @@ export default class VCanvasPreview extends VComponent {
}
}
}

/**
* ImageData 를 _targetWidth_ 와 _targetHeight_ 에 맞도록 리스케일링 합니다.
* @reference
* https://stackoverflow.com/questions/55340888/fast-way-to-resize-imagedata-in-browser
*/
function rescaleImageData(originalImageData: ImageData, targetWidth: number, targetHeight: number) {
const targetImageData = new ImageData(targetWidth, targetHeight)
const h1 = originalImageData.height
const w1 = originalImageData.width
const h2 = targetImageData.height
const w2 = targetImageData.width
const kh = h1 / h2
const kw = w1 / w2
const cur_img1pixel_sum = new Int32Array(4)

for (let i2 = 0; i2 < h2; i2 += 1) {
for (let j2 = 0; j2 < w2; j2 += 1) {
for (const i in cur_img1pixel_sum) cur_img1pixel_sum[i] = 0
let cur_img1pixel_n = 0
for (let i1 = Math.ceil(i2 * kh); i1 < (i2 + 1) * kh; i1 += 1) {
for (let j1 = Math.ceil(j2 * kw); j1 < (j2 + 1) * kw; j1 += 1) {
const cur_p1 = (i1 * w1 + j1) * 4
for (let k = 0; k < 4; k += 1) {
cur_img1pixel_sum[k] += originalImageData.data[cur_p1 + k]
}
cur_img1pixel_n += 1
}
}
const cur_p2 = (i2 * w2 + j2) * 4
for (let k = 0; k < 4; k += 1) {
targetImageData.data[cur_p2 + k] = cur_img1pixel_sum[k] / cur_img1pixel_n
}
}
}
return targetImageData
}
26 changes: 19 additions & 7 deletions client/src/components/molecules/archive-menu/archive-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import type { ReflectAttributeParam } from '@/modules/v-component/types'
import VCanvasPreview from '@atoms/canvas-preview/canvas-preview'
import type { Archive } from '@/services/archive'

type ArchivePreview = Omit<Archive, 'snapshot' | 'images'>
type ArchivePreview = Omit<Archive, 'images'>

const template = document.createElement('template')
template.innerHTML = `
<style>
@media screen and (min-width: 421px) {
@media screen and (min-width: 431px) {
:host div.preview-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
Expand All @@ -20,13 +20,13 @@ template.innerHTML = `
}
}
@media screen and (max-width: 420px) {
@media screen and (max-width: 430px) {
:host div.preview-container {
display: grid;
grid-auto-flow: column;
grid-template-rows: repeat(2, 1fr);
grid-auto-flow: row;
grid-template-columns: repeat(2, 1fr);
gap: 10px 15px;
width: 200px;
width: 220px;
height: 270px;
overflow-x: scroll;
}
Expand Down Expand Up @@ -55,7 +55,7 @@ template.innerHTML = `
<div class="preview-container" slot="content">
<div data-value="add" class="add-new-canvas-button">
<v-icon icon="add-circle" size="large"></v-icon>
new canvas
create!
</div>
<!-- lazy load archives -->
</div>
Expand Down Expand Up @@ -93,6 +93,7 @@ export default class VArchiveMenu extends VComponent {
set archives(newValue: ArchivePreview[]) {
this._archives = newValue
this.updateArchives()
this.updatePreview()
this.updateValueProp(this.value)
}

Expand All @@ -112,6 +113,17 @@ export default class VArchiveMenu extends VComponent {
}
}

private updatePreview() {
const $previews = this.$root.querySelectorAll<VCanvasPreview>('v-canvas-preview')
this.archives.forEach((archive, idx) => {
const imageData = {
image: archive.imageSnapshot,
drawing: archive.snapshot,
}
$previews[idx].imageData = imageData
})
}

protected bindEventListener() {
this.$root.addEventListener('click', this.handleClickArchive)
this.$root.addEventListener('preview:delete', this.handleDeletePreview.bind(this))
Expand Down
5 changes: 5 additions & 0 deletions client/src/components/molecules/canvas-layer/image-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
drawArc,
getBearingDegree,
isTouchEvent,
takeSnapshot,
} from '@/modules/canvas-engine'
import { Point } from '@/modules/canvas-engine/types'
import type { Anchor, ImageTransform, ImageObject } from './types'
Expand Down Expand Up @@ -77,6 +78,10 @@ export default class VCanvasImageLayer extends VComponent<HTMLCanvasElement> {
return ['cursor', 'gallery'].includes(this.phase)
}

get imageSnapshot() {
return takeSnapshot(this.$root)
}

constructor() {
super(template)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,12 @@ export default class VCanvasContainer extends VComponent {
height: image.height,
degree: image.degree,
}))
const imageSnapshot = images.length > 0 ? this.imageLayer.imageSnapshot : undefined
await addOrUpdateArchive({
id: this.sid,
title: this.title,
snapshot: lastOf(this.snapshots),
imageSnapshot,
images,
})
ArchiveContext.dispatch({ action: 'FETCH_ARCHIVES_FROM_IDB' })
Expand All @@ -119,6 +121,7 @@ export default class VCanvasContainer extends VComponent {
id,
title: this.title,
snapshot: undefined,
imageSnapshot: undefined,
images: [],
}),
ArchiveContext.dispatch({ action: 'SET_SESSION_ID', data: id }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ export default class VCanvasToolbox extends VComponent {
const archivePreviews = archives.map((archive) => ({
id: archive.id,
title: archive.title,
snapshot: archive.snapshot,
imageSnapshot: archive.imageSnapshot,
}))
this.$archiveMenu.archives = archivePreviews
}
Expand Down
1 change: 1 addition & 0 deletions client/src/services/archive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface Archive {
id: string
title: string
snapshot?: ImageData
imageSnapshot?: ImageData
images: Omit<ImageObject, 'ref'>[] // NOTE: HTMLImageElement 는 웹 스토리지에 저장할 수 없기 때문에 제외
}

Expand Down

1 comment on commit f50d1cf

@vercel
Copy link

@vercel vercel bot commented on f50d1cf Jun 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

editty – ./

editty.vercel.app
editty-sohnjunior.vercel.app
editty-git-main-sohnjunior.vercel.app

Please sign in to comment.