-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
264 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
/** | ||
* @param {import('@commas/api/types').API} commas | ||
*/ | ||
module.exports = function (commas) { | ||
if (commas.app.isMainProcess()) { | ||
require('./dist/main').default() | ||
} else { | ||
require('./dist/renderer').default() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
{ | ||
"name": "@commas/camera", | ||
"private": true, | ||
"version": "0.1.0", | ||
"productName": "Camera", | ||
"description": "Capture page contents", | ||
"main": "index.js", | ||
"author": "commas", | ||
"license": "ISC", | ||
"commas:icon": { | ||
"name": "lucide-camera" | ||
}, | ||
"commas:i18n": { | ||
"zh-CN": { | ||
"productName": "照相机", | ||
"description": "捕获页面内容" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import * as commas from 'commas:api/main' | ||
import type { NativeImage, Rectangle } from 'electron' | ||
|
||
declare module '@commas/electron-ipc' { | ||
export interface Commands { | ||
'capture-page': (rect: Rectangle) => NativeImage, | ||
} | ||
} | ||
|
||
export default () => { | ||
|
||
commas.ipcMain.handle('capture-page', async (event, rect) => { | ||
return event.sender.capturePage(rect) | ||
}) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
<script lang="ts" setup> | ||
import { ipcRenderer } from '@commas/electron-ipc' | ||
import { refAutoReset } from '@vueuse/core' | ||
import * as commas from 'commas:api/renderer' | ||
import { clipboard, nativeImage } from 'electron' | ||
import icon from './assets/icon-stroked.svg' | ||
defineOptions({ | ||
inheritAttrs: false, | ||
}) | ||
const { VisualIcon } = commas.ui.vueAssets | ||
const theme = commas.remote.useTheme() | ||
let feedbacking = $(refAutoReset(false, 1000)) | ||
function createCanvas(width: number, height: number) { | ||
const canvas = document.createElement('canvas') | ||
canvas.width = width | ||
canvas.height = height | ||
const context = canvas.getContext('2d')! | ||
return { canvas, context } | ||
} | ||
function loadingElement<T extends HTMLElement>(element: T) { | ||
return new Promise<T>((resolve, reject) => { | ||
function handleLoad() { | ||
dispose() | ||
resolve(element) | ||
} | ||
function handleError(event: ErrorEvent) { | ||
reject(event.error) | ||
} | ||
function dispose() { | ||
element.removeEventListener('load', handleLoad) | ||
element.removeEventListener('error', handleError) | ||
} | ||
element.addEventListener('load', handleLoad, { once: true }) | ||
element.addEventListener('error', handleError, { once: true }) | ||
}) | ||
} | ||
async function loadImage(url: string) { | ||
const image = new Image() | ||
image.src = url | ||
return loadingElement(image) | ||
} | ||
interface Rect { | ||
x: number, | ||
y: number, | ||
width: number, | ||
height: number, | ||
} | ||
function roundCorners( | ||
context: CanvasRenderingContext2D, | ||
rect: Rect, | ||
radius: number, | ||
) { | ||
const { x, y, width, height } = rect | ||
context.moveTo(x + radius, y) | ||
context.lineTo(x + width - radius, y) | ||
context.quadraticCurveTo(x + width, y, x + width, y + radius) | ||
context.lineTo(x + width, y + height - radius) | ||
context.quadraticCurveTo(x + width, y + height, x + width - radius, y + height) | ||
context.lineTo(x + radius, y + height) | ||
context.quadraticCurveTo(x, y + height, x, y + height - radius) | ||
context.lineTo(x, y + radius) | ||
context.quadraticCurveTo(x, y, x + radius, y) | ||
} | ||
async function createIconCanvas(color: string) { | ||
const size = 24 * devicePixelRatio | ||
const { canvas, context } = createCanvas(size, size) | ||
const image = await loadImage(icon) | ||
context.drawImage(image, 0, 0, canvas.width, canvas.height) | ||
const data = context.getImageData(0, 0, canvas.width, canvas.height) | ||
const bytes = data.data | ||
const rgba = commas.helper.toRGBA(color) | ||
for (let i = 0; i < bytes.length; i += 4) { | ||
bytes[i] = rgba.r | ||
bytes[i + 1] = rgba.g | ||
bytes[i + 2] = rgba.b | ||
bytes[i + 3] = bytes[i + 3] * rgba.a | ||
} | ||
context.putImageData(data, 0, 0) | ||
return canvas | ||
} | ||
async function capture() { | ||
const activeElement = document.querySelector('.terminal-view > .active') | ||
if (activeElement) { | ||
const { x, y, width, height } = activeElement.getBoundingClientRect() | ||
const screenshot = await ipcRenderer.invoke('capture-page', { x, y, width, height }) | ||
const size = screenshot.getSize() | ||
const { canvas, context } = createCanvas( | ||
size.width + 128 * devicePixelRatio, | ||
size.height + 128 * devicePixelRatio, | ||
) | ||
const watermarkColor = 'rgb(255 255 255 / 50%)' | ||
const iconCanvas = await createIconCanvas(watermarkColor) | ||
const screenshotImage = await loadImage(screenshot.toDataURL()) | ||
// Gradient background | ||
context.save() | ||
const gradient = context.createLinearGradient(0, 0, canvas.width, canvas.height) | ||
const systemAccentRGBA = commas.helper.toRGBA(theme.systemAccent) | ||
const backgroundRGBA = commas.helper.toRGBA(theme.background) | ||
gradient.addColorStop(0, commas.helper.toCSSHEX(commas.helper.mix(systemAccentRGBA, backgroundRGBA, 0.12))) | ||
gradient.addColorStop(1, commas.helper.toCSSHEX(commas.helper.mix(systemAccentRGBA, backgroundRGBA, 0.48))) | ||
context.fillStyle = gradient | ||
context.fillRect(0, 0, canvas.width, canvas.height) | ||
context.restore() | ||
// Translucent border | ||
context.save() | ||
context.beginPath() | ||
context.fillStyle = commas.helper.toCSSHEX({ ...backgroundRGBA, a: 0.5 }) | ||
context.shadowOffsetX = 0 | ||
context.shadowOffsetY = 2 * devicePixelRatio | ||
context.shadowBlur = 4 * devicePixelRatio | ||
context.shadowColor = 'rgb(0 0 0 / 10%)' | ||
roundCorners( | ||
context, | ||
{ | ||
x: (canvas.width - (size.width + 8 * devicePixelRatio)) / 2, | ||
y: (canvas.height - (size.height + 8 * devicePixelRatio)) / 2, | ||
width: size.width + 8 * devicePixelRatio, | ||
height: size.height + 8 * devicePixelRatio, | ||
}, | ||
8 * devicePixelRatio, | ||
) | ||
context.fill() | ||
context.closePath() | ||
context.clip() | ||
context.restore() | ||
// Screenshot | ||
context.save() | ||
context.beginPath() | ||
context.fillStyle = theme.background | ||
roundCorners( | ||
context, | ||
{ | ||
x: (canvas.width - size.width) / 2, | ||
y: (canvas.height - size.height) / 2, | ||
width: size.width, | ||
height: size.height, | ||
}, | ||
8 * devicePixelRatio, | ||
) | ||
context.fill() | ||
context.closePath() | ||
context.clip() | ||
context.drawImage( | ||
screenshotImage, | ||
(canvas.width - size.width) / 2, | ||
(canvas.height - size.height) / 2, | ||
) | ||
context.restore() | ||
// Watermark text | ||
context.save() | ||
context.font = 'bold 24px system-ui' | ||
context.fillStyle = watermarkColor | ||
context.textBaseline = 'middle' | ||
const text = 'Commas' | ||
const metrics = context.measureText(text) | ||
const spacing = 8 * devicePixelRatio | ||
const watermarkSize = iconCanvas.width + spacing + metrics.width | ||
context.fillText( | ||
text, | ||
canvas.width / 2 - watermarkSize / 2 + iconCanvas.width + spacing, | ||
canvas.height - (canvas.height - size.height) / 4, | ||
) | ||
context.restore() | ||
// Watermark icon | ||
context.save() | ||
context.drawImage( | ||
iconCanvas, | ||
canvas.width / 2 - watermarkSize / 2, | ||
canvas.height - (canvas.height - size.height) / 4 - iconCanvas.height / 2, | ||
) | ||
context.restore() | ||
clipboard.writeImage(nativeImage.createFromDataURL(canvas.toDataURL('png'))) | ||
feedbacking = true | ||
} | ||
} | ||
</script> | ||
|
||
<template> | ||
<div | ||
v-bind="$attrs" | ||
class="camera-anchor" | ||
@click="capture" | ||
> | ||
<VisualIcon v-if="feedbacking" name="lucide-clipboard-check" /> | ||
<VisualIcon v-else name="lucide-camera" /> | ||
</div> | ||
</template> |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import * as commas from 'commas:api/renderer' | ||
import CameraAnchor from './CameraAnchor.vue' | ||
|
||
export default () => { | ||
|
||
commas.context.provide('terminal.ui-right-action-anchor', CameraAnchor) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters