From 7b3aeee90f84a3a2307e9650ae77c34f026d99c9 Mon Sep 17 00:00:00 2001 From: Edo <0xe1216@gmail.com> Date: Fri, 19 Sep 2025 14:43:28 +0400 Subject: [PATCH 1/2] feat: update demo --- react_app_demo/README.md | 110 +- react_app_demo/config-overrides.js | 25 - react_app_demo/index.html | 17 + react_app_demo/package.json | 45 +- react_app_demo/src/App.css | 160 +-- react_app_demo/src/App.js | 26 - react_app_demo/src/App.test.js | 9 - react_app_demo/src/App.tsx | 33 + .../src/components/Canvas/Canvas.css | 167 +++ .../src/components/Canvas/Canvas.js | 136 -- .../src/components/Canvas/Canvas.tsx | 241 ++++ .../src/components/Canvas/package.json | 6 - react_app_demo/src/index.css | 29 +- react_app_demo/src/index.js | 12 - react_app_demo/src/main.tsx | 10 + react_app_demo/src/serviceWorker.js | 135 -- react_app_demo/src/types.d.ts | 4 + react_app_demo/src/vite-env.d.ts | 1 + react_app_demo/tsconfig.json | 19 + react_app_demo/vite.config.ts | 17 + site/src/demo.html | 76 +- site/src/scripts/demo.js | 1209 ++++++++++------- site/src/scripts/script.js | 760 +++++++---- webpack_demo/README.md | 14 +- webpack_demo/index.html | 372 +++-- webpack_demo/js/index.js | 368 ----- webpack_demo/package.json | 37 +- .../{js => src/assets}/blue_metro.jpg | Bin .../{js => src/assets}/daisies_fuji.jpg | Bin .../{js => src/assets}/daisies_med.jpg | Bin .../{js => src/assets}/nine_yards.jpg | Bin .../{js => src/assets}/underground.jpg | Bin webpack_demo/{js => src/assets}/wasm_logo.png | Bin webpack_demo/src/global.d.ts | 14 + webpack_demo/src/index.ts | 507 +++++++ webpack_demo/tsconfig.json | 14 + webpack_demo/webpack.config.js | 112 +- 37 files changed, 2599 insertions(+), 2086 deletions(-) delete mode 100644 react_app_demo/config-overrides.js create mode 100644 react_app_demo/index.html delete mode 100644 react_app_demo/src/App.js delete mode 100644 react_app_demo/src/App.test.js create mode 100644 react_app_demo/src/App.tsx delete mode 100644 react_app_demo/src/components/Canvas/Canvas.js create mode 100644 react_app_demo/src/components/Canvas/Canvas.tsx delete mode 100644 react_app_demo/src/components/Canvas/package.json delete mode 100644 react_app_demo/src/index.js create mode 100644 react_app_demo/src/main.tsx delete mode 100644 react_app_demo/src/serviceWorker.js create mode 100644 react_app_demo/src/types.d.ts create mode 100644 react_app_demo/src/vite-env.d.ts create mode 100644 react_app_demo/tsconfig.json create mode 100644 react_app_demo/vite.config.ts delete mode 100644 webpack_demo/js/index.js rename webpack_demo/{js => src/assets}/blue_metro.jpg (100%) rename webpack_demo/{js => src/assets}/daisies_fuji.jpg (100%) rename webpack_demo/{js => src/assets}/daisies_med.jpg (100%) rename webpack_demo/{js => src/assets}/nine_yards.jpg (100%) rename webpack_demo/{js => src/assets}/underground.jpg (100%) rename webpack_demo/{js => src/assets}/wasm_logo.png (100%) create mode 100644 webpack_demo/src/global.d.ts create mode 100644 webpack_demo/src/index.ts create mode 100644 webpack_demo/tsconfig.json diff --git a/react_app_demo/README.md b/react_app_demo/README.md index d2d55c1..e1de96b 100644 --- a/react_app_demo/README.md +++ b/react_app_demo/README.md @@ -3,120 +3,56 @@ #### Instructions for Running 1. Clone this repo. -```sh -git clone https://github.com/silvia-odwyer/photon -``` - -2. Navigate to `crate` and run `wasm-pack build`. Ensure you have wasm-pack installed. - -```sh -cd crate -wasm-pack build -``` - -This will create an npm package, which can be found in `crate/pkg`. - -3. Navigate to pkg, and run `npm link`. - ```sh -cd pkg -npm link +git clone https://github.com/silvia-odwyer/photon ``` -5. Then, navigate to this dir, and install dependencies. - +2. Install the demo dependencies. ```sh -cd ../../react_app_demo +cd react_app_demo npm install ``` -This app also requires changing the project's webpack.config.js to load WASM files; however this can't be done -without ejecting, so to combat this, I used react-app-rewired to allow for overriding the webpack config. - -Install react-app-rewired and wasm-loader: - +3. Start a development server at localhost:5173. ```sh -npm install react-app-rewired wasm-loader -D +npm run dev ``` +Vite will open the app in your browser and hot-reload when you edit sources. -4. Run `npm link photon`. -```sh -npm link photon -``` - -6. Start a development server at localhost:3000 - +4. Build an optimised production bundle. ```sh -npm run start +npm run build ``` +Use `npm run preview` to verify the build locally, or `npm run lint` to run TypeScript in no-emit mode. -### Create React App Notes +> Want to test against a locally compiled Photon crate? Run `wasm-pack build` inside `crate/` and point the demo at the generated `crate/pkg` output (e.g. via `npm link`). -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +### Vite + React Notes + +This project is bootstrapped with [Vite](https://vitejs.dev/) and React 19 with TypeScript. The Photon WebAssembly package is loaded directly from npm and initialised before the UI becomes interactive. ## Available Scripts In the project directory, you can run: -### `npm start` - -Runs the app in the development mode.
-Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +### `npm run dev` -The page will reload if you make edits.
-You will also see any lint errors in the console. - -### `npm test` - -Launches the test runner in the interactive watch mode.
-See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. +Runs the app in development mode at [http://localhost:5173](http://localhost:5173) with hot module replacement. ### `npm run build` -Builds the app for production to the `build` folder.
-It correctly bundles React in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.
-Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `npm run eject` +Generates a production-ready bundle in `dist/` and runs the TypeScript compiler as part of the build pipeline. -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** +### `npm run preview` -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. +Serves the production bundle locally so you can sanity-check the optimised output. -Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. +### `npm run lint` -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. +Type-checks the project (`tsc --noEmit`) without writing any build artifacts. ## Learn More -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). - -To learn React, check out the [React documentation](https://reactjs.org/). - -### Code Splitting - -This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting - -### Analyzing the Bundle Size - -This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size - -### Making a Progressive Web App - -This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app - -### Advanced Configuration - -This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration - -### Deployment - -This section has moved here: https://facebook.github.io/create-react-app/docs/deployment - -### `npm run build` fails to minify - -This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify +- [Photon documentation](https://silvia-odwyer.github.io/photon/docs/photon/index.html) +- [Vite documentation](https://vitejs.dev/guide/) +- [React documentation](https://react.dev/) diff --git a/react_app_demo/config-overrides.js b/react_app_demo/config-overrides.js deleted file mode 100644 index 559c4cd..0000000 --- a/react_app_demo/config-overrides.js +++ /dev/null @@ -1,25 +0,0 @@ -const path = require('path'); - -module.exports = function override(config, env) { - const wasmExtensionRegExp = /\.wasm$/; - - config.resolve.extensions.push('.wasm'); - - config.module.rules.forEach(rule => { - (rule.oneOf || []).forEach(oneOf => { - if (oneOf.loader && oneOf.loader.indexOf('file-loader') >= 0) { - // Make file-loader ignore WASM files - oneOf.exclude.push(wasmExtensionRegExp); - } - }); - }); - - // Add a dedicated loader for WASM - config.module.rules.push({ - test: wasmExtensionRegExp, - include: path.resolve(__dirname, 'src'), - use: [{ loader: require.resolve('wasm-loader'), options: {} }] - }); - - return config; -}; \ No newline at end of file diff --git a/react_app_demo/index.html b/react_app_demo/index.html new file mode 100644 index 0000000..5578b48 --- /dev/null +++ b/react_app_demo/index.html @@ -0,0 +1,17 @@ + + + + + + Photon React Demo + + + + +
+ + + diff --git a/react_app_demo/package.json b/react_app_demo/package.json index 26be2e8..a6ab7a4 100644 --- a/react_app_demo/package.json +++ b/react_app_demo/package.json @@ -1,37 +1,24 @@ { - "name": "photon-react-app", - "version": "0.1.0", + "name": "photon-react-demo", + "version": "1.0.0", "private": true, - "dependencies": { - "@silvia-odwyer/photon": "^0.1.0", - "@silvia-odwyer/photon-node": "^0.1.0", - "react": "^16.8.6", - "react-dom": "^16.8.6", - "react-scripts": "3.0.1", - "wasm-worker": "^0.4.0" - }, + "type": "module", "scripts": { - "start": "react-app-rewired start", - "build": "react-app-rewired build", - "test": "react-app-rewired test" + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "tsc --noEmit" }, - "eslintConfig": { - "extends": "react-app" - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] + "dependencies": { + "@silvia-odwyer/photon": "^0.3.3", + "react": "^19.1.1", + "react-dom": "^19.1.1" }, "devDependencies": { - "react-app-rewired": "^2.1.3", - "wasm-loader": "^1.3.0" + "@types/react": "^19.1.13", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.3", + "typescript": "^5.9.2", + "vite": "^7.1.6" } } diff --git a/react_app_demo/src/App.css b/react_app_demo/src/App.css index 87e82c4..67b2f47 100644 --- a/react_app_demo/src/App.css +++ b/react_app_demo/src/App.css @@ -1,146 +1,38 @@ -.App-header { - background-color: #282c34; - display: flex; - flex-direction: column; - color: white; -} - -.App-link { - color: #61dafb; -} - -body { - background-color: rgb(34, 34, 34); - font-size: 22px; -} - -.default { - background-color: #282c34; - height: 100%; +.app { + min-height: 100vh; + background-color: transparent; } - -.main-sidebar, .main-navbar, .dark { - background-color: rgb(34, 34, 34); - color: rgb(105, 105, 105); -} - -.main { - margin-left: 40vh; - padding: 0 10px; - background-color: rgb(34, 34, 34); -} - -.main_content { - padding-top: 14vh; -} - -.sidebar { - height: 100%; - width: 40vh; - position: fixed; - z-index: 1; - top: 0; - left: 0; - background-color: rgb(31, 31, 31); - overflow-x: hidden; - padding-top: 20px; -} - -.sidebar li { - display: block; - color: rgb(105, 105, 105); -} - -@media screen and (max-height: 450px) { - .sidebar {padding-top: 15px;} - .sidebar a {font-size: 18px;} -} - -nav { - overflow: hidden; - background-color: rgb(29, 29, 29); - position: fixed; /* Set the navbar to fixed position */ - top: 0; /* Position the navbar at the top of the page */ - width: 100%; /* Full width */ -} - -nav li { - float: left; - display: block; - color: #f2f2f2; - text-align: center; - padding: 14px 16px; - text-decoration: none; - font-family: "Roboto", sans-serif; -} - -ul li { - text-decoration: none; - list-style: none; - padding-right: 4em; -} - -ul li:hover { - color: silver; -} - -.tab_nav { +.app__header { display: flex; - flex-direction: row; - flex-wrap: wrap; - margin-left: 0; -} - -.logo { - color: white; - font-size: 1em; - margin-left: 1em; + flex-direction: column; + min-height: 100vh; } -h2, h3, h4, h5 { - color: white; - font-family: "Roboto", sans-serif; - font-weight: 300; +.app__nav { + display: flex; + justify-content: flex-end; + gap: 1.5rem; + padding: 1.25rem 2.5rem; + background: rgba(13, 17, 23, 0.92); + backdrop-filter: blur(18px); + box-shadow: 0 2px 12px rgba(4, 6, 10, 0.35); + position: sticky; + top: 0; + z-index: 10; } -h4 { +.app__link { + font-size: 0.95rem; + letter-spacing: 0.06em; text-transform: uppercase; - font-size: 0.7em; - letter-spacing: 4px; -} - -.content { - grid-area: mainLeft; - width: 80%; - -} - -.benchmarks { - color: white; - font-size: 2em; - grid-area: mainRight; -} - -.main_content { - display: grid; - grid-template-columns: 75% 25%; - grid-template-rows: auto; - grid-template-areas: "mainLeft mainRight"; -} - - -a { + color: #d0d6f9; text-decoration: none; - color: #f2f2f2; + transition: color 0.2s ease, transform 0.2s ease; } -li:hover{ - cursor: pointer; +.app__link:hover, +.app__link:focus-visible { + color: #8ab4ff; + transform: translateY(-1px); } - -.topnav { - display: flex; - flex-direction: row; - justify-content: flex-end; -} \ No newline at end of file diff --git a/react_app_demo/src/App.js b/react_app_demo/src/App.js deleted file mode 100644 index 6199173..0000000 --- a/react_app_demo/src/App.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import './App.css'; - -import Canvas from "./components/Canvas" - -const App = () => { - - return ( -
-
- - - -
- -
- ); -}; - -export default App; \ No newline at end of file diff --git a/react_app_demo/src/App.test.js b/react_app_demo/src/App.test.js deleted file mode 100644 index a754b20..0000000 --- a/react_app_demo/src/App.test.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './App'; - -it('renders without crashing', () => { - const div = document.createElement('div'); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); -}); diff --git a/react_app_demo/src/App.tsx b/react_app_demo/src/App.tsx new file mode 100644 index 0000000..5687839 --- /dev/null +++ b/react_app_demo/src/App.tsx @@ -0,0 +1,33 @@ +import type { JSX } from "react"; +import Canvas from "./components/Canvas/Canvas"; +import "./App.css"; + +const App = (): JSX.Element => { + return ( +
+
+ + +
+
+ ); +}; + +export default App; diff --git a/react_app_demo/src/components/Canvas/Canvas.css b/react_app_demo/src/components/Canvas/Canvas.css index e69de29..c4589ad 100644 --- a/react_app_demo/src/components/Canvas/Canvas.css +++ b/react_app_demo/src/components/Canvas/Canvas.css @@ -0,0 +1,167 @@ +.canvas-shell { + flex: 1; + display: flex; + min-height: calc(100vh - 4rem); + background: linear-gradient(135deg, rgba(12, 16, 24, 0.95) 0%, rgba(14, 20, 32, 0.9) 100%); + border-top: 1px solid rgba(84, 104, 255, 0.15); +} + +.canvas-sidebar { + width: 280px; + display: flex; + flex-direction: column; + gap: 1.5rem; + padding: 2.25rem 1.75rem; + background: rgba(9, 13, 22, 0.85); + backdrop-filter: blur(16px); + border-right: 1px solid rgba(84, 104, 255, 0.14); +} + +.canvas-sidebar__header { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.canvas-sidebar__logo { + font-size: 1.25rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #ffffff; +} + +.canvas-sidebar__subtitle { + font-size: 0.75rem; + color: #7d8fff; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.canvas-sidebar__section { + display: flex; + flex-direction: column; + gap: 0.65rem; +} + +.canvas-sidebar__title { + margin: 0; + font-size: 0.75rem; + letter-spacing: 0.2em; + text-transform: uppercase; + color: #9fa7ff; +} + +.canvas-sidebar__button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.55rem 0.75rem; + border-radius: 8px; + border: 1px solid rgba(113, 122, 255, 0.35); + background: rgba(27, 34, 51, 0.75); + color: #e9ebff; + cursor: pointer; + transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; +} + +.canvas-sidebar__button:hover, +.canvas-sidebar__button:focus-visible { + transform: translateY(-1px); + border-color: rgba(138, 148, 255, 0.8); + box-shadow: 0 6px 16px rgba(34, 45, 86, 0.35); +} + +.canvas-sidebar__button:disabled { + cursor: not-allowed; + opacity: 0.45; + transform: none; + box-shadow: none; +} + +.canvas-sidebar__button--secondary { + margin-top: auto; + border-color: rgba(116, 206, 255, 0.4); + background: rgba(19, 95, 139, 0.3); +} + +.canvas-stage { + flex: 1; + display: flex; + flex-direction: column; + padding: 2rem clamp(1.5rem, 4vw, 3rem); + gap: 1.5rem; +} + +.canvas-stage__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.canvas-stage__title { + margin: 0; + font-weight: 600; + letter-spacing: 0.04em; +} + +.canvas-stage__element { + flex: 1; + width: 100%; + max-width: min(960px, 90vw); + box-shadow: 0 20px 40px rgba(6, 10, 20, 0.5); + border-radius: 18px; + border: 1px solid rgba(109, 132, 255, 0.25); + background: radial-gradient(circle at top left, rgba(255, 255, 255, 0.08), rgba(13, 19, 30, 0.92)); +} + +.canvas-status { + font-size: 0.8rem; + letter-spacing: 0.08em; + text-transform: uppercase; + padding: 0.35rem 0.75rem; + border-radius: 999px; + border: 1px solid currentColor; + backdrop-filter: blur(10px); +} + +.canvas-status--loading { + color: #f0c674; +} + +.canvas-status--ready { + color: #9aff9a; +} + +.canvas-status--error { + color: #ff8a8a; +} + +@media (max-width: 980px) { + .canvas-shell { + flex-direction: column; + } + + .canvas-sidebar { + width: 100%; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + gap: 1rem 1.25rem; + padding: 1.5rem clamp(1rem, 5vw, 2rem); + } + + .canvas-sidebar__section { + flex: 1 1 220px; + } + + .canvas-sidebar__button--secondary { + margin-top: 0; + } + + .canvas-stage__element { + max-width: 100%; + min-height: 320px; + } +} diff --git a/react_app_demo/src/components/Canvas/Canvas.js b/react_app_demo/src/components/Canvas/Canvas.js deleted file mode 100644 index 3b9b70f..0000000 --- a/react_app_demo/src/components/Canvas/Canvas.js +++ /dev/null @@ -1,136 +0,0 @@ -import React from 'react'; -import img_src from './daisies.jpg'; - -class Canvas extends React.Component { - constructor(props) { - super(props); - this.state = { - count: 0, - loadedWasm: false, - isLoaded: false, - wasm: null, - img: null - }; - } - componentDidMount() { - this.loadWasm(); - } - - drawOriginalImage = async () => { - const img = new Image(); - - img.onload = () => { - this.img = img; - const canvas = this.refs.canvas; - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext("2d"); - - ctx.drawImage(img, 0, 0); - - } - img.src = img_src; - } - - loadWasm = async () => { - - try { - const photon = await import('@silvia-odwyer/photon'); - - this.wasm = photon; - - this.drawOriginalImage(); - - } finally { - console.log("loaded wasm successfully"); - this.loadedWasm = true; - console.log("this.loadedWasm is", this.loadedWasm); - } - - } - - alterChannel = async (channel_index) => { - const canvas1 = this.refs.canvas; - const ctx = canvas1.getContext("2d"); - - ctx.drawImage(this.img, 0, 0); - - let photon = this.wasm; - - // Convert the canvas and context to a PhotonImage - let image = photon.open_image(canvas1, ctx); - - // Filter the image - photon.alter_channel(image, channel_index, 50); - - // Replace the current canvas' ImageData with the new image's ImageData. - photon.putImageData(canvas1, ctx, image); - - } - - effectPipeline = async() => { - const canvas1 = this.refs.canvas; - const ctx = canvas1.getContext("2d"); - - ctx.drawImage(this.img, 0, 0); - - let photon = this.wasm; - let phtimg = photon.open_image(canvas1, ctx); - - console.time("PHOTON_WITH_RAWPIX"); - photon.alter_channel(phtimg, 2, 70); - photon.grayscale(phtimg); - console.timeEnd("PHOTON_WITH_RAWPIX"); - - // // Replace the current canvas' ImageData with the new image's ImageData. - photon.putImageData(canvas1, ctx, phtimg); - - - - console.time("PHOTON_CONSTR"); - // photon.canvas_wasm_only(canvas1, ctx); - console.timeEnd("PHOTON_CONSTR"); - } - - render() { - return( -
- -
-

Photon

- -
    -

    Channels

    -
  • this.alterChannel(0)}>Increase Red Channel
  • -
  • this.alterChannel(1)}>Increase Green Channel
  • -
  • this.alterChannel(2)}>Increase Blue Channel
  • - -
  • Inc Channel + Threshold
  • - -
-
- - -
-
- -
-

Image

- -
- -
-
-
-
- -
- -
- -
- ) - } -} - -export default Canvas \ No newline at end of file diff --git a/react_app_demo/src/components/Canvas/Canvas.tsx b/react_app_demo/src/components/Canvas/Canvas.tsx new file mode 100644 index 0000000..99d63b9 --- /dev/null +++ b/react_app_demo/src/components/Canvas/Canvas.tsx @@ -0,0 +1,241 @@ +import type { JSX } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import demoImageUrl from "./daisies.jpg"; +import "./Canvas.css"; + +type PhotonModule = typeof import("@silvia-odwyer/photon"); +type PhotonImage = ReturnType; +type Channel = 0 | 1 | 2; +type Status = "loading" | "ready" | "error"; + +type WasmAssetModule = { default: string }; + +const loadPhoton = async (): Promise => { + const [photonModule, wasmModule] = await Promise.all([ + import("@silvia-odwyer/photon"), + import( + "@silvia-odwyer/photon/photon_rs_bg.wasm?url" + ) as Promise, + ]); + + const init = photonModule.default as + | ((moduleOrPath?: unknown) => Promise) + | undefined; + + if (typeof init === "function") { + await init(wasmModule.default); + } + + return photonModule; +}; + +const loadDemoImage = async (): Promise => + new Promise((resolve, reject) => { + const image = new Image(); + image.src = demoImageUrl; + image.onload = () => resolve(image); + image.onerror = () => reject(new Error("Unable to load demo image asset.")); + }); + +const Canvas = (): JSX.Element => { + const canvasRef = useRef(null); + const photonRef = useRef(null); + const sourceImageRef = useRef(null); + const [status, setStatus] = useState("loading"); + const [message, setMessage] = useState("Loading Photon WebAssembly…"); + + const handleError = useCallback((err: unknown) => { + console.error(err); + setStatus("error"); + setMessage(err instanceof Error ? err.message : "Unexpected error"); + }, []); + + const drawOriginal = useCallback((nextImage?: HTMLImageElement) => { + const canvas = canvasRef.current; + const image = nextImage ?? sourceImageRef.current; + if (!canvas) { + throw new Error("Canvas element is unavailable."); + } + if (!image) { + throw new Error("Image asset has not loaded yet."); + } + const ctx = canvas.getContext("2d"); + if (!ctx) { + throw new Error("Unable to acquire 2D rendering context."); + } + + const width = image.naturalWidth || image.width; + const height = image.naturalHeight || image.height; + + canvas.width = width; + canvas.height = height; + ctx.clearRect(0, 0, width, height); + ctx.drawImage(image, 0, 0, width, height); + }, []); + + const ensureContext = useCallback(() => { + const canvas = canvasRef.current; + const photon = photonRef.current; + const image = sourceImageRef.current; + + if (!canvas || !photon || !image) { + throw new Error("Photon is still initialising. Please wait a moment."); + } + + const ctx = canvas.getContext("2d"); + if (!ctx) { + throw new Error("Unable to acquire 2D rendering context."); + } + + return { canvas, ctx, image, photon }; + }, []); + + const applyChannelAdjustment = useCallback( + (channel: Channel, delta: number) => { + try { + const { canvas, ctx, image, photon } = ensureContext(); + ctx.drawImage(image, 0, 0); + const photonImage: PhotonImage = photon.open_image(canvas, ctx); + photon.alter_channel(photonImage, channel, delta); + photon.putImageData(canvas, ctx, photonImage); + setStatus("ready"); + setMessage(`Adjusted channel ${channel + 1}`); + } catch (error) { + handleError(error); + } + }, + [ensureContext, handleError], + ); + + const runEffectPipeline = useCallback(() => { + try { + const { canvas, ctx, image, photon } = ensureContext(); + ctx.drawImage(image, 0, 0); + const photonImage: PhotonImage = photon.open_image(canvas, ctx); + console.time("photon-effect-pipeline"); + photon.alter_channel(photonImage, 2, 70); + photon.grayscale(photonImage); + console.timeEnd("photon-effect-pipeline"); + photon.putImageData(canvas, ctx, photonImage); + setStatus("ready"); + setMessage("Applied channel boost + grayscale"); + } catch (error) { + handleError(error); + } + }, [ensureContext, handleError]); + + const resetImage = useCallback(() => { + try { + drawOriginal(); + setStatus("ready"); + setMessage("Original image restored"); + } catch (error) { + handleError(error); + } + }, [drawOriginal, handleError]); + + useEffect(() => { + let cancelled = false; + + const bootstrap = async () => { + try { + setStatus("loading"); + setMessage("Loading Photon WebAssembly…"); + const [photonModule, demoImage] = await Promise.all([ + loadPhoton(), + loadDemoImage(), + ]); + if (cancelled) { + return; + } + photonRef.current = photonModule; + sourceImageRef.current = demoImage; + drawOriginal(demoImage); + setStatus("ready"); + setMessage("Photon ready"); + } catch (error) { + if (!cancelled) { + handleError(error); + } + } + }; + + bootstrap(); + + return () => { + cancelled = true; + }; + }, [drawOriginal, handleError]); + + return ( +
+ + +
+
+

Image

+ + {message} + +
+ +
+
+ ); +}; + +export default Canvas; diff --git a/react_app_demo/src/components/Canvas/package.json b/react_app_demo/src/components/Canvas/package.json deleted file mode 100644 index 0be08da..0000000 --- a/react_app_demo/src/components/Canvas/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Canvas", - "version": "0.0.0", - "private": true, - "main": "./Canvas.js" -} diff --git a/react_app_demo/src/index.css b/react_app_demo/src/index.css index 4a1df4d..7210c11 100644 --- a/react_app_demo/src/index.css +++ b/react_app_demo/src/index.css @@ -1,13 +1,26 @@ +:root { + color-scheme: dark; + font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + line-height: 1.5; + font-weight: 400; + background-color: #111418; + color: #f8f9fb; +} + +* { + box-sizing: border-box; +} + body { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", - "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + min-height: 100vh; + background: radial-gradient(circle at top left, #1f2533 0%, #111418 45%, #0b0d10 100%); +} + +a { + color: inherit; } -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", - monospace; +button { + font: inherit; } diff --git a/react_app_demo/src/index.js b/react_app_demo/src/index.js deleted file mode 100644 index 87d1be5..0000000 --- a/react_app_demo/src/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import './index.css'; -import App from './App'; -import * as serviceWorker from './serviceWorker'; - -ReactDOM.render(, document.getElementById('root')); - -// If you want your app to work offline and load faster, you can change -// unregister() to register() below. Note this comes with some pitfalls. -// Learn more about service workers: https://bit.ly/CRA-PWA -serviceWorker.unregister(); diff --git a/react_app_demo/src/main.tsx b/react_app_demo/src/main.tsx new file mode 100644 index 0000000..9aa52ff --- /dev/null +++ b/react_app_demo/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './index.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/react_app_demo/src/serviceWorker.js b/react_app_demo/src/serviceWorker.js deleted file mode 100644 index f8c7e50..0000000 --- a/react_app_demo/src/serviceWorker.js +++ /dev/null @@ -1,135 +0,0 @@ -// This optional code is used to register a service worker. -// register() is not called by default. - -// This lets the app load faster on subsequent visits in production, and gives -// it offline capabilities. However, it also means that developers (and users) -// will only see deployed updates on subsequent visits to a page, after all the -// existing tabs open on the page have been closed, since previously cached -// resources are updated in the background. - -// To learn more about the benefits of this model and instructions on how to -// opt-in, read https://bit.ly/CRA-PWA - -const isLocalhost = Boolean( - window.location.hostname === 'localhost' || - // [::1] is the IPv6 localhost address. - window.location.hostname === '[::1]' || - // 127.0.0.1/8 is considered localhost for IPv4. - window.location.hostname.match( - /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ - ) -); - -export function register(config) { - if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { - // The URL constructor is available in all browsers that support SW. - const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); - if (publicUrl.origin !== window.location.origin) { - // Our service worker won't work if PUBLIC_URL is on a different origin - // from what our page is served on. This might happen if a CDN is used to - // serve assets; see https://github.com/facebook/create-react-app/issues/2374 - return; - } - - window.addEventListener('load', () => { - const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; - - if (isLocalhost) { - // This is running on localhost. Let's check if a service worker still exists or not. - checkValidServiceWorker(swUrl, config); - - // Add some additional logging to localhost, pointing developers to the - // service worker/PWA documentation. - navigator.serviceWorker.ready.then(() => { - console.log( - 'This web app is being served cache-first by a service ' + - 'worker. To learn more, visit https://bit.ly/CRA-PWA' - ); - }); - } else { - // Is not localhost. Just register service worker - registerValidSW(swUrl, config); - } - }); - } -} - -function registerValidSW(swUrl, config) { - navigator.serviceWorker - .register(swUrl) - .then(registration => { - registration.onupdatefound = () => { - const installingWorker = registration.installing; - if (installingWorker == null) { - return; - } - installingWorker.onstatechange = () => { - if (installingWorker.state === 'installed') { - if (navigator.serviceWorker.controller) { - // At this point, the updated precached content has been fetched, - // but the previous service worker will still serve the older - // content until all client tabs are closed. - console.log( - 'New content is available and will be used when all ' + - 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' - ); - - // Execute callback - if (config && config.onUpdate) { - config.onUpdate(registration); - } - } else { - // At this point, everything has been precached. - // It's the perfect time to display a - // "Content is cached for offline use." message. - console.log('Content is cached for offline use.'); - - // Execute callback - if (config && config.onSuccess) { - config.onSuccess(registration); - } - } - } - }; - }; - }) - .catch(error => { - console.error('Error during service worker registration:', error); - }); -} - -function checkValidServiceWorker(swUrl, config) { - // Check if the service worker can be found. If it can't reload the page. - fetch(swUrl) - .then(response => { - // Ensure service worker exists, and that we really are getting a JS file. - const contentType = response.headers.get('content-type'); - if ( - response.status === 404 || - (contentType != null && contentType.indexOf('javascript') === -1) - ) { - // No service worker found. Probably a different app. Reload the page. - navigator.serviceWorker.ready.then(registration => { - registration.unregister().then(() => { - window.location.reload(); - }); - }); - } else { - // Service worker found. Proceed as normal. - registerValidSW(swUrl, config); - } - }) - .catch(() => { - console.log( - 'No internet connection found. App is running in offline mode.' - ); - }); -} - -export function unregister() { - if ('serviceWorker' in navigator) { - navigator.serviceWorker.ready.then(registration => { - registration.unregister(); - }); - } -} diff --git a/react_app_demo/src/types.d.ts b/react_app_demo/src/types.d.ts new file mode 100644 index 0000000..c416c30 --- /dev/null +++ b/react_app_demo/src/types.d.ts @@ -0,0 +1,4 @@ +declare module '*.wasm?url' { + const wasmUrl: string; + export default wasmUrl; +} diff --git a/react_app_demo/src/vite-env.d.ts b/react_app_demo/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/react_app_demo/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/react_app_demo/tsconfig.json b/react_app_demo/tsconfig.json new file mode 100644 index 0000000..ee6fac9 --- /dev/null +++ b/react_app_demo/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "moduleResolution": "Bundler", + "allowJs": false, + "strict": true, + "jsx": "react-jsx", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "useDefineForClassFields": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "types": ["vite/client"] + }, + "include": ["src"], + "references": [] +} diff --git a/react_app_demo/vite.config.ts b/react_app_demo/vite.config.ts new file mode 100644 index 0000000..22924ec --- /dev/null +++ b/react_app_demo/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + open: true, + }, + build: { + target: 'esnext', + }, + optimizeDeps: { + exclude: ['@silvia-odwyer/photon'], + }, + assetsInclude: ['**/*.wasm'], +}); diff --git a/site/src/demo.html b/site/src/demo.html index 51ea222..699d18d 100644 --- a/site/src/demo.html +++ b/site/src/demo.html @@ -6,17 +6,17 @@ - + + -
- - - -
-
- + +

Colour Spaces

+
  • Hue Rotate HSL
  • +
  • Hue Rotate HSV
  • +
  • Hue Rotate LCh
  • +
  • Lighten HSL
  • +
  • Lighten HSV
  • +
  • Lighten LCh
  • +
  • Darken HSL
  • +
  • Darken HSV
  • +
  • Darken LCh
  • +
  • Saturate HSL
  • +
  • Saturate HSV
  • +
  • Saturate LCh
  • +
  • Desaturate HSL
  • +
  • Desaturate HSV
  • +
  • Desaturate LCh
  • + +

    Conv

    +
  • Emboss
  • +
  • Box Blur
  • +
  • Gaussian Blur
  • +
  • Sharpen
  • +
  • JavaScript
  • + +

    Noise

    +
  • Add Noise Rand
  • +
  • Pink Noise
  • + +

    Multiple

    +
    Blend
    +
  • Blend
  • +
  • Overlay
  • +
  • Atop
  • +
  • Plus
  • +
  • Multiply
  • +
  • Burn
  • +
  • Difference
  • +
  • Soft Light
  • +
  • Hard Light
  • +
  • Dodge
  • +
  • Exclusion
  • +
  • Lighten
  • +
  • Darken
  • + +

    Change Image

    +
      +
    • Blue Metro
    • +
    • Underground
    • +
    • Nine Yards
    • +
    • Daisies
    • +
    + +
    + +
    +
    + - -
    -

    Photon WebAssembly Demo

    -

    Original

    -
    - - - -
    - -
    -
    -
    -
    - -
    -
    - -
    - -
    - -
    - - - - + +
    +

    Photon WebAssembly Demo

    +

    Original

    +
    + + +
    + +
    +
    +
    +
    +
    +
    + + + diff --git a/webpack_demo/js/index.js b/webpack_demo/js/index.js deleted file mode 100644 index 7b18a72..0000000 --- a/webpack_demo/js/index.js +++ /dev/null @@ -1,368 +0,0 @@ -import MainImage from "./nine_yards.jpg"; -import Underground from "./underground.jpg"; -import NineYards from "./nine_yards.jpg"; -import BlueMetro from "./blue_metro.jpg"; -import Watermark from "./wasm_logo.png" - - -// Setup global variables -var canvas, canvas2, watermark_canvas; -var ctx, ctx2, watermark_ctx; - -import("../../crate/pkg").then(module => { - var startTime; - var endTime; - - // Setup images - const newimg = new Image(); - newimg.src = MainImage; - newimg.style.display = "none"; - newimg.onload = () => { - setUpCanvas(); - } - - const img2 = new Image(); - img2.src = NineYards; - img2.style.display = "none"; - img2.onload=() => { - setUpCanvas2(); - } - - const watermark_img = new Image(); - watermark_img.src = Watermark; - watermark_img.style.display = "none"; - watermark_img.onload = () => { - setUpWatermark(); - } - - // Setup event listeners - let hue_rotate_elem = document.getElementById("hue_rotate"); - hue_rotate_elem.addEventListener('click', function(){console.time("js_edit_time"); editImage(canvas, ctx); console.timeEnd("js_edit_time")}, false); - - let filter_buttons = document.getElementsByClassName("filter"); - for (let i = 0; i < filter_buttons.length; i++) { - let button = filter_buttons[i]; - button.addEventListener("click", function(){filterImage(event)}, false); - } - - let effect_buttons = document.getElementsByClassName("effect"); - for (let i = 0; i < effect_buttons.length; i++) { - let button = effect_buttons[i]; - button.addEventListener("click", function(){applyEffect(event)}, false); - } - - let noise_buttons = document.getElementsByClassName("noise"); - for (let i = 0; i < noise_buttons.length; i++) { - let button = noise_buttons[i]; - button.addEventListener("click", function(){applyEffect(event)}, false); - } - - let blend_buttons = document.getElementsByClassName("blend"); - for (let i = 0; i < blend_buttons.length; i++) { - let button = blend_buttons[i]; - button.addEventListener("click", function(){blendImages(event)}, false); - } - - let resize_btn = document.getElementById("resize"); - resize_btn.addEventListener("click", resize, false); - - function resize() { - console.time("resize") - - let resized_img_container = document.getElementById("resized_imgs"); - - console.log("RESIZE IMAGE") - ctx.drawImage(newimg, 0, 0); - startTime = performance.now(); - - let photon_img = module.open_image(canvas, ctx); - - let newcanvas = module.resize_img_browser(photon_img, 200, 200, 1); - resized_img_container.appendChild(newcanvas); - let newctx = newcanvas.getContext("2d"); - let newimgdata = newctx.getImageData(0, 0, newcanvas.width, newcanvas.height); - console.log(newimgdata); - console.log(newcanvas); - endTime = performance.now(); - updateBenchmarks(); - updateEffectName(event.target); - console.timeEnd("resize"); - } - - let change_image_btn = document.getElementById("change_img"); - change_image_btn.addEventListener("click", changeImageFromNav, false); - - function applyEffect(event) { - console.time("wasm_time"); - - ctx.drawImage(newimg, 0, 0); - startTime = performance.now(); - - // Get the name of the effect the user wishes to apply to the image - // This is the id of the element they clicked on - let filter_name = event.target.id; - - // Convert the ImageData to a PhotonImage (so that it can communicate with the core Rust library) - let rust_image = module.open_image(canvas, ctx); - - - // Setup watermark - let watermark_img = module.open_image(watermark_canvas, watermark_ctx); - - // Maps the name of an effect to its relevant function in the Rust library - let filter_dict = {"grayscale" : function(){return module.grayscale(rust_image)}, - "offset_red": function(){return module.offset(rust_image, 0, 15)}, - "offset_blue": function(){return module.offset(rust_image, 1, 15)}, - "offset_green": function(){return module.offset(rust_image, 2, 15)}, - "primary" : function() {return module.primary(rust_image)}, - "solarize" : function() {return module.solarize(rust_image)}, - "threshold" : function() {return module.threshold(rust_image, 200)}, - "sepia" : function() {return module.sepia(rust_image)}, - "decompose_min" : function(){return module.decompose_min(rust_image)}, - "decompose_max" : function(){return module.decompose_max(rust_image)}, - "grayscale_shades": function(){return module.grayscale_shades(rust_image)}, - "red_channel_grayscale": function() {module.single_channel_grayscale(rust_image, 0)}, - "green_channel_grayscale": function() {module.single_channel_grayscale(rust_image, 1)}, - "blue_channel_grayscale": function() {module.single_channel_grayscale(rust_image, 2)}, - "hue_rotate_hsl": function() {module.hue_rotate_hsl(rust_image, 0.3)}, - "hue_rotate_hsv": function() {module.hue_rotate_hsv(rust_image, 0.3)}, - "hue_rotate_lch": function() {module.hue_rotate_lch(rust_image, 0.3)}, - "lighten_hsl": function() {module.lighten_hsl(rust_image, 0.3)}, - "lighten_hsv": function() {module.lighten_hsv(rust_image, 0.3)}, - "lighten_lch": function() {module.lighten_lch(rust_image, 0.3)}, - "darken_hsl": function() {module.darken_hsl(rust_image, 0.3)}, - "darken_hsv": function() {module.darken_hsv(rust_image, 0.3)}, - "darken_lch": function() {module.darken_lch(rust_image, 0.3 )}, - "desaturate_hsl": function() {module.desaturate_hsl(rust_image, 0.3)}, - "desaturate_hsv": function() {module.desaturate_hsv(rust_image, 0.3)}, - "desaturate_lch": function() {module.desaturate_lch(rust_image, 0.3)}, - "saturate_hsl": function() {module.saturate_hsl(rust_image, 0.3)}, - "saturate_hsv": function() {module.saturate_hsv(rust_image, 0.3)}, - "saturate_lch": function() {module.saturate_lch(rust_image, 0.3)}, - "inc_red_channel": function() {return module.alter_red_channel(rust_image, 120)}, - "inc_blue_channel": function() {return module.alter_channel(rust_image, 2, 100)}, - "inc_green_channel": function() {return module.alter_channel(rust_image, 1, 100)}, - "inc_two_channels": function() {return module.alter_channel(rust_image, 1, 30);}, - "dec_red_channel": function() {return module.alter_channel(rust_image, 0, -30)}, - "dec_blue_channel": function() {return module.alter_channel(rust_image, 2, -30)}, - "dec_green_channel": function() {return module.alter_channel(rust_image, 1, -30)}, - "swap_rg_channels": function() {return module.swap_channels(rust_image, 0, 1);}, - "swap_rb_channels": function() {return module.swap_channels(rust_image, 0, 2);}, - "swap_gb_channels": function() {return module.swap_channels(rust_image, 1, 2);}, - "remove_red_channel": function() {return module.remove_red_channel(rust_image, 250);}, - "remove_green_channel": function() {return module.remove_green_channel(rust_image, 250)}, - "remove_blue_channel": function() {return module.remove_blue_channel(rust_image, 250)}, - "emboss": function() {return module.emboss(rust_image)}, - "box_blur": function() {return module.box_blur(rust_image)}, - "sharpen": function() {return module.sharpen(rust_image)}, - "lix": function() {return module.lix(rust_image)}, - "neue": function() {return module.neue(rust_image)}, - "ryo": function() {return module.ryo(rust_image)}, - "gaussian_blur": function() {return module.gaussian_blur(rust_image)}, - "inc_brightness": function() {return module.inc_brightness(rust_image, 20)}, - "inc_lum": function() {return module.inc_luminosity(rust_image)}, - "grayscale_human_corrected": function() {return module.grayscale_human_corrected(rust_image)}, - "blend": function() {return module.blend(rust_image, rust_image2, "over")}, - "overlay": function() {return module.blend(rust_image, rust_image2, "overlay")}, - "atop": function() {return module.blend(rust_image, rust_image2, "atop")}, - "xor": function() {return module.blend(rust_image, rust_image2, "xor")}, - "plus": function() {return module.blend(rust_image, rust_image2, "plus")}, - "multiply": function() {return module.blend(rust_image, rust_image2, "multiply")}, - "burn": function() {return module.blend(rust_image, rust_image2, "burn")}, - "difference": function() {return module.blend(rust_image, rust_image2, "difference")}, - "soft_light": function() {return module.blend(rust_image, rust_image2, "soft_light")}, - "hard_light": function() {return module.blend(rust_image, rust_image2, "hard_light")}, - "dodge": function() {return module.blend(rust_image, rust_image2, "dodge")}, - "exclusion": function() {return module.blend(rust_image, rust_image2, "exclusion")}, - "lighten": function() {return module.blend(rust_image, rust_image2, "lighten")}, - "darken": function() {return module.blend(rust_image, rust_image2, "darken")}, - "watermark": function() {return module.watermark(rust_image, watermark_img, 10, 30)}, - "text": function() {return module.draw_text(rust_image, "welcome to WebAssembly", 10, 20)}, - "text_border": function() {return module.draw_text_with_border(rust_image, "welcome to the edge", 10, 20)}, - "test": function() {return module.filter(rust_image, "rosetint")}, - "pink_noise": function() {return module.pink_noise(rust_image)}, - "add_noise_rand": function() {return module.add_noise_rand(rust_image)}, - }; - - // Filter the image, the PhotonImage's raw pixels are modified and - // the PhotonImage is returned - filter_dict[filter_name](); - - // Update the canvas with the new imagedata - module.putImageData(canvas, ctx, rust_image); - console.timeEnd("wasm_time"); - endTime = performance.now(); - updateBenchmarks(); - updateEffectName(event.target); - } - - function updateEffectName(elem) { - let effect_name = elem.innerHTML; - console.log(effect_name); - let effect_name_elem = document.getElementById("effect_name"); - effect_name_elem.innerHTML = effect_name; - } - - function blendImages(event) { - console.time("wasm_blend_time"); - - ctx.drawImage(newimg, 0, 0); - startTime = performance.now(); - - // Get the name of the effect the user wishes to apply to the image - // This is the id of the element they clicked on - let filter_name = event.target.id; - - // Convert the ImageData to a PhotonImage (so that it can communicate with the core Rust library) - let rust_image = module.open_image(canvas, ctx); - - let rust_image2 = module.open_image(canvas2, ctx2); - - // Maps the name of an effect to its relevant function in the Rust library - let filter_dict = { - "blend": function() {return module.blend(rust_image, rust_image2, "over")}, - "overlay": function() {return module.blend(rust_image, rust_image2, "overlay")}, - "atop": function() {return module.blend(rust_image, rust_image2, "atop")}, - "plus": function() {return module.blend(rust_image, rust_image2, "plus")}, - "multiply": function() {return module.blend(rust_image, rust_image2, "multiply")}, - "burn": function() {return module.blend(rust_image, rust_image2, "burn")}, - "difference": function() {return module.blend(rust_image, rust_image2, "difference")}, - "soft_light": function() {return module.blend(rust_image, rust_image2, "soft_light")}, - "hard_light": function() {return module.blend(rust_image, rust_image2, "hard_light")}, - "dodge": function() {return module.blend(rust_image, rust_image2, "dodge")}, - "exclusion": function() {return module.blend(rust_image, rust_image2, "exclusion")}, - "lighten": function() {return module.blend(rust_image, rust_image2, "lighten")}, - "darken": function() {return module.blend(rust_image, rust_image2, "darken")}, - "watermark": function() {return module.watermark(rust_image, watermark_img, 10, 30)}, - "text": function() {return module.draw_text(rust_image, "welcome to WebAssembly", 10, 20)}, - "text_border": function() {return module.draw_text_with_border(rust_image, "welcome to the edge", 10, 20)}, - }; - - // Filter the image, the PhotonImage's raw pixels are modified and - // the PhotonImage is returned - filter_dict[filter_name](); - - // Update the canvas with the new imagedata - module.putImageData(canvas, ctx, rust_image); - console.timeEnd("wasm_blend_time"); - endTime = performance.now() - updateBenchmarks(); - updateEffectName(event.target); - } - - function updateCanvas(new_image) { - let new_pixels = module.to_image_data(new_image); - - // Place the pixels back on the canvas - ctx.putImageData(new_pixels, 0, 0); - } - - function filterImage(event) { - startTime = performance.now(); - ctx.drawImage(newimg, 0, 0); - let filter_name = event.target.id; - - console.time("wasm_time"); - - // Convert the ImageData to a PhotonImage (so that it can communicate with the core Rust library) - let rust_image = module.open_image(canvas, ctx); - - // Filter the image, the PhotonImage's raw pixels are modified and - // the PhotonImage is returned - module.filter(rust_image, filter_name); - - // Place the pixels back on the canvas - module.putImageData(canvas, ctx, rust_image); - - endTime = performance.now(); - updateBenchmarks(); - updateEffectName(event.target); - console.timeEnd("wasm_time"); - } - - function setUpCanvas() { - let element = document.getElementById("image_container"); - element.appendChild(newimg); - - canvas = document.getElementById("canvas"); - canvas.width = newimg.width; - canvas.height = newimg.height; - - ctx = canvas.getContext("2d"); - ctx.drawImage(newimg, 0, 0); - } - - function setUpCanvas2() { - let element = document.getElementById("image_container"); - element.appendChild(img2); - canvas2 = document.createElement("canvas"); - canvas2.width = img2.width; - canvas2.height = img2.width; - - ctx2 = canvas2.getContext("2d"); - ctx2.drawImage(img2, 0, 0); - - } - - function setUpWatermark() { - let element = document.getElementById("image_container"); - element.appendChild(watermark_img); - watermark_canvas = document.createElement("canvas"); - watermark_canvas.width = watermark_img.width; - watermark_canvas.height = watermark_img.height; - - watermark_ctx = watermark_canvas.getContext("2d"); - watermark_ctx.drawImage(watermark_img, 0, 0); - - } - - function updateBenchmarks() { - console.log("update benchmarks"); - let time_taken = endTime - startTime; - let time_elem = document.getElementById("time"); - time_elem.innerHTML = `Time: ${time_taken}ms`; - } - - // Change the image currently being edited. - let change_image_elems = document.getElementsByClassName("change_image"); - - for (let i = 0; i < change_image_elems.length; i++) { - let change_image_elem = change_image_elems[i]; - - change_image_elem.addEventListener("click", changeImage, false); - } - - function changeImage(event) { - console.log("image changed") - let img_name = event.target.id; - let imgNamesToImages = {"underground": Underground, "blue_metro": BlueMetro, "nine_yards": NineYards, "fruit": MainImage}; - newimg.src = imgNamesToImages[img_name]; - newimg.onload = () => { - canvas.width = newimg.width; - canvas.height = newimg.height; - ctx.drawImage(newimg, 0, 0); - - }} - - - function changeImageFromNav() { - console.log("image_changed"); - newimg.src = Underground; - newimg.onload = () => { - canvas.width = newimg.width; - canvas.height = newimg.height; - ctx.drawImage(newimg, 0, 0); - - } - } - - - function editImage(canvas, ctx) { - let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); - for (let i = 0; i < imgData.data.length; i += 4) { - imgData[i] += 30; - } - ctx.putImageData(imgData, 0, 0); - } - -}); \ No newline at end of file diff --git a/webpack_demo/package.json b/webpack_demo/package.json index ed4efdb..67a7cbf 100644 --- a/webpack_demo/package.json +++ b/webpack_demo/package.json @@ -1,27 +1,26 @@ { - "author": "Silvia O'Dwyer ", - "name": "photon-wasm", - "version": "0.1.0", - "repository": "https://github.com/silvia-odwyer/photon", - "license": "Apache-2.0", + "author": "Photon Contributors", + "name": "photon-wasm-webpack-demo", + "version": "1.0.0", + "private": true, "scripts": { - "start": "webpack-dev-server", - "build": "webpack" + "dev": "webpack serve --mode development --config webpack.config.js", + "build": "webpack --mode production --config webpack.config.js", + "typecheck": "tsc --noEmit" }, - "bin": { - "create-rust-webpack": ".bin/create-rust-webpack.js" + "dependencies": { + "@silvia-odwyer/photon": "^0.3.3" }, "devDependencies": { - "clean-webpack-plugin": "^4.0.0", - "file-loader": "^6.2.0", - "@wasm-tool/wasm-pack-plugin": "1.7.0", - "html-webpack-plugin": "^5.6.3", - "webpack": "^5", + "@types/webpack-dev-server": "^4.7.2", + "@types/webpack-env": "^1.18.8", + "css-loader": "^7.1.2", + "html-webpack-plugin": "^5.6.4", + "style-loader": "^4.0.0", + "ts-loader": "^9.5.4", + "typescript": "^5.9.2", + "webpack": "^5.101.3", "webpack-cli": "^6.0.1", "webpack-dev-server": "^5.2.2" - }, - "dependencies": { - "@silvia-odwyer/photon": "^0.3.3", - "jimp": "^0.6.4" } -} \ No newline at end of file +} diff --git a/webpack_demo/js/blue_metro.jpg b/webpack_demo/src/assets/blue_metro.jpg similarity index 100% rename from webpack_demo/js/blue_metro.jpg rename to webpack_demo/src/assets/blue_metro.jpg diff --git a/webpack_demo/js/daisies_fuji.jpg b/webpack_demo/src/assets/daisies_fuji.jpg similarity index 100% rename from webpack_demo/js/daisies_fuji.jpg rename to webpack_demo/src/assets/daisies_fuji.jpg diff --git a/webpack_demo/js/daisies_med.jpg b/webpack_demo/src/assets/daisies_med.jpg similarity index 100% rename from webpack_demo/js/daisies_med.jpg rename to webpack_demo/src/assets/daisies_med.jpg diff --git a/webpack_demo/js/nine_yards.jpg b/webpack_demo/src/assets/nine_yards.jpg similarity index 100% rename from webpack_demo/js/nine_yards.jpg rename to webpack_demo/src/assets/nine_yards.jpg diff --git a/webpack_demo/js/underground.jpg b/webpack_demo/src/assets/underground.jpg similarity index 100% rename from webpack_demo/js/underground.jpg rename to webpack_demo/src/assets/underground.jpg diff --git a/webpack_demo/js/wasm_logo.png b/webpack_demo/src/assets/wasm_logo.png similarity index 100% rename from webpack_demo/js/wasm_logo.png rename to webpack_demo/src/assets/wasm_logo.png diff --git a/webpack_demo/src/global.d.ts b/webpack_demo/src/global.d.ts new file mode 100644 index 0000000..de67ea5 --- /dev/null +++ b/webpack_demo/src/global.d.ts @@ -0,0 +1,14 @@ +declare module '*.wasm?url' { + const wasmUrl: string; + export default wasmUrl; +} + +declare module '*.jpg' { + const src: string; + export default src; +} + +declare module '*.png' { + const src: string; + export default src; +} diff --git a/webpack_demo/src/index.ts b/webpack_demo/src/index.ts new file mode 100644 index 0000000..863861e --- /dev/null +++ b/webpack_demo/src/index.ts @@ -0,0 +1,507 @@ +import MainImage from "./assets/nine_yards.jpg"; +import Underground from "./assets/underground.jpg"; +import NineYards from "./assets/nine_yards.jpg"; +import BlueMetro from "./assets/blue_metro.jpg"; +import Watermark from "./assets/wasm_logo.png"; + +type PhotonModule = typeof import("@silvia-odwyer/photon"); +type ExtendedPhotonModule = PhotonModule & + Record unknown>; +type WasmAssetModule = { default: string }; + +type Timing = { + start: number; + end: number; +}; + +const timing: Timing = { + start: 0, + end: 0, +}; + +let canvas: HTMLCanvasElement; +let canvas2: HTMLCanvasElement; +let watermarkCanvas: HTMLCanvasElement; +let ctx: CanvasRenderingContext2D; +let ctx2: CanvasRenderingContext2D; +let watermarkCtx: CanvasRenderingContext2D; +let newimg: HTMLImageElement; +let img2: HTMLImageElement; +let watermarkImg: HTMLImageElement; + +const loadPhoton = async (): Promise => { + const [module, wasmModule] = await Promise.all([ + import("@silvia-odwyer/photon"), + import( + "@silvia-odwyer/photon/photon_rs_bg.wasm?url" + ) as Promise, + ]); + + const init = module.default as + | ((moduleOrPath?: unknown) => Promise) + | undefined; + + if (typeof init === "function") { + await init(wasmModule.default); + } + return module as ExtendedPhotonModule; +}; + +const bootstrap = async () => { + let photon: ExtendedPhotonModule; + try { + photon = await loadPhoton(); + } catch (error) { + console.error("Failed to initialise Photon WASM module", error); + return; + } + + const newImage = new Image(); + newImage.src = MainImage; + newImage.style.display = "none"; + newImage.onload = () => { + newimg = newImage; + setUpCanvas(); + ensureBlendCanvasSize(); + }; + + const overlayImage = new Image(); + overlayImage.src = NineYards; + overlayImage.style.display = "none"; + overlayImage.onload = () => { + img2 = overlayImage; + setUpCanvas2(); + ensureBlendCanvasSize(); + }; + + const watermarkImage = new Image(); + watermarkImage.src = Watermark; + watermarkImage.style.display = "none"; + watermarkImage.onload = () => { + watermarkImg = watermarkImage; + setUpWatermark(); + }; + + const hueRotateElem = document.getElementById("hue_rotate"); + hueRotateElem?.addEventListener("click", () => { + console.time("js_edit_time"); + editImage(canvas, ctx); + console.timeEnd("js_edit_time"); + }); + + const filterButtons = document.getElementsByClassName("filter"); + for (let i = 0; i < filterButtons.length; i += 1) { + const button = filterButtons[i] as HTMLElement; + button.addEventListener("click", (event) => filterImage(event, photon)); + } + + const effectButtons = document.getElementsByClassName("effect"); + for (let i = 0; i < effectButtons.length; i += 1) { + const button = effectButtons[i] as HTMLElement; + button.addEventListener("click", (event) => applyEffect(event, photon)); + } + + const noiseButtons = document.getElementsByClassName("noise"); + for (let i = 0; i < noiseButtons.length; i += 1) { + const button = noiseButtons[i] as HTMLElement; + button.addEventListener("click", (event) => applyEffect(event, photon)); + } + + const blendButtons = document.getElementsByClassName("blend"); + for (let i = 0; i < blendButtons.length; i += 1) { + const button = blendButtons[i] as HTMLElement; + button.addEventListener("click", (event) => blendImages(event, photon)); + } + + const resizeBtn = document.getElementById("resize"); + resizeBtn?.addEventListener("click", (event) => resize(event, photon)); + + const changeImageBtn = document.getElementById("change_img"); + changeImageBtn?.addEventListener("click", changeImageFromNav); + + const changeImageElems = document.getElementsByClassName("change_image"); + for (let i = 0; i < changeImageElems.length; i += 1) { + const changeImageElem = changeImageElems[i] as HTMLElement; + changeImageElem.addEventListener("click", changeImage); + } +}; + +const ensureContexts = (): boolean => { + if (!canvas || !ctx || !newimg) { + console.warn("Photon demo is still initialising."); + return false; + } + return true; +}; + +const ensureBlendCanvasSize = () => { + if (!canvas2 || !ctx2 || !newimg || !img2) { + return; + } + const targetWidth = Math.min(newimg.width, img2.width); + const targetHeight = Math.min(newimg.height, img2.height); + if (canvas2.width !== targetWidth || canvas2.height !== targetHeight) { + canvas2.width = targetWidth; + canvas2.height = targetHeight; + } + ctx2.clearRect(0, 0, canvas2.width, canvas2.height); + ctx2.drawImage(img2, 0, 0, canvas2.width, canvas2.height); +}; + +const blendWithMode = ( + photon: ExtendedPhotonModule, + base: ReturnType, + overlay: ReturnType, + mode: string, +) => { + if (!overlay) { + return; + } + try { + photon.blend(base, overlay, mode); + } catch (error) { + console.warn(`Blend mode "${mode}" failed`, error); + } +}; + +const filterImage = (event: Event, photon: ExtendedPhotonModule) => { + if (!ensureContexts()) { + return; + } + const target = event.target as HTMLElement | null; + if (!target) { + return; + } + timing.start = performance.now(); + ctx.drawImage(newimg, 0, 0); + const filterName = target.id; + + console.time("wasm_time"); + + const rustImage = photon.open_image(canvas, ctx); + photon.filter(rustImage, filterName); + photon.putImageData(canvas, ctx, rustImage); + + timing.end = performance.now(); + updateBenchmarks(); + updateEffectName(target); + console.timeEnd("wasm_time"); +}; + +const applyEffect = (event: Event, photon: ExtendedPhotonModule) => { + if (!ensureContexts() || !watermarkCanvas || !watermarkCtx) { + return; + } + console.time("wasm_time"); + + ctx.drawImage(newimg, 0, 0); + timing.start = performance.now(); + + const target = event.target as HTMLElement | null; + if (!target) { + return; + } + const filterName = target.id; + const rustImage = photon.open_image(canvas, ctx); + // const rustImage2 = canvas2 && ctx2 ? photon.open_image(canvas2, ctx2) : null; + const watermarkImage = photon.open_image(watermarkCanvas, watermarkCtx); + + const filters: Record void> = { + grayscale: () => photon.grayscale(rustImage), + offset_red: () => photon.offset(rustImage, 0, 15), + offset_blue: () => photon.offset(rustImage, 1, 15), + offset_green: () => photon.offset(rustImage, 2, 15), + primary: () => photon.primary(rustImage), + solarize: () => photon.solarize(rustImage), + threshold: () => photon.threshold(rustImage, 200), + sepia: () => photon.sepia(rustImage), + decompose_min: () => photon.decompose_min(rustImage), + decompose_max: () => photon.decompose_max(rustImage), + grayscale_shades: () => photon.grayscale_shades(rustImage, 1), + red_channel_grayscale: () => photon.single_channel_grayscale(rustImage, 0), + green_channel_grayscale: () => + photon.single_channel_grayscale(rustImage, 1), + blue_channel_grayscale: () => photon.single_channel_grayscale(rustImage, 2), + hue_rotate_hsl: () => photon.hue_rotate_hsl(rustImage, 0.3), + hue_rotate_hsv: () => photon.hue_rotate_hsv(rustImage, 0.3), + hue_rotate_lch: () => photon.hue_rotate_lch(rustImage, 0.3), + lighten_hsl: () => photon.lighten_hsl(rustImage, 0.1), + lighten_hsv: () => photon.lighten_hsv(rustImage, 0.1), + lighten_lch: () => photon.lighten_lch(rustImage, 0.1), + darken_hsl: () => photon.darken_hsl(rustImage, 0.1), + darken_hsv: () => photon.darken_hsv(rustImage, 0.1), + darken_lch: () => photon.darken_lch(rustImage, 0.1), + desaturate_hsl: () => photon.desaturate_hsl(rustImage, 0.3), + desaturate_hsv: () => photon.desaturate_hsv(rustImage, 0.3), + desaturate_lch: () => photon.desaturate_lch(rustImage, 0.3), + saturate_hsl: () => photon.saturate_hsl(rustImage, 0.3), + saturate_hsv: () => photon.saturate_hsv(rustImage, 0.3), + saturate_lch: () => photon.saturate_lch(rustImage, 0.3), + inc_red_channel: () => photon.alter_red_channel(rustImage, 120), + inc_blue_channel: () => photon.alter_channel(rustImage, 2, 100), + inc_green_channel: () => photon.alter_channel(rustImage, 1, 100), + inc_two_channels: () => photon.alter_channel(rustImage, 1, 30), + dec_red_channel: () => photon.alter_channel(rustImage, 0, -30), + dec_blue_channel: () => photon.alter_channel(rustImage, 2, -30), + dec_green_channel: () => photon.alter_channel(rustImage, 1, -30), + swap_rg_channels: () => photon.swap_channels(rustImage, 0, 1), + swap_rb_channels: () => photon.swap_channels(rustImage, 0, 2), + swap_gb_channels: () => photon.swap_channels(rustImage, 1, 2), + remove_red_channel: () => photon.remove_red_channel(rustImage, 250), + remove_green_channel: () => photon.remove_green_channel(rustImage, 250), + remove_blue_channel: () => photon.remove_blue_channel(rustImage, 250), + emboss: () => photon.emboss(rustImage), + box_blur: () => photon.box_blur(rustImage), + sharpen: () => photon.sharpen(rustImage), + lix: () => photon.lix(rustImage), + neue: () => photon.neue(rustImage), + ryo: () => photon.ryo(rustImage), + gaussian_blur: () => photon.gaussian_blur(rustImage, 3), + inc_brightness: () => photon.inc_brightness(rustImage, 20), + // inc_lum: () => photon.inc_luminosity(rustImage), + grayscale_human_corrected: () => + photon.grayscale_human_corrected(rustImage), + watermark: () => + photon.watermark(rustImage, watermarkImage, BigInt(10), BigInt(30)), + text: () => + photon.draw_text(rustImage, "welcome to WebAssembly", 10, 20, 20), + text_border: () => + photon.draw_text_with_border( + rustImage, + "welcome to the edge", + 10, + 20, + 20, + ), + test: () => photon.filter(rustImage, "rosetint"), + pink_noise: () => { + console.warn("pink_noise is unsupported in this WASM build."); + }, + add_noise_rand: () => { + console.warn("add_noise_rand is unsupported in this WASM build."); + }, + }; + + const handler = filters[filterName]; + if (handler) { + handler(); + photon.putImageData(canvas, ctx, rustImage); + } + + console.timeEnd("wasm_time"); + timing.end = performance.now(); + updateBenchmarks(); + updateEffectName(target); +}; + +const blendImages = (event: Event, photon: ExtendedPhotonModule) => { + if ( + !ensureContexts() || + !canvas2 || + !ctx2 || + !watermarkCanvas || + !watermarkCtx + ) { + return; + } + ensureBlendCanvasSize(); + console.time("wasm_blend_time"); + + ctx.drawImage(newimg, 0, 0); + timing.start = performance.now(); + + const target = event.target as HTMLElement | null; + if (!target) { + return; + } + const filterName = target.id; + + const rustImage = photon.open_image(canvas, ctx); + const rustImage2 = photon.open_image(canvas2, ctx2); + const watermarkImage = photon.open_image(watermarkCanvas, watermarkCtx); + + const blends: Record void> = { + blend: () => blendWithMode(photon, rustImage, rustImage2, "over"), + overlay: () => blendWithMode(photon, rustImage, rustImage2, "overlay"), + atop: () => blendWithMode(photon, rustImage, rustImage2, "atop"), + xor: () => blendWithMode(photon, rustImage, rustImage2, "xor"), + plus: () => blendWithMode(photon, rustImage, rustImage2, "plus"), + multiply: () => blendWithMode(photon, rustImage, rustImage2, "multiply"), + burn: () => blendWithMode(photon, rustImage, rustImage2, "burn"), + difference: () => + blendWithMode(photon, rustImage, rustImage2, "difference"), + soft_light: () => + blendWithMode(photon, rustImage, rustImage2, "soft_light"), + hard_light: () => + blendWithMode(photon, rustImage, rustImage2, "hard_light"), + dodge: () => blendWithMode(photon, rustImage, rustImage2, "dodge"), + exclusion: () => blendWithMode(photon, rustImage, rustImage2, "exclusion"), + lighten: () => blendWithMode(photon, rustImage, rustImage2, "lighten"), + darken: () => blendWithMode(photon, rustImage, rustImage2, "darken"), + watermark: () => + photon.watermark(rustImage, watermarkImage, BigInt(10), BigInt(30)), + text: () => + photon.draw_text(rustImage, "welcome to WebAssembly", 10, 20, 20), + text_border: () => + photon.draw_text_with_border( + rustImage, + "welcome to the edge", + 10, + 20, + 20, + ), + }; + + const handler = blends[filterName]; + if (handler) { + handler(); + photon.putImageData(canvas, ctx, rustImage); + } + + console.timeEnd("wasm_blend_time"); + timing.end = performance.now(); + updateBenchmarks(); + updateEffectName(target); +}; + +const resize = (event: Event, photon: ExtendedPhotonModule) => { + if (!ensureContexts()) { + return; + } + console.time("resize"); + const resizedContainer = document.getElementById("resized_imgs"); + if (!resizedContainer) { + return; + } + + ctx.drawImage(newimg, 0, 0); + timing.start = performance.now(); + + const photonImg = photon.open_image(canvas, ctx); + const newCanvas = photon.resize_img_browser(photonImg, 200, 200, 1); + resizedContainer.appendChild(newCanvas); + timing.end = performance.now(); + updateBenchmarks(); + if (event.target instanceof HTMLElement) { + updateEffectName(event.target); + } + console.timeEnd("resize"); +}; + +const updateEffectName = (elem: HTMLElement) => { + const effectName = elem.innerHTML; + const effectElem = document.getElementById("effect_name"); + if (effectElem) { + effectElem.innerHTML = effectName; + } +}; + +const changeImage = (event: Event) => { + const target = event.target as HTMLElement; + const imgName = target.id; + const imgNamesToImages: Record = { + underground: Underground, + blue_metro: BlueMetro, + nine_yards: NineYards, + fruit: MainImage, + }; + + const src = imgNamesToImages[imgName]; + if (!src) { + return; + } + + newimg.src = src; + newimg.onload = () => { + canvas.width = newimg.width; + canvas.height = newimg.height; + ctx.drawImage(newimg, 0, 0); + ensureBlendCanvasSize(); + }; +}; + +const changeImageFromNav = () => { + newimg.src = Underground; + newimg.onload = () => { + canvas.width = newimg.width; + canvas.height = newimg.height; + ctx.drawImage(newimg, 0, 0); + ensureBlendCanvasSize(); + }; +}; + +const setUpCanvas = () => { + const container = document.getElementById("image_container"); + container?.appendChild(newimg); + + canvas = document.getElementById("canvas") as HTMLCanvasElement; + canvas.width = newimg.width; + canvas.height = newimg.height; + const context = canvas.getContext("2d"); + if (!context) { + throw new Error("Unable to get canvas context."); + } + ctx = context; + ctx.drawImage(newimg, 0, 0); + ensureBlendCanvasSize(); +}; + +const setUpCanvas2 = () => { + const container = document.getElementById("image_container"); + container?.appendChild(img2); + canvas2 = document.createElement("canvas"); + canvas2.width = img2.width; + canvas2.height = img2.height; + const context = canvas2.getContext("2d"); + if (!context) { + throw new Error("Unable to get secondary canvas context."); + } + ctx2 = context; + ctx2.drawImage(img2, 0, 0); +}; + +const setUpWatermark = () => { + const container = document.getElementById("image_container"); + container?.appendChild(watermarkImg); + watermarkCanvas = document.createElement("canvas"); + watermarkCanvas.width = watermarkImg.width; + watermarkCanvas.height = watermarkImg.height; + const context = watermarkCanvas.getContext("2d"); + if (!context) { + throw new Error("Unable to get watermark canvas context."); + } + watermarkCtx = context; + watermarkCtx.drawImage(watermarkImg, 0, 0); +}; + +const updateBenchmarks = () => { + const timeTaken = timing.end - timing.start; + const timeElem = document.getElementById("time"); + if (timeElem) { + timeElem.innerHTML = `Time: ${timeTaken.toFixed(2)}ms`; + } +}; + +const editImage = ( + activeCanvas: HTMLCanvasElement, + context: CanvasRenderingContext2D, +) => { + const imgData = context.getImageData( + 0, + 0, + activeCanvas.width, + activeCanvas.height, + ); + for (let i = 0; i < imgData.data.length; i += 4) { + imgData.data[i] += 30; + } + context.putImageData(imgData, 0, 0); +}; + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", () => { + void bootstrap(); + }); +} else { + void bootstrap(); +} diff --git a/webpack_demo/tsconfig.json b/webpack_demo/tsconfig.json new file mode 100644 index 0000000..acc627e --- /dev/null +++ b/webpack_demo/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2023", "DOM"], + "moduleResolution": "Bundler", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "types": ["webpack-env"] + }, + "include": ["src"] +} diff --git a/webpack_demo/webpack.config.js b/webpack_demo/webpack.config.js index e364fc9..638399d 100644 --- a/webpack_demo/webpack.config.js +++ b/webpack_demo/webpack.config.js @@ -1,83 +1,59 @@ -const path = require("path"); -const HtmlWebpackPlugin = require("html-webpack-plugin"); +const path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); -const dist = path.resolve(__dirname, "dist"); -const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); -const CleanWebpackPlugin = require('clean-webpack-plugin'); - -var mainConfig = { - entry: "./js/index.js", +/** @type {import('webpack').Configuration} */ +module.exports = { + entry: path.resolve(__dirname, 'src/index.ts'), output: { - path: dist, - filename: "bundle.js" - }, - devServer: { - static: { - directory: dist - }, + filename: '[name].[contenthash].js', + path: path.resolve(__dirname, 'dist'), + publicPath: '/', + clean: true, }, - mode: "none", - experiments: { - asyncWebAssembly: true, + resolve: { + extensions: ['.ts', '.js', '.wasm'], }, - ignoreWarnings: [ - (warning) => { - const msg = warning.message; - return ( - msg.includes("The following asset(s) exceed the recommended size limit (244 KiB).") - ); - }, - ], - module: { rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/, + }, { test: /\.css$/, - use: [ - 'style-loader', - 'css-loader' - ] + use: ['style-loader', 'css-loader'], + }, + { + test: /\.(png|jpe?g|gif)$/i, + type: 'asset/resource', + }, + { + test: /\.wasm$/i, + type: 'asset/resource', }, - { - test: /\.(png|svg|jpg|gif)$/, - use: [ - 'file-loader' - ] - } - ] + ], }, - resolve: { - extensions: [".js", ".wasm"] + experiments: { + asyncWebAssembly: true, + topLevelAwait: true, }, + devtool: 'source-map', plugins: [ new HtmlWebpackPlugin({ - template: 'index.html' + template: path.resolve(__dirname, 'index.html'), }), - - new WasmPackPlugin({ - crateDirectory: path.resolve(__dirname, "../crate"), - // WasmPackPlugin defaults to compiling in "dev" profile. To change that, use forceMode: 'release': - forceMode: 'release' - }), - ] + ], + devServer: { + static: { + directory: path.resolve(__dirname, 'dist'), + }, + hot: true, + port: 8080, + open: true, + historyApiFallback: true, + }, + performance: { + hints: false, + }, }; - -// const workerConfig = { -// entry: "./js/worker.js", -// target: "webworker", -// plugins: [ -// new WasmPackPlugin({ -// crateDirectory: path.resolve(__dirname, "../crate") -// }) -// ], -// resolve: { -// extensions: [".js", ".wasm"] -// }, -// output: { -// path: dist, -// filename: "worker.js" -// } -// }; - - -module.exports = [mainConfig] \ No newline at end of file From d15af4c4ee746b316fa64ccf0c9ab9972e708aa0 Mon Sep 17 00:00:00 2001 From: Edo <0xe1216@gmail.com> Date: Fri, 19 Sep 2025 14:49:11 +0400 Subject: [PATCH 2/2] fix: darken not work --- webpack_demo/src/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/webpack_demo/src/index.ts b/webpack_demo/src/index.ts index 863861e..c585401 100644 --- a/webpack_demo/src/index.ts +++ b/webpack_demo/src/index.ts @@ -138,8 +138,9 @@ const ensureBlendCanvasSize = () => { if (!canvas2 || !ctx2 || !newimg || !img2) { return; } - const targetWidth = Math.min(newimg.width, img2.width); - const targetHeight = Math.min(newimg.height, img2.height); + const targetWidth = newimg.width >= img2.width ? newimg.width + 1 : img2.width; + const targetHeight = + newimg.height >= img2.height ? newimg.height + 1 : img2.height; if (canvas2.width !== targetWidth || canvas2.height !== targetHeight) { canvas2.width = targetWidth; canvas2.height = targetHeight;